Windows XP Update!
In order to see the full range of features that SkinControls provide under XP I've knobbled theming. An unfortunate side effect of this is that the dialog caption and border (the non-client area) also become un-themed. Don't worry though if you're interested in using this under XP, because I'm currently devising a more sophisticated solution which will allow SkinControls to work under XP without quite such drastic effects.
Preface
Like most people who post articles on CodeProject, I love programming and often just for its own sake.
The following article, in which I describe a system for the automated skinning of Windows controls, is one such project that I developed for no other reason than to see how far I could get. It's as much an investigation into the degree to which the default visuals of Windows controls can be modified in an un-intrusive manner, as it is a system that you would actually want to include in a commercial application. Nevertheless, I've still tried to design and implement it as robustly as possible and with as much attention to detail as I can muster.
As a final comment though, I'm not advocating its use in your own programs unless you are familiar and comfortable with the sort of implementation details I will describe below. I.e.. if you go ahead and use it in an application, and then it goes horribly wrong and you subsequently find you have no idea how to investigate the problem then ...
Oh, and one more thing, skinning the scrollbar thumb can be done but not in the way that the rest of the skinning has been implemented and that is why it has been omitted (more on that later).
Introduction
Over the last couple of years I've been working on, amongst other things, a skinning system that heavily modifies the non-client area of a window, by incorporating special non-client controls (i.e.. not HWND
based) including toolbars, menubars, titles, text and status bars.
However, I found that grooving up the border of a window only seemed to highlight how relatively un-groovy the standard Windows controls were in contrast.
I spoke to my employer about this but they were uninterested in taking it any further since the product we were developing would be hosting Internet Explorer for its primary client UI.
So I thought 'What the hell, I'll do it in my own time'.
The result is a system that will automatically skin every control in a given application in as subtle or as strong a style as your mood takes you.
The following specific features are included:
- Unskinned mode (see top-left quadrant in the screenshot).
- Default 'rounded-corner' mode (see bottom-right quadrant in the screenshot).
- Support for bitmap replacement of most clickable UI elements (see top-right quadrant in the screenshot).
- Callbacks for partial or total overriding of the drawing process (see bottom-left quadrant in the screenshot).
- Support for 'hot-tracking' of all controls.
- Handling of OwnerDraw push buttons (see the button with the black rectangle).
- Ability to change the default background color as well as the default system colors.
The design
The fundamental design is really very simple:
Install a Windows hook to trap those Windows messages which indicate when controls are being created or displayed, and then subclass them to override their default drawing behaviour.
Since I had already implemented a similar design in a previous article for skinning menus I felt confident enough not to have to do a 'proof-of-concept' but instead to proceed on with the detailed design.
So, ignoring for a moment that many Windows controls have specific quirks that have to be handled by custom code, all (standard) Windows controls have the following essential makeup:
- A client area, where application specific data is displayed,
- A non-client area, which may or may not display one or both scrollbars depending on how much data is being displayed in the client area.
So I knew at the very least that I would need to handle three different classes of drawing:
- client drawing to handle modification of the control's background and text colors (fairly, but not entirely, trivial),
- non-client drawing to handle drawing the client border of the window (if
WS_BORDER
was defined) and the scrollbar buttons if they were visible,
- and, where the rendering of a control is typically achieved entirely within the client area (e.g. buttons), client drawing to completely re-implement the control.
Client drawing
Windows already provides a number of messaging callbacks to allow the overriding of the background and text colors of a range of controls.
The following list summarizes those messages and the controls that may be used to modify. However, I'm not going to give any further explanation because a simple search on MSDN will give a far better one.
Message |
Controls affected |
WM_CTLCOLORBTN |
Check boxes, radio buttons and group boxes but not push buttons |
WM_CTLCOLORDLG |
Dialog boxes, property sheets, property pages |
WM_CTLCOLOREDIT |
Edit boxes, including those in the IPAddress control |
WM_CTLCOLORLISTBOX |
List boxes, combo list boxes |
WM_CTLCOLORSCROLLBAR |
Scrollbar background only (i.e. not the thumb or buttons) |
WM_CTLCOLORSTATIC |
Static text, read-only edit boxes |
WM_NOTIFY (custom draw) |
Treectrls, listctrls, rebars, trackbars |
For those controls where Windows provides no message hooks for overriding or where custom drawing was required within the client area, the following additional messages were required.
(For a more detailed discussion of the quirks of these and other controls, refer to the detailed implementation section further on.)
Message |
Controls affected |
WM_ERASEBKGND |
Combo boxes, datetime picker, tab control |
WM_DRAWITEM |
Ownerdraw push buttons |
WM_PAINT |
IP address, combo boxes, spin buttons, static frames, progress bars, datetime picker, all button types, headerctrls, tabctrls |
Non-Client drawing
Heavens only know what was motivating Microsoft when they implemented the non-client drawing of controls but clearly they never anticipated anyone wanting to override their default implementation.
In their defense, I can only surmise that it was an issue of efficiency in the early days of Windows running on 386s, and to their credit their implementation of non-client drawing is very fast.
Anyhow the end result is that the only way to override any of the non-client drawing is to first let Windows do its stuff and then draw over the top if it.
And the reason why the obvious solution (of simply doing all the non-client drawing and cutting Windows out of the equation altogether) will not work relates to the way Microsoft implemented scrollbar drawing (again probably relating to efficiency).
What they did (and this is well documented) was to put all the standard scrollbar drawing in the default Window procedure for WM_NCPAINT
and then supplement it with optimized scrollbar redrawing in a dozen other places in response to user input. I.e. knackers any chance of allowing someone to override the default implementation!
Through long and tedious trial and error I found the following Windows messages might lead to redrawing of the scrollbars in one or more Windows controls:
WM_NCPAINT
WM_HSCROLL
WM_VSCROLL
WM_KEYDOWN
WM_KEYUP
WM_CHAR
WM_NCLBUTTONDOWN
WM_NCLBUTTONUP
WM_COMMAND
WM_SIZE
WM_MOVE
WM_KILLFOCUS
WM_SETFOCUS
WM_STYLECHANGED
WM_ENABLE
So, if you try to draw the entire scrollbar and then eat the WM_NCPAINT
message, it simply won't work because Windows draws over the scrollbars in any number of other places whenever it chooses to.
It's further well documented that the only way to fully override the default scrollbar drawing is to hook the scrollbar drawing routines in whichever system DLL they are implemented (sorry I forget which), and redirect the DLL functions to your own custom functions.
Since this was not something that I felt, fell into the realm of an 'un-intrusive' implementation, I have not taken this line of opportunity.
However, my experimentation did reveal that it's still quite feasible to overdraw the scrollbar buttons and achieve very reasonable results even with Windows seemingly doing its hardest to spoil the party.
Broad Implementation
Apart from the extensive trial and error required to achieve the results shown by the demo executable, the implementation was fairly straightforward.
For the hooking I used CHookMgr
to implement a WH_CALLWNDPROC
application hook, and hooked WM_STYLECHANGED
, WM_PARENTNOTIFY
, WM_WINDOWPOSCHANGED
and WM_SHOWWINDOW
to initialize skinning and WM_NCDESTROY
to remove the skinning.
If you're wondering why I did not just hook WM_CREATE
and be done with it, I can answer that I've found on occasions that some system classes are just not ready to be hooked that early in their life and, from the point of view of efficiency, there is no need to actually subclass a control until it's about to be shown (since this is all about modifying its UI).
Once the CHookMgr
derived CSkinCtrlMgr
has decided that a given control is appropriate for subclassing, it instantiates a specific instance of a CSkinCtrl
derived class to carry out the actual skinning.
Almost all the hard work of drawing scrollbar buttons/dropbuttons and the various types of borders is implemented in the CSkinCtrl
base class, which also provides many virtual message handlers which can be overridden in the derived classes.
The benefit of this, apart from re-use and maintenance, is that all of the built-in classes which derive from CSkinCtrl
to provide the custom drawing required by the standard Windows controls can be squeezed into a single source file 62 Kb in size, which is not bad considering that CSkinCtrl
itself takes up 45 Kb.
Detailed implementation
Because this exercise was mostly experiment it's probably worth highlighting which of the Windows controls gave me the most grief:
Win32 class |
MFC class |
Comments |
Button |
CButton |
The button class has always struck me as a bit odd since within one window class it effectively incorporates four very distinct sub-classes: push buttons, check boxes, radio buttons and group boxes.
Needless to say having to re-implement all of them was rather a pain, although, the satisfaction at being able to skin even QwnerDraw push buttons more than made up for it. |
Edit |
CEdit |
The trickiest part of this was figuring out when the scrollbars might appear or disappear, which I concluded was in response to an EN_UPDATE notification or a backspace or delete key press. |
ComboBox |
CComboBox |
Don't get me started! This was the most difficult by far and there are still some minor pixel level issues I'm not entirely happy about.
There were three main problems:
- Drawing the drop button.
Like scrollbar buttons, it was difficult to determine when this would get redrawn so I suspect I've ended up drawing it more often than is strictly necessary.
- Drawing the ListBox portion when the ComboBox has the
CBS_SIMPLE style.
The client rect of the ComboBox incorporates the child ListBox as well so the height of what we know as the ComboBox has to be inferred by offsetting from the top of the list box.
- Handling the currently selected item.
If you try to change the background color of a ComboBox via WM_CTLCOLORLISTBOX all that happens is that the background color of the ListBox portion changes, so this had to be done manually too. |
ListBox |
CListBox |
I was quite unable to figure out to handle the LBS_DISABLENOSCROLL style, so if you try this in a dialog box, the ListBox will not be skinned. |
Scrollbar |
CScrollBar |
Even worse than trying to draw the embedded scrollbars in a window. I couldn't get any of it to work! |
msctls_updown32 |
CSpinButtonCtrl |
This worked out surprisingly well when you also consider that spin buttons are embedded in tab controls and DateTime pickers. |
msctls_progress32 |
CProgressCtrl |
Straightforward but had to be completely overridden. |
msctls_trackbar32 |
CSliderCtrl |
Fairly easily handled via CustomDraw but there some odd behaviour in the trackbar control in that once it's been skinned you can't force it to redraw itself except by sending it a WM_SETFOCUS message - go figure. |
msctls_hotkey32 |
CHotKeyCtrl |
This provides no straightforward way to modify either the background or text colors except by redrawing it entirely and because this could involve localization issues that I might get wrong, only the border is currently redrawn. |
ShellDll_DefView |
- |
This parents the listview in the file open dialog for the purpose, as far as I can tell, of handling various shell notifications. Whatever, the result is that all CustomDraw notifications get trapped by it, so it must be hooked so that these notifications can be passed back to the list control for modifying the text and background colours. |
SysDateTimePick32 |
CDateTimeCtrl |
Much the same as a ComboBox except that whilst with ComboBoxes I was able to clip out the drop button during the drawing process to prevent it being overwritten, here I had to redraw the entire control because the drop button gets drawn regardless of the clip rect.
Furthermore, the dropbutton gets redrawn even more unpredictably, so I had to implement a nasty timer based redraw mechanism which produces a reasonable result but at the expense of being a hack. |
SysMonthCal32 |
CMonthCalControl |
I must have got to this soon after finishing the ComboBox or datetime picker because I've clearly chickened out and only handled the border. I think it was when I realized that the forward and back buttons were handled in the WM_PAINT code that I postponed it. |
tooltips_class32 |
CTooltipCtrl |
These windows never actually get subclassed, but I do take the opportunity at the occasion of subclassing to send them the TTM_SETTIPBKCOLOR and TTM_SETTIPTEXTCOLOR messages instead. |
SysHeader32 |
CHeaderCtrl |
This was quite easy although I did have to draw it from scratch. Unfortunately, I don't seem to currently handle Imagelist s although clearly I intended to from the adjacent code. |
SysListView32 |
CListCtrl |
The only tricky bit was having to skin the header control on the fly when the user switches to report mode because, for some unknown reason, the expected Windows notifications never seem to reach the CSkinCtrlMgr . |
SysTreeView32 |
CTreeCtrl |
Some time before I started on this project, I tried to see if I could hack CTreeCtrl to display alternatives to the standard '+' and '-' buttons.
I did finally get it working, albeit with all sorts of fudging, and that code is included here, although I have disabled at present because it seems so esoteric as to be almost pointless except as an intellectual exercise. |
SysTabControl32 |
CTabCtrl |
This had to be done from scratch too but fortunately I was able to rely on the work I'd done for a previous article. |
SysIPAddress32 |
CIPAddressCtrl |
All I had to do here was redraw the dots between the edit fields which, because they too are skinned, handle their borders themselves. |
Using the code
- Add the following source files to your project:
Note: You may need to edit the #include
s if you have a different project structure to the sample app.
CHookMgr
(skinwindows\hookmgr.h) - template class for simplifying hooking.
CSkinBase
(skinwindows\skinbase.h/.cpp) - some core skin related helper methods.
CSkinGlobals
(skinwindows\skinglobals.h/.cpp, skinwindows\ skinglobalsdata.h) - helper classes for providing global color overrides.
CSkinCtrl
(skinwindows\skinctrl.h/.cpp) - base class for all the derived window skin classes.
CSkinButton, ...
(skinwindows\skinctrls.h/.cpp) - the derived window skin classes.
CSkinCtrlMgr
(skinwindows\skinctrlmgr.h/.cpp) - control window hooking and management.
CSubclassWnd
(shared\subclass.h/.cpp) - subclassing helper class (heavily modified from Paul DiLascia's original).
CWinClasses
(shared\winclasses.h/.cpp) - helper class for retrieving and testing window classes.
CDelayRedraw
(shared\delayredraw.h/.cpp) - helper class for redrawing a window after a defined delay.
CEnBitmap
(shared\enbitmap.h/.cpp) - helper class for loading bitmaps.
CRoundCorner
(shared\roundcorner.h/.cpp) - helper class for rendering round corner 3D edges..
- wclassdefines.h - convenient
#define
s for all window classes (and some others).
- syscolors.h - color mappings
- Add
NO_SKIN_INI
to the preprocessor definitions in your project settings. This is to avoid compilation problems due to missing files, because this project can be built to load a skin from an XML file, which is not included here for copyright reasons.
- Initialize the skin control manager in your
CWinApp
derived application InitInstance()
method as follows: #include "..\skinwindows\skinctrlmgr.h"
BOOL CMyApp::InitInstance()
{
:
:
CSkinCtrlMgr::Initialize();
:
:
}
Have a look at the implementation of CSkinCtrlMgr::Initialize()
for more detail on the options available. In particular you can elect to have controls display a 'hot' state when the cursor moves over them and/or provide a callback interface for overriding all or part of the drawing process.
Further work
- Tab controls with bottom, left or right tabs.
- Header control images.
- Validating pager controls although these may work as-is.
- MonthCal control buttons.
- HotKey controls.
Copyright
The code is supplied here for you to use and abuse without restriction, except that you may not modify it and pass it off as your own.
History
- 1.0
- 1.1
- edit control scroll bug fixed (thanks to lamdacore).
- support added for buttons with
BS_ICON
and BS_BITMAP
styles.
- theming disabled under XP so that skinning works (see note at the top).