Click here to Skip to main content
13,549,157 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

15.5K views
25 bookmarked
Posted 11 Dec 2014
Licenced CPOL

Custom Controls in Win32 API: Encapsulation of Customized Controls

, 11 Dec 2014
Rate this:
Please Sign up or sign in to vote.
Few techniques for getting rid of customized-control-specific code from parent window procedure.

Articles in this series

Introduction

The last time, we have discussed some techniques for customization of controls. However we saw that more or less all the techniques require some level of direct cooperation between the code customizing the control and the procedure of the parent window (usually the dialog or top-level window). If you briefly take a look on the customization techniques presented in the previous article, you can see that owner drawing, custom drawing and notifications even require by design an active cooperation of the parent window procedure.

At the first glance, superclassing and subclassing look better from this point of view. But in practice, complex customization need to follow changes in the underlying control logic. For example, if the superclassed or subclassed control overrides WM_PAINT, it may need to be informed when internal state of the control changes so it can paint the control in the right way. Usually the underlying control informs the world by sending the notification to its parent window, so the easiest solution is to catch the notification in the parent and resend it back to the customized control so it's customized window procedure can handle it. Hence even superclassing and subclassing share the problem as well.

That has a fundamental impact: Reuse of such controls in other dialogs or applications is more complicated, as the logic from the parent window needs to be reimplemented in new one. Hence, we will try to address this problem in this article.

Software engineers already recognized many years ago that encapsulation of logically related code is a desired property of a reusable code. In OOP (object-oriented programming) paradigm, the encapsulation is even one of the most important and central points. The encapsulation allows to easily take the reusable code and reuse it in other module of the application, or even outside of it (e.g. to a standalone library, so it can even be reused in other applications). It is also much easier to read, review and maintain such code, as it allows a reader to mentally split the application code into smaller, palatable bits.

Although OOP guides and manuals speak mainly about classes, I believe the concept is very valuable on all application design levels: When breaking the application into the core program and set of libraries, when splitting the code into source files with understandable public interface and, in our context, also the customized controls.

I do not say that encapsulating every Win32 control you ever customize is a good idea. If you have a single instance of a customized control in your application, the given control is logically bound to the parent window and you know you will never need to reuse the control elsewhere, feel free to be lazy. Actually the encapsulation code could be more complex then the customization itself and it may not be worth of the gains.

But if your customized control needs to be reused in multiple dialogs, if its handling in parent window's code is complex, if the dialog procedure is complicated on its own, if your dialog procedure needs to understand internal details of logic in the control although it has nothing to do with the higher logic of the application, then the encapsulation may provide you interesting benefits.

So, what does the encapsulation for the customized control actually means? It means that every logic we want to mentally understand as an implementation of the control, goes out from the parent window procedure, that it is separated from it. If you once more take a look on all customization techniques presented in the previous article, you may notice that the parent window procedure actually has to handle some notifications sent by the customized control.

Wrapper Window

The obvious solution of the problem is to wrap the customized control in another window, wrapping the control to the outside world.

The wrapper window is parent of the core control we customize and it implements all the stuff which has to be in parent window procedure of a customized control. I.e. it tracks the notifications from the customized control (the original control procedure) and it can also cooperate tightly with the subclass or superclass procedure.

To the outside world, the wrapper simply is the control. From point of the view if the application, the child of the wrapper which does majority of the work is just an implementation detail of the control and the application should not care about it at all.

So the wrapper actually serves as a proxy between the application (the dialog procedure) and the core control (the child window). I.e. the wrapper has to understand all messages which constitute the public API of the customized control and (in most cases) it re-sends them to the child. Similarly, the wrapper resends all publicly visible notifications sent by the child up to application.

As straightforward as this approach is, it is also somewhat clumsy and inelegant: The existence of the wrapper just means an unnecessary layer of indirection in your application, which eats some system resources associated with HWND handle, yet it actually does no useful work with the exception of some wiring. Additionally it makes the tree hierarchy of all windows one level deeper, which may impose some negative implications for utilities looking at the hierarchy via Windows Automation API or Accessibility API.

Message Reflection

Better way how to deal with that problem is resending the notifications (and other notification-like messages) from the dialog procedure back to the control itself and handle them directly in superclass or subclass procedure instead of the wrapper window. In the world of Win32 development, this technique is so common that it has its own name: message reflection.

The Win32API header olectl.h contains the following preprocessor macro definitions:

#define OCM__BASE             (WM_USER+0x1c00)
#define OCM_COMMAND           (OCM__BASE + WM_COMMAND)
 
#define OCM_CTLCOLORBTN       (OCM__BASE + WM_CTLCOLORBTN)
#define OCM_CTLCOLOREDIT      (OCM__BASE + WM_CTLCOLOREDIT)
#define OCM_CTLCOLORDLG       (OCM__BASE + WM_CTLCOLORDLG)
#define OCM_CTLCOLORLISTBOX   (OCM__BASE + WM_CTLCOLORLISTBOX)
#define OCM_CTLCOLORMSGBOX    (OCM__BASE + WM_CTLCOLORMSGBOX)
#define OCM_CTLCOLORSCROLLBAR (OCM__BASE + WM_CTLCOLORSCROLLBAR)
#define OCM_CTLCOLORSTATIC    (OCM__BASE + WM_CTLCOLORSTATIC)
 
#define OCM_DRAWITEM          (OCM__BASE + WM_DRAWITEM)
#define OCM_MEASUREITEM       (OCM__BASE + WM_MEASUREITEM)
#define OCM_DELETEITEM        (OCM__BASE + WM_DELETEITEM)
#define OCM_VKEYTOITEM        (OCM__BASE + WM_VKEYTOITEM)
#define OCM_CHARTOITEM        (OCM__BASE + WM_CHARTOITEM)
#define OCM_COMPAREITEM       (OCM__BASE + WM_COMPAREITEM)
#define OCM_HSCROLL           (OCM__BASE + WM_HSCROLL)
#define OCM_VSCROLL           (OCM__BASE + WM_VSCROLL)
#define OCM_PARENTNOTIFY      (OCM__BASE + WM_PARENTNOTIFY)
#define OCM_NOTIFY            (OCM__BASE + WM_NOTIFY)

As you can see, all Win32API notification and notification-like messages do have its counterpart with the OCM_ prefix, whose numeric ID is increased by OCM__BASE.

Using these macros, implementing the message reflection in your parent window procedure is then quite straightforward:

  1. The parent window's code needs to handle the standard WM_xxxx messages in the list above.
  2. If the message is sent by a customized control who wants to handle the message itself, the parent window needs to send the corresponding OCM_xxxx message (with the original WPARAM and LPARAM values) to the control.
  3. The superclass or subclass procedure of the control should handle the OCM_xxxx message, or just return zero.

The only unclear thing may be the point 2: How can the parent know which controls expect some messages to be reflected? In simple dialogs with few controls, you can simply know IDs of few controls which need such treatment and hardcode them in the parent's procedure.

But there is also simple generic way: Assuming all the control are well-designed and they reserve the range of listed OCM_xxxx for the purpose of message reflection, the parent may by default resend all such messages to any control when it has no better idea what to do with the message. (Note that reserving the range 0x2000 (OCM_BASE) through 0x23ff (OCM_BASE+WM_USER-1) was suggested in Custom Controls in Win32 API: The Basics.)

When sent to such polite uncustomized control which does not handle the OCM_xxxx messages, it is just passed to DefWindowProc(). That function has no idea what to do with OCM_xxxx messages, so it does nothing and just returns zero.

Below is a code snippet showing how the reflection can be handled. Surely, the reflection could be scattered in all relevant message handlers directly, but the code below isolates the reflection to a single place, the DefParentProc(), which is our wrapper of standard DefWindowProc(), and which can be reused by all parent window procedures in your application.

#include <olectl.h>

static LRESULT
DefParentProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        case WM_NOTIFY:
        {
            NMHDR* nmhdr = (NMHDR*) lParam;
            if(nmhdr->hwndFrom != NULL)
                return SendMessage(nmhdr->hwndFrom, uMsg + OCM__BASE, wParam, lParam);
            break;
        }

        // All of these provide the control's HHWND in LPARAM
        case WM_COMMAND:
        case WM_CTLCOLORBTN:
        case WM_CTLCOLOREDIT:
        case WM_CTLCOLORDLG:
        case WM_CTLCOLORLISTBOX:
        case WM_CTLCOLORMSGBOX:
        case WM_CTLCOLORSCROLLBAR:
        case WM_CTLCOLORSTATIC:
        case WM_VKEYTOITEM:
        case WM_CHARTOITEM:
            if(lParam != 0)
               return SendMessage((HWND) lParam, uMsg + OCM__BASE, wParam, lParam);
            break;

        // All of these provide ID of the control in WPARAM:
        case WM_DRAWITEM:
        case WM_MEASUREITEM:
        case WM_DELETEITEM:
        case WM_COMPAREITEM:
            if(wParam != 0) {
                HWND hwndControl = GetDlgItem(hwnd, wParam);
                if(hwndControl)
                    return SendMessage(hwndControl, uMsg + OCM__BASE, wParam, lParam);
            }
            break;

         // Note we do not reflect WM_PARENTNOTIFY -> OCM_PARENTNOTIFY as that 
         // usually does not make much sense.
     }
     
     return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK
MainWinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        case WM_COMMAND:
            switch(HIWORD(wParam)) {
                case ID_SOMECONTROL:
                    ...  // Handle some control explicitly
                    return 0;
                case ID_OTHERCONTROL:
                    ...  // Handle other control explicitly
                    return 0;
            }
            break;  // Propagate the message into DefParentProc() below.

        case WM_NOTIFY:
            switch(wParam) {
                case ID_SOMECONTROL:
                    ...  // Handle some control explicitly
                    return 0;
                case ID_OTHERCONTROL:
                    ...  // Handle other control explicitly
                    return 0;
            }
            break;  // Propagate the message into DefParentProc() below.

    
        ... // and so one for all messages the parent needs to handle explicitly
    }

    // Pass unhandled messages into DefParentProc()
    return DefParentProc(hwnd, uMsg, wParam, lParam);
}

Injected Message Reflection

Strictly speaking, the message reflection, as presented in the previous section, does not reach full code encapsulation. Although it can be realized in a generic way, the parent still has to provide some cooperation with the child controls (i.e. perform the reflection).

When the parent window procedure is not under our direct control, or when requirements for re-usability of the control are so high you want to avoid need for any special treatment of the control in parent window procedure, we ned to be able to get the notifications from the original control procedure in the customization (superclass or subclass) procedure.

With some more work, that can be achieved. And it may be achieved with technique you should already know from the previous article: The subclassing. The control may, during its creation (WM_NCCREATE), customize its parent window and force it to do the reflection.

However we need to be careful: We cannot blindly reflect messages for all child controls as we do not have any idea what the parent procedure handles and what it does not as we do not want to clash with it. In example below, we solve this by keeping a list of controls for which to do the reflection. The parent subclass procedure consults the list and if the HWND is present, it does the reflection instead of calling the original window procedure.

This has one side effect: The parent window cannot override the behavior, but arguably, it may even be an advantage: The customized control sees the notifications sent by the underlying original control and parent cannot break its logic in any way.

#include <olectl.h>

// A container keeping list of the HWND handles for controls which need the message reflection.
// (Its implementation is left to the reader as an exercise.)
typedef struct { ... } CONTROL_LIST;

CONTROL_LIST* ControlListCreate(void);
void ControlListDestroy(CONTROL_LIST* pList);
BOOL ControlListAppend(CONTROL_LIST* pList, HWND hWnd);
void ControlListRemove(CONTROL_LIST* pList, HWND hWnd);
BOOL ControlListContains(CONTROL_LIST* pList, HWND hWnd);
BOOL ControlListEmpty(CONTROL_LIST* pList);

// Parent window subclass procedure. For messages which may need the reflection, it consults
// the list of controls and, according to the result, it either does the reflection or calls
// the original parent window procedure.
static LRESULT CALLBACK
ParentSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwData)
{
    CONTROL_LIST* pControlList = (CONTROL_LIST*) dwData;
    UINT i;

    // Reflect the message (if needed):
    switch(uMsg) {
        case WM_NOTIFY:
        {
            NMHDR* nmhdr = (NMHDR*) lParam;

            if(ControlListContains(pControlList, nmhdr->hwndFrom))
                return SendMessage(nmhdr->hwndFrom, uMsg + OCM__BASE, wParam, lParam);
            break;
        }

        // All of these provide the control's HHWND in LPARAM
        case WM_COMMAND:
        case WM_CTLCOLORBTN:
        case WM_CTLCOLOREDIT:
        case WM_CTLCOLORDLG:
        case WM_CTLCOLORLISTBOX:
        case WM_CTLCOLORMSGBOX:
        case WM_CTLCOLORSCROLLBAR:
        case WM_CTLCOLORSTATIC:
        case WM_VKEYTOITEM:
        case WM_CHARTOITEM:
            if(ControlListContains(pControlList, (HWND) lParam)
                return SendMessage((HWND) lParam, uMsg + OCM__BASE, wParam, lParam);
            break;

        // All of these provide ID of the control in WPARAM:
        case WM_DRAWITEM:
        case WM_MEASUREITEM:
        case WM_DELETEITEM:
        case WM_COMPAREITEM:
            if(wParam != 0) {
                HWND hwndControl = GetDlgItem(hwnd, wParam);
                if(ControlListContains(pControlList, hwndControl)
                    return SendMessage(hwndControl, uMsg + OCM__BASE, wParam, lParam);
            }
            break;

        // Note we do not reflect WM_PARENTNOTIFY -> OCM_PARENTNOTIFY as that 
        // usually does not make much sense.
    }

    // If not reflected, call the original parent procedure:
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

// The implementation of the customized control. 
// Note that on creation/destruction we update the list of controls that need the message
// reflection and also we make sure that the parent is subclassed for the lifetime
// of the control. The subclass then does the reflection for us.
static LRESULT CALLBACK
ControlSuperclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        case WM_NCCREATE:
        {
            CREATESTRUCT* pCs = (CREATESTRUCT*) lParam;
            CONTROL_LIST* pControlList;

            // Ensure the parent window shall reflect notification to us:
            if(GetWindowSubclass(hWnd, ParentSubclassProc, 0, (DWORD_PTR*) &pControlList)) {
                if(!ControlListAppend(pControlList, hWnd))
                    return -1;
            } else {
                pControlList = ControlListCreate();
                if(pControlList == NULL)
                    return -1;
                ControlListAppend(pControlList, hWnd);
                SetWindowSubclass(hWnd, ParentSubclassProc, 0, (DWORD_PTR) pControlList);
            }

            // Break to CallWindowProc() so the underlying control is properly 
            // created and initialized:
            break;  
        }

        case WM_NCDESTROY:
        {
            LRESULT lRes;
            CONTROL_LIST* pControlList;

            lRes = CallWindowProc(lpfnOriginalProc, hWnd, uMsg, wParam, lParam);

            GetWindowSubclass(hWnd, ParentSubclassProc, 0, (DWORD_PTR*) &pControlList);
            ControlListRemove(pControlList, hWnd);
            if(ControlListEmpty(pControlList)) {
                ControlListDestroy(pControlList);
                RemoveWindowSubclass(GetParent(hWnd, ParentSubclassProc, 0));
            }
            return lRes;
        }

        // Customize the underlying control as desired. We may handle the reflected 
        // messages (OCM__xxx) as needed.
        ....  
    }

    // By default, propagate the message to the underlying control procedure:
    return CallWindowProc(lpfnOriginalProc, hWnd, uMsg, wParam, lParam);
}

Obviously, the presented code fails if you ever attempt to reparent the control to another parent window with SetWindowParent(). Such support could be added at the cost of more complexity but, arguably, it is not worth of it. Especially when taking into account that most standard controls do not support it as explained in Raymond Chen's blog post Why does my control send its notifications to the wrong window after I reparent it?.

Next Time: Scrolling Support and Non-client Area

With this article we leave the topic of the control customization finally behind.

Next time we will take a look how to add a scrollbar support into your controland how to paint in a non-client area (especially with the respect to the visual styles). At first it may seem as two quite unrelated topics, but the connection does make a sense if you realize the scrollbars are part of the non-client area.

So stay tuned, this series is not yet coming to an end although the delay since the previous article took much more time then I anticipated. Hopefully the delay of the next article will be much shorter Smile | :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Martin Mitáš
Team Leader
Czech Republic Czech Republic
No Biography provided

You may also be interested in...

Pro

Comments and Discussions

 
QuestionHow to create a control like windows standard control which can be shown on the toolbox pannel Pin
viqfoai19-Sep-17 16:59
memberviqfoai19-Sep-17 16:59 
QuestionHow much do you use Custom Controls in Win32 Pin
ToothRobber15-Dec-14 10:22
memberToothRobber15-Dec-14 10:22 
AnswerRe: How much do you use Custom Controls in Win32 Pin
Martin Mitáš15-Dec-14 22:56
professionalMartin Mitáš15-Dec-14 22:56 
GeneralRe: How much do you use Custom Controls in Win32 Pin
ToothRobber22-Dec-14 6:24
memberToothRobber22-Dec-14 6:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01-2016 | 2.8.180515.1 | Last Updated 11 Dec 2014
Article Copyright 2014 by Martin Mitáš
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid