Microsoft .NET Feature Story - Powerful Forms Interaction

Microsoft Windows provides great power in manipulating the forms in your application, however, the .NET Framework masks a lot of what can be done to your forms for consistency and ease of use.

You have probably seen applications that control their size and positions with greater fluidity than you can get with normal .NET Forms, such as maintaining an aspect ratio while resizing, or docking to the side of a screen. Thankfully there are ways of gaining access to the more powerful aspects of Windows, but they are a bit ugly. In this article, I want to help you write a subclass of Form, which has greater power over its position - namely, I would like to keep the form from ever leaving the screen, not even a part of the side. In the process, we will explore ways to find out about the messages that Windows sends to forms, and how to retrieve their extra data and process those messages in custom ways.

The code in this article was written using C# and Visual Studio 2005. This code can be converted to VB and created using Visual Studio 2002 and 2003.

Appearance While Forms Move
As you move a form around, Windows can do one of two things, depending on how it is configured:

You should test your code in both configurations to see how it works. In Windows XP, switching configurations is straightforward:
  1. Right-click on "My Computer" and select "Properties."
  2. Go to the "Advanced" tab.
  3. Under "Performance" click "Settings."
  4. Select "Adjust for best performance" or "Adjust for best appearance," depending on which you would prefer.
  5. Click "OK."
Windows will take a moment to switch between visual styles. For quicker switching, you may want to choose "Custom" and check and uncheck "Show window contents while dragging." This "windows contents" effect is the only important effect for positioning forms.

Pure .NET Approach
To keep a form on the screen using a typical approach in C#, we would create a form and in the OnMove method we would write the code in Listing 1.

Depending on whether you have Windows, move window contents with your mouse (A), or not, (B), and you will see two different effects:

Neither of these effects is very desirable. Although option B is a little better, you do not have control over the user's environment to force that effect. Unfortunately, the .NET Framework does not naturally provide a way for you to improve these effects. Windows (Win32) does provide ways for you to control these effects, though. But before we can delve too much into Win32 code (which is a little ugly and nasty compared to .NET code), we need to get a little history lesson.

A Message-Based OS
Win32 is a message-based operating system. The Move event we caught in Listing 1 was the result of a common message that Win32 generates for us. The process of how this event is normally handled by programmers is illustrated in Figure 1. Windows first generates the "move" message, which is defined by a constant called WM_MOVE, and places that message into the "message queue" of your application (step 1). At the root of all Windows applications is a special piece of code called the "message pump" (see figure 1) which continually grabs and processes messages from the queue. In this case, it takes the WM_MOVE message (step 2) and hands it off to the WndProc method of the form that needs to process it (step 3). We will further explore the WndProc method later. The .NET Framework then processes the message and ultimately calls the OnMove method. This method raises the Move event, which calls all registered Move event handlers (step 4). In one of these handlers, programmers typically perform custom processing relevant to that event. To save some time, we subclassed Form directly, then performed an override of the OnMove method, which is where we put our custom code - the style of programming to use when catching events on the object that raises them. You should always call the base implementation of all OnWhatever methods you override so the base class can raise the associated event for any listeners. Catching events through event handlers is usually best for external classes being notified when things happen. For example, a form will register for a Button Click event because the Form class is external to the Button class.

Every time you press a button on your keyboard, Win32 typically generates three messages that get processed by your application: a WM_KEYDOWN, WM_KEYUP, and WM_CHAR. Pressing mouse buttons typically generates two other messages (e.g., WM_LBUTTONDOWN and WM_LBUTTONUP). As an application is moved on the screen, properties are changed, or painting needs to occur, dozens to hundreds of messages are generated. Across the life of an application, thousands of messages are generated and passed to your application, and sometimes from your application back to Windows.

Your application even generates messages and sends them to itself. All these messages must be processed by the message pump, and can only be generated by the thread the message pump is on; otherwise Win32 cannot keep messages and their data synchronized. As a result, alternate threads that try to alter properties on a Form must do so by using the Control.Invoke method. I bring this up to warn and inform you about the legacy of Win32 code: you will have to read up on the details of that method on your own.

