原文链接:
https://www.codeproject.com/Articles/646482/Custom-Controls-in-Win-API-Control-Customization
Articles in this series
- Custom Controls in Win32 API: The Basics
- Custom Controls in Win32 API: The Painting
- Custom Controls in Win32 API: Visual Styles
- Custom Controls in Win32 API: Standard Messages
- Custom Controls in Win32 API: Control Customization
- Custom Controls in Win32 API: Encapsulation of Customized Controls
- Custom Controls in Win32 API: Scrolling
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:
typedef struct tagNMCUSTOMDRAWINFO { NMHDR hdr; DWORD dwDrawStage; HDC hdc; RECT rc; DWORD_PTR dwItemSpec; UINT uItemState; LPARAM lItemlParam; } NMCUSTOMDRAW, *LPNMCUSTOMDRAW;
hdr
is the common notification header, so nothing interesting here.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:
|
CDDS_POSTERASE | Sent after the erasing, if the app. asked for it by returning Return value is ignored. |
CDDS_PREPAINT | Sent before the control starts the painting. The notification return value can use the following bits:
|
(CDDS_ITEM | CDDS_PREPAINT) | Sent before the control starts painting each item. Sent only if the The notification return value can use the following bits:
|
(CDDS_SUBITEM | CDDS_PREPAINT) | Sent before the control starts painting each subitem. Sent only if the The notification return value can use the following bits:
|
(CDDS_SUBITEM | CDDS_POSTPAINT) | Sent after the subitem painting, if the app. asked for it by returning Return value is ignored. |
(CDDS_ITEM | CDDS_POSTPAINT) | Sent after the item painting, if the app. asked for it by returning Return value is ignored. |
CDDS_POSTPAINT | Sent after the control painting, if the app. asked for it by returning 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()
, andDefWindowProc()
returns zero forWM_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.
#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:
#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()
andGetProp()
. 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:
#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:
- Button control: https://github.com/mirrors/wine/blob/master/dlls/user32/button.c
- Listbox control: https://github.com/mirrors/wine/blob/master/dlls/user32/listbox.c
- and many other controls in .
Custom drawing in some standard control (re)implementation of the Wine project:
- Header control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/header.c
- Listview control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/listview.c
- Rebar control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/rebar.c
- Treeview control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/treeview.c
- and many other controls in .
Custom drawing support in a non-standard control implementation in mCtrl:
- Tree-list view control: https://github.com/mity/mctrl/blob/master/src/treelist.c
Superclassing used in Wine's (re)implementation of to provide support for visual themes for the base controls living in :
- Themed button: https://github.com/mirrors/wine/blob/master/dlls/comctl32/theme_button.c
- Themed combo: https://github.com/mirrors/wine/blob/master/dlls/comctl32/theme_combo.c
- Themed edit: https://github.com/mirrors/wine/blob/master/dlls/comctl32/theme_edit.c
- Themed listbox: https://github.com/mirrors/wine/blob/master/dlls/comctl32/theme_listbox.c
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):
- Button subclass: https://github.com/mity/mctrl/blob/master/src/button.c
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:
- List view control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/listview.c
- IP address control: https://github.com/mirrors/wine/blob/master/dlls/comctl32/ipaddress.c
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.