Click here to Skip to main content
16,019,983 members
Articles / Programming Languages / C#
Article

Crafting a C# Forms Editor from scratch

Rate me:
Please Sign up or sign in to vote.
4.95/5 (150 votes)
1 Mar 2006CPOL8 min read 540.6K   17.9K   563   96
Crafting a C# Forms Editor by implementing a RectTracker and a transparent control.

Sample Image - SharpFormEditorDemo.jpg

Contents

Introduction

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.

Approaches to doing Form edit

There are two major tasks that needs to be performed when you edit a control on a form:

  1. Prevent the controls in the container form from accepting the system events, such as mouse click and key press.
  2. 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:

  1. 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.
  2. 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.
  3. 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.

How does Visual Studio do it?

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.

ExploreWithSpy++

Implementing your own RectTracker and SelectionUIOverlay

What we need now is a RectTracker to track the selected objects, and a truly transparent SelectionUIOverlay on which to draw the selection rectangle.

C# RectTracker

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.

C#
//
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.

C#
//
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.

Transparent SelectionUIOverlay

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:

  1. Add the transparent style to the control window:
    C#
    //
    protected override CreateParams CreateParams 
    { 
           get 
           { 
                  CreateParams cp=base.CreateParams; 
                  cp.ExStyle|=0x00000020; //WS_EX_TRANSPARENT 
                  return cp; 
    
           } 
    }
  2. Override the OnPaintBackground event. This is necessary to prevent the background from being painted.
    C#
    protected override void OnPaintBackground(PaintEventArgs pevent) 
    { 
           //do nothing 
    }
  3. 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.
    C#
    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
           }
    }

Add a control at runtime

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.

C#
//
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;

}

Edit a control at runtime

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.

Copy & Paste a control

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.

About the IDE

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.

Final words

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.

History

  • 0.9 demo - Mar-01-2006.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
China China
liuxiaoweide # gmail.com

Comments and Discussions

 
GeneralRe: Forms Designer Pin
ShawnTassie10-Mar-06 2:13
ShawnTassie10-Mar-06 2:13 
GeneralRe: Forms Designer Pin
lxwde10-Mar-06 4:27
lxwde10-Mar-06 4:27 
GeneralRe: Forms Designer Pin
ShawnTassie10-Mar-06 5:48
ShawnTassie10-Mar-06 5:48 
GeneralF I V E Pin
Marco [Stinger]7-Mar-06 2:11
Marco [Stinger]7-Mar-06 2:11 
GeneralNet 2.0 DesignSurface Pin
Santiago Corredoira L.6-Mar-06 21:22
Santiago Corredoira L.6-Mar-06 21:22 
GeneralGreat Work! Pin
Rainer Schuster6-Mar-06 3:52
Rainer Schuster6-Mar-06 3:52 
Generalgood work Pin
AnasHashki5-Mar-06 3:37
AnasHashki5-Mar-06 3:37 
GeneralRe: good work Pin
lxwde5-Mar-06 19:34
lxwde5-Mar-06 19:34 
Thanks.
为人民服务。
Smile | :) I like chairman mao's slogan.
GeneralExcellent Pin
Craig Lemon2-Mar-06 16:25
Craig Lemon2-Mar-06 16:25 
QuestionLicensing? Pin
brentbeardsley2-Mar-06 7:51
brentbeardsley2-Mar-06 7:51 
GeneralBrilliant work! Pin
Nish Nishant2-Mar-06 7:43
sitebuilderNish Nishant2-Mar-06 7:43 
GeneralRe: Brilliant work! Pin
lxwde2-Mar-06 16:16
lxwde2-Mar-06 16:16 
GeneralVery nice Pin
Dan Letecky2-Mar-06 6:48
Dan Letecky2-Mar-06 6:48 
GeneralExcellent Pin
Marc Clifton2-Mar-06 2:14
mvaMarc Clifton2-Mar-06 2:14 
GeneralRe: Excellent Pin
lxwde2-Mar-06 15:02
lxwde2-Mar-06 15:02 
GeneralGreat job Pin
Kok Hui1-Mar-06 22:08
Kok Hui1-Mar-06 22:08 
GeneralRe: Great job Pin
lxwde2-Mar-06 14:43
lxwde2-Mar-06 14:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.