Win32SDK自定义控件编写思路

xingyun86 2021-6-29 1549

原文链接:

https://www.codeproject.com/Articles/646482/Custom-Controls-in-Win-API-Control-Customization


Articles in this series

Introduction

This series is about implementation of custom controls. However, implementing new control from scratch is often a lot of work, and in many cases the desired effect can also be achieved by augmenting behavior, look or both of an existing control, often with much less effort. In today's article we will take a look on several techniques how to customize existing controls.

In addition, the topic is also interesting when you are implementing new control from scratch: Good controls allow the applicatoion some level of customization, and some customization techniques do require some support from the control itself. Hence, we will also discuss how to implement such support in a new control.

Note that there is no strict boundary between "customizing a control" and "using a control". The two cases overlap and different people may have different opinion where one ends and the other one begins. Actually, even setting of a control style (e.g. instructing the control to paint differently) can be understood as a simple example of control customization. For our purposes, we will use the term "control customization" for augmenting the control's look or behavior, which involves a (non-trivial) code on the application side.

The article shall present several customization techniques. They differ in many aspects, for example:

  • Whether they are specialized for a specific purpose (e.g. to only customize look of the control) or not,
  • whether they require special support from the control itself or not,
  • or whether they principally need cooperation of parent window (typically dialog) or not.

Owner Drawing

First customization technique we take a look at is owner drawing. The technique is useful only for changing how the particular control (or its item) is painted, and it can only be used for controls which implement a support for it.

Controls which provide such support, always have some knob, which can be used by the application to specify that it shall paint the control (or its item) on its own. Depending on the control, the knob can be a window style bit or a flag in some structure (e.g. structure describing an item of the control).

When the knob is used, it causes the control to send WM_DRAWITEM to its parent instead of the normal painting code when handling WM_PAINT or WM_PRINTCLIENT. Also, depending on the control, WM_MEASUREITEM can precede the WM_DRAWITEM.

For some controls, the knob enables the owner-drawing for the control as a whole, for others it enables it for some or all items.

When the owner drawing is enabled, the control may send WM_MEASUREITEM to let the application determine, how large the item (or items) are so it can handle the items properly (e.g. setup scrollbars and layout of items etc.). As a rule of thumb, if the owner-drawing overrides painting of the whole control, this message is not sent at all. If all the items are supposed to have the same size, then it is sent once, usually during control creation in the context of CreateWindow(). If the item size can differ, it is sent for each item in the control (e.g. when the item is being inserted).

The table below summarizes the standard controls that can be owner-drawn. It specifies the knob for enabling the owner drawing, whether the control sends WM_MEASUREITEM (and if it does, whether it is sent once for all items or if it is sent for each item), and whether WM_DRAWITEM is called to paint whole control or if it is called for every item the owner drawing applies to.

Control The Knob Description
Button ("BUTTON") Style BS_OWNERDRAW Sends WM_DRAWITEM to paint whole control.
Static ("STATIC") Style SS_OWNERDRAW Sends WM_DRAWITEM to paint whole control.
List box ("ListBox") Style LBS_OWNERDRAWFIXED Sends WM_MEASUREITEM once during control creation and WM_DRAWITEM on per-item basis.
Style LBS_OWNERDRAWVARIABLE Sends WM_MEASUREITEM as well as WM_DRAWITEM on per-item basis.
Combo box ("ComboBoxEx32") Style CBS_OWNERDRAWFIXED Sends WM_MEASUREITEM once during control creation and WM_DRAWITEM on per-item basis.
Style CBS_OWNERDRAWVARIABLE Sends WM_MEASUREITEM as well as WM_DRAWITEM on per-item basis.
List view ("SysListView32") (with style LVS_REPORT only) Style LVS_OWNERDRAWFIXED Sends WM_MEASUREITEM once during control creation and WM_DRAWITEM on per-item basis.
Tab control ("SysTabControl32") Style TCS_OWNERDRAWFIXED Sends WM_DRAWITEM on per-item basis. Note WM_MEASUREITEM is not sent, instead size specified by message TCM_SETITEMSIZE is used.
Header ("SysHeader32") Item flag HDF_OWNERDRAW Sends WM_DRAWITEM on per-item basis. Note WM_MEASUREITEM is not sent, instead item geometry data is used.
Status bar ("msctls_statusbar32") Flag SBT_OWNERDRAW (see message SB_SETTEXT) Sends WM_DRAWITEM on per-part basis. Note WM_MEASUREITEM is not sent, instead part geometry data is used.
Menu item Flag MFT_OWNERDRAW (see InsertMenuItem() or SetMenuItemInfo()) Sends WM_MEASUREITEM as well as WM_DRAWITEM on per-item basis.

