Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / MFC
Article

SkinControls 1.1 - A journey in automating the skinning of Windows controls

Rate me:
Please Sign up or sign in to vote.
4.97/5 (80 votes)
11 Oct 200314 min read 357.5K   19.8K   273   90
A self-contained, user-extensible, application-wide skinning architecture for Windows controls.

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.

Image 1

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.

MessageControls affected
WM_CTLCOLORBTNCheck boxes, radio buttons and group boxes but not push buttons
WM_CTLCOLORDLGDialog boxes, property sheets, property pages
WM_CTLCOLOREDITEdit boxes, including those in the IPAddress control
WM_CTLCOLORLISTBOXList boxes, combo list boxes
WM_CTLCOLORSCROLLBARScrollbar background only (i.e. not the thumb or buttons)
WM_CTLCOLORSTATICStatic 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.)

MessageControls affected
WM_ERASEBKGNDCombo boxes, datetime picker, tab control
WM_DRAWITEMOwnerdraw push buttons
WM_PAINTIP 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 classMFC classComments
ButtonCButton

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.

EditCEditThe 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.
ComboBoxCComboBox

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.

ListBoxCListBoxI 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.
ScrollbarCScrollBarEven worse than trying to draw the embedded scrollbars in a window. I couldn't get any of it to work!
msctls_updown32CSpinButtonCtrlThis worked out surprisingly well when you also consider that spin buttons are embedded in tab controls and DateTime pickers.
msctls_progress32CProgressCtrlStraightforward but had to be completely overridden.
msctls_trackbar32CSliderCtrlFairly 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_hotkey32CHotKeyCtrlThis 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.
SysDateTimePick32CDateTimeCtrl

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.

SysMonthCal32CMonthCalControlI 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_class32CTooltipCtrlThese 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.
SysHeader32CHeaderCtrlThis was quite easy although I did have to draw it from scratch. Unfortunately, I don't seem to currently handle Imagelists although clearly I intended to from the adjacent code.
SysListView32CListCtrlThe 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.
SysTreeView32CTreeCtrl

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.

SysTabControl32CTabCtrlThis had to be done from scratch too but fortunately I was able to rely on the work I'd done for a previous article.
SysIPAddress32CIPAddressCtrlAll 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 #includes 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 #defines 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"
    // assumes files are in same structure as sample app
    
    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
    • Initial release.
  • 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).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer Maptek
Australia Australia
.dan.g. is a naturalised Australian and has been developing commercial windows software since 1998.

Comments and Discussions

 
PraiseDemo Pin
AmateurProgrammer27-May-18 19:48
AmateurProgrammer27-May-18 19:48 
GeneralMy vote of 5 Pin
Manikandan106-Jul-14 2:00
professionalManikandan106-Jul-14 2:00 
QuestionBrilliant Stuff! Thank you so much! (64 bit support) Pin
yogeshddd24-Jun-11 6:19
yogeshddd24-Jun-11 6:19 
GeneralMy vote of 5 Pin
xuplus25-Sep-10 20:39
xuplus25-Sep-10 20:39 
Questioncan someone help me with license terms for this cods? Pin
inbalmunitz25-Jul-10 4:23
inbalmunitz25-Jul-10 4:23 
QuestionGreat work, but cann't we do the same with ComCtl32.dll version 6 and theming API? Pin
kvreddy25-Jul-10 2:18
kvreddy25-Jul-10 2:18 
QuestionHow to remove the rounded corners? Pin
shobley13-Oct-09 2:20
shobley13-Oct-09 2:20 
GeneralSome Critical Fixes that i'v made Pin
d_edery26-Jan-09 11:34
d_edery26-Jan-09 11:34 
GeneralGreat job!Thanks! Pin
amtf2211-Nov-08 19:00
amtf2211-Nov-08 19:00 
Questionanyone has a unicode version? Pin
edgasdgasgf5-Nov-08 5:00
edgasdgasgf5-Nov-08 5:00 
AnswerRe: anyone has a unicode version? Pin
shobley13-Oct-09 6:58
shobley13-Oct-09 6:58 
Generalvery good job ! Pin
laoliu725623-Oct-07 22:43
laoliu725623-Oct-07 22:43 
Generalcustom bitmaps Pin
EpicYeti6-Oct-06 5:12
EpicYeti6-Oct-06 5:12 
GeneralProblem with Dialog box and Property Sheet Pin
julius007300030-Jun-06 0:23
julius007300030-Jun-06 0:23 
GeneralRe: Problem with Dialog box and Property Sheet Pin
Muzel21-Apr-08 2:38
Muzel21-Apr-08 2:38 
Jokeskin controls on FormView (MDI) does appear on first document Pin
julius007300028-Jun-06 0:09
julius007300028-Jun-06 0:09 
Jokeskin controls on FormView (MDI) does not appear on first document :: Need Help ... Pin
julius007300027-Jun-06 23:51
julius007300027-Jun-06 23:51 
Jokeskin controls on FormView (MDI) does not appear on first document :: Need Help ... Pin
julius007300027-Jun-06 23:48
julius007300027-Jun-06 23:48 
Generalskinning in Vista Pin
ghostforce21-Jun-06 1:54
ghostforce21-Jun-06 1:54 
Generalcaption in dialog box Pin
microbit6-Jun-06 1:13
microbit6-Jun-06 1:13 
GeneralVB.NET Pin
ser4u5-Jun-06 4:22
ser4u5-Jun-06 4:22 
QuestionSlow response time when hovering Pin
_Stilgar_19-Apr-06 8:35
_Stilgar_19-Apr-06 8:35 
GeneralASCII / UNICODE Pin
nemeth.istvan22-Mar-06 0:06
nemeth.istvan22-Mar-06 0:06 
Generalgoo job! Pin
Andy Byron11-Mar-06 8:59
Andy Byron11-Mar-06 8:59 
GeneralCould you provide a simple sample Pin
BillTPE9-Mar-06 0:23
BillTPE9-Mar-06 0:23 

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

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