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