This technique is quite simple for use as well as for implementation of owner drawing support in a new control: It is just about adding an if in WM_PAINT and WM_PRINTCLIENT handler and sending the mentioned messages to the parent window.

The most prominent limitation of the technique is quite obvious: You get all or nothing. Either the control (or the particular item) is painted completely by the control itself or - by applying the knob - the parent takes all the work on its own shoulder. There is no way how it can be used, for example, to just change a text color of some control item without reimplementation of all the painting code in parent window procedure.

Custom Drawing

Custom drawing is similar to owner drawing in that the control lets the parent to paint it or its part, but it gives the parent more liberty to cooperate with the control on the desired results. However more possibilities comes with a price: Custom drawing is more complex, both from the perspective of the control which supports it as well as from the perspective of an application which want to take advantage of it.

The basic idea is that the control sends the notification NM_CUSTOMDRAW multiple times, in various phases during painting of the control. Each time, the application has a possibility to augment the given painting phase to some degree, or (re)implement the phase on its own, and the application can also ask to get or not to get further more detailed notifications in subsequent drawing phases.

Parameter LPARAM of the notification is address of the structure NMCUSTOMDRAW, or (for some controls) some bigger structure having NMCUSTOMDRAW as its first member (in the same way as many notifications provide more data by sending a structure with NMHDR as its 1st member). Members of the structure are filled with default values/attributes the control wants to use during the given drawing phase. The application can override some of the attributes (e.g. font, some colors etc.) by resetting the appropriate structure member to another value, or it can even tell the control to skip the drawing phase altogether (so the application can paint something fairly different on its own).

The structure NMCUSTOMDRAW looks as follows:

C
typedef struct tagNMCUSTOMDRAWINFO {
    NMHDR     hdr;
    DWORD     dwDrawStage;
    HDC       hdc;
    RECT      rc;
    DWORD_PTR dwItemSpec;
    UINT      uItemState;
    LPARAM    lItemlParam;
} NMCUSTOMDRAW, *LPNMCUSTOMDRAW;

The member dwDrawStage is crucial for custom draw processing. In each draw stage, the application can customize different things. We shall discuss this further soon.

The member hdc is the device context the control uses for the painting, so the application can partly customize it (e.g. by selecting another font to it), or use it for its own painting.

The member rc specifies the bounding rectangle for the painting, depending on the current stage, but note it is only defined for a stage (CDDS_ITEM | CDDS_PREPAINT) and (since version 6) for CDDS_PREPAINT.

The member dwItemSpec specifies what item is being painted. The interpretation of the value depends on the control. For example standard tree-view control stores HTREEITEM here, while list-view stores the index of the item here.

The member uItemState is a bit mask describing the state of the item to be painted, so if the application overrides the painting of an item, it knows whether the item is selected, focused, disabled etc., and can paint the item accordingly. Refer to MSDN for all the flags. They are not that important for understanding how the stuff works.

The member lItemlParam is application-defined data associated with the item. Usually this is propagated from some per-item LPARAM value, usually specified when the item is inserted into the control, or later modified. For example the standard listview control propagates LVITEM::lParam here.

Last but not least, the return value of the notification NM_CUSTOMDRAW is also very important. The return value is actually a bit mask and relevant bits the application can return depend on the current draw stage. In some stages, the app can also ask for more finer grained notifications after the stage is over and/or notifications on a nested level (e.g. for each item or subitem), by returning a proper return value from the parent window procedure.

The table below lists the draw stages of a complex control, which has some items and subitems of a kind. Less complex control without items or subitems would just use fewer draw stages.

dwDrawStage Description
CDDS_PREERASE

Sent before the control is being erased.

The notification return value can use the following bits:

  • CDRF_DODEFAULT (0): Do the erasing normally.
  • CDRF_SKIPDEFAULT: Skip the erasing (the app. should erase instead).
  • CDRF_NOTIFYPOSTERASE: Asks control to also send CDDS_POSTERASE after the erasing.
CDDS_POSTERASE

Sent after the erasing, if the app. asked for it by returning CDRF_NOTIFYPOSTERASE.

Return value is ignored.

CDDS_PREPAINT

Sent before the control starts the painting.

The notification return value can use the following bits:

  • CDRF_DODEFAULT (0): Do the painting (on control level) normally.
  • CDRF_SKIPDEFAULT: Skip the whole painting, including painting of items and subitems (the app. should do it instead).
  • CDRF_DOERASE: The control only paints the background. (Only on Vista and newer.)
  • CDRF_SKIPPOSTPAINT: Skip painting of focus rectangle.
  • CDRF_NOTIFYITEMDRAW: Asks control to also send the custom draw notification for each item.
  • CDRF_NOTIFYPOSTPAINT: Asks control to also send CDDS_POSTPAINT after the control painting.
(CDDS_ITEM | CDDS_PREPAINT)

Sent before the control starts painting each item. Sent only if the CDDS_PREPAINT handler above used CDRF_NOTIFYITEMDRAW.

The notification return value can use the following bits:

  • CDRF_DODEFAULT (0): Do the painting of the item normally.
  • CDRF_SKIPDEFAULT: Skip the painting of the item, including painting of subitems (the app. should do it instead).
  • CDRF_NEWFONT: The app. selected another font in the provided device context and the control should use it instead for painting the item.
  • CDRF_NOTIFYSUBITEMDRAW: Asks control to also send the custom draw notification for each subitem.
  • CDRF_NOTIFYPOSTPAINT: Asks control to also send CDDS_POSTPAINT after the item painting.
(CDDS_SUBITEM | CDDS_PREPAINT)

Sent before the control starts painting each subitem. Sent only if the (CDDS_ITEM | CDDS_PREPAINT) handler above used CDRF_NOTIFYSUBITEMDRAW.

The notification return value can use the following bits:

  • CDRF_DODEFAULT (0): Do the painting of the subitem normally.
  • CDRF_SKIPDEFAULT: Skip the painting of the subitem (the app. should do it instead).
  • CDRF_NEWFONT: The app. selected another font in the provided device context and the control should use it instead for painting the subitem.
  • CDRF_NOTIFYPOSTPAINT: Asks control to also send CDDS_POSTPAINT after the subitem painting.
(CDDS_SUBITEM | CDDS_POSTPAINT)

Sent after the subitem painting, if the app. asked for it by returning CDRF_NOTIFYPOSTPAINT from handling of the (CDDS_SUBITEM | CDDS_PREPAINT).

Return value is ignored.

(CDDS_ITEM | CDDS_POSTPAINT)

Sent after the item painting, if the app. asked for it by returning CDRF_NOTIFYPOSTPAINT from handling of the (CDDS_ITEM | CDDS_PREPAINT).

Return value is ignored.

CDDS_POSTPAINT

Sent after the control painting, if the app. asked for it by returning CDRF_NOTIFYPOSTPAINT from handling of the CDDS_PREPAINT.

Return value is ignored.

Of course, you may also support the custom drawing in your custom controls, so apps can augment the control as they need. Such WM_PAINT handler may look like the following code skeleton presents. The code assumes the control uses items but not subitems. If your control needs also subitems, you have to simply add one more nested cycle with similar handling as the items get here:

C

static void
CustomPaint(HWND hwnd, HDC hDC, RECT* rcDirty, BOOL bErase)
{
    NMCUSTOMDRAW nmcd;      // The custom draw structure
    LRESULT cdControlMode;  // Return value of NM_CUSTOMDRAW for CDDS_PREPAINT
    LRESULT cdItemMode;     // Return value of NM_CUSTOMDRAW for CDDS_ITEM | CDDS_PREPAINT

    // Initialize members of the custom draw structure used for all the stages below:
    nmcd.hdr.hwndFrom = hwnd;
    nmcd.hdr.idFrom = GetWindowLong(hwnd, GWL_ID);
    nmcd.hdr.code = NM_CUSTOMDRAW;
    nmcd.hdc = hDC;

    if(bErase) {
        LRESULT cdEraseMode;

        // Send control pre-erase notification:
        nmcd.dwDrawStage = CDDS_PREERASE;
        cdEraseMode = SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);

        if(!(cdEraseMode & CDRF_SKIPDEFAULT)) {
            // Do the erasing:
            ...

            // Send control post-erase notification:
            if(cdEraseMode & CDRF_NOTIFYPOSTERASE) {
                nmcd.dwDrawStage = CDDS_POSTERASE;
                SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
            }
        }
    }

    // Send control pre-paint notification:
    nmcd.dwDrawStage = CDDS_PREPAINT;
    GetClientRect(hwnd, &nmcd.rc);
    cdControlMode = SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);

    if(!(cdControlMode & (CDRF_SKIPDEFAULT | CDRF_DOERASE))) {
        // Do the control (as a whole) painting
        // (e.g. some kind of background or frame)
        ...

        // Iterate through all control items.
        // (If the control does not support any items, just omit all this for-loop.)
        for(...) {
            // Send item pre-paint notification (if desired by the app):
            if(cdControlMode & CDRF_NOTIFYITEMDRAW) {
                nmcd.dwDrawStage = CDDS_ITEM | CDDS_PREPAINT;
                // Set some attributes describing the items. The app. can change
                // them to augment painting of the item:
                nmcd.rc = ...;

                // Identify the item (in per control specific way) so app. knows
                // what item is augmenting.
                nmcd.dwItemSpec = ...;

                // Tell app. if selection box, focus highlight etc. should be
                // painted for the item.
                nmcd.uItemState = ...;

                // Fill in also any data the app. may have associated with the
                // item so it can use them for augmenting of the control.
                nmcd.lItemParam = ...;

                // Send the notification.
                cdItemMode = SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
            } else {
                cdItemMode = CDRF_DODEFAULT;
            }

            // Do the item painting (unlesse suppressed by the app.)
            if(!(cdItemMode & CDRF_SKIPDEFAULT)) {
                // Note you should be ready for a case hdc may have different font selected
                // if (cdItemMode & CDRF_NEWFONT)). In such case you should reset it to
                // the default control font after this particular item is painted:
                ...

                // Do the item (as a whole) painting
                // (e.g. some kind of item background or frame)
                ...

                // If the item is composed from a set of subitems, another similar
                // nested loop would be here to handle them. You would also need
                // yet another variable (cdSubitemMode) and handle the subitems
                // in similar way. nmcd.dwDrawStage would just set CDDS_SUBITEM
                // instead of CDDS_ITEM.
                ...

                // Do item "post-painting"
                // (e.g. paint a focus rectangle if the item is selected and
                // control has a focus).
                ...

                // Send item post-paint notification:
                if(cdItemMode & CDRF_NOTIFYPOSTPAINT) {
                    nmcd.dwDrawStage = CDDS_ITEM | CDDS_POSTERASE;
                    SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
                }
            }
        }

        // Do control "post-painting":
        if(!(cdControlMode & CDRF_SKIPPOSTPAINT)) {
            // ...
        }

        // Send control post-paint notification:
        if(cdControlMode & CDRF_NOTIFYPOSTPAINT) {
            nmcd.dwDrawStage = CDDS_POSTERASE;
            SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
        }
    }
}

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        // ...

        case WM_ERASEBKGND:
            return FALSE;  // Defer erasing into WM_PAINT

        case WM_PAINT:
        {
            PAINTSTRUCT ps;
            BeginPaint(hwnd, &ps);
            CustomPaint(hwnd, ps.hdc, &ps.rcPaint, ps.fErase);
            EndPaint(hwnd, &ps);
            return 0;
        }

        case WM_PRINTCLIENT:
        {
            RECT rc;
            GetClientRect(hwnd, &rc);
            CustomPaint(hwnd, (HDC) wParam, &rc, TRUE);
            return 0;
        }

        // ...
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


(Note we handle control erasing directly in its painting handler. See one of the previous articles in this series, Custom Controls in Win32 API: The Painting, for the discussion about this. The pre-erasing and post-erasing notifications might be of course sent from WM_ERASEBKGND if the control would do erasing there instead.)

The screenshot below shows the power of the custom drawing with the standard list view control. Complete MSVC project of the demo application this image is taken from can be downloaded at the top of this article.

Custom draw demo screenshot

Ok, so that's about painting customization and now lets also take a look how to modify a behavior of a control.

Decision-Making Notifications

Many Windows standard controls send notifications which allow to modify some default logic of the control. In most cases, the notification defers some decision making how the control should behave in the current situation to the parent. Even with our definition of control customization, as involving non-trivial code on application side, it is a bit blurry whether this means using the control or customizing it. But lets talk at least a bit about it anyway.

An example of such notification in standard control is when user clicks to any (unselected) item in a tree view control. The control sends TVN_SELCHANGING to the parent window. When the parent returns zero from it, the control normally changes the selection (and then sends TVN_SELCHANGED notification). However when TVN_SELCHANGING returns non-zero, it instructs the control to suppress the change of selection.

This design pattern is used by many standard controls in many situations. When there is something what normally changes state of the control in a significant way, the control may want to allow some customization of that behavior and the control usually implements it in a way sketched by the following pseudo-code:

C

...
if(SendNotification(hwndParent, XXN_STATECHANGING, wParam, lParam) == 0) {
    // Do the control state change by modifying the control data
    // and (if needed) invalidate the control or its part to repaint
    // it so user can see the new control state.
    ...

    // Inform the parent the state has really changed.
    SendNotification(hwndParent, XXN_STATECHANGED, wParam, lParam);
}
...

Whenever you implement a reusable custom control, you should consider, whether an application might want sometimes to disable the default behavior, or hook some other customized functionality to the events. If so, do not be lazy and add the notifications to support it.

There is one thing I would like to highlight: The implementation should always work so that return value of zero from the XXN_STATECHANGING triggers a behavior which is considered to be default by the control. This is important for two reasons:

  • When parent does not handle the notification, it should pass it into DefWindowProc(), and DefWindowProc() returns zero for WM_NOTIFY. Hence the control should behave in the default way in such case.
  • It is also important for forward compatibility: Often such notification is added into the control implementation only in some later version. The "default behavior" should in general correspond to older behavior so old applications not aware of the new notification continue to behave the same way, and new applications can change the behavior by handling the notification.

Superclassing and Subclassing

Superclassing and subclassing are two techniques based on exchange of control window procedure. As such, these techniques have one very strong advantage: They both actually do not require any explicit support in the control being customized. However the power is not for free: You must be very careful to not break the logic of the original control procedure (especially if you don't have access to its source code).

Superclassing

Superclassing defines new window class, which is derived from an existing one. The following sample code provides a function for creating very simple superclassed control from a standard button.

C
#include <tchar.h>
#include <windows.h>

typedef struct SuperButtonData_tag SuperButtonData;
struct SuperButtonData_tag {
    // ... data for the superclass implementation
};


static WNDPROC lpfnButtonProc;
static int cbButtonExtra;

static LRESULT CALLBACK
SuperButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    SuperButtonData* lpData = (SuperButtonData*) GetWindowLongPtr(hwnd, cbButtonExtra);

    switch(uMsg) {
        // Handle all messages we want to customize:
        ...

        case WM_NCCREATE:
            if(!CallWindowProc(lpfnButtonProc, hwnd, uMsg, wParam, lParam))
                return FALSE;
            lpData = (SuperButtonData*) malloc(sizeof(SuperButtonData));
            if(lpData == NULL)
                return FALSE;
            ... // Setup the lpData structure
            SetWindowLongPtr(hwnd, cbButtonExtra, (LONG_PTR) lpData);
            return TRUE;

        case WM_NCDESTROY:
            if(lpData)
                free(lpData);
            break;
    }

    // Instead of DefWindowProc(), we propagate messages into the original
    // button procedure for their default handling. Note it has to be called
    // indirectly via CallWindowProc().
    return CallWindowProc(lpfnButtonProc, hwnd, uMsg, wParam, lParam);
}

void
RegisterSuperButton(void)
{
    WNDSCLASS wc;

    GetClassInfo(NULL, _T("BUTTON"), &wc);

    // Remember some original data.
    lpfnButtonProc = wc.lpfnWndProc;
    cbButtonExtra = wc.cbWndExtra;

    // Name our class differently.
    wc.lpszClassName = _T("SUPERBUTTON");

    // We register the class as local one for the .EXE module calling this function.
    wc.style &= ~CS_GLOBALCLASS;
    wc.hInstance = GetModuleHandle(NULL);

    // Add few more extra bytes for our own purposes.
    wc.cbWndExtra += sizeof(SuperButtonData*);

    // Set our own window procedure.
    wc.lpfnWndProc = SuperButtonProc;

    // Finally, register the new class.
    RegisterClass(&wc);
}

As you can see, instead of initializing new WNDCLASS, we start with an existing window class, provide new window class name and augmenting some existing window class attributes. In particular we make the new window class use our window procedure (which can call the original one as the sample code demonstrates) and we also increase wc.cbWndExtra so our own data can be stored there. Plase pay special attention to the handling of wc.cbWndExtra. The original window class likely stores some data in the extra bytes, so we must be careful to not rewrite them. We store the original wc.cbWndExtra value and use it as an offset, where our own data are stored.

Subclassing

Subclassing works differently: It changes the window procedure of an existing control. This is fundamentally different approach: Superclassing defines new recipe (i.e. window class) for creating an arbitrary count of (customized) controls with CreateWindow() while subclassing changes single existing control instance referred with a given HWND handle.

Although this approach seems dirtier then the superclassing, sometimes it is very useful: you can modify look or behavior of a control you did not create (e.g. customizing control in a common control dialog, or a dialog created by a 3rd party DLL your application uses).

There are two ways how to implement it:

  • Using the specialized API for this purpose, i.e. the functions SetWindowSubclass() and its relatives.
  • The old legacy way is to manually reset pointer to window procedure with SetWindowLongPtr(GWLP_WNDPROC).

Lets start with the legacy way:

C
#include <tchar.h>
#include <windows.h>

typedef struct SubButtonData_tag SubButtonData;
struct SubButtonData_tag {
    WNDPROC lpfnButtonProc;
    // ... data for the subclass implementation
};

static LPCTSTR pstrSubButtonId = _T("SUBBUTON");


static LRESULT CALLBACK
SubButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    SubButtonData* lpData = (SubButtonData*) GetProp(hwnd, pstrSubButtonId);

    switch(uMsg) {
        // Handle all messages we want to customize:
        ...
    }

    // Instead of DefWindowProc(), we propagate messages into the original
    // button procedure for their default handling. Note it is called
    // indirectly via CallWindowProc().
    return CallWindowProc(lpData->lpfnButtonProc, hwnd, uMsg, wParam, lParam);
}

void
SubclassButton(HWND hwnd)
{
    SubButtonData* lpData;

    lpData = (SubButtonData*) malloc(sizeof(SubButtonData));
    lpData->lpfnButtonProc = (WNDPROC) GetWindowLongPtr(hwnd, GWLP_WNDPROC);

    // Setup subclass data as needed.
    ...

    SetProp(hwnd, pstrSubButtonId, lpData);
    SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR) SubButtonProc);
}

void
UnsubclassButton(HWND hwnd)
{
    SubButtonData* lpData = (SubButtonData*) GetProp(hwnd, pstrSubButtonId);
    SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR) lpData->lpfnButtonProc);
    RemoveProp(hwnd, pstrSubButtonId);
    free(lpData);
}

The code is simple enough to understand without any discussion. But if you take a closer look on the code above, few issues can be observed:

  • If a single control is subclassed multiple times, GetWindowLongPtr(GWLP_WNDPROC) as called from the 2nd subclass gets pointer to window procedure of the 1st subclass. If 1st subclass decides to uninstall itself, 2nd subclass is not aware of it and still propagates messages to the window procedure it remembers: i.e. the procedure of 1st subclass. That will likely lead to a crash as 1st subclassed has uninstalled and likely released all resources required for its functioning.
  • There is no place where to store subclass specific data and hence the code uses SetProp() and GetProp(). These are much less effective then (e.g.) the extra bytes (as specified by window class). The window property functions are designed to store arbitrary count of data slots with the window so they are quite complex under the hood.

The SetWindowSubclass() based API solves both of these issues (assuming all subclasses of the single control instance use this API) and therefore it should be preferred. However it is only available since version 6 or newer (i.e. on Windows XP, and only for application specifying its compatibility with version 6 of the library as discussed earlier in Custom Controls in Win32 API: Visual Styles.)

Using the API, the code example above can be rewritten as follows:

C
#include <tchar.h>
#include <windows.h>
#include <commctrl.h>

typedef struct SubButtonData_tag SubButtonData;
struct SubButtonData_tag {
    // ... data for the subclass implementation
};

// The API treats the pair (uSubclassId, SubButtonProc) as unique identification
// of the subclass. Assuming we do not need multiple subclass levels of
// the same control (which would share the subclass procedure), we do not need
// to deal much with the ID. Only if we would need such esoteric generality, we
// would use it for distinguishing among the subclasses.
static UINT_PTR uSubButtonId = 0;


static LRESULT CALLBACK
SubButtonProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uSubclassId, DWORD_PTR dwData)
{
    SubButtonData* lpData = (SubButtonData*) dwData;

    switch(uMsg) {
        // Handle all messages we want to customize:
        ...
    }

    return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}

void
SubclassButton(HWND hwnd)
{
    SubButtonData* lpData;

    lpData = (SubButtonData*) malloc(sizeof(SubButtonData));

    // Setup subclass data as needed.
    ...

    SetWindowSubclass(hwnd, SubButtonProc, uSubButtonId, (DWORD_PTR) lpData);
}

void
UnsubclassButton(HWND hwnd)
{
    SubButtonData* lpData;

    GetWindowSubclass(hwnd, SubButtonProc, uSubButtonId, (DWORD_PTR*) &lpData);
    RemoveWindowSubclass(hwnd, SubButtonProc, uSubButtonId);
    free(lpData);
}

Difficulties

As I already noted in the introduction of this section, superclassing and subclassing require a big amount of care: Consider that whenever a control is implemented, its procedure handles a number of messages and usually many messages are supposed to play some part in a concert. For example, if the control responds to some mouse clicks, it likely handles button-down events as well as button-up events. If the new window procedures propagates one to the original procedure, but not the other, the internal state of the underlying control can easily end in an inconsistent state. Such concert of multiple message handlers in non-trivial controls can be played by dozen of messages and it is easy to introduce subtle bugs by overriding such messages.

This is especially complicated if you attempt to customize a control which evolves in a time. For example, if you customize a standard control in a dialog, you need to take into account that there are different versions of and in various Windows versions, and that the control implementation evolves in them. Newer versions may have more features, new bugs, provide fixes of old bugs, and other subtle changes in their behavior and implementation.

Another difficulty is about notifications. Often, the derived window procedure may need to know when and how the underlying control state changes. The underlying window procedure usually informs the world about such change by sending a notification message to its parent. And that's the problem: The code of superclass/subclass window procedure cannot get the notification and hence you cannot easily react to the change of the control state, nor customize the notification. This often means that your application needs to send the notification back to the control in some way so the customized window procedure can react to it accordingly. We shall take a look on this problem in next article of this series.

To conclude, programming is about trade-offs: The techniques described in this section are indeed very powerful, but use of the power requires a big deal of responsibility and care, and it has also its limitations. Before you start customizing of a control, think what the original control does, how it is (likely) implemented, and how it can clash with your window procedure. And last but not least, test your new control against all relevant versions of the underlying control.

Real World Code

In previous articles, I usually provided some links to a real world code for studying of the topic presented in the article. This article is no exception.

Owner drawing in some standard control (re)implementation of the Wine project:

Custom drawing in some standard control (re)implementation of the Wine project:

Custom drawing support in a non-standard control implementation in mCtrl:

Superclassing used in Wine's (re)implementation of to provide support for visual themes for the base controls living in :

Superclassing in mCtrl to backport some features of standard buttons to older Windows versions (themed BS_ICON on Windows XP, and BS_SPLITBUTTON on Windows 2000 and XP):

Subclassing in Wine, for customizing the standard edit control for label editing in some more complex controls (e.g. list view) or for 4 IP components in IP address control:

Next Time: Customized Control Encapsulation

Today we took a look at several ways how to customize existing control rather then implementing a new one from scratch. We saw that it usually results in moving some code into the parent window's procedure.

That can be problematic for easy reuse of such controls in other dialogs, or even in context of other applications. The problem is even bigger if we are not in control of the parent window's code.

Therefore the next article will continue where this one stops and it will be dedicated to this problem, and describe few possibilities how to address it in your code.


上传的附件:
×
打赏作者
最新回复 (0)
只看楼主
全部楼主
返回