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 the 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 specified user 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. This approach has severe disadvantages: each event handler is responsible for handling a variety of actions that are not related to each other. In short, it ignores encapsulation.
As an experienced C++ developer, you want to get rid of these if-statements and provide individual objects for each user action. The goal of this article is to show you a new approach for encapsulating user actions into separate objects that support MFC message maps. These special objects, called Plug-in components, can be reused among different window and view objects without code duplication. To give you a real world example for using this plug-in approach, this article includes a convenient reusable MFC class called CMSJIntelliMousePlugin that can be attached to any CWnd or CView class. The component provides support for IntelliMouse scrolling, zooming, and panning. No code change is necessary in the component source code to use its functionality with different window and view classes.
There are existing approaches that encapsulate user actions into separate objects and do not use if-statements, but these solutions lack support for the MFC message map. Consequently, most MFC developers avoid these approaches.
The first 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. The following code snippet demonstrates how to delegate the WM_MOUSEMOVE message to an attached object:
void CMyView::OnMouseMove(UINT nFlags, CPoint point) { // forward this event to an attached object m_pObject->OnMouseMove(nFlags, point); CView::OnMouseMove(nFlags, point); } |
The disadvantage of this approach is obvious. There is a tight coupling between the window class and the user action component. Whenever you need to process a new message in the component, you need 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 pre-defined message handlers for each window message, but this approach has the disadvantage that it results in a large amount of messages and relatively few of them will be used.
The second approach is to override the WindowProc() method that is the 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. The following code shows what a typical event handler looks like:
void CUserActionComponent::HandleMessage(UINT nMessage, WPARAM wParam, LPARAM lParam) { switch (nMessage) { case WM_MOUSEMOVE: OnMouseMove(wParam, CPoint(LOWORD(lParam), HIWORD(lParam)); break; } } |
This approach allows you to 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. This approach requires you to perform the tedious task of decoding the WPARAM and LPARAM parameters into useful information. After decoding a few of these parameters, you will wish you could still use the ClassWizard to add new messages.
Both approaches are insufficient because they lack support for MFC message maps in the user action components. Given these alternatives, the if-statement approach is the most attractive alternative even if it entails copying and pasting code from one window class to another to provide the same functionality for different views or windows.
Using plug-in components lets you override WindowProc() and forward all windows messages to a user action component that fully supports MFC message maps. ClassWizard lets you add message handlers, as with any other MFC window class.
Before demonstrating how to implement the plug-in approach, let us describe the requirements for this approach and show how to encapsulate the solution in one common base class for plug-in components.
Here are the requirements for the plug-in approach:
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.
Here is a more detailed discussion for each of these requirements and its solutions:
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. One interesting feature of OnWndMsg() is that it correctly dispatches messages whether or not a valid windows handle is attached to the CWnd object. OnWndMsg() is completely independent of the CWnd's m_hWnd attribute. It works correctly even if you never called CWnd::Create().
Armed with this knowledge, we could derive the plug-in component base class from CWnd that does not need to be attached to a windows handle and provides an entry point for each window message. The entry point for window messages is the plug-in component's HandleMessage() method.
The following code shows how HandleMessage() is implemented. The method calls the protected CWnd::OnWndMsg() member, which then searches the message map and calls the correct message handler. The meaning of the attributes m_bExitMesssage and m_bSkipOtherPlugins is discussed later in this article.
BOOL CMSJPluginComponent::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) { m_bSkipOtherPlugins = FALSE; m_bExitMesssage = FALSE; return CWnd::OnWndMsg(message, wParam, lParam, pResult); } |
The next code snippet 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.
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); } |
CWnd is a thin wrapper class for a window handle and provides many 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 the window handle. The member function is declared as an inline method in afxwin.inl using the following code:
_AFXWIN_INLINE void CWnd::Invalidate(BOOL bErase) { ASSERT(::IsWindow(m_hWnd)); ::InvalidateRect(m_hWnd, NULL, bErase); } |
Many 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 asserts because m_hWnd is not a valid window handle. To solve this problem, we need to provide a valid windows handle for the plug-in component's m_hWnd attribute.
Consider the following two issues:
CWnd::OnWndMsg disregards the value of the m_hWnd attribute, so we can assign any value to it.
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.
If you are an experienced MFC developer, you may question the legality of assigning the same windows handle to different CWnd objects. In the case of CWnd::Attach(), you cannot assign the same windows handle to different CWnd objects. If you attempt to do this, MFC will assert. Internally, MFC only 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 us to call CWnd::Attach(). Instead, we only assign the windows handle to m_hWnd, which is safe. However, you should 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.
As mentioned earlier, another requirement is to avoid redundant calls to the default window procedure of the parent window. The solution to this problem is to override the virtual DefWindowProc() method for the plug-in component class and return immediately, as shown in the following code snippet below. Then, only the parent window is calling the default window procedure.
LRESULT CMSJPluginComponent::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 processed twice). return 0; } |
The CGXPluginComponent class is the base class for plug-in components.
Here is a short overview on member functions and attributes.
Plugin()— Call this method to attach the component to a window object. The method assigns the window's handle to the plug-in component's m_hWnd attribute.
m_bExitMessage — If you set m_bExitMessage equal to TRUE, the window procedure should return after the plug-in component has processed the message. See the source code for WindowProc() earlier in this chapter to see 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.
To show you how easy it is to use the plug-in approach, this section documents the development steps for implementing a sample auto-scroll component. The auto-scroll component checks if the user presses the left mouse button. In response to this event, a timer is started and WM_VSCROLL messages are sent to the parent window. When the user moves the mouse up or down, the parent window scrolls into 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 when the user presses the ESC key stop the operation. The component can easily be reused and attached to any view or window class without changing its source code.
The major steps for implementing the sample component are:
Create a small MFC application with AppWizard using CView as the main window's base class. For instance, create a class called CMyView.
Derive a class called CAutoScrollPlugin from the CGXPluginComponent class, where you add the functionality that should be encapsulated in the plug-in component.
In the CMyView class, override WindowProc() and then call the OnWndMsg() method of the plug-in component. To do this, call CGXPluginComponent::PlugIn(this) in your CMyView::OnInitialUpdate()override.
Here is a more detailed explanation:
When you create the project using the MFC AppWizard, please derive the view class from CScrollView. We recommend you name the view class CMyView. 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); |
Next, create the CAutoScrollPlugin class. Use ClassWizard to derive a class from a generic CWnd and name it CAutoScrollPlugin. After you generate the class, you can derive it from CGXPluginComponent. To do this, edit the header and implementation file and replace all occurrences of CWnd with CGXPluginComponent. 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 following listing shows the final implementation of the CAutoScrollPlugin component.
// AutoPlug.cpp : implementation file // #include "stdafx.h" #include "autoscrl.h" #include "AutoPlug.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif const int nScrollTimer = 991; //////////////////////////////////////////////////////////// CAutoScrollPlugin CAutoScrollPlugin::CAutoScrollPlugin() { m_bIsAutoScrolling = FALSE; m_nTimer = 0; } CAutoScrollPlugin::~CAutoScrollPlugin() { } BEGIN_MESSAGE_MAP(CAutoScrollPlugin, CGXPluginComponent) //{{AFX_MSG_MAP(CAutoScrollPlugin) ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_TIMER() ON_WM_KEYDOWN() ON_WM_CANCELMODE() //}}AFX_MSG_MAP END_MESSAGE_MAP() ////////////////////////////////////////////////////////// // CAutoScrollPlugin message handlers void CAutoScrollPlugin::OnLButtonDown(UINT nFlags, CPoint point) { m_ptMouseDown = point; ClientToScreen(&m_ptMouseDown); m_bIsAutoScrolling = TRUE; SetCapture(); m_nTimer = SetTimer(nScrollTimer, 10, NULL); m_bExitMesssage = TRUE; } void CAutoScrollPlugin::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bIsAutoScrolling) AbortScrolling(); CGXPluginComponent::OnLButtonUp(nFlags, point); } void CAutoScrollPlugin::OnTimer(UINT nIDEvent) { CPoint pt; GetCursorPos(&pt); UINT nSBCode = SB_LINEUP; if (pt.y > m_ptMouseDown.y) nSBCode = SB_LINEDOWN; SendMessage(WM_VSCROLL, MAKEWPARAM(nSBCode, 0), NULL); CGXPluginComponent::OnTimer(nIDEvent); } void CAutoScrollPlugin::AbortScrolling() { if (m_bIsAutoScrolling) { m_bIsAutoScrolling = FALSE; ReleaseCapture(); KillTimer(m_nTimer); } } void CAutoScrollPlugin::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar == VK_ESCAPE) AbortScrolling(); CGXPluginComponent::OnKeyDown(nChar, nRepCnt, nFlags); } void CAutoScrollPlugin::OnCancelMode() { AbortScrolling(); } |
The next step is to add a pointer to the plug-in object in your view class. To do this, add this code to the class declaration:
class CMyView: public CScrollView { ... CGXPluginComponent* m_pPlugin; |
In myview.cpp, instantiate the auto-scroll component and call its Plugin() method in the OnInitialUpdate() routine:
m_pPlugin = new CAutoScrollPlugin; m_pPlugin->PlugIn(this); |
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 following listing shows the implementation of the CMyView class:
// myView.cpp : implementation of the CMyView class // #include "stdafx.h" #include "autoscrl.h" #include "MyDoc.h" #include "myView.h" #include "autoplug.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif //////////////////////////////////////////////////////////// CMyView GRID_IMPLEMENT_DYNCREATE(CMyView, CScrollView) BEGIN_MESSAGE_MAP(CMyView, CScrollView) //{{AFX_MSG_MAP(CMyView) //}}AFX_MSG_MAP END_MESSAGE_MAP() //////////////////////////////////////////////////////////// CMyView construction/destruction CMyView::CMyView() { m_pPlugin = NULL; } CMyView::~CMyView() { delete m_pPlugin; } void CMyView::OnDraw(CDC* pDC) { } void CMyView::OnInitialUpdate() { m_pPlugin = new CAutoScrollPlugin; m_pPlugin->PlugIn(this); CScrollView::OnInitialUpdate(); CSize sizeTotal; sizeTotal.cx = sizeTotal.cy = 15000; SetScrollSizes(MM_TEXT, sizeTotal); } 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); } |
A real world example for using this plug-in approach is the convenient and reusable CGXIntelliMousePlugin component that can be attached to any CWnd or CView class. The component provides support for IntelliMouse scrolling, zooming and panning. No code change is necessary in the component source code to use its functionality with different window and view classes.
Here is a short overview of the functionality implemented with the component. The implementation is very similar to MS Excel and Internet Explorer 4.0. The following features are provided:
Scroll by rolling the mouse wheel.
Scroll horizontally by pressing and holding SHIFT while rolling the mouse wheel.
Zoom in and out by pressing and holding CTRL while rolling the mouse wheel.
Auto-Scroll by pressing the mouse wheel button down while dragging the mouse up, down, left, or right.
Click-Lock for the mouse wheel button: Just press and hold down the mouse wheel button for a moment to lock your click. With Click-Lock, you can scroll through the grid easily by simply dragging the mouse. Its functionality is identical to Auto-Scroll, except you don't need to hold the mouse wheel button. Press (click) the mouse wheel button again to release Click-Lock.
The CGXIntelliMousePlugin class integrates into any advanced view that may itself process many window messages.
Check out the ogmouse sample (samples\grid\plugin\ogmouse) to see how easy it is to add IntelliMouse support to any existing view. All you have to do is follow these steps from the previous section:
Add a pointer to the plug-in object in your view class. Allocate a CGXIntelliMousePlugin object and call CGXPluginComponent::Plugin() at initialization time.
Override WindowProc() and call the HandleMessage() method of the plug-in component.
With CGXGridCore, using IntelliMouse support is even easier. CGXGridCore already provides internally the necessary dispatching of messages in the WindowProc() method. All you need to do is register the IntelliMouse component. To do this, simply call EnableIntelliMouse(); at initialization time of your grid (for example in OnInitialUpdate().)
EnableIntelliMouse() calls CGXGridCore::AddPlugin(). CGXGridCore::AddPlugin() registers the plug-in component within the grid. This registration is necessary to let the grid know that it should dispatch WindowProc() events to this component. When the grid goes out of scope, the plug-in object will be automatically destroyed.
Check out the GridApp sample for using IntelliMouse within a grid.
Copyright © Rogue Wave Software, Inc. All Rights Reserved.
The Rogue Wave name and logo, and Stingray, are registered trademarks of Rogue Wave Software. All other trademarks are the property of their respective owners.
Provide feedback to Rogue Wave about its documentation.