从头开始制作 C# 表单编辑器
通过实现 RectTracker 和透明控件来制作 C# Forms 编辑器。
目录
- 引言
- 进行窗体编辑的方法
- Visual Studio 是如何做的?
- 实现你自己的 RectTracker 和 SelectionUIOverlay
- 在运行时添加控件
- 在运行时编辑控件
- 复制粘贴控件
- 关于 IDE
- 最后的寄语
- 历史
引言
Forms 编辑器允许你在窗体上添加、移动、调整大小和删除控件。VC6 的对话框编辑器和 VS.NET 的 Forms 设计器是我们设计时常用的 Forms 编辑器。
在 .NET 框架中,此功能被封装为几个服务,以使程序员能够编写自己的 Forms 编辑器,这些编辑器以类似于 VS.NET 的 Forms 设计器的方式创建窗体。通过依赖 .NET 框架,程序员无需关心如何选择/移动/调整控件大小以及绘制选择矩形。他只需要实现一些基本服务,框架就会处理控件的选择并绘制选择效果。
本文尝试展示如何在不实现基本服务的情况下编写 Forms 编辑器,即自己完成控件的选择和选择效果的绘制。我希望通过这些,你可以推断出当你依赖 .NET 框架来实现自己的 Forms 编辑器时,幕后发生了什么。
进行窗体编辑的方法
编辑窗体上的控件时需要执行两个主要任务
- 阻止容器窗体中的控件接受系统事件,例如鼠标点击和键盘输入。
- 当选定的控件在容器窗体中的其他控件上移动时,在其他控件“之上”绘制选择矩形(请参见演示图片)。
第二项更令人头疼,因为 Windows 控件是由操作系统在你在容器窗体表面绘制选择矩形后绘制的。因此,如果选择矩形与其他容器控件重叠,它将始终被控件覆盖。
以下项目从不同方面解决了上述两个问题
- Johan Rosengren 的 DialogEditorDemo。这是一个出色的对话框编辑器应用程序,具有许多其他功能。它不在对话框上放置真实的 Windows 控件。实际上,它只是在对话框表面绘制看起来像 Windows 控件的 GDI 对象,然后绘制选择矩形。由于伪控件和选择矩形的绘制都由用户应用程序执行,因此很容易在伪控件重叠时在其“上方”绘制选择矩形。Johan 的应用程序是一个 MFC 项目,我在 .NET 环境中用 C# 测试了他的方法,对于简单控件来说效果相当好,因为 .NET 框架已经在
System.Windows.Forms.ControlPaint
中实现了几种控件绘制方法,如DrawButton()
、DrawCheckBox()
、DrawComboButton()
和DrawRadioButton()
。但是,这种方法有其局限性,绘制其他复杂控件会很困难,而且由于绘制的控件不是真实的 Windows 控件,因此无法在运行时编辑它们的属性。 - Nashcontrol 的 C# Rect Tracker。这种方法通过将选择矩形跟踪器(
RectTracker
)实现为UserControl
来部分解决上述两个问题。通过这样做,RectTraker
可以作为UserControl
提升到最前面,并由操作系统绘制在 Z 顺序较低的其他控件“之上”。但效果不尽人意,有时移动/调整控件大小不如预期。 - SharpDevelop。它是 .NET 平台的开源 IDE。如前所述,在 .NET 平台中,选择/移动/调整控件大小的功能已经封装在 .NET 框架中;你只需要实现
IDesignerHost
、IContainer
、IToolboxService
、IUIService
和ISelectionService
等接口和基本服务。但这种方法没有揭示底层原理,因为框架为你完成了繁重的工作。
Visual Studio 是如何做的?
答案是,在容器窗体之上的透明覆盖层上绘制选择矩形。当你在 Visual Studio 的 Forms 设计器中使用 Spy++ 检查窗体时,你会发现有一个名为“SelectionUIOverlay
”的透明控件,它正好位于正在编辑的容器窗体的上方。
实现你自己的 RectTracker 和 SelectionUIOverlay
我们现在需要一个 RectTracker
来跟踪选定的对象,以及一个真正透明的 SelectionUIOverlay
来绘制选择矩形。
C# RectTracker
MFC 的 CRectTracker
类已经存在很长时间了,它一直用作矩形对象的跟踪器。你可以在“AFXEXT.h”中找到它的定义,并在“...\atlmfc\src\mfc\trckrect.cpp”中找到其源代码。
由于 .NET 框架没有封装所有的 Windows API,因此将该类移植到 C#,你仍然需要通过 System.Runtime.InteropServices
调用一些 API 函数。但是,你可以使用 ControlPaint.DrawReversibleLine
或 ControlPaint.DrawReversibleFrame
绘制橡皮筋线(鼠标指针移动时跟随的焦点矩形,当按下鼠标左键时)和拖动矩形,这可以避免调用 GDI+ API。
//
private void DrawDragRect(Graphics gs,Rectangle rect,Rectangle rectLast)
{
ControlPaint.DrawReversibleFrame(rectLast,Color.White,FrameStyle.Dashed);
ControlPaint.DrawReversibleFrame(rect,Color.White,FrameStyle.Dashed);
}
我还派生了一个 FormRectTracker
,它继承自 C# 的 RectTracker
,用于选择和调整容器窗体的大小。它只需重写 RectTracker
的 HitTesthanles()
方法,以防止通过拖动其顶部/左边界来移动或调整容器窗体的大小。
//
protected override TrackerHit HitTestHandles(System.Drawing.Point point)
{
TrackerHit hit = base.HitTestHandles (point);
switch(hit) {
case TrackerHit.hitTopLeft:
case TrackerHit.hitTop:
case TrackerHit.hitLeft:
case TrackerHit.hitTopRight:
case TrackerHit.hitBottomLeft:
case TrackerHit.hitMiddle:
hit = TrackerHit.hitNothing;
break;
default:
break;
}
return hit;
}
在我将 CRectTracker
从 MFC 移植到 C# 的过程中,我在网上发现了 fanjunxing 的实现 here。由于两个实现都是从同一个 MFC 类到 C# 的逐条翻译,因此这两个实现的大部分部分非常相似。区别在于如何绘制橡皮筋线以及在调整选择矩形大小时如何重建 Rectangle
对象。
透明 SelectionUIOverlay
由于 .NET 框架不直接支持透明控件,你可能会认为我们可以使用一个透明的子窗体作为透明控件,方法是将 TransparencyKey
设置为其 BackColor
的值。不幸的是,这种方法不起作用。如果子窗体的父窗体不是透明的,将子窗体的 TransparencyKey
设置为其 BackColor
将不会使子窗体透明。
要实现一个透明控件,你需要
- 为控件窗口添加透明样式
// protected override CreateParams CreateParams { get { CreateParams cp=base.CreateParams; cp.ExStyle|=0x00000020; //WS_EX_TRANSPARENT return cp; } }
- 重写
OnPaintBackground
事件。这是防止背景被绘制所必需的。protected override void OnPaintBackground(PaintEventArgs pevent) { //do nothing }
- 编写一个实现
IMessageFilter
接口的消息过滤器。这是真正透明控件中最棘手的部分。如果你想在透明控件上绘制东西,同时又移动/调整它下面的控件的大小,你可以编写一个消息过滤器来阻止控件刷新,然后扩展透明控件的Invalidate()
函数来绘制你自己的项目以及它下面的其他控件。public class MessageFilter:System.Windows.Forms.IMessageFilter { ... public bool PreFilterMessage(ref System.Windows.Forms.Message m) { Debug.Assert(frmMain != null); Debug.Assert(frmCtrlContainer != null); Control ctrl= Control)Control.FromHandle(m.HWnd); if(frmCtrlContainer.Contains(ctrl) && m.Msg == WM_PAINT) { //let the main form redraw other sub forms, controls frmMain.Refresh(); //prevent the controls from being painted return true; } return false; } } // public class SelectionUIOverlay : System.Windows.Forms.Control { ... private void InvalidateEx() { Invalidate(); //let parent redraw the background if(Parent==null) return; Rectangle rc=new Rectangle(this.Location,this.Size); Parent.Invalidate(rc,true); ... //move and refresh the controls here } }
在运行时添加控件
使用 C#,你可以使用“new
”运算符创建一个对象,或者通过反射来创建它。我想知道 .NET CLR 是否只是使用相同的机制来处理这两种方法。我注意到只有当你从调试器运行应用程序时,反射似乎存在细微的性能问题。这就是为什么我在演示代码中添加了一个 switch
语句和一些“new
”运算符。但是,如果你直接运行应用程序,无论它是调试版本还是发布版本,反射似乎都没有性能问题。
//
public static Control CreateControl(string ctrlName,string partialName)
{
Control ctrl;
switch(ctrlName)
{
case "Label":
ctrl = new Label();
break;
case "TextBox":
...
default:
Assembly controlAsm =
Assembly.LoadWithPartialName(partialName);
Type controlType =
controlAsm.GetType(partialName + "." + ctrlName);
ctrl = (Control)Activator.CreateInstance(controlType);
break;
}
return ctrl;
}
在运行时编辑控件
由于 .NET 框架已经有了 PropertyGrid
控件来执行运行时属性编辑,因此在创建控件后编辑其属性非常容易。你只需将 PropertyGrid
的 SelectedObject
属性设置为选定的控件。
透明的 SelectionUIOverlay
位于容器窗体的上方,因此它可以自然地阻止容器窗体和容器中的控件接收鼠标点击和键盘事件。你需要做的另一件事是将透明控件的 Graphics
在无效时传递给 RectTracker
,然后 RectTracker
将使用它来绘制选择矩形。
复制粘贴控件
我发布了另一篇文章来提供一个初步的解决方案,你可以在 here 找到它。我尝试通过序列化控件的属性来复制粘贴 Windows Forms 控件。但是,这种方法有其局限性,并且需要对 TreeView
控件和 ListView
控件进行额外的处理。
关于 IDE
演示应用程序使用了 luo wei fen 出色的停靠库 WinFormsUI 和 Aju.George 的 ToolBox 类。我对 ToolBox
类进行了一些小的修改,以使其适应 WinFormsUI
框架。
最后的寄语
我决定写这篇文章,不是因为我想提供一些类来替代 .NET 框架封装的相应功能。我的目的是说明编写 Forms 编辑器的一些基本原理,并提供一个具体的实现。我认为 C# RectTracker
类和透明控件的实现也可以应用于其他场景,例如形状编辑和图片/图表编辑。
历史
- 0.9 演示 - 2006 年 3 月 1 日。