Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WinForm Extended

0.00/5 (No votes)
21 Jul 2010 1  
A WinForm that extend the standard features provided by Microsoft
Screen_Shot.jpg

Introduction

In this article, I present you with an extended version Windows Form. It has a few more features than regular Forms provided by the .NET Framework.

Originally, I implemented these features separately to help MSDN users on their requests. So I've decided to put it all together and build a Form to present in this article.

The additional features of the FormEx are:

  • Paint on the title bar and form Frame
  • Attach a Form to the desktop, like Windows Vista and 7 sidebar
  • Set the Form in Full Screen mode (covers everything including task bar)
  • Make the Form Unmovable
  • Make the Form Unsizable (Even when FormBorderStyle is not Fixed)
  • Get the KeyState (Up and Down / Toggled and Untoggled) at any moment
  • Disable the Close Button of the Window's Title Bar.

All of these features, except getting the key state, are available through the designer, so it's very easy to use as I'll explain below.

Using the Code

As I mentioned, it's very easy to use the features. In the sample project provided, you can see everyone of them just as the screenshot. We will go through them one by one, but first, let's see how we use the form.

You have two ways to add the new Form to your solution:

  • Add the binary provided on the top of the article to your project. You do this by going to Solution Explorer -> Your Project -> Right-Click References... -> Add Reference... -> Browse -> Locate and Select the FormEx.dll
  • Alternatively, you can include the project with source code (also provided) and Add the Reference the same way as above, but select Projects instead of Browse.

When you have done that, you're ready to start coding. First select a form from your project and edit its source code. You'll have to modify it to inherit from FormEx instead of Form:

using FormExNS;

namespace TestProject
{
    public partial class TestForm : FormEx
    { 

Now, to the features:

1 - Paint on the title bar and form Frame:

As you can see in the screenshot, I drew over the title bar. To do that, I implemented a new event: PaintFrameArea. All you have to do is go to the Events in the designer and create an event handler just the same way you would do with the Paint method. It works exactly the same and the Graphics object is set to cover the whole window minus the client area.

The drawing of this is done after Windows draws the window in its themed style, so you might experience some flicker while resizing. You should also not draw over the ControlBox because it will cover the minimize, maximize and close buttons (but not forever, if you mouse over them, they will reappear).

2 - Attach the Form to the Desktop:

I added a new property called DesktopAttached. By setting this property to true, the window will get "glued" to the desktop and will not show on the task bar. It will be below all regular windows, much like windows side bar.

3 - Full Screen Mode:

I added a new property called FullScreen. By setting this property to true, the window will take all the area of the current monitor and will be always over the taskbar and any other non TopMost windows. Note that if you use this in combination with DesktopAttached, the Window will take the whole screen, but will remain under every window and the taskbar. Also note that the window will remember its previous state automatically when you set FullScreen to false again.

4 - Movable:

I added a new property called Movable. By setting this property to false, the window will no longer be movable. The user won't be able to move the window by dragging it through the title bar. When the form is unmovable, it also implicates that it's not Sizable either

5 - Sizable:

I added a new property called Sizable. By setting this property to false, the window will no longer be sizable, despite its FormBorderStyle being set to Sizable. This might be useful if you want the look of a sizable form, but want it fixed. This property has no interference with the Movable property.

6 - Get Key State:

I added two new methods for the Form that are related to the KeyState. They are:

  • KeyState GetKeyState(Keys key) - The return type as an enum that has two possible values: 0 - KeyState.Up and 1 - KeyState.Down. The parameter is the standard Keys enum provided by Windows Forms
  • KeyValue GetKeyValue(Keys key) - The return type as an enum that has two possible values: 0 - KeyValue.Untoggled and 1 - KeyValue.Toggled. The parameter is the standard Keys enum provided by Windows Forms

7 - CloseButton:

I added a new property called CloseButton. By setting this property to false, the window close button will be grayed out and the user will no longer be able to close the form. The form is still closable if: The parent form gets closed, the task manager closes the application, windows shuts down or there is a call to Application.Exit()

How the Code Works

One of the things that often passes by unnoticed by Windows desktop developers is how the presentation layer works. Have you ever thought of how Windows treats resizing, clicking, moving and drawing the windows?

Windows works through a messaging system. Everytime a window needs redrawing, a WM_PAINT (client area) or WM_NCPAINT (Frame Area) message is sent to the form. When the mouse moves over a form, a form is resized or is moved, a message is also sent to the form (sometimes hundreds of messages per second). So pretty much everything that happens to the form (in the form of events in .NET) are through messages. All Controls and Forms in Windows Forms framework implement void WndProc(ref Message m) method. All messages are sent through this method, where they get processed. Features 1, 4 and 5 are directly implemented by overriding this method and treating the right messages. Feature 3, is partially dependant on overriding this method also.

In the Message structure, I use three properties:

