Author: Yorai Aminov
Creating Themed Custom Controls
Answer:
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 "Visual Styles in Delphi", which deals with drawing
system elements.
Borderline Personality
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.
Shoplifting
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 borrowhttp://www.shoplifting.com/
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:
1 procedure TThemedCustomControl.WMNCPaint(varmessage: TMessage);
2 var3 R: TRect;
4 Details: TThemedElementDetails;
5 DC: HDC;
6 XEdge, YEdge: Integer;
7 begin8 inherited;
9 if (ThemeServices.ThemesEnabled) and Ctl3D then10 begin11 R := Rect(0, 0, Width, Height);
12 DC := GetWindowDC(Handle);
13 XEdge := GetSystemMetrics(SM_CXEDGE);
14 YEdge := GetSystemMetrics(SM_CYEDGE);
15 ExcludeClipRect(DC, XEdge, YEdge, Width - XEdge, Height - YEdge);
16 try17 Details := ThemeServices.GetElementDetails(teEditRoot);
18 ThemeServices.DrawParentBackground(Handle, DC, @Details, True);
19 ThemeServices.DrawElement(DC, Details, R);
20 finally21 ReleaseDC(Handle, DC);
22 end;
23 end;
24 end;
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.
Conclusion
That's it. If you download the controlhttp://download.shorterpath.com/ThemeCC.zip,
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.
Component Download:
http://download.shorterpath.com/ThemeCC.ziphttp://download.shorterpath.com/ThemeCC.z
ip