Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Plug-In Components for MFC


Dr. Dobb's Journal August 1998: Plug-In Components for MFC

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:

  1. 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."

  2. 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.

  3. 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);
    }
    

    Back to Article

    Listing Two

    void CUserActionComponent::HandleMessage(UINT nMessage, WPARAM wParam, LPARAM lParam)
    {
        switch (nMessage)
        {
        case WM_MOUSEMOVE:
            OnMouseMove(wParam, CPoint(LOWORD(lParam), HIWORD(lParam));
            break;
        }
    }
    }
    

    Back to Article

    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);
    }
    

    Back to Article

    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);
    }
    

    Back to Article

    Listing Five

    _AFXWIN_INLINE void CWnd::Invalidate(BOOL bErase)
        { ASSERT(::IsWindow(m_hWnd)); ::InvalidateRect(m_hWnd, NULL, bErase); }
    

    Back to Article

    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;
    }
    

    Back to Article

    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_
    

    Back to Article

    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
    

    Back to Article


    Copyright © 1998, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.