  • Msg - This is the actual Window Message sent to the form
  • WParam - One of the parameters of the message (W means word, but it's just historical, it's actually a long)
  • LParam - Another of the parameters of the message (L correctly stands for Long)

The constants:

//Parameters to EnableMenuItem Win32 function
private const int SC_CLOSE = 0xF060; //The Close Box identifier
private const int MF_ENABLED = 0x0;  //Enabled Value
private const int MF_DISABLED = 0x2; //Disabled Value

//Windows Messages
private const int WM_NCPAINT = 0x85;//Paint non client area message
private const int WM_PAINT = 0xF;//Paint client area message
private const int WM_SIZE = 0x5;//Resize the form message
private const int WM_IME_NOTIFY = 0x282;//Notify IME Window message
private const int WM_SETFOCUS = 0x0007;//Form.Activate message
private const int WM_SYSCOMMAND = 0x112; //SysCommand message
private const int WM_SIZING = 0x214; //Resize Message
private const int WM_NCLBUTTONDOWN = 0xA1; //L Mouse Btn on Non-Client Area is Down
private const int WM_NCACTIVATE = 0x86; //Message sent to the window when it's
                                        //activated or deactivated
                                        
//WM_SIZING WParams that stands for Hit Tests in the direction the form is resizing
private const int HHT_ONHEADER = 0x0002; 
private const int HT_TOPLEFT = 0XD; 
private const int HT_TOP = 0XC; 
private const int HT_TOPRIGHT = 0XE;
private const int HT_RIGHT = 0XB;
private const int HT_BOTTOMRIGHT = 0X11;
private const int HT_BOTTOM = 0XF;
private const int HT_BOTTOMLEFT = 0X10;
private const int HT_LEFT = 0XA;

//WM_SYSCOMMAND WParams that stands for which operation is being done
private const int SC_DRAGMOVE = 0xF012; //SysCommand Dragmove parameter
private const int SC_MOVE = 0xF010; //SysCommand Move with keyboard command

If you look at the overridden WndProc method of the FormEx class, you'll notice that I intercept some of these messages:

// Prevents moving or resizing through the task bar
if ((m.Msg == WM_SYSCOMMAND && (m.WParam == new IntPtr(SC_DRAGMOVE) 
    || m.WParam == new IntPtr(SC_MOVE))))
{
    if (m_FullScreen || !m_Movable)
        return;
}

// Prevents Resizing from dragging the borders
if (m.Msg == WM_SIZING || (m.Msg == WM_NCLBUTTONDOWN && 

    (m.WParam == new IntPtr(HT_TOPLEFT) || m.WParam == new IntPtr(HT_TOP)
    || m.WParam == new IntPtr(HT_TOPRIGHT) || m.WParam == new IntPtr(HT_RIGHT) 
    
    || m.WParam == new IntPtr(HT_BOTTOMRIGHT)
    || m.WParam == new IntPtr(HT_BOTTOM) 
    
    || m.WParam == new IntPtr(HT_BOTTOMLEFT) 
    || m.WParam == new IntPtr(HT_LEFT))))
{
    if (m_FullScreen || !m_Sizable || !m_Movable)
        return;
}

As you can see above, I intercept WM_SYSCOMMAND message to prevent the window from moving. I can't simply intercept this message only as it's used for several other functions on a window, I also check the WParam parameter, to verify if the WM_SYSCOMMAND message is of the type that is trying to move the window. If it is, I return and the message is discarded, so the Window won't be moved.

You might wonder why I simply won't save the location of the window and set it back everytime the user tries to move the form. This is not a good solution as the message to move will be sent and despite the message to move back happens really quick, you can see the Form moving and it does not look good, the form will keep following the cursor and will flicker enormously while you hold the mouse button down.

On the other block, I intercept both WM_SIZING and WM_NCLBUTTONDOWN to prevent the form from resizing. WM_SIZING is sent when the user tries to resize the window from the dropdown menu when you click the Form's icon, and WM_NCLBUTTONDOWN is sent whenever the user left-clicks the non-client area of the form. This message in conjunction with its hit-test parameters (WParam) lets the application determine wether the user is clicking on the edges of the form, where it's resizable. If the message falls in this condition, I return and the message is discarded, preventing the form from sizing.

If none of the conditions above are met, I forward the message to the base Form (base.WndProc(ref m);) and it's processed normally. This ensures that the remaining behaviour of a window is unchanged.

Last, but not least, there is the handling of the painting of the non-client area. It's done after the call to base.WndProc(ref m), so the window has the chance to draw its own borders on its own themed style. Afterwards, I also intercept the messages so I can let the user to do its custom drawing over the original drawing. The messages I intercept are WM_NCPAINT, WM_IME_NOTIFY, WM_SIZE and WM_NCACTIVATE. All of them causes the Non-Client area to be redrawn. The WM_NCACTIVATE message is sent when the form looses focus and changes its active state:

base.WndProc(ref m);

// Handles painting of the Non Client Area
if (m.Msg == WM_NCPAINT || m.Msg == WM_IME_NOTIFY || m.Msg == WM_SIZE 
    || m.Msg == 0x86)
{
    // To avoid unnecessary graphics recreation and thus improving performance
    if (m_GraphicsFrameArea == null || m.Msg == WM_SIZE)
    {
        ReleaseDC(this.Handle, m_WndHdc); //Release old handle
        m_WndHdc = GetWindowDC(this.Handle); //Get Graphics of full window area
        m_GraphicsFrameArea = Graphics.FromHdc(m_WndHdc);
        
        Rectangle clientRecToScreen = new Rectangle(
          this.PointToScreen(new Point(this.ClientRectangle.X, 
          this.ClientRectangle.Y)), new System.Drawing.Size(
          this.ClientRectangle.Width, this.ClientRectangle.Height));
          
        Rectangle clientRectangle = new Rectangle(clientRecToScreen.X - 
        this.Location.X, clientRecToScreen.Y - this.Location.Y, 
        clientRecToScreen.Width, clientRecToScreen.Height);
        
        m_GraphicsFrameArea.ExcludeClip(clientRectangle); //Remove client area
    }
    
    RectangleF recF = m_GraphicsFrameArea.VisibleClipBounds;
    
    PaintEventArgs pea = new PaintEventArgs(m_GraphicsFrameArea, new 
      Rectangle((int)recF.X, (int)recF.Y, (int)recF.Width, (int)recF.Height));
      
    OnPaintFrameArea(pea);
    CloseBoxEnable(m_EnableCloseButton);
    this.Refresh(); //Forces repainting of the client area to remove shadows
} 

Basically, what I do here after realizing the window needs repainting is:

  1. Get the DeviceContext of the Window through GetWindowDC Win32 API call. From the device context, I create the Graphics object that will contain the whole window, not only the client area as of regular Paint event.
  2. Exclude the region of the client area (ExcludeClip), we want to constrain the painting to there only.
  3. Create the PaintEventArgs of our region of the window and Graphics object so we can pass it to the new event handler.
  4. Make the call to OnPaintFrameArea that handles our new PaintFrameArea event, passing the newly created PaintEventArgs variable.
  5. Refresh the client area, because when the user resizes the form, the drawing remains in the position and will get that "shadow effect" when drawing on the sides of the form.

Aero Glass

As of current version of the article, painting over Aero Glass of Windows 7 / Vista is not supported. Unlike regular windows, Aero is not painted by the form, it uses DWM[^] to do the painting. For this reason, painting intercepting WM_NCPAINT will not work without disabling Aero. I plan to extend this article soon to also cover DWM.

What About the Other Features?

So how about features 2, 3, 6, 7? Well, they have everything to do with Windows messaging system too, but I didn't directly change the behaviour by treating the messages, I made OS API calls.

Something very useful that most developers I see in everyday life miss, is the ability to interoperate directly to the OS. Most of Windows Forms framework is nothing more than a wrapper to native OS resources. What I did here was pretty much the same thing. I built a wrapper to API calls that was not implemented in the vanilla Form.

Below are all Win32 API calls that made features 2, 3, 6 and 7 possible. Their comments on code are self explanatory. To use it, you need to make a reference to System.Runtime.InteropServices namespace. Just put it as a using statement on the header of the CS file:

//GetSystemMenu Win32 API Declaration (The Window Title bar is SystemMenu)
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

//EnableMenuItem Win32 API Declaration (Set the enabled values of the 
//title bar items)
[DllImport("user32.dll")]
private static extern int EnableMenuItem(IntPtr hMenu, int wIDEnable, int wValue);

//Get Desktop Window Handle
[DllImport("user32.dll")]
private static extern IntPtr GetDesktopWindow();

//Set Parent Window, used to set the desktop's parent as the window parent
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

//Find any Window in the OS. We will look for the parent of where the desktop is
[DllImport("User32.dll")]
public static extern IntPtr FindWindow(String lpClassName, String lpWindowName);

//Get the device component of the window to allow drawing on the title bar and 
//frame
[DllImport("User32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);

//Releases the Device Component after it's been used
[DllImport("User32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

As you can see, all of the APIs can turn possible a lot of things we might think are impossible. And really, there is a lot of stuff you can do through these calls. If you look in the API Reference linked below, you'll be able to find every one of the methods used in this article. For example, as in the reference, there is the ReleaseDC method:

int ReleaseDC(
  __in  HWND hWnd,
  __in  HDC hDC
);

This won't work in C#, as it does not have HWND or HDC types. the __in tells us that the parameter will be read by the method and will not be outputted. The types are pointers to handles, so we can simply replace them by the IntPtr .NET Framework provides. And the return type is pretty obvious, an integer.

Points of Interest

To implement all of these features, I had to do several calls to Win32 APIs of user32.dll. The most annoying part was to grab all the correct window messages sent to the form and the constant values as the .NET Framework does not have any mapping of these messages.

As a resource, I used MSDN's Windows API Reference, Visual C++ winuser.h header file and the Output window of Visual Studio to watch incoming messages (System.Diagnostics.Debug.WriteLine) as I messed with the form to make it repaint. Which is the case of message 0x86(WM_NCACTIVATE), that was unlabeled on the previous version of the article because I didn't find proper documentation (thanks Spectre2x).

I hope you enjoy the code. Please, feel free to leave your feedback.

History

  • July 15th 2010 - Article published
  • July 20th 2010 - Updated to point out 0x86 Message label. Full update on the code and article pending
  • July 21st 2010 - Updated to label 0x86 Message on the project and article. Also added a note to Aero Support

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here