Microsoft has thousands of different messages defined for different events that occur in Win32. These messages are the common ones, such as painting a form, typing characters, and mouse movements. There are also exotic messages such as tree-view node expansion, click or collapse, and logon/logoff events. If you want a look at some of these messages, search for and open the "WinUser.h" file on your computer: it will be deep inside the "Program Files" directory, wherever you have installed the Visual C++ (VC7 or VC8), if you have it installed. This file contains many of the messages that Win32 will process. Here is a taste of the contents of this file:

#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_CHAR 0x0102

There are a lot of #define statements in this file; some of them are messages, and others are constants related to these messages. Thankfully, Microsoft is a very rigid company when it comes to their coding standards. Once you know some key things about reading their source code, you can read just about anything written there. For example, many of the messages sent by Win32 start with WM_, which is short for "Windows Message." Constants that are related all start with the same characters, followed by an underscore. If you have your MSDN documents installed and unfiltered, you can type in one of these message names into the index and read all about that message. For example, type in WM_SIZE, and you can find out about the Size event, and the different constants that can be passed in with it (each constant starts with SIZE_).

The WndProc method
The message pump is the core piece of code that grabs messages from the OS and dispatches them to your application, but the WndProc method is where all the magic happens. This method is one giant switch statement that takes in the message and then runs the appropriate code. With each message, there are usually pieces of information passed along that give more details, like which key was pressed, the device context (Graphics object) to paint your form in, or the bounds of where your form is supposed to be placed.

The WndProc method is where we will concentrate our efforts because there are messages it receives, but that the .NET Framework does not pass to us. One of these is a message called WM_MOVING (which is different from WM_MOVE, which we handled earlier). This message does three things for us:

  1. It tells our form that it is going to be moved.
  2. It tells our form where it will be moved to.
  3. It gives our form the chance to tell Windows to move our form somewhere else.

You might be asking "Where is all this information?" That is the nasty part of this problem. Because Windows was written before the CLR and its nice objects, this code will be a little ugly; but because Windows was written in a very object-oriented way, the code is still well structured and straightforward. First, create a new C# Windows application and open the source code for Form1. Then override the WndProc method with the code in Listing 2.

Notice that this code references an object of type RECT. This type is the Win32 Rectangle, and is used in all places where 4 pixel-based integers are used. Deep in the .NET Framework, there is an implementation of this type to help .NET convert from Win32 (unmanaged) to .NET (managed) code; however, we cannot reach that type of declaration, so we have to implement our own. Because working with the Rectangle structure is much easier, I also provide some explicit casts to and from Rectangle so that it can be used more intuitively. The code for the RECT type is provided in Listing 3.

The code provided in Listings 2 and 3 perform a few key functions. First, it identifies when the WM_MOVING message is passed in. Second, it gets where Win32 proposes to place your form. Third, it changes those bounds to a specific location in OnFormMoving. Finally it gives Win32 the bounds of its desired destination. We will explore these steps in the next couple paragraphs.

The WndProc method gets passed a Message structure which has some important properties:

These properties directly correlate to information Win32 passes for all messages. In this case, if the Msg property has the constant value WM_MOVING, we know the message is important for our code. Notice that the value of WM_MOVING is taken straight from the "WinUser.h" file mentioned earlier.

The Handle property can be ignored: it is used by Win32 to identify where to send the message. However, the LParam and WParam properties often have information related to message. These two properties are integers, so they can hold each 4 bytes of information. But notice that the RECT structure holds four integers, or 16 bytes of information. In this instance, LParam is actually holding an unmanaged pointer to another place in memory which has the whole RECT structure. To get the value out of the unmanaged world into the managed world we use the Marshal object to copy that memory back and forth. PtrToStructure copies the unmanaged values into our managed structure, whereas the StructureToPtr method copies our managed structure back to where the unmanaged pointer is pointing.

Now, let's run our code and see what happens. In this case, when you try and move the form, it's forced to go to one place on the screen and does not move from that spot. If you are in A mode, you could also set the size of the form in the destination location, but if you're in B mode, the form will only move to the desired location and will not take on the size you specify.

Improving Form Positioning
We want this to work under all Windows configurations. The form will show up at the location the user places it, but it will not be sized; therefore, once it's placed, we need to update its size. Thankfully, when the form is finally moved, Win32 sends the WM_EXITSIZEMOVE message, which we can catch and use to set the form's size. Add the following class-level variables:

