SkyOS: Message Handling in C++


Having used the Windows Template Library (WTL) for some years now, I thought it would be interesting to try and apply some of the same ideas to SkyOS.  I decided to start with some simple message handling to see if this would work, and came up with a design concept that features a class template and a set of preprocessor macros.  Each unique window is based on the class template, and uses the windowData field in the s_window structure  to store a pointer to the window object; the generic window procedure then extracts this pointer and invokes the appropriate object method.  To avoid a lot of excess code, a set of preprocessor macros tidy up all the method handling and make them readable.

Window Class Template

The window class template (Window) is a "mix-in" class that provides some basic functionality common to all windows.  Currently, the Window class:

The header file for the Window class is a fairly generic class declaration, except for the WindowProc() function, which looks like this:

 
 static HRESULT
 WindowProc(HANDLE hWnd, s_gi_msg * pMsg)
 {
 s_window *  pw = reinterpret_cast<s_window *>( hWnd );

 if (GOOD_PTR(pw) && GOOD_PTR(pw->windowData))
    {
    T* pT = *(reinterpret_cast<T**>(pw->windowData));

    if (GOOD_PTR(pT))
        return pT->HandleMessage(hWnd, pMsg);
    }

 return 0;
 }
 

The 'magic' occurs in the line that reads T* pT = *(reinterpret_cast<T**>(pw->windowData)); which extracts a pointer from the windowData field of the s_window structure and casts it to the appropriate window subclass.  Because this is a template, this cast can be done before the window subclass is declared, and any undefined references will generate errors at link time; more importantly, all subclasses can safely share the same WindowProc code..  This is the same technique that the WTL uses, and is explained well in a tutorial by Michael Dunn; for now, just take it on faith that this generates relatively compact code with minimal overhead.

Message Routing Macros

The same header file also defines a set of preprocessor macros that can be used to easily code message handlers for window classes.  These are simplified versions of the WTL macros, and are as follows:

Unlike their WTL inspiration, which uses 'message cracking' to send message-specific parameters to each function, this version uses a simple 'one size fits all' approach: every command handler must be prototyped as bool myCmdFunc(unsigned int, HRESULT &), while every message handler must be prototyped as bool myMsgFunc(s_gi_msg const &, HRESULT &).  If the function handles the command, i.e., no further processing is necessary, it should return true; returning a false result will cause the message to be handed off to the default window procedure.  The function can also indicate success or failure by changing the HRESULT parameter, which defaults to S_OK (success).

Sample Window Class: WSplash

To see how this Window template class is put into use, let's look at a subclass called WSplash (WSplash.h, WSplash.cpp). The class declaration begins with class WSplash : public Window<WSplash> which declares WSplash as a subclass of Window, and automagically gives embodies WSplash with all the variables and methods belonging to the Window template class.   The window subclass also defines a message map that connects the various methods to the commands and messages:

 
     BEGIN_MSG_MAP("WSplash")
         MAP_MSG( MSG_GUI_REDRAW,            onRedraw )
         MAP_MSG( MSG_MOUSE_BUT1_RELEASED,   onMouseBut1Released )
         MAP_MSG( MSG_TIMER_REACHED,         onTimerReached )
         MAP_MSG( MSG_WINDOW_DESTROY,        onWindowDestroy )

         MAP_CMD( ID_APP_EXIT,               onAppExit )
     END_MSG_MAP

 

In this particular example, all MSG_GUI_REDRAW messages will be routed to the WSplash::onRedraw() method; if that method isn't overridden by the WSplash subclass, they will be sent to Window::onRedraw() instead.  Likewise, the ID_APP_EXIT command is automatically routed to the onAppExit() method.  Keep in mind that a window subclass does not need to include all commands and messages in the message map, only those that it is interested in.  The others will be handled by the Window class, or passed to the default window procedure.  The only methods that every window subclass should implement are the Create(), Destroy(), and Draw() methods.  The  Create() method actually creates the window, and must also store a pointer to itself in the windowData field.  This can be done by passing address of this to the GI_create_window() call as follows (code omitted for clarity):

 
 HRESULT
 WSplash::Create(
     s_window *  pParent)
 {
 HANDLE      hWnd;

 WSplash *   pwsThis = this;
 

 hWnd = GI_create_window(pParent, TEXT("Splash!"),
                        
nXPos, nYPos, nWidth, nHeight,
                         kWinFlags,
                         (unsigned long)(base_class::WindowProc),
                         &pwsThis, sizeof(WSplash *));
 if (NULL_HND(hWnd))
     return S_FAILED;

 m_pWindow = static_cast<s_window *>( hWnd );

 return base_class::Create(pParent);
 }   /* end of Create() */
 

Both Create() and Destroy() must invoke the base class method as well.  The base_class typedef can be used to explicitly access the desired method of the Window class; writing return Window<WSplash>::Create(); would also be correct, but not as easy to understand. As one would expect, the Destroy() method is called when the window is being destroyed, while the Draw() method is invoked to draw the contents of the window client.

Conclusion

So far, this technique seems to be working fine, and make window management a lot more streamlined to both code and debug.  Nonetheless, this is very much a 'work in progress', so please feel free to suggest improvements or alternatives.  Stay tuned...


 

2004.08.09-11:27