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

Customizing Window Behavior


SP 94: Customizing Window Behavior

Customizing Window Behavior

Subclassing windows with MFC

Vinod Anantharaman

Vinod is a software-design engineer at Microsoft working with desktop applications and graphical user interfaces. He can be reached at One Microsoft Way, Redmond, WA 98052.


Subclassing is a well-known meth-od of enhancing and modifying default window behavior under Microsoft Windows. The way subclassing works is by letting an application intercept any Windows message before it reaches its target window, thereby giving the application the first shot at performing some action in response to the message. Subclassing is widely used for developing specialized types of window controls from existing ones. For instance, masked edit controls and multiple-selection list boxes can be developed from the edit and list-box controls that come with Windows. Potentially expensive reimplementation of existing control functionality can be avoided by using this technique.

When discussing ways to avoid reimplementation, the term "object oriented" can never be far behind. Although objects have been touted as the panacea for all programming ills for several years now, only in recent times is this paradigm finding widespread acceptance in the world of GUI-based Windows programming. This has a lot to do with the growing acceptance, by developers, of object-oriented frameworks for Windows, such as the Microsoft Foundation Class (MFC) library and Borland's Object Windows Library (OWL). By combining powerful visual tools with well-designed Windows wrapper classes, class libraries bring method to the madness of API calls. Programmers leverage from the classes provided in such libraries by inheriting their functionality in their own derived classes and then extending and modifying the inherited behavior as desired in an object-oriented fashion. Class libraries support and simplify code reuse, substantially reducing the time and effort needed to design and implement Windows applications. In this article, I'll discuss how to use subclassing in MFC and illustrate this with an MFC DLL that lets you change the default look of windows running on your system to whatever you choose.

Automatic Subclassed Windows in MFC

With MFC, custom controls and standard windows inheriting from MFC's CWnd class get subclassed automatically. You just need to handle messages appropriately with "message maps"--tables connecting messages to the member functions that handle them. For example, the CEdit and CListBox classes both inherit from CWnd. To subclass one of these classes and create your own custom control, you simply:

  • Make your new, derived class inherit from the base class you want; for example, CMaskedEdit from CEdit.
  • Write the message map for your derived class. The messages to be included in the map are usually evident from the functionality of the control you want to build. For instance, masked edit controls restrict the data input to specific formats, preventing the entry of invalid characters. Evidently, WM_CHAR needs to be on CMasked Edit's message map.
  • Write "message-handler" functions for your class. These are the member functions that handle the commands mapped to them in the message map.
  • Construct an instance of your new class and call its creation function. Note that the call to your class's creation function should eventually lead to one of CWnd's Create or CreateEx member functions (possibly through an intermediate call to your immediate base class's Create function) for subclassing to be automatic.
How does the class library implement subclassing? This isn't much of a mystery, really. CWnd's Create and CreateEx member functions call the Windows function SetWindowsHook to set up a window-procedure filtering hook (of the type WH_CALLWNDPROC) with a corresponding callback function. Windows ensures that this library callback function will get called by the system whenever the SendMessage function is invoked. This enables MFC to intercept, among others, all the WM_CREATE messages sent by the system, as in
Figure 1(a). When MFC's callback function gets a creation message, it subclasses the window by calling SetWindowLong to reset the window procedure to AfxWndProc, the window procedure for all windows in MFC. Figure 1(b) shows how AfxWndProc routes all Windows messages through the attached message map once the window is subclassed.

Windows MFC Does Not Subclass

So much for windows created by the MFC library. The interesting problem is that of subclassing windows created with CreateWindow or CreateWindowEx, the usual Windows API functions.

Assume you want to subclass the window myHwnd. One simple solution would be to declare a CWnd object and then use CWnd's Attach member function, as in Example 1(a). But this does not work because Attach does not subclass myHwnd, and myHwnd must be subclassed prior to the call to Attach. Fortunately, CWnd has a member function, SubclassWindow, that enables you to do exactly what you want. So you might be tempted to use SubclassWindow along the lines of Example 1(b).

But this will only get you into more trouble. Why? Remember that MFC itself subclasses all windows that it creates. On subclassing, MFC saves the window's original window procedure, its Super WNDPROC, so that this procedure can continue to process messages not handled by AfxWndProc's message map. Now, a CWnd class can be subclassed with SubclassWindow exactly once so that all objects of that class have exactly the same behavior. It is likely that the CWnd class has already been subclassed by MFC--typically, this would have happened when a CWnd object was created in your application, either explicitly or implicitly, by the MFC framework. If so, the myCWndObject.SubclassWindow(myHwnd) call in Example 1(b) fails to subclass myHwnd.

To get over this hurdle, derive a class from CWnd and make this class store its Super WNDPROC in a location different from that of CWnd. For this, the derived class has to override CWnd's GetSuperWndProcAddr function. This virtual function returns a pointer to a window procedure. For subclassed windows, this return value is a pointer to the original window procedure. Your derived class should override this function by declaring a static WNDPROC member variable, initialized to NULL, and returning a pointer to this static in GetSuperWndProcAddr, as in Example 1(c). Because the WNDPROC member variable is declared static, all subclassed windows belonging to the class CMyWnd share a single Super WNDPROC.

It always helps to see how things fit into a real program, so I'll step through a subclassing DLL that I developed with MFC. The DLL subclasses all windows, system-wide, that are not already attached to a CWnd object and that have a caption and a system-menu box (this includes both preexisting and newly created windows). To visually demonstrate the subclassing, the DLL replaces their default Windows-provided looks with a nonclient look that I want to experiment with.

In a nutshell, my DLL will:

  • Set the subclass style to be windows that have a caption bar and a system menu.
  • Dynamically subclass every visible subclass-style window running under Windows that is not already attached to a CWnd object, attaching each such window to a C++ object of a class CMyWnd (which inherits from MFC's CWnd class).
  • Install a window-procedure filtering hook (a Windows hook of the type WH_CALLWNDPROC) to catch every new window-creation message meant for a subclass-style window. This way, we can subclass, on the fly, any newly created subclass-style windows.
  • Intercept nonclient paint (WM_NCPAINT) messages before they reach their target subclass-style windows and call my own paint function, which gives subclassed windows the nonclient look that I fancy.

The Pieces of the Subclassing DLL

For easy readability, I've organized the source code for the DLL in three separate listings. Listing One contains some typedefs and globals for the DLL.

The code for CWinApp and the Windows hook is in Listing Two . In MFC, CWinApp is the base class from which you derive a Windows application object; this class provides member functions for initializing your application and for cleanup before termination. My DLL derives the class CSubclassDLL from CWinApp and overrides its InitInstance and ExitInstance member functions.

The InitInstance function, called during DLL initialization, sets up a window-procedure filtering hook with the exported callback function CallWndProc, which is called by the system whenever SendMessage is called. CallWndProc monitors all WM_NCCREATE messages--if it was meant for a subclass-style window (for example, my function FSubclassStyle returns TRUE for this window), it calls CMyWnd's MySubclassWindow member function to promptly subclass this newly created window on the fly. The ExitInstance function removes this hook before termination. Note that since there can be only one instance of a DLL, the InitInstance code is called just once and could equivalently be in an overridden InitApplication member function.

An MFC application needs to declare its derived CWinApp object at the global level; my DLL's CSubclassDLL object is simply called subclassDLL.

The CMyWnd and subclassing-related functions are in Listing Three . Each window that gets dynamically subclassed is attached to an object of my class CMyWnd, which inherits from MFC's CWnd. CMyWnd overrides CWnd's virtual GetSuperWndProcAddr function, as discussed previously. CMyWnd adds two new functions, MySubclassWindow and UnsubclassWindow. MySubclassWindow takes a window handle, subclasses the window if it does not have a CWnd object already attached to it, and if successful, returns a pointer to the CMyWnd object that it attached to the window. The UnsubclassWindow member function retrieves the Super WNDPROC pointer from GetSuperWndProcAddr, does a SetWindowLong to reset the Super WNDPROC as the window's window procedure, and calls Detach to detach the window handle from the CMyWnd object.

The DLL's exported function SetSubclassingState is used to start or terminate subclassing for this demo. This needs to be called from your driver program. To start subclassing, call SetSubclassingState with the fStart parameter set to TRUE. This results in a call to InitSubclassing, which, together with the recursive InitSubclassingHwnd function, does a depth-first scan on the system's window tree (starting from the desktop window), subclasses all the windows that belong to the subclass style, and calls the redraw function for those windows. When terminating subclassing, call SetSubclassState with fStart set to FALSE--this leads to a call to TerminateSubclassing, which provides the means to restore the system back to its pre-subclassed state (a good feature to have, too!). TerminateSubclassing, together with the recursive TerminateSubclassingHwnd function, scans depth-first for subclassed windows in the system's window tree, un-subclasses these by calling UnsubclassWindow, and calls their redraw function.

Finally, the message map of CMyWnd is set to include, among other messages, the WM_NCPAINT message. CMyWnd's OnNcPaint handler function calls MyPaint, which does my nonclient painting. Figure 2 shows what windows look like under the version of MyPaint that I use. Notice that the frame has a 3-D look, the title-bar font is different, and the nonclient area buttons use new visuals and are no longer in their default positions. Because the title bar, borders, caption-bar buttons, and so on, no longer conform to the Windows defaults, the message map needs to add the WM_NCHITTEST message, and the corresponding handler, OnNcHitTest, must return the correct value (HTCAPTION, HTMINBUTTON, and so on) to indicate the position of the cursor on our window's nonclient area. We also need to detect mouse clicks over our repositioned, nonclient area buttons ourselves, so the message map includes the WM_NCLBUTTONDOWN and WM_LBUTTONUP messages. To do this, I check in CMyWnd's OnNcLButtonDown handler to see if the mouse-down was on one of the repositioned buttons; if so, I set a flag and capture the mouse. In the OnLButtonUp handler function, I check if the mouse-up occurred on a button, and from the flags value, I determine if the mouse-up occurred on the same button that we recorded the mouse-down on. If it did, a mouse click occurred, so I do whatever is appropriate for that button. The code for MyPaint and the OnNcHitTest, OnNcLButtonDown, and OnLButtonUp handler functions is not shown in the listings, but you can customize these functions to create any window look or nonclient button positions (and functionality) of your own choice.

Figure 1 (a) Message path after MFC's hook has been set up, before the window is subclassed; (b) message path after MFC subclasses the window.

Example 1: (a) Declaring a CWnd object and using CWnd's Attach member function to subclass the window myHwnd; (b) using SubclassWindow to subclass myHwnd; (c) declaring a static WNDPROC member variable to override a function.

(a) CWnd myCwndObject;
    myCWndObject.Attach (myHwnd);

(b) CWnd myCwndObject;
    myCWndObject.SubclassWindow (myHwnd);

(c) class CMyWnd : public CWnd
    {
    protected:
             static WNDPROC lpfnSuperWndProc;
             virtual WNDPROC* GetSuperWndProcAddr()
                             { return &lpfnSuperWndProc; }
    }
    WNDPROC CMyWnd::lpfnSuperWndProc = NULL; CMyWnd myCwndObject;
    myCWndObject.SubclassWindow (myHwnd);

Figure 2 New-look subclassed windows.

Listing One


// A couple of typedefs
typedef struct tagCWPSTRUCT
        {
        LPARAM    lParam;
        WPARAM    wParam;
        UINT msg;
        HWND hWnd;
        }
CWPSTRUCT;

typedef CWPSTRUCT FAR* LPCWPSTRUCT;

// Global handle of the installed Windows hook
HHOOK vhHookCallWnd = NULL;

// The subclass-style
const long lSubclassStyle = (WS_CAPTION | WS_SYSMENU);


Listing Two


// Our CWinApp class, and the Windows hook related stuff

// CSubclassDLL is our CWinApp class
class CSubclassDLL : public CWinApp
{
public:
        virtual BOOL InitInstance();
        virtual int ExitInstance();
        
        // No special code for the constructor
        CSubclassDLL(const char* pszAppName) : CWinApp (pszAppName) { }
};

// Our global CWinApp object
CSubclassDLL NEAR subclassDLL("subclass.dll");

// InitInstance is called on DLL initialization, it sets up the Windows hook.
BOOL CSubclassDLL::InitInstance() {
 HMODULE hmodule=::GetModuleHandle((LPCSTR)MAKELONG(AfxGetInstanceHandle(),0));
        vhHookCallWnd = ::SetWindowsHookEx(WH_CALLWNDPROC,
             (HOOKPROC)CallWndProc, hmodule, NULL);
        return (vhHookCallWnd != NULL);
}               
// ExitInstance removes the hook if there was one.
int CSubclassDLL::ExitInstance()  
{
        if (vhHookCallWnd != NULL)
             ::UnhookWindowsHookEx(vhHookCallWnd);
        return CWinApp::ExitInstance(); 
}
// The hook callback function
LRESULT CALLBACK AFX_EXPORT CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
        LPCWPSTRUCT lpCall;
        LPCREATESTRUCT lpcs;
        CMyWnd *pMyWnd;
        if (code < 0)
             {
             CallNextHookEx (vhHookCallWnd, code, wParam, lParam);
             }
        else
             {
             lpCall = (LPCWPSTRUCT) lParam;
             switch (lpCall->msg)
                  {
                  case WM_CREATE:
                       lpcs = (LPCREATESTRUCT) lpCall->lParam;
                       if (FSubclassStyle(lpcs->style)) 
                            { 
                                 HWND hwnd;
                                 hwnd = lpCall->hWnd;
                                 pMyWnd = CMyWnd::MySubclassWindow(hwnd);
                            }                   
                       break;
                  }
             }
        return 0L;
}


Listing Three


// Subclassing functions and the class CMyWnd

// FSubclassStyle determines if lStyle conforms to our subclass-style
BOOL FSubclassStyle (LONG lStyle)
{
        return ((lStyle & lSubclassStyle) == lSubclassStyle);
}
// SetSubclassingState is the exported function that needs to 
//  be called from your driver program to begin or terminate subclassing
void FAR PASCAL __export SetSubclassingState(BOOL fStart)
{                  
        if (fStart)   
             InitSubclassing();
        else
             TerminateSubclassing();
}

// InitSubclassing, along with InitSubclassingHwnd, enumerates all 
//  the windows and subclasses relevant ones. void InitSubclassing()
{
        HCURSOR hcurSave;
        HWND hwnd;
        hcurSave = SetCursor(LoadCursor(NULL, IDC_WAIT));
        hwnd = GetDesktopWindow();
        InitSubclassingHwnd(hwnd);
        SetCursor(hcurSave);
}
void InitSubclassingHwnd(HWND hwndParent)
{
        HWND hwndChild = GetWindow(hwndParent, GW_CHILD);
        static CMyWnd * pMyWnd;
        while (hwndChild)
             {
             long lStyle = GetWindowLong(hwndChild, GWL_STYLE);
             BOOL fActive;
             
             if (FSubclassStyle(lStyle) && IsWindowVisible (hwndChild))
                  {
                  pMyWnd = CMyWnd::MySubclassWindow(hwndChild);
                  if (pMyWnd != NULL)
                       pMyWnd->RedrawWindow(NULL, NULL, 
                                      RDW_INVALIDATE | RDW_FRAME);
                  }
             InitSubclassingHwnd(hwndChild);
             hwndChild = GetWindow (hwndChild, GW_HWNDNEXT);
             }
}
// TerminateSubclassing, along with TerminateSubclassingHwnd, 
//   enumerates all the windows and un-subclasses relevant ones.
void TerminateSubclassing()
{
        HCURSOR hcurSave;
        HWND hwnd;
        
        hcurSave = SetCursor(LoadCursor(NULL, IDC_WAIT));
        
        hwnd = GetDesktopWindow();
        TerminateSubclassingHwnd(hwnd);
        
        SetCursor(hcurSave);
}
void TerminateSubclassingHwnd(HWND hwndParent)
{
        HWND hwndChild = GetWindow(hwndParent, GW_CHILD);
        static CMyWnd * pMyWnd;
        
        while (hwndChild)
             {                  
                  if ((pMyWnd = (CMyWnd *) 
                                 CWnd::FromHandlePermanent(hwndChild)) != NULL)
                  {
                  pMyWnd->UnsubclassWindow();
                  if (IsWindowVisible (hwndChild))                                  RedrawWindow(hwndChild, NULL, NULL, RDW_INVALIDATE | 
                                                                    RDW_FRAME);
                  }
             TerminateSubclassingHwnd(hwndChild);
             hwndChild = GetWindow (hwndChild, GW_HWNDNEXT);
             }
}
// The class CMyWnd and its member functions
class CMyWnd : public CWnd
{
protected:
        static WNDPROC lpfnSuperWndProc;
        virtual WNDPROC* GetSuperWndProcAddr();                  
        void OnNcPaint();
public:
        CMyWnd* MySubclassWindow(HWND hwnd);
        BOOL UnsubclassWindow();
};
WNDPROC CMyWnd::lpfnSuperWndProc = NULL;
WNDPROC* CMyWnd::GetSuperWndProcAddr() 
{ 
        return &lpfnSuperWndProc; 
}
// CMyWnd::MySubclassWindow subclasses hwnd if it is doesn't already have a
//   CWnd attached to it. If successful, it returns a pointer to the CMyWnd 
//   object that it attached to the window.
CMyWnd* CMyWnd::MySubclassWindow(HWND hwnd)
{                          
        CMyWnd * pMyWnd = NULL; 

        if (CWnd::FromHandlePermanent(hwnd) == NULL)
             { 
             pMyWnd = new CMyWnd;
             pMyWnd->SubclassWindow(hwnd);
             }              
        return pMyWnd;
}                              
// CMyWnd::UnsubclassWindow un-subclasses the window
BOOL CMyWnd::UnsubclassWindow()
{
        WNDPROC* lplpfn;
        WNDPROC  oldWndProc;
        RedrawWindow(NULL, NULL, RDW_FRAME | RDW_INVALIDATE | 
                                               RDW_UPDATENOW | RDW_NOCHILDREN);
        lplpfn = GetSuperWndProcAddr();
        oldWndProc =  (WNDPROC) ::SetWindowLong (m_hWnd, GWL_WNDPROC, 
                                                           (DWORD) (*lplpfn));
        Detach();
        
        return TRUE;
}
// CMyWnd's message map
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
        ON_WM_NCPAINT()
        ON_WM_NCHITTEST() 
        ON_WM_NCLBUTTONDOWN()
        ON_WM_LBUTTONUP()
END_MESSAGE_MAP()
// CMyWnd's WM_NCPAINT message handler
afx_msg void CMyWnd::OnNcPaint ()
{   
        MyPaint();
}


Copyright © 1994, 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.