2PANE Illuminates Windows

Top-level windows and the message loop

Dick Wilmot

Dick is a longtime Windows programmer and editor of The Independent RAID Report. He can be contacted at dwilmot@crl.com.


How many top-level windows does a windows program have? As many as it wants. This isn't a trick question. An initial study of Windows programming might lead you to believe that each instance of a Windows application has exactly one top-level window, but that is not at all true. I even wrote one useful Windows application program that had no windows.

One version of that little program didn't even have a message loop or a window procedure; its only job was to minimize the active window and terminate. Having no message loop is unusual, since Windows programs are message driven. We all know that at the heart of nearly every Windows application is a message loop, but we will see from the instrumentation in 2PANE, the program presented with this article, that a huge number of messages bypass a program's message loop.

2PANE's instrumentation lets you probe how Windows messaging and window procedures work. Listing One is the include file for the Windows 3 implementation of 2PANE, and Listing Two , the C source file. Other necessary files (RES, OBJ, RC, MAK, and so on) along with an NT version are available electronically; see "Availability," page 3.

Creating Two Windows

Creating two or more windows--like the 2PANE-generated ones in Figure 1--is straightforward, but managing them is easier if they have separate captions. This permits the program to later know for which window each message has been sent. 2PANE registers its window class in a completely normal fashion and creates all its windows with the same registered class name, but each window gets its own unique caption from a table of captions. Although the program creates only two windows, with minor changes it could be generalized to create as many as our desktop and other resources have room to handle.

Why create two windows? In this case, to illustrate that it can be done, but why might you do this in a real application? One reason is that it is extremely easy to do. If your application outgrows the typical main window and Windows dialogs don't allow enough independent viewing and interaction, then multiple main windows might be what you need. Separate document windows can be handled with the multiple-document interface (MDI) but MDI comes to feel terribly confining at times--restricted to the boundaries of that single client area. It is hard to get one window out of the way to see another, and yet, that is often just what the user needs to do. Multiple windows, as in 2PANE, can handle multiple interactive windows that the user is free to manipulate, move, and resize at will.

What does having multiple open windows cost you? Really nothing more than a bit of bookkeeping to track which window is being acted upon. This does not appear excessive and might be less so than using MDI. If you need multiple windows with very different types of interaction, such as spreadsheets and charts, then you might need to change the appearance of the menu bar(s) each time the focus shifts from one window type to the other. Using multiple independent windows allows each window to have its own menus tailored to its purposes.

2PANE creates all its windows as part of initialization, but in actual applications you aren't constrained to creating all windows up front--you can create them as interactions with the user and other program's progress. The programming might be easier if window creation is isolated into a single subroutine that can create windows and track their handles. This would make for easier uniformity of common elements and keep the minutiae of window creation away from the application-handling code.

Managing multiple messages from a single application instance is an alternative way to communicate large quantities of data between windows without using a DLL. For simple, multiwindow interaction to common data, a multiwindow application--a fleshed-out 2PANE--involves less programming work than implementing a separate DLL. Interwindow communication should also be as fast as for a DLL, since all variables are in the same application instance. There is a single heap and a single stack. I also found this form of program easier to debug than a program with a separate library. All the code and variables are readily accessible in a single debugging window.

Some simple programming at the end of 2PANE.C allows users to close windows at will; see Example 1(a). The application code stays alive if there are any windows still open. This is the same behavior as that of the usual DLL, which remains active until its client closes.

At the beginning of its window procedure, 2PANE.C converts the message handle (addressee: window to whom the message is addressed) into an integer window number; see Example 1(b). The if statement following the window-handle lookup handles the case where a message handle does not correspond to any of the windows 2PANE created. This indeed happens, so you need to guard against it and, in this case, just convert it to the first window. Conversion of message-window handles to integer-window numbers is exceedingly handy for instancing variables that pertain to different windows, such as user-selected options and "within-window" positioning information. Robust handling of out-of-range window handles was omitted here for brevity.

How Many Messages?

Like most Windows application programs, 2PANE has a message loop to retrieve messages from its message queue. The only difference here is that 2PANE's message loop has been instrumented with two counters; see Example 2. The msg_ct variable counts all messages retrieved by the GetMessage loop within the WinMain function. WM_PAINT messages are counted separately because they offer an easy probe into the messaging system. Another set of counters in the windows procedure, WinProc, tallies the numbers of messages and number of WM_PAINT messages that pass through that function.

