WinForm Extended






4.94/5 (44 votes)
A WinForm that extend the standard features provided by Microsoft

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 thePaint
method. It works exactly the same and theGraphics
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 calledDesktopAttached
. By setting this property totrue
, 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 calledFullScreen
. By setting this property totrue
, the window will take all the area of the current monitor and will be always over the taskbar and any other nonTopMost
windows. Note that if you use this in combination withDesktopAttached
, 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 setFullScreen
tofalse
again.
4 - Movable:
I added a new property calledMovable
. By setting this property tofalse
, 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 notSizable
either
5 - Sizable:
I added a new property calledSizable
. By setting this property tofalse
, the window will no longer be sizable, despite itsFormBorderStyle
being set toSizable
. This might be useful if you want the look of a sizable form, but want it fixed. This property has no interference with theMovable
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 anenum
that has two possible values: 0 -KeyState.Up
and 1 -KeyState.Down
. The parameter is the standardKeys enum
provided by Windows FormsKeyValue GetKeyValue(Keys key)
- The return type as anenum
that has two possible values: 0 -KeyValue.Untoggled
and 1 -KeyValue.Toggled
. The parameter is the standardKeys enum
provided by Windows Forms
7 - CloseButton:
I added a new property calledCloseButton
. By setting this property tofalse
, 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 toApplication.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 formWParam
- 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:
- Get the
DeviceContext
of the Window throughGetWindowDC
Win32 API call. From the device context, I create theGraphics
object that will contain the whole window, not only the client area as of regularPaint
event. - Exclude the region of the client area (
ExcludeClip
), we want to constrain the painting to there only. - Create the
PaintEventArgs
of our region of the window andGraphics
object so we can pass it to the new event handler. - Make the call to
OnPaintFrameArea
that handles our newPaintFrameArea
event, passing the newly createdPaintEventArgs
variable. - 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