Creating Themed Custom Controls
Wednesday, February 12, 2003
I have to admit: visual styles are starting to annoy me. It's not that I don't like them - it's that every piece of visual code I write now has to take them into account.
In this short article, I'll discuss some of the implications of supporting visual styles when developing custom controls in Delphi. You can read more about visual styles in my previous article, which deals with drawing system elements.
In terms of visual design, every custom control can contain both standard and non-standard elements. Standard elements are the parts that make the control look like other visual elements in Windows, such as its border and scroll bars. Non-standard elements are what makes the control unique. In most Windows controls, this concept is implemented by separating the client and non-client areas of the control.
Traditionally, the non-client area of a control is painted by Windows. This works well in most cases. A form's non-client area, for example, includes the form's border, its caption (and caption buttons), and scroll bars. By letting Windows take over this part, windows have a standard look throughout the system. Windows also provides some default non-client area handling for other controls. Unfortunately, the default processing does not fully support visual styles. The control border has to be drawn separately.
In ancient times (that is, before visual styles were inflicted on us), most controls had just three border styles: a sunken 3D border, a flat border, or no border at all. Many Delphi controls expose these styles by using the BorderStyle and Ctl3D properties. These styles were implemented internally by Windows, and were controlled by setting various bits in the call to CreateWindow or CreateWindowEx. The WS_BORDER style gave a control a flat border, while the WS_EX_CLIENTEDGE extended style gave it a sunken border.
Control borders in Windows XP are a little more complicated. Instead of a fixed set of styles, XP controls can have any sort of border. Borders can have a variety of colors, patterns, shapes, and levels of transparency. Different controls can have different border styles. For example, group boxes have round corners, while edit boxes have a rectangular border, but use a different color. Because there is no single standard for borders, the default non-client area painting code doesn't draw any of the new border styles. This means we have to do it ourselves.
Obviously, there's a catch. We want our control to look "right" when using visual styles, so we need a standard border. The problem is that there is no single border style. One solution offered by Microsoft is to borrow elements from other controls. That's the solution I'll use here.
TThemedCustomControl is a simple TCustomControl descendant. It handles the WM_NCPAINT message to draw a themed border:
procedure TThemedCustomControl.WMNCPaint(var Message: TMessage);
XEdge, YEdge: Integer;
if (ThemeServices.ThemesEnabled) and Ctl3D then
R := Rect(0, 0, Width, Height);
DC := GetWindowDC(Handle);
XEdge := GetSystemMetrics(SM_CXEDGE);
YEdge := GetSystemMetrics(SM_CYEDGE);
ExcludeClipRect(DC, XEdge, YEdge, Width - XEdge, Height - YEdge);
Details := ThemeServices.GetElementDetails(teEditRoot);
ThemeServices.DrawParentBackground(Handle, DC, @Details, True);
ThemeServices.DrawElement(DC, Details, R);
Although the code is fairly simple, certain parts require explanation. Let's start at the top.
The procedure starts by invoking the default handler for the message. Although this causes the standard border to be drawn even if themes are supported, we need this in order to draw other non-client elements - specifically, scroll bars.
Once we've established that a themed border needs to be drawn, we get the control's device context. To make sure we only draw over the border area, we call ExcludeClipRect so that Windows clips everything else. The calls to GetSystemMetrics get the size of the standard border that was already painted. The border is usually 2 pixels wide, but it's safer to ask.
To draw our border, we need access to the theme data. This is where we decide what our border looks like. I've decided to use the same border as an edit box, but you can use any control you want. Simply replace teEditRoot with the appropriate value.
That's it. If you download the control, you'll see a little more code. That's just housekeeping code for handling the Ctl3D property.
The code was written in Delphi 7, but should work with earlier versions. You will the ThemeServices class, though. You can download it from Mike Lischke's Delphi Gems site.