As Figure 2 illustrates, the four message tallies are displayed in the four corners of each of 2PANE's initial windows. It is evident from the initial 2PANE window displays that a great many messages reach WinProc without having gone through WinMain. This actually makes perfect sense since 2PANE has called for the creation, showing, and updating of two windows before the code even reaches the GetMessage loop. Windows could just place the messages related to these construction activities in the application's message queue, but it doesn't. If it did try to just queue up these messages for later action, then it would overflow the default-message queue, which can only hold eight messages. Immediate execution for these messages also makes for easier error handling since deferred actions that went awry would not indicate which part of a program to notify. So the first window is created using 19 messages, none of which went through the GetMessage loop, because the program has yet to reach that part of its code.

After creating each window, the program performs a ShowWindow, followed by UpdateWindow. ShowWindow creates a WM_PAINT message, but WM_PAINT messages are low priority and can stay toward the bottom of the queue. If the update region for a window is not empty, then UpdateWindow forces the sending of a waiting WM_PAINT. The program works fine with UpdateWindow omitted, but painting might be unacceptably delayed in a high-traffic environment where the low-priority WM_PAINT messages could keep getting pushed aside.

The tally information shown in 2PANE's windows is from the time each window was painted, but this counts for the whole application--tallies are not separated by window, although they could be.

WM_PAINT messages are often sent directly to the window's window procedure, WinProc, bypassing the application message queue entirely. You can, by the way, give the window procedure any name you like. You just need to export this procedure so Windows can find it, and it must be named in registering the window class. Windows will then find and directly invoke the window procedure's name, as it does for the UpdateWindow call.

ShowWindow is not the only way that WM_PAINT messages originate. Anything that causes a window's content to be invalidated will prod Windows into generating a WM_PAINT message, which is really just an object-oriented command to "go paint yourself."

If you activate a window that was partly obscured or drag a window away from one that it was covering, Windows will treat the newly uncovered portions as invalid and will send WM_PAINT messages to the affected windows. Spy, the diagnostic utility that comes with the Microsoft Windows SDK, lets you see what messages are being sent to a window. Covering and then uncovering one of the 2PANE windows with Spy's window is a handy way to generate just the message sequence from an uncovering event while also spying on that message traffic.

As Figure 3 shows, Spy indicates that five messages were generated, one of which was a WM_PAINT. It would make sense for 2PANE to get a repaint (WM_PAINT) message when a part of its client area has been uncovered, and it receives a paint message passed by DispatchMessage to the WM_PAINT case in its window procedure. That code uses BeginPaint to obtain a device context for the screen and begin its painting work. However, before returning to 2PANE's WM_PAINT code, BeginPaint calls WinProc with a WM_ERASEBKGND message, as shown by the Spy trace. So a WM_ERASEBKGND message is issued and entirely processed (by the default window procedure, since 2PANE has no case for that message type) before 2PANE has finished with its WM_PAINT case. Recursive calls are issued to WinProc.

2PANE's WM_PAINT code invalidates the entire client area of its window with a call to InvalidateRect. If it didn't do that, then only the portion of the window that was newly uncovered would be painted, and you wouldn't get to see the updated message counts.

Someone Else's Paint Messages

You'll probably want to know why the count of total paint messages (in the upper-right corner) in Figure 3 has jumped from 0 to 2. One paint message revealed from uncovering Pane 2 came through the GetMessage loop and was duly counted, but another was there that didn't belong to 2PANE. It was for the desktop behind Spy, which had to be repainted after the Spy window was moved to cover Pane 2. Windows receives paint messages for the desktop. These messages are, however, intercepted by DispatchMessage, which does not send them to the application's window procedure. Spy does not show these messages. They can only be seen with a debugger, but they are not completely ghost messages: 2PANE's tallies confirm their passage through the message loop.

Character Messages

2PANE has also been instrumented to count the messages involved in keyboard input. The counting process is the same as that for messages retrieved from the application's queue and the number processed in WinProc. The counts are displayed in a somewhat-different fashion so as not to distort the counting. The WM_PAINT case uses the DrawText Windows API function, but DrawText will only draw its output where the screen is invalid. You could invalidate the screen, but this would force a repaint, and that would disturb the painting and general-message counts. So the code in the WM_CHAR case uses TextOut with some positioning of the current output point.

