Features
Microsoft .NET Feature Story - Powerful Forms Interaction
Receiving obscure Windows messages
Nov. 17, 2006 12:15 PM
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:
- Appearance (A): Windows will move the form with your mouse.
- Performance (B): Windows will give you an outlined preview of where the form will be placed and will move the form only when the mouse is released.
You should test your code in both configurations to see how it works. In Windows XP, switching configurations is straightforward:
- Right-click on "My Computer" and select "Properties."
- Go to the "Advanced" tab.
- Under "Performance" click "Settings."
- Select "Adjust for best performance" or "Adjust for best appearance," depending on which you would prefer.
- 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:
- A: As you move the form outside the bounds of the screen, it will flicker back and forth between where Windows wants to put it, and where you want to put it.
- B: The preview outline will go outside the screen, and on mouse-up, the form will appear inside the screen.
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:
- It tells our form that it is going to be moved.
- It tells our form where it will be moved to.
- It gives our form the chance to tell Windows to move our form somewhere else.
About Richard ArthurRichard Arthur is currently an instructor of C# at Northface University (www.northface.edu) in Salt Lake City, Utah. He has gained extensive experience doing Automation of MS Office products through VBA and COM at previous jobs.
In his spare time he learns about the inner workings of many current and future .NET Technologies, including Windows Forms, ASP.NET, Generics, P/Invoke, and COM Interoperability.
If anyone has any questions regarding this article, please contact Richard at startether@startether.com.