Billy, Are You Skinning in There?
Sounds kinda nasty.. but rest assured gentle reader, it is a perfectly safe and socially acceptable thing to do (at least in the current context). Skinning is what the uxtheme.dll is likely doing in your Operating System right now, that is, subclassing a control in order to alter its appearance via various methods. Most of the common controls still look much the same as they did back in the days of Win95, just flat grey blocks with very little in the way of aesthetic appeal, and certainly not acceptable in today's OS market where users have come to expect a lot of flash and polish. The theming engine uses a variety of techniques to polish the UI, but its primary method is to blit graphics stored in a DLL over the surface of the control. With Vista, we are also seeing some additional fireworks of pulsing and fading UI elements, but beneath it all, many of the same controls we used more than a decade ago. There are also several popular commercial skinning applications that can further enhance the interface, and a few libraries ported towards developers that can give your GUI the much sought after edge in an application's visual appeal. The problem, however, is that the theme library is difficult to work with, and some of the API is obscure with few examples in the wild; also, if you intend on doing anything outside the expected norm, you are forced to draw your components from scratch. Using methods provided by VS designers or within the root classes like owner-drawn, custom drawn, and even WPF, has the disadvantage that you are limited to the methods and interfaces provided by the authors of that solution, and without the source code to the control or library, you are trapped within the limits of their vision. So.. I am providing a solution, a nexus of classes from which you can skin the form and most of the common controls. Think of this as the beginning of an Open Source theming library, evolve it in whatever direction you wish.
Using this Library
I have designed this library to be as automated as possible, with only three commands needed to start skinning the form and controls:
- Instantiate the class and add the form's handle.
- Use the
Add
method to add the control type and its associated bitmap(s).
- If you are skinning the
Form
control, call the Start
method.
That's all there is to it.
The bitmaps used to skin the controls are formatted in the same way as most common skinning engines like Win-blinds, with literally thousands of skins to choose from. I have included the bitmaps and PNGs used in the demo to give you an idea of the format, but that can also be determined by each class' state enum; for example, the radio button has eight states that directly correspond to the RADIOBUTTONSTATES
enum:
private enum RADIOBUTTONSTATES : int
{
RBS_UNCHECKEDNORMAL = 1,
RBS_UNCHECKEDHOT = 2,
RBS_UNCHECKEDPRESSED = 3,
RBS_UNCHECKEDDISABLED = 4,
RBS_CHECKEDNORMAL = 5,
RBS_CHECKEDHOT = 6,
RBS_CHECKEDPRESSED = 7,
RBS_CHECKEDDISABLED = 8,
};
The form's frame has four parts: caption, left side, right side, and bottom. Each part has two states, active and inactive. The form's buttons have three states: normal, hover, and pressed. To determine the state layout of an image, simply check the enum for that control type in its class.
Have DLL, Will Travel
Since this is a library, it is portable and not bound only to C# developers. That's right, it can be used in VB! I know how much you VB guys love your user controls, so I included a VB demo project. For that matter, this should be accessible to all .NET language implementations, perhaps even from VS6, (though I haven't tried it). The classes are also designed so that they can be employed independently. Most of them will require the cStoreDc
and cGraphics
classes which can be simply embedded into the parent class.
How Bill Stole Christmas, and Other Tales of High Misadventure
Well.. it just wouldn't be one of my articles if I didn't complain about Microsoft, right? Initially, I had intended on writing a library that could skin any control, any form, regardless of its origin (MFC/.NET), but this soon proved to be a very challenging task. The form and button skinning classes (cRCM
and cButton
) should be able to skin MFC controls, but it was after the button class fiasco that I broke from the original design spec. It seems that in .NET, the checkbox, radio button, and command buttons are all just an owner drawn button control, so using GetClassName
to retrieve the control type always yielded the same long .NET namespace. I managed to work through this by first testing if the control handle belonged to a .NET class; if so, a control is created with the handle and the necessary properties extracted from the control.
private BUTTONPARTS buttonStyle(IntPtr handle)
{
String name = String.Empty;
Control ctl = Control.FromHandle(handle);
Type t = ctl.GetType();
name = t.Name.ToLower();
if (name.Contains("radio"))
{
ctl.Paint += new PaintEventHandler(ctl_Paint);
return BUTTONPARTS.BP_RADIOBUTTON;
}
if (name.Contains("checkbox"))
{
ctl.Paint += new PaintEventHandler(ctl_Paint);
return BUTTONPARTS.BP_CHECKBOX;
}
if (name.Contains("button"))
{
ctl.Paint += new PaintEventHandler(ctl_Paint);
return BUTTONPARTS.BP_PUSHBUTTON;
}
else
return BUTTONPARTS.BP_UNKNOWN;
}
This (of course) was just the first serious snafu, but enough to convince me of two things: creating a library that skins controls outside of the .NET empire might be a bit too ambitious, and C# was probably not the best language for something like that anyways. You will find a lot of 'unmanaged' code mixed throughout this library, simply because the Graphics
class is just too slow to do real-time complex rendering (try replacing BitBlt
with Graphics.DrawImage
in cRCM
::drawWindow
, then resize the form, and the truth of this will become quite obvious). The other problem is that even though there seems to be a multitude of properties associated with every control (many are redundant), it seems that every control lacked some property that is required to custom draw an element, like the scrollbar button size, or a combobox button handle. So, no matter what, you need to use the API to compete with the level of graphics sophistication possible in a language like C++.
private void drawCombo()
{
ComboBox cb = (ComboBox)Control.FromHandle(_hComboWnd);
COMBOBOXINFO cbi = new COMBOBOXINFO();
RECT tr = new RECT(cb.ClientRectangle.Width - 12, 1, 12,
cb.ClientRectangle.Height - 2);
RECT client = new RECT(cb.ClientRectangle.Width, 0, 0,
cb.ClientRectangle.Height);
int width = _oComboboxBitmap.Width / 4;
IntPtr hdc = GetDC(_hComboWnd);
Rectangle cr = cb.ClientRectangle;
int offset = 0;
cbi.cbSize = Marshal.SizeOf(cbi);
if (SendMessage(_hComboWnd, CB_GETCOMBOBOXINFO, 0, ref cbi) != 0)
tr = cbi.rcButton;
else
tr = new RECT(cb.ClientRectangle.Width - width, 1, width,
cb.ClientRectangle.Height - 2);
...
As you can see from the above snippet, the combobox button size is extracted via a call from SendMessage
using the CB_
GETCOMBOBOXINFO
flag. Think of SendMessage
as the ultimate Get/Set accessor; there are many properties in the root classes that would otherwise be unavailable (intentionally or otherwise).
To the Heart of It
Forms
Possibly the most problematic and downright difficult controls to draw. There are many messages that can trigger an unexpected need to repaint part or all of the non-client area of a form. There is also a lack of good examples in the wild of skinning a form properly, so it required some serious trial and error to get it right. Now make no mistake, this is not using SetWindowRgn
on a borderless form, this is painting over the frame in much the same way that window themes are painted. One problem I have seen in other examples of this, is how to resize the client area to the proper dimension, given the window type and size of the new frame parts. This is done by intercepting the WM_NCCALCSIZE
message and changing the RECT
sizes in the NCCALCSIZE_PARAMS
structure. The structure contains an array of three rectangles; the first contains the current client size; we modify these values, then copy the RECT
to the second element, which tells the form the new client dimensions.
case WM_NCCALCSIZE:
if (m.WParam != IntPtr.Zero)
{
NCCALCSIZE_PARAMS ncsize = (NCCALCSIZE_PARAMS)
Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
WINDOWPOS wp = (WINDOWPOS)Marshal.PtrToStructure
(ncsize.lppos, typeof(WINDOWPOS));
ncsize.rect0 = calculateFrameSize(wp.x, wp.y, wp.cx, wp.cy);
ncsize.rect1 = ncsize.rect0;
Marshal.StructureToPtr(ncsize, m.LParam, false);
m.Result = (IntPtr)WVR_VALIDRECTS;
}
else
{
RECT rc = (RECT)m.GetLParam(typeof(RECT));
rc = calculateFrameSize
(rc.Left, rc.Top, rc.Right - rc.Left, rc.Bottom - rc.Top);;
Marshal.StructureToPtr(rc, m.LParam, true);
m.Result = MESSAGE_PROCESS;
}
base.WndProc(ref m);
In order to do this properly, we first have to subtract the default window sizes from the source RECT
, which vary depending on the windows style:
private RECT calculateFrameSize(int x, int y, int cx, int cy)
{
RECT windowRect = new RECT(x, y, x + cx, y + cy);
if ((GetWindowLong(ParentWnd, GWL_STYLE) & WS_THICKFRAME) == WS_THICKFRAME)
{
windowRect.Left -= GetSystemMetrics(SYSTEM_METRIC.SM_CXFRAME);
windowRect.Right += GetSystemMetrics(SYSTEM_METRIC.SM_CXFRAME);
windowRect.Top -= (GetSystemMetrics(SYSTEM_METRIC.SM_CYCAPTION) +
GetSystemMetrics(SYSTEM_METRIC.SM_CYFRAME));
windowRect.Bottom += GetSystemMetrics(SYSTEM_METRIC.SM_CYFRAME);
}
else if ((GetWindowLong(ParentWnd, GWL_STYLE) & WS_DLGFRAME) == WS_DLGFRAME)
{
windowRect.Left -= GetSystemMetrics(SYSTEM_METRIC.SM_CXDLGFRAME);
windowRect.Right += GetSystemMetrics(SYSTEM_METRIC.SM_CXDLGFRAME);
windowRect.Top -= (GetSystemMetrics(SYSTEM_METRIC.SM_CYSMCAPTION) +
GetSystemMetrics(SYSTEM_METRIC.SM_CYDLGFRAME));
windowRect.Bottom += GetSystemMetrics(SYSTEM_METRIC.SM_CYFRAME);
if ((GetWindowLong(ParentWnd, GWL_EXSTYLE) &
WS_EX_WINDOWEDGE) == WS_EX_WINDOWEDGE)
windowRect.Top -= 2;
}
if ((GetWindowLong(ParentWnd, GWL_EXSTYLE) & WS_EX_CLIENTEDGE) == WS_EX_CLIENTEDGE)
InflateRect(ref windowRect, 2, 2);
if ((GetWindowLong(ParentWnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW)
windowRect.Top += 2;
windowRect.Left += (_oLeftFrameBitmap.Width / 2);
windowRect.Right -= (_oRightFrameBitmap.Width / 2);
windowRect.Bottom -= (_oBottomFrameBitmap.Height / 2);
windowRect.Top += (_oCaptionBarBitmap.Height / 2);
return windowRect;
}
Another issue is trying to emulate the caption button behavior, for example, when the mouse is held down, then dragged away from the button and released, getting the button visual state to change correctly, as there is no NC_MOUSELEAVE
message. We do this by starting a timer that runs through the window procedure fired after a NC_MOUSEMOVE
message:
case WM_NCMOUSEMOVE:
_eLastButtonHit = hitTest();
if ((_eLastButtonHit == HIT_CONSTANTS.HTCLOSE) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMAXBUTTON) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMINBUTTON))
{
startTimer();
invalidateWindow();
}
base.WndProc(ref m);
break;
case WM_TIMER:
_buttonTimer += 1;
HIT_CONSTANTS hitTimer = hitTest();
if ((hitTimer == HIT_CONSTANTS.HTCLOSE) ||
(hitTimer == HIT_CONSTANTS.HTMAXBUTTON) ||
(hitTimer == HIT_CONSTANTS.HTMINBUTTON))
{
if (hitTimer != _eLastButtonHit)
{
stopTimer();
invalidateWindow();
}
else
{
if (_buttonTimer > 500)
stopTimer();
}
}
else
{
if (!leftKeyPressed())
{
stopTimer();
invalidateWindow();
}
}
base.WndProc(ref m);
break;
private void startTimer()
{
if (_buttonTimer > 0)
stopTimer();
SetTimer(ParentWnd, 66, 100, IntPtr.Zero);
}
private void stopTimer()
{
if (_buttonTimer > 0)
{
KillTimer(ParentWnd, 66);
_buttonTimer = 0;
}
}
When using buttons of different sizes from the default, we have to let the OS know so that the hit testing can trigger internal events, like showing the tooltip when hovering over a caption button. This is done by storing the button sizes, doing relative hit testing, then passing the correct HIT_CONSTANTS
flag through the result in WM_NCHITTEST
:
case WM_NCHITTEST:
_eLastWindowHit = (HIT_CONSTANTS)DefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam);
_eLastButtonHit = hitTest();
if ((_eLastButtonHit == HIT_CONSTANTS.HTCLOSE) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMAXBUTTON) ||
(_eLastButtonHit == HIT_CONSTANTS.HTMINBUTTON))
{
m.Result = (IntPtr)_eLastButtonHit;
}
else
{
m.Result = (IntPtr)_eLastWindowHit;
base.WndProc(ref m);
}
break;
The classes in the library are all set to public
to allow independent access, but the primary interfaces are housed in the cRcm
class. Addition of controls to the skinning engine is done through the Add
method, with overloads for different control types. The Add
method uses the EnumChildWindows
API that returns child control handles through a delegate. These are then tested for their type, and added to dictionaries that store the control's skinning class instance with the control's handle used as a key. Controls can be removed either by group or by handle using the Remove
methods.
private bool EnumWindow(IntPtr handle, IntPtr pointer)
{
GCHandle gch = GCHandle.FromIntPtr(pointer);
List<intptr> list = gch.Target as List<intptr>;
if (list != null)
{
list.Add(handle);
return true;
}
return false;
}
private List<intptr> GetChildWindows(IntPtr parent)
{
List<intptr> result = new List<intptr>();
GCHandle listHandle = GCHandle.Alloc(result);
try
{
EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
EnumChildWindows(parent, childProc, GCHandle.ToIntPtr(listHandle));
}
finally
{
if (listHandle.IsAllocated)
listHandle.Free();
}
return result;
}
public void Add(ControlType ct, Bitmap thumb, Bitmap track)
{
if (thumb == null)
throw new Exception("Required image is either missing or invalid.");
if (track == null)
throw new Exception("Required image is either missing or invalid.");
List<intptr> list = GetChildWindows(_hParentWnd);
StringBuilder nameBldr = new StringBuilder(100);
string ctlname = ct.ToString().ToLower();
if (ctlname == "trackbar")
{
if (_oTrackBarSkin == null)
_oTrackBarSkin = new Dictionary<IntPtr, cTrackBar>();
for (int i = 0; i < list.Count; i++)
{
if (list[i] != IntPtr.Zero)
{
Control ctl = Control.FromHandle(list[i]);
if (ctl != null)
{
Type t = ctl.GetType();
string name = t.Name.ToLower();
if (name == ctlname)
{
_oTrackBarSkin.Add(ctl.Handle,
new cTrackBar(ctl.Handle, thumb, track));
ctl.Refresh();
}
}
}
}
}
}
Buttons
The cButton
class differs from the rest of the control classes in one significant way. All other classes are one instance per control, created automatically and stored in dictionaries by simply adding the control type and associated image(s) to the library:
_cRcm.Add(RCM.ControlType.Button, RCM_Harness.Properties.Resources.vista_command);
The button is added the same way as all the other controls, only the one class handles all controls of type Button
, CheckBox
, and RadioButton
. That is because as I mentioned earlier, these are all the same .NET class, and all owner drawn buttons. The button works more like a managed user control, employing the control's Paint
event to do the work, whereas the other controls are all rendered through their window procedures. It keeps track of the various objects by storing their handles and type using the SetProp
/GetProp
API:
public void Add(IntPtr handle)
{
SetProp(handle, "style", (IntPtr)buttonStyle(handle));
}
public void Remove(IntPtr handle)
{
SetProp(handle, "style", (IntPtr)BUTTONPARTS.BP_UNKNOWN);
}
The command button style is rendered on whole by the class, but the CheckBox
and RadioButton
controls use an ImageList
to draw their images; the text, however, is unaltered.
private void drawCheckbox(Graphics g, RECT bounds, int state)
{
int height = _oCheckboxBitmap.Height;
int offset = (bounds.Bottom - height) / 2;
RECT picRect = new RECT(0, 0, 16, 16);
IntPtr hdc = g.GetHdc();
IntPtr hbrush = CreateSolidBrush(GetPixel(hdc, 0, 0));
FillRect(hdc, ref picRect, hbrush);
g.ReleaseHdc();
DeleteObject(hbrush);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
_oCheckboxIml.Draw(g, 1, offset, height, height, state);
}
This method also demonstrates the marriage between API and managed code used throughout these classes. Managed code is used in many places where rendering speed is not a crucial concern.
ScrollBars
There are, of course, two types of scrollbars, standalone components, and the scrollbars painted inside of compound controls like a treeview
or combobox
. Both are very different in that an internal scrollbar is not a scrollbar at all, but rather drawn into the DC of the control to simulate a scrollbar window. They are both alike in one respect though, they are nearly impossible to customize. If you are writing in straight C, you can simply over-paint the control with very little flickering, but this is just not possible given the overhead of .NET. They would flicker like a bad bulb. So, if you can't paint over them, you create a window that sits on top of them and draw on that! Using the CreateWindowEx
API, you create an owner-drawn tool window, assign it as a child to the control (with a listview
/treeview
, etc.), or as a child of the parent. This keeps it anchored in place. Tool windows with these style settings are also click through, so mouse events fire as normal, with all painting done on the mask window. This allows us to skin scrollbars on any control with no flicker.
private void createScrollBarMask()
{
Type t = typeof(cScrollBar);
Module m = t.Module;
IntPtr hInstance = Marshal.GetHINSTANCE(m);
IntPtr hParent = GetParent(_hScrollBarWnd);
RECT tr = new RECT();
Point pt = new Point();
GetWindowRect(_hScrollBarWnd, ref tr);
pt.X = tr.Left;
pt.Y = tr.Top;
ScreenToClient(hParent, ref pt);
_hMaskWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
"STATIC", "",
SS_OWNERDRAW | WS_CHILD | WS_CLIPSIBLINGS | WS_OVERLAPPED | WS_VISIBLE,
pt.X, pt.Y,
(tr.Right - tr.Left), (tr.Bottom - tr.Top),
hParent,
IntPtr.Zero, hInstance, IntPtr.Zero);
SetWindowPos(_hMaskWnd, HWND_TOP,
0, 0,
0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
}
ListView Headers
Here is something I have seen tried many times, but rarely with good results. The trick is to get the header handle from the listview
using SendMessage
, then blit the columns within a PAINTSTRUCT
; this renders the columns without flicker.
private IntPtr headerWnd
{
get { return (SendMessage(_hListviewWnd, LVM_GETHEADER, 0, 0)); }
}
protected override void WndProc(ref Message m)
{
PAINTSTRUCT ps = new PAINTSTRUCT();
switch (m.Msg)
{
case WM_PAINT:
if (!_bPainting)
{
_bPainting = true;
BeginPaint(m.HWnd, ref ps);
drawHeader();
ValidateRect(m.HWnd, ref ps.rcPaint);
EndPaint(m.HWnd, ref ps);
_bPainting = false;
m.Result = MSG_HANDLED;
}
else
{
base.WndProc(ref m);
}
break;
default:
base.WndProc(ref m);
break;
}
}
I have also added the filter button and sort arrows. The sort arrow uses the DrawThemeBackground
API to render the arrow:
private bool drawThemeArrow(IntPtr hdc, Rectangle bounds, bool down)
{
if (IsAppThemed())
{
IntPtr hTheme = OpenThemeData(GetParent(_hHeaderWnd), "Header");
if (hTheme != IntPtr.Zero)
{
RECT tr = new RECT(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
DrawThemeBackground(hTheme, hdc, HP_HEADERSORTARROW, down ?
HSAS_SORTEDDOWN : HSAS_SORTEDUP, ref tr, ref tr);
CloseThemeData(hTheme);
return true;
}
}
return false;
}
What's Left?
There is simply no time to cover all of the controls in depth, but here is the rest of the line up so far: Command Button, CheckBox
, ComboBox
, ListBox
, ListView
, NumericUpDown
, ProgressBar
, RadioButton
, ScrollBar
, TabControl
, TrackBar
, TreeView
... You might be asking 'but what about the menu/status/tool bar controls?' Well, I kind of covered those in vtExtender
, so not a high priority right now. They are also a bit involved (especially menus - eech), but I'll try and get to them sometime soon. What I am working on now, started the way things usually do, looking at the fading buttons and pulsing progress bars and asking: How do they do that? I think I know, and am writing another class for this library currently. I might also add corners with odd shapes to the form skinner, and do a little tweaking here and there, we'll see.. enjoy.
Change Log
June 21 - Well, I got around to installing XP this weekend, and what a nasty surprise! Checkboxes and radiobuttons not working at all (a minor change to repair), and scrollbar buttons not working via SendMessage
(though it works fine in Vista). Worst of all, when passing over the form for the first time, the caption bar groupbox does a nasty flicker. This happens when the WM_SETCURSOR
message notifies the window to redraw the area, but only the first time the message is sent. The trick was to modify the invalidateWindow
function by adding the RDM_ERASE
message to erase the caption area, then call it after the cursor is changed.
case WM_SETCURSOR:
if (!windowMaximized)
base.WndProc(ref m);
else
m.Result = MESSAGE_PROCESS;
invalidateWindow();
break;
The WM_NCLBUTTONDOWN
message response was also modified to consume the message if the relative button position was not hit tested, but the system coordinates of the button are a match (say, if your button sizes or offsets differ from the defaults).
- June 23 - Added a couple more fixes for XP, form flickered when menu was invoked by clicking the icon on the caption bar.
- June 24 - Added a resizing method for the scrollbar mask in
cScrollbar
to keep them in sync with parent control.
case WM_SIZE:
case WM_MOVE:
sizeCheck();
base.WndProc(ref m);
break;
private void sizeCheck()
{
RECT tr = new RECT();
RECT tw = new RECT();
GetWindowRect(_hMaskWnd, ref tw);
GetWindowRect(_hScrollBarWnd, ref tr);
if (!EqualRect(ref tr, ref tw))
SetWindowPos(_hMaskWnd, IntPtr.Zero,
tr.Left, tr.Top, tr.Right - tr.Left, tr.Bottom - tr.Top,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
}
A similar routine was added to the cInternalScrollBar
class.
The new code has been uploaded in version 1.4. I'll see if I can get a chance to test it on Windows 7 soon.