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:
typedef struct tagNMCUSTOMDRAWINFO {
NMHDR hdr;
DWORD dwDrawStage;
HDC hdc;
RECT rc;
DWORD_PTR dwItemSpec;
UINT uItemState;
LPARAM lItemlParam;
} NMCUSTOMDRAW, *LPNMCUSTOMDRAW;
The member 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 COMCTL32.DLL 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:
static void
CustomPaint(HWND hwnd, HDC hDC, RECT* rcDirty, BOOL bErase)
{
NMCUSTOMDRAW nmcd; LRESULT cdControlMode; LRESULT cdItemMode;
nmcd.hdr.hwndFrom = hwnd;
nmcd.hdr.idFrom = GetWindowLong(hwnd, GWL_ID);
nmcd.hdr.code = NM_CUSTOMDRAW;
nmcd.hdc = hDC;
if(bErase) {
LRESULT cdEraseMode;
nmcd.dwDrawStage = CDDS_PREERASE;
cdEraseMode = SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
if(!(cdEraseMode & CDRF_SKIPDEFAULT)) {
...
if(cdEraseMode & CDRF_NOTIFYPOSTERASE) {
nmcd.dwDrawStage = CDDS_POSTERASE;
SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
}
}
}
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))) {
...
for(...) {
if(cdControlMode & CDRF_NOTIFYITEMDRAW) {
nmcd.dwDrawStage = CDDS_ITEM | CDDS_PREPAINT;
nmcd.rc = ...;
nmcd.dwItemSpec = ...;
nmcd.uItemState = ...;
nmcd.lItemParam = ...;
cdItemMode = SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
} else {
cdItemMode = CDRF_DODEFAULT;
}
if(!(cdItemMode & CDRF_SKIPDEFAULT)) {
...
...
...
...
if(cdItemMode & CDRF_NOTIFYPOSTPAINT) {
nmcd.dwDrawStage = CDDS_ITEM | CDDS_POSTERASE;
SendMessage(GetParent(hwnd), WM_NOTIFY, nmcd.hdr.code, (LPARAM) &nmcd);
}
}
}
if(!(cdControlMode & CDRF_SKIPPOSTPAINT)) {
}
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;
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:
...
if(SendNotification(hwndParent, XXN_STATECHANGING, wParam, lParam) == 0) {
...
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.
#include <tchar.h>
#include <windows.h>
typedef struct SuperButtonData_tag SuperButtonData;
struct SuperButtonData_tag {
};
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) {
...
case WM_NCCREATE:
if(!CallWindowProc(lpfnButtonProc, hwnd, uMsg, wParam, lParam))
return FALSE;
lpData = (SuperButtonData*) malloc(sizeof(SuperButtonData));
if(lpData == NULL)
return FALSE;
... SetWindowLongPtr(hwnd, cbButtonExtra, (LONG_PTR) lpData);
return TRUE;
case WM_NCDESTROY:
if(lpData)
free(lpData);
break;
}
return CallWindowProc(lpfnButtonProc, hwnd, uMsg, wParam, lParam);
}
void
RegisterSuperButton(void)
{
WNDSCLASS wc;
GetClassInfo(NULL, _T("BUTTON"), &wc);
lpfnButtonProc = wc.lpfnWndProc;
cbButtonExtra = wc.cbWndExtra;
wc.lpszClassName = _T("SUPERBUTTON");
wc.style &= ~CS_GLOBALCLASS;
wc.hInstance = GetModuleHandle(NULL);
wc.cbWndExtra += sizeof(SuperButtonData*);
wc.lpfnWndProc = SuperButtonProc;
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;
};
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) {
...
}
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);
...
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 COMCTL32.DLL 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 {
};
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) {
...
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}
void
SubclassButton(HWND hwnd)
{
SubButtonData* lpData;
lpData = (SubButtonData*) malloc(sizeof(SubButtonData));
...
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 USER32.DLL and COMCTL32.DLL 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 COMCTL32.DLL to provide support for visual themes for the base controls living in USER32.DLL:
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.