Stefan is software architect at Stingray Software, a division of Rogue Wave Software, and author of Objective Grid, a grid extension for MFC. He can be contacted at [email protected].
Microsoft's Developer Studio is an integrated development environment that supports the creation of Windows applications with MFC. The ClassWizard tool lets you add a message handler for any window message to a derived window class. This gives you a degree of flexibility not possible in the Windows SDK. In the SDK, you had to specify a callback function for a window and filter out windows messages through a switch statement. ClassWizard and MFC message maps provide a type-safe, robust, and easy way to handle window messages. (MFC message maps let you designate which functions in a particular class will handle various messages for events like keystrokes or mouse clicks. Message maps contain one or more macros that specify which messages will be handled by which functions in a class. Message maps are tightly coupled with ClassWizard. ClassWizard automatically creates message-map entries in source files when you use it to associate message-handling functions with messages.)
However, MFC has its limitations when it comes to encapsulating different window functionality into separate objects. When you program with MFC, you often have to implement different window actions based on user events. For example, when a user presses a mouse button, the MFC window is set into a special context. In subsequent mouse messages, you check the context of the window and give graphical feedback to the user based on the mouse movements. Once the user releases the mouse button, you reset the window context and perform the user-specified action. Suppose you want to add support for more user actions. The easiest way is to add if statements for each context in your message handlers. However, this approach has a severe disadvantage: Each event handler is responsible for handling a variety of actions that are not related to each other. In short, it ignores encapsulation.
Clearly, you want to avoid if statements and provide individual objects for each user action. In this article, I'll present an approach for encapsulating user actions into separate objects that support MFC message maps. These special objects, which I call "plug-in components," can be reused among different window and view objects without code duplication. For the purposes of illustration, I include a reusable MFC class called CDDJIntelliMousePlugin that can be attached to any CWnd or CView class. CDDJIntelliMousePlugin provides support for intelli-mouse scrolling, zooming, and panning. Code changes in the component source code are not necessary to use its functionality with different window and view classes.
Existing Approaches
While there are existing approaches that encapsulate user actions into separate objects and do not use if statements, these solutions lack support for the MFC message map. Consequently, most MFC developers avoid these approaches.
One approach is to add message handlers to the window class and forward each of these messages to the attached component object that is responsible for handling the user actions. Listing One emonstrates how to delegate the WM_MOUSEMOVE message to an attached object.
The disadvantage of this approach is obvious. There is a tight coupling between the window class and user action component. Whenever you need to process a new message in the component, you have to add a message handler in the parent window class and forward it to the component. You might try to solve the problem by providing predefined message handlers for each window message, but this approach has the disadvantage that it results in a large number of messages, few of which will be used.
Another approach is to override the WindowProc method (entry point for all window messages sent to a window). In the overridden method, you can forward each window message to the attached user action component object. In the attached component, you implement a switch statement that provides handlers for the window messages you want to handle. Listing Two is a typical event handler.
This approach lets you add messages in the user action component without changing the parent window class. However, it is a step backwards, akin to early C-like Windows SDK development in that it requires you to decode the WPARAM and LPARAM parameters into useful information. After decoding a few of these parameters, you'll wish you could still use the ClassWizard to add new messages.
The Plug-in Approach
An alternative to these techniques is the plug-in approach. To implement this approach, I had to:
- Determine one point of entry for searching the MFC message map and dispatching any window messages to the correct message handler in a derived class.
- Ensure that source code for user actions in existing window classes can be reused without making major changes.
- Avoid redundant calls to the default window procedure. Only the parent window object should call this method.
To accomplish this, I had to deal with message dispatching. MFC message dispatching is implemented by CWnd's OnWndMsg member function. OnWndMsg searches the window's message map and calls the correct message handler in a derived class. OnWndMsg correctly dispatches messages whether or not a valid window handle is attached to the CWnd object. OnWndMsg is completely independent of the CWnd's m_hWnd attribute. It works correctly even if you've never called CWnd::Create.
With this knowledge, I could derive the plug-in component base class from CWnd. The entry point for window messages is the plug-in component's HandleMessage method.
Listing Three implements HandleMessage. The method calls the protected CWnd::OnWndMsg member that then searches the message map and calls the correct message handler. Listing Four shows how messages are forwarded from the parent window class to the plug-in component. m_pPlugin is a pointer to a plug-in component object.
Reuse of Existing Code
CWnd is a thin wrapper class for a window handle and provides member functions that rely on the m_hWnd attribute. For example, CWnd::Invalidate is a wrapper to the equivalent Windows SDK method and passes m_hWnd as a window handle. The member function is declared as an inline method in afxwin.inl (an MFC header file) as shown in Listing Five. Other CWnd member functions are implemented in exactly the same way. If you port existing code to a plug-in component and call a CWnd member function, your application would assert if m_hWnd is not a valid window handle. To solve this problem, I needed to provide a valid window handle for the plug-in component's m_hWnd attribute. To do so, I had to take into consideration that CWnd::OnWndMsg disregards the value of the m_hWnd attribute so I can assign any value to it. Also, a plug-in component is not a real window object. The plug-in component should operate directly on the parent window object. It receives the same messages that the parent window object receives, and any window operations that are executed in the plug-in component need to affect the parent window.
Assigning the parent's window handle to the plug-in component's m_hWnd attribute is the ideal solution. Using the parent's window handle lets you port existing code to a plug-in component without changing any existing calls to CWnd member functions. All CWnd member functions now operate directly on the parent window.
You may question the legality of assigning the same window handle to different CWnd objects. In the case of CWnd::Attach, you cannot assign the same window handle to different CWnd objects. If you try to do this, MFC will assert. Internally, MFC allows one window object for each window handle. The window handles and CWnd objects are maintained in the window handle map. However, the plug-in approach does not require you to call CWnd::Attach. Instead, you only assign the window handle to m_hWnd, which is safe. However, be aware that whenever you call CWnd::FromHandle(m_hWnd), MFC returns a pointer to the parent CWnd object because this is the window that is registered in the MFC window handle map.
Default Window Procedure
Recall that another requirement is to avoid redundant calls to the default window procedure of the parent window. The solution is to override the virtual DefWindowProc method for the plug-in component class and return immediately; see Listing Six. Then, only the parent window is calling the default window procedure.
The CDDJPluginComponent Class
The CDDJPluginComponent class is the resulting base class for plug-in components (CDDJPluginComponent is available electronically; see "Resource Center," page 3). Listing Seven shows the declaration of the CDDJPluginComponent class, and Listing Eight shows its implementation. Here's an overview of the member functions and attributes:
- Plugin. Call this method to attach the component to a window object. It assigns the window handle to the plug-in component's m_hWnd attribute.
- m_bExitMessage. If you set m_bExitMessage to True, the window procedure returns after the plug-in component has processed the message. The source code for WindowProc (Listing Four) illustrates how to process this attribute in the override of the WindowProc method in your parent window class.
- m_bSkipOtherPlugins. Use this attribute to coordinate several plug-ins. If you want to attach several plug-ins to a window object, check this attribute in the WindowProc method of the parent window class.
Using the Plug-in Approach
To illustrate how to use the plug-in approach, I describe the steps for implementing an "auto-scroll" component which checks whether users press the left mouse button. In response to this event, a timer starts and WM_VSCROLL messages are sent to the parent window. When a user moves the mouse up or down, the parent window scrolls in the given direction. Once the user releases the mouse button, the timer is killed and the auto-scroll operation ends. Other events (such as the WM_CANCELMODE message or pressing Esc) also stop the operation. The component can be reused and attached to any view or window class without changing its source code.
To implement the auto-scroll component:
- You create the MFC AppWizard and derive the view class from CScrollView. Name the view class CMyView (an implementation of CMyView is available electronically). After you generate the project, enlarge the scroll range specified in OnInitialUpdate. For example:
CSize sizeTotal; sizeTotal.cx = sizeTotal.cy = 15000; SetScrollSizes(MM_TEXT, sizeTotal);
CDDJPluginComponent is implemented in ddjplgin.h and ddjplgin.cpp (Listings Seven and Eight). Add ddjplgin.cpp to your project. In the stdafx.h file, include "ddjplgin.h."
- Next, create the CAutoScrollPlugin class. Use ClassWizard to derive a class from a generic CWnd and name it CAutoScrollPlugin. After generating the class, you can derive it from CDDJPluginComponent. To do this, edit the header and implementation file and replace all occurrences of CWnd with CDDJPluginComponent. If you remove the existing ClassWizard (.clw) file from the project directory and press Ctrl-W, the ClassWizard file is regenerated. You can then add message handlers to the CAutoScrollPlugin class with ClassWizard. The final implementation of the CAutoScrollPlugin component is available electronically.
- The next step is to add a pointer to the plug-in object in your view class. To do this, add a pointer in the class declaration:
class CMyView: public CScrollView { ... CDDJPluginComponent* m_pPlugin;
In myview.cpp, instantiate the auto-scroll component and call its Plugin method in the OnInitialUpdate routine:
<blockquote>m_pPlugin = new CAutoScrollPlugin; m_pPlugin->PlugIn(this); </blockquote>
Don't forget to include the header file for the CAutoScrollPlugin class in myview.cpp. Finally, override WindowProc and call the HandleMessage method of the plug-in component.
The Intelli-Mouse Example
A real-world example for using this approach is the reusable CDDJIntelliMousePlugin component that can be attached to any CWnd or CView class. The implementation is similar to Microsoft Excel and Internet Explorer 4.0. The following features are provided:
- Scrolling by rolling the mouse wheel.
- Scroll horizontally by clicking Shift and rolling the mouse wheel.
- Zoom in and out by clicking Ctrl and rolling the mouse wheel.
- Auto-scrolling by clicking the mouse-wheel button, then dragging the mouse up, down, to the left, or to the right.
- Click-lock for the mouse-wheel button: Click and hold down the mouse button for a moment to lock your click. With click-lock, you can scroll easily by simply dragging the mouse. Its functionality is identical to auto-scroll, except you don't need to hold the mouse-wheel button. Click again to release click-lock.
The CDDJIntelliPlugin class integrates in any advanced view that processes a lot of window messages. Figure 1 shows the final CDDJIntelliPlugin intelli-mouse panning support at work in a grid view. The implementation of the intelli-mouse plug-in component is available electronically.
Conclusion
In this article, I presented a completely new approach for encapsulating window functionality into different objects. I was really excited when I discovered how easily this new approach can be implemented with MFC. It also made me wonder why the MFC team had never considered such a solution. Once I perfected the plug-in approach, I couldn't help but think of how the architectures of my past projects would have differed if I had known about this technique. At Stingray, we use this technique to produce granular and user-friendly components. This approach lets our library users plug only the functionality they need into derived window classes; any unused functionality does not need to be linked into the application, dramatically decreasing application size.
DDJ
Listing One
void CMyView::OnMouseMove(UINT nFlags, CPoint point) { // forward this event to an attached object m_pObject->OnMouseMove(nFlags, point); CView::OnMouseMove(nFlags, point); }
Listing Two
void CUserActionComponent::HandleMessage(UINT nMessage, WPARAM wParam, LPARAM lParam) { switch (nMessage) { case WM_MOUSEMOVE: OnMouseMove(wParam, CPoint(LOWORD(lParam), HIWORD(lParam)); break; } } }
Listing Three
BOOL CDDJPluginComponent::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) { m_bSkipOtherPlugins = FALSE; m_bExitMesssage = FALSE; return CWnd::OnWndMsg(message, wParam, lParam, pResult); }
Listing Four
LRESULT CMyView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { if (m_pPlugin) { LRESULT lResult; m_pPlugin->HandleMessage(message, wParam, lParam, &lResult); if (m_pPlugin->m_bExitMesssage) return lResult; } return CScrollView::WindowProc(message, wParam, lParam); }
Listing Five
_AFXWIN_INLINE void CWnd::Invalidate(BOOL bErase) { ASSERT(::IsWindow(m_hWnd)); ::InvalidateRect(m_hWnd, NULL, bErase); }
Listing Six
LRESULT CDDJPluginComponent::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // do nothing - this makes sure that calls to Default() will have no // effect (and thus make sure that the same message is not process twice). return 0; }
Listing Seven
// ddjplgin.h : interface of the CDDJPluginComponent class#ifndef _DDJPLGIN_H_ #define _DDJPLGIN_H_ class CDDJPluginComponent: public CWnd { DECLARE_DYNAMIC(CDDJPluginComponent); public: CDDJPluginComponent(); BOOL PlugIn(CWnd* pParentWnd); virtual ~CDDJPluginComponent(); public: // Attributes // Reserved for later usage with a PluginManager BOOL m_bSkipOtherPlugins; // set to TRUE from within message handler if // no other plugins should be // called for this message BOOL m_bExitMesssage; // set to TRUE from within your message handler // if no other plugins and also not the // default window message should be called // Generated message map functions protected: //{{AFX_MSG(CDDJPluginComponent) //}}AFX_MSG DECLARE_MESSAGE_MAP() public: // for processing Windows messages BOOL HandleMessage(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult); protected: // for handling default processing virtual LRESULT DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; #endif //_DDJPLGIN_H_
Listing Eight
// ddjplgin.cpp : implementation of the CDDJPluginComponent class#include "stdafx.h" #include "resource.h" </p> #include "ddjplgin.h" </p> #ifdef _DEBUG #undef THIS_FILE static char BASED_CODE THIS_FILE[] = __FILE__; #endif </p> IMPLEMENT_DYNAMIC(CDDJPluginComponent, CWnd); </p> CDDJPluginComponent::CDDJPluginComponent() { m_bSkipOtherPlugins = FALSE; m_bExitMesssage = FALSE; } CDDJPluginComponent::~CDDJPluginComponent() { // make sure Detach won't get called m_hWnd = NULL; } BEGIN_MESSAGE_MAP(CDDJPluginComponent, CWnd) //{{AFX_MSG_MAP(CDDJPluginComponent) //}}AFX_MSG_MAP END_MESSAGE_MAP() BOOL CDDJPluginComponent::PlugIn(CWnd* pParentWnd) { m_hWnd = pParentWnd->GetSafeHwnd(); return TRUE; } BOOL CDDJPluginComponent::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) { m_bSkipOtherPlugins = FALSE; m_bExitMesssage = FALSE; #if _MFC_VER >= 0x0400 return CWnd::OnWndMsg(message, wParam, lParam, pResult); #else *pResult = CWnd::WindowProc(message, wParam, lParam); return TRUE; #endif } LRESULT CDDJPluginComponent::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // do nothing - this makes sure that calls to Default() will have no effect // (and thus make sure that the same message is not process twice). // Unused: message, wParam, lParam; return 0; } #ifdef _DEBUG void CDDJPluginComponent::AssertValid() const { if (m_hWnd == NULL) return; // null (unattached) windows are valid // should be a normal window ASSERT(::IsWindow(m_hWnd)); // Regular CWnd's check the permanent or temporary handle map and compare // the pointer to this. This will fail for a CDDJPluginComponent because // several Plugin objects share the same HWND. Therefore we must not // call CWnd::AsssertValid. } void CDDJPluginComponent::Dump(CDumpContext& dc) const { dc << "PluginComponent"; // It is safe to call CWnd::Dump CWnd::Dump(dc); } #endif
Copyright © 1998, Dr. Dobb's Journal