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:
private const int SC_CLOSE = 0xF060; private const int MF_ENABLED = 0x0; private const int MF_DISABLED = 0x2;
private const int WM_NCPAINT = 0x85;private const int WM_PAINT = 0xF;private const int WM_SIZE = 0x5;private const int WM_IME_NOTIFY = 0x282;private const int WM_SETFOCUS = 0x0007;private const int WM_SYSCOMMAND = 0x112; private const int WM_SIZING = 0x214; private const int WM_NCLBUTTONDOWN = 0xA1; private const int WM_NCACTIVATE = 0x86;
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;
private const int SC_DRAGMOVE = 0xF012; private const int SC_MOVE = 0xF010;
If you look at the overridden WndProc
method of the FormEx
class, you'll notice that I intercept some of these messages:
if ((m.Msg == WM_SYSCOMMAND && (m.WParam == new IntPtr(SC_DRAGMOVE)
|| m.WParam == new IntPtr(SC_MOVE))))
{
if (m_FullScreen || !m_Movable)
return;
}
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);
if (m.Msg == WM_NCPAINT || m.Msg == WM_IME_NOTIFY || m.Msg == WM_SIZE
|| m.Msg == 0x86)
{
if (m_GraphicsFrameArea == null || m.Msg == WM_SIZE)
{
ReleaseDC(this.Handle, m_WndHdc); m_WndHdc = GetWindowDC(this.Handle); 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); }
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(); }
Basically, what I do here after realizing the window needs repainting is:
- 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.
- 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 and Graphics
object so we can pass it to the new event handler.
- Make the call to
OnPaintFrameArea
that handles our new PaintFrameArea
event, passing the newly created PaintEventArgs
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:
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern int EnableMenuItem(IntPtr hMenu, int wIDEnable, int wValue);
[DllImport("user32.dll")]
private static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("User32.dll")]
public static extern IntPtr FindWindow(String lpClassName, String lpWindowName);
[DllImport("User32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);
[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