private const int WM_EXITSIZEMOVE = 0x0232;
private Rectangle lastBounds;

Then add these lines to the beginning of the WndProc method, before the call to the base implementation of WndProc:

if (m.Msg == WM_EXITSIZEMOVE)
this.Size = lastBounds.Size;

Replace the contents of the OnFormMoving method with this code:

lastBounds = new Rectangle(100, 200, 500, 100);
destination = lastBounds;
return true;

Whether you run your application in A or B mode, it will have the same output when the user moves the form and releases the mouse. In the .NET Framework 2.0, they have added the ResizeBegin and ResizeEnd events that correspond to the WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE messages. In this case, you may use the ResizeEnd events instead of catching WM_EXITSIZEMOVE.

Keeping the Form on the Screen
It is time to smoothly keep the form inside the screen. Replace the contents of the OnFormMoving method with the code in Listing 4.

Since we are not trying to change the size of the form, you should remove the code related to the WM_EXITSIZEMOVE message. When you run the application, the form will stay on the screen the mouse is on. If you do not want the form to cover any application bars you have docked (like the task bar), find the first line of the OnFormMoving method and change Bounds to WorkingArea. As you attempt to position the form outside of the bounds of the screen, you will notice that the bounds code keeps the form inside the screen, but the mouse no longer stays relative to the original click-point on the title bar as we move away from the borders. This is not very user-friendly. It would be much better if the form were a little more "springy" (i.e., it got pushed around near the borders, but snapped back when the mouse moves far enough back from the borders). A good way of doing this is to:

  1. Track where the mouse went down on the border of the Form.
  2. Try and position the bounds relative to that mouse position.
  3. If near a border, shift the bounds accordingly.
Unfortunately, there is no event provided by .NET that tells us when the user clicks on the title bar. It only tells us when the user clicks in the client area of the form. However, there is a Win32 message that does tell us when the user clicks outside the client area of a form: WM_NCLBUTTONDOWN (Non-Client Left Button Down). Add these class-level declarations:

private const int WM_NCLBUTTONDOWN = 0x00A1;
private int deltaX;
private int deltaY;

Then add the following code to the top of the WndProc method:

if (m.Msg == WM_NCLBUTTONDOWN)
{
    deltaX = MousePosition.X - this.Left;
    deltaY = MousePosition.Y - this.Top;
}

Finally, add the following code just after the first two lines of code in the OnFormMoving method:

if ((MousePosition.X - manipulate.Left) != deltaX)
    manipulate.X = MousePosition.X - deltaX;
if ((MousePosition.Y - manipulate.Top) != deltaY)
    manipulate.Y = MousePosition.Y - deltaY;

When you run this code, you will finally have a form that can be positioned by the user, and it will stay within the bounds of the screen. This form will also reflect its future position correctly when Windows is in performance mode (B).

Wrap Up
Windows has an incredible number of messages that it sends to forms during their lives. Although .NET forms do not directly expose these messages to us, .NET developers can catch them and respond in useful ways. With these kinds of messages, you could build applications that do any of the following things, and more:

When forms "dock" to the edge of the screen, they just fill up screen space, but they do not affect the working area of your desktop. To actually dock an application to the side of your screen in a similar way to the Windows Task Bar, you need to read up on the AppBar notifications that Windows provides. (These constants all start with ABM_.) When you do this, however, you may want to allow resizing of the form only on the side next to the desktop, without having the mouse feedback about the other sides of the form. If you read up on the other WM_NC messages, you can do this as well. In fact, if you are doing custom border drawing, you can help Windows know where your custom, minimize, maximize, and close buttons are by using the WM_NC messages.

Microsoft Windows is an incredibly powerful operating system, and even in the .NET world, we can take advantage of the messages provided by Windows. These messages allow us to create applications that are smoother and slicker, as compared with normal .NET development, with regards to how they allow user interaction. Knowing how Windows is built allows us to write applications more intelligently (like using Control.Invoke in multi-threaded apps), and deepens our respect for the vast code base Microsoft has provided to make our lives richer.

© 2008 SYS-CON Media