Contents
A Forms Editor allows you to add, move, resize, and delete controls on your form. The dialog editor of VC6 and the Forms Designer of VS.NET are Forms Editors we commonly use at design time.
In the .NET framework, this functionality is wrapped as several services, to enable programmers to write their own Forms Editors that create forms in a manner similar to the way used in the Forms Designer of VS.NET. By relying on the .NET framework, the programmer doesn't need to care about how to select/move/resize a control and draw the selection rectangle. He only needs to implement some base services, and the framework will do the control selection and draw the selection effect.
This article tries to show you how to write a Forms Editor without implementing the base services, that is, performing the control selection and drawing the selection effect by yourself. I hope, from this, you can deduce what's happening under the covers when you rely on the .NET framework to implement your own Forms Editor.
There are two major tasks that needs to be performed when you edit a control on a form:
- Prevent the controls in the container form from accepting the system events, such as mouse click and key press.
- Paint the selection rectangle "above" the other controls in the container form when the selected controls are being moved across other controls (see the demo picture).
The second item is more annoying because Windows controls are painted by the OS after you paint the selection rectangle on the surface of the container form. So if the selection rectangle is overlapped with another control in the container form, it will always be covered by the control.
The following projects solve the above two problems from different aspects:
- Johan Rosengren's DialogEditorDemo. This is an excellent dialog editor application that has many other features. It doesn't put real Windows controls on the dialog. Actually, it just draws the GDI objects that look like Windows controls on the surface of the dialog, and then draws the selection rectangles. Since the painting of the pseudo controls and the selection rectangles are all performed by the user application, it's easy for you to paint a selection rectangle "above" the pseudo control when they are overlapped. Johan's application is an MFC project, I've tested his approach in the .NET environment with C#, and it works pretty fine with simple controls, because the .NET framework has already implemented several control painting methods such as
DrawButton()
, DrawCheckBox()
, DrawComboButton()
, and DrawRadioButton()
in System.Windows.Forms.ControlPaint
. However, this approach has its limitations, it would be difficult to paint other complex controls, and because the painted controls are not real Windows controls, you can not edit their properties at runtime.
- Nashcontrol's C# Rect Tracker. This approach partially solves the above mentioned two problems by implementing the selection rectangle tracker (
RectTracker
) as a UserControl
. By doing so, the RectTraker
can be brought to the front as a UserControl
and be painted by the OS "above" other controls that has a lower Z-Order. But the effect is not satisfactory, sometimes moving/resizing a control does not work as you expect.
- SharpDevelop. It is an open source IDE for the .NET platform. As I said above, in the .NET platform, the functionality to select/move/resize a control is already wrapped into the .NET framework; all you have to do is to implement the interfaces and the base services such as
IDesignerHost
, IContainer
, IToolboxService
, IUIService
, and ISelectionService
. But this approach doesn't show you what is under the hood since the framework does the tough part for you.
The answer is, draws the selection rectangle on a transparent overlay on top of the container form. When you explore a form in the Forms Designer of Visual Studio with Spy++, you'll find there is a transparent control called "SelectionUIOverlay
" and it is just above the container form that is being edited.
What we need now is a RectTracker
to track the selected objects, and a truly transparent SelectionUIOverlay
on which to draw the selection rectangle.
The CRectTracker
class of MFC has existed for a long time, it was always used as a tracker for rectangle objects. You can find its definition in "AFXEXT.h", and its source code in "...\atlmfc\src\mfc\trckrect.cpp".
Since the .NET framework doesn't wrap all the Windows APIs, to port the class to C#, you still need to call some API functions through System.Runtime.InteropServices
. However, you can draw a rubber band (a focus rectangle that tracks with the mouse pointer while you hold down the left mouse button) and a dragged rectangle by using ControlPaint.DrawReversibleLine
or ControlPaint.DrawReversibleFrame
, this can avoid calling the GDI+ APIs.
private void DrawDragRect(Graphics gs,Rectangle rect,Rectangle rectLast)
{
ControlPaint.DrawReversibleFrame(rectLast,Color.White,FrameStyle.Dashed);
ControlPaint.DrawReversibleFrame(rect,Color.White,FrameStyle.Dashed);
}
I've also derived a FormRectTracker
from the C# RectTracker
to select and resize the container form. It simply overrides the HitTesthanles()
method of RectTracker
to prevent the container form from being moved or resized by dragging on its top/left boundaries.
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;
}
During my porting of CRectTracker
from MFC to C#, I found fanjunxing's implementation here on the internet. Since both implementations are all statement to statement translations from the same MFC class to C#, most parts of the two implementations are very similar. The difference lies in how to draw a rubber band and reconstruct a Rectangle
object when you resize the selection rectangle.
Because the .NET framework doesn't support transparent controls directly, you may think that we can use a transparent sub-form as a transparent control by setting the TransparencyKey
to the value of the its BackColor
. Unfortunately, this approach doesn't work. If the parent form of a sub-form is not transparent, setting the sub-form's TransparencyKey
to its BackColor
won't make the sub-form transparent.
To implement a transparent control, you need to:
- Add the transparent style to the control window:
protected override CreateParams CreateParams
{
get
{
CreateParams cp=base.CreateParams;
cp.ExStyle|=0x00000020;
return cp;
}
}
- Override the
OnPaintBackground
event. This is necessary to prevent the background from being painted.
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
- Write a message filter that implements the
IMessageFilter
interface. This is the most tricky part of a truly transparent control. If you want to draw something on the transparent control, and at the meantime move/resize the controls beneath it, you can write a message filter to prevent the controls from refreshing, and then expand the transparent control's Invalidate()
function to draw your own item and the other controls beneath it.
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)
{
frmMain.Refresh();
return true;
}
return false;
}
}
public class SelectionUIOverlay : System.Windows.Forms.Control
{
...
private void InvalidateEx()
{
Invalidate();
if(Parent==null)
return;
Rectangle rc=new Rectangle(this.Location,this.Size);
Parent.Invalidate(rc,true);
...
}
}
With C#, you can create an object with the "new
" operator, or you can create it through reflection. I'm wondering if the .NET CLR just uses the same mechanism to treat these two approaches. I noticed that only when you run the application from the debugger, there seems to be a minor performance issue with reflection. That's why I add a switch
statement and some "new
" operators in the demo code. But if you run the application directly, there seems to be no performance issues with reflection at all, no matter it's a debug or a release version.
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;
}
Since the .NET framework already has a PropertyGrid
control to do runtime property edits, it's fairly easy to edit a control's properties after you create it. All you have to do is set the PropertyGrid
's SelectedObject
property to the selected control.
The transparent SelectionUIOverlay
is put on top of the container form, so it can naturally prevent the container form and the controls in the container from receiving mouse click and keyboard events. Another thing you need to do is to pass the Graphics
of the transparent control to the RectTracker
when invalidated, and the RectTracker
will then draw the selection rectangle with it.
I've posted another article to present a tentative solution, which you can find here. I tried to copy & paste a Windows Forms control through serializing its properties. However, this approach has its limitations, and needs extra handling of the TreeView
control and the ListView
control.
The demo application uses luo wei fen's excellent docking library, WinFormsUI, and Aju.George's ToolBox class. I made some minor modifications with the ToolBox
class to adapt it to the WinFormsUI
framework.
I decided to write this article not because I wanted to give some classes as replacements for the corresponding functionalities wrapped by the .NET framework. My intention is to illustrate some basic principles on how to write a Forms editor and give a concrete implementation of it. I think the implementation of the C# RectTracker
class and the transparent control can also be applied to other circumstances, such as shape edit and picture/diagram edit.