Character input from the keyboard does not cause repainting but it does engender quite a few messages (see Figure 4). The message count from the GetMessage loop includes a WM_KEYDOWN, a WM_CHAR, and a WM_KEYUP message for each character keystroke. These messages are then multiplied into even more messages that are seen by WinProc. WM_KEYUP messages generate WM_GETTEXT messages. If the user pauses in typing, then a stream of WM_KEYUP messages is seen in the GetMessage loop, and WinProc sees a long stream of pairs of WM_KEYUP, WM_GETTEXT messages. Why Windows needs to send repetitive notifications that a key has moved to the up position is a mystery. Weirder still, the wParam value for the WM_KEYUP messages is the "S" key even if that key hasn't been pushed.

Sending vs. Posting

When a program wants to pass a message to a window, it can either "send" the message or "post" it. Posting the message will deposit it in the receiving application's queue, while sending it will immediately call the receiving application's WinProc, which will process the message. 2PANE has been equipped with both these forwarding methods for its character input. It does not forward the WM_KEYDOWN and WM_KEYUP raw keystrokes but, rather, the distilled character interpretations of them. An Echo menu allows the user (experimenter) to select either no echoing, PostMessage echoing, or SendMessage forwarding of characters between windows. Before forwarding a character, the code checks to be sure that it is receiving the character from the keyboard and not from the other window (that it has the "focus"). Without this check, the character would be echoed endlessly back and forth between windows, making for an undesired type of autorepeat.

When the user selects send-mode echoing, the receiving window shows no increase in GetMessage loop count, just as you would expect. Sending is indeed an immediate call to the receiver's window procedure. Keying characters seems to usually generate more than 20 messages per keystroke to the window procedure, but when only distilled characters are forwarded to the other window, as done in 2PANE, then the receiving window procedure is entered only once per stroke (actually less, since Shift keys are not forwarded); see Figure 5.

Character forwarding can be used with many different kinds of windows. In Windows 3.1, a program can send characters to nearly any window, whether or not it owns or is otherwise associated with that window.

When programming messages to other windows, programmers should keep in mind that SendMessage is a form of call and will process completely through the receiving window's window procedure. This might not be good for overall performance, since it gives no other windows a share of the computer. PostMessage has several advantages: It places the message in the receiving queue and returns to your program without processing the message. When the receiving application invokes GetMessage, Windows can give other applications a turn at the CPU. If the receiving program has a problem, then it will have to field the problem or fail. The sending program will not fail from a crash in the destination window's code.

In using the SendMessage function, it is important to be sure that the receiving window's code will always be available and ready to run. It is advisable to have the destination window have a window procedure. Also, a deadlock situation can be created if the destination-window procedure yields control (for example, with GetMessage, PeekMessage, Yield, DialogBox, and so on) after being sent a message. The destination-window procedure should call InSendMessage before using any Windows functions that might yield control and then, for sent messages, use ReplyMessage to return control to the sending application to avoid the deadlock condition. In the absence of source code, it may be difficult to ascertain whether a destination-window procedure is deadlock safe. If a target procedure yields control, then the sending procedure is left waiting for a return from its SendMessage call and will be unable to continue processing messages.

PostMessage is safer but SendMessage is the only way to get a return code from the destination window. If you need to be sure of what happened to the message, then you need SendMessage, otherwise you are probably better off using PostMessage.

Porting 2Pane to NT

Since I was in the process of converting to Windows NT, I ported the 2PANE program to Visual C++ NT. The porting process was mostly straightforward, but one very interesting obstacle caused an hour-long phone call to Microsoft.

The Windows 3.1 version of the program was developed using Borland C++ 3.1 and uses a menu that I, of course, named 2PANE. I called Microsoft and found out that I couldn't name a menu 2PANE or any other name that begins with a numeral. The compiler didn't complain, and the program executed fine. There just wasn't any menu when the main title bar appeared. Taking the "2" out of the menu name in the program as well as the resource file (2PANE.RC) did the trick and generated a menu. I don't know whether this feature is peculiar to the NT version of Visual C++ or might also be encountered in the 16-bit version.

A general change was to remove the _export directive from function definitions. There also appeared to be a slight difference in behavior of the break statement in Visual C++, which caused me to use an intermediate variable in the NT version.

I liked using the Visual C++ NT development system. So far, it has been handy for debugging, as I can stay right in the full Windows NT environment while doing so. I teach Windows programming at night school, and NT has been a good, bulletproof base for grading the beginning students' programs.

The Visual C++ NT compiler also pointed up a problem in my programming practice when it objected to using NULL as a parameter where a number was expected. Consequently, I changed the last parameters in SendMessage and Post-Message from NULL to 0L to reflect that these last parameters are long integers. NULL should be used to indicate an unused pointer, not an unspecified integer.

2PANE behaves in the same way under NT as with Windows 3.1, but the counts are not identical. Of course, the underlying systems are not even similar.

Example 1: (a) This code at the end of 2PANE.C allows users to close windows at will; (b) converting the message handle into an integer window number.

(a) case WM_DESTROY:
      if (--iWin_ct < 1) PostQuitMessage (0) ; // only die after last window   
       closes
      return 0 ;
(b) for (iWinNbr = 0; iWinNbr <= MAX_NBR_WIN; iWinNbr++)
      if (hCurrWin == hWnd [iWinNbr])
         break ;
    if (iWinNbr > MAX_NBR_WIN - 1)    // not one of our handles.
          iWinNbr = 0 ;               // pretend 1st window

Example 2: The 2PANE message loop has been instrumented with two counters.

while (GetMessage (&msg, NULL, 0, 0))
    {
    msg_ct++ ;                // count messages
    if (msg.message == WM_PAINT)
      paintmsg_ct++ ;         //count paint messages
    TranslateMessage (&msg) ;
    DispatchMessage (&msg) ;
    }
   return msg.wParam ;

Figure 1 Sample windows generated by 2PANE.

Figure 2 Four message tallies are displayed in the four corners of each of 2PANE's initial windows.

Figure 3 Using Spy to track messages.

Figure 4 2PANE's handling of character messages.

Figure 5 Character forwarding under 2PANE.

Listing One


/* -----------------------
    2PANE.H header file
   ----------------------- */

#define IDM_NOECHO      0
#define IDM_POST        1
#define IDM_SEND        2
#define MAX_NBR_WIN 2


Listing Two


/*--------------------------------------------------------
   2PANE.C -- Displays "2 Panes" from 1 program & counts messages
         Copyright 1993 Dick Wilmot 
  --------------------------------------------------------*/
#define STRICT             // get more warnings
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include "2pane.h"

static HWND   hWnd [MAX_NBR_WIN + 1] ;  // Cannot be local variables! Else not 
                                        //   available in WinProc
static int    iWin_ct   = 0;  // count of open windows
static int    msg_ct, proc_ct, paintmsg_ct, paintproc_ct ;

long FAR PASCAL _export WinProc (HWND, UINT, UINT, LONG) ;

int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                               LPSTR lpszCmdParam, int nCmdShow)
     {
     static char szAppName[ ] = "twopane" ;
     LPCSTR lpszCaption [ ] = {"Pane 1", "Pane 2"} ;
     MSG         msg ;
     WNDCLASS    wndclass ;
     int         i ;

     msg_ct = proc_ct = paintmsg_ct = paintproc_ct = 0 ;  // zero all counters

     if (!hPrevInstance)
      {
      wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
      wndclass.lpfnWndProc   = WinProc ;
      wndclass.cbClsExtra    = 0 ;
      wndclass.cbWndExtra    = 0 ;
      wndclass.hInstance     = hInstance ;
      wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
      wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
      wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
      wndclass.lpszMenuName  = "2pane" ;
      wndclass.lpszClassName = szAppName ;

      RegisterClass (&wndclass) ;
      }
     for (i = 0; i <= MAX_NBR_WIN - 1; i++)
         {
         hWnd [i] = CreateWindow (szAppName,         // window class name
            lpszCaption [i],         // window caption
            WS_OVERLAPPEDWINDOW,     // window style
            (int) (100 + i * 200),   // initial x position
            (int) (100 + i * 200),   // initial y position
            200,                     // initial x size
            200,                     // initial y size
            NULL,                    // parent window handle
            NULL,                    // window menu handle
            hInstance,               // program instance handle
            NULL) ;                  // creation parameters
         iWin_ct++ ;
         ShowWindow (hWnd [i], nCmdShow) ;  // show the window just created
         UpdateWindow (hWnd [i]);    // force painting of window
         }
     while (GetMessage (&msg, NULL, 0, 0))
      {
      msg_ct++ ;                     // count messages

      if (msg.message == WM_PAINT)
            paintmsg_ct++ ;          // count paint messages
      TranslateMessage (&msg) ;
      DispatchMessage (&msg) ;       // hand message to Windows' dispatcher
      }
     return msg.wParam ;
     }

long FAR PASCAL _export WinProc (HWND hCurrWin, UINT message,
                WPARAM wParam, LPARAM lParam)
     {
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rect ;
     static char szBuffer[10] ;
     static int  iEchoType[MAX_NBR_WIN] ;        // flag for type of 
                                                 // echoing from each window
     static int  xCaret [MAX_NBR_WIN], cxCharSize, cyCharSize ;
     static LPCSTR lpszBuffer = szBuffer ;  // pointer to text buffer
     TEXTMETRIC  tm ;
     int         iWinNbr ;                  // window number
     HWND        hOtherWnd ;
     int         i ;
     char        ch ; 

     proc_ct++ ;                            // increment WinProc count

     for (iWinNbr = 0; iWinNbr <= MAX_NBR_WIN ;
        iWinNbr++)            // translate handle to our window number
         if (hCurrWin == hWnd [iWinNbr])
            break ;
     if (iWinNbr > MAX_NBR_WIN - 1)         // not one of our handles.
            iWinNbr = 0 ;                   // pretend 1st window

hOtherWnd = hWnd [(iWinNbr + 1) % MAX_NBR_WIN] ; // establish other window handle switch (message) { case WM_COMMAND: iEchoType [iWinNbr] = wParam ; // remember what kind of echoing user wants return 0 ; case WM_CHAR : ch = (char) wParam ; szBuffer [0] = ch ; // put character in text buffer szBuffer [1] = '\0' ; // terminate with zero hdc = GetDC (hCurrWin) ; // get device context to draw on SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; GetTextMetrics (hdc, &tm) ; cxCharSize = tm.tmAveCharWidth ; // How wide characters are. cyCharSize = tm.tmHeight+tm.tmExternalLeading ; //How tall characters need be TextOut (hdc, xCaret [iWinNbr]++ * cxCharSize, 30, lpszBuffer, 1) ; SetTextAlign (hdc, TA_TOP) ; TextOut (hdc, 0, 0, strupr( itoa (msg_ct, szBuffer, 10 )), strlen (szBuffer)) ; GetClientRect (hCurrWin, &rect) ; SetTextAlign (hdc, TA_UPDATECP) ; // position to bottom of window MoveTo (hdc, 0, rect.bottom - cyCharSize ) ; // move up 1 text line TextOut (hdc, 0, 0, strupr( itoa (proc_ct, szBuffer, 10 )), strlen (szBuffer)) ; ReleaseDC (hCurrWin, hdc) ; if ((GetFocus ()) == hCurrWin) // echo if requested & this window has focus { if (hCurrWin == hOtherWnd) MessageBox (hCurrWin, "Sending to Self", "Warning", MB_ICONSTOP) ; if (iEchoType [iWinNbr] == IDM_SEND) // user wants send echoing SendMessage (hOtherWnd, WM_CHAR, wParam, 0L) ; if (iEchoType [iWinNbr] == IDM_POST) // user wants post echoing PostMessage (hOtherWnd, WM_CHAR, wParam , 0L) ; } return 0 ; case WM_PAINT: paintproc_ct++ ; InvalidateRect (hCurrWin, NULL, TRUE) ; // erase whole background for repaint hdc = BeginPaint (hCurrWin, &ps) ; GetClientRect (hCurrWin, &rect) ; DrawText (hdc, strupr( itoa (msg_ct, szBuffer, 10 )), -1, &rect, DT_SINGLELINE | DT_LEFT | DT_TOP); DrawText (hdc, strupr( itoa (proc_ct, szBuffer, 10 )), -1, &rect, DT_SINGLELINE | DT_LEFT | DT_BOTTOM); DrawText (hdc, strupr( itoa (paintmsg_ct, szBuffer, 10 )), -1, &rect, DT_SINGLELINE | DT_RIGHT | DT_TOP); DrawText (hdc, strupr( itoa (paintproc_ct, szBuffer, 10 )), -1, &rect, DT_SINGLELINE | DT_RIGHT | DT_BOTTOM); EndPaint (hCurrWin, &ps) ; return 0 ; case WM_DESTROY: if (--iWin_ct < 1) PostQuitMessage (0) ; // only die after last return 0 ; // window closes } return DefWindowProc (hCurrWin, message, wParam, lParam) ; }


Copyright © 1994, Dr. Dobb's Journal