Contents
With the advent of XP, Microsoft have attempted to ensure that a lot of the
responsibility for rendering the user interface now lies with the operating
system rather than the programmer. Fine in most cases, but there are still some
situations when you want to be able to draw the control yourself. One of the
most common situations is a button with an image on it, and in this article we
discuss implementing such a control using the native Win32 API. Although the
discussion concerns owner-draw buttons specifically, the topics covered here
will apply to all owner-draw controls. In this article we discuss how to
implement a simple native Win32 application which contains a theme-aware
owner-draw button that will run on both themed and non-themed Microsoft Windows
32-bit operating systems.
Open up the sample project (OwnerDraw.dsp) and have a look at
IDD_MAIN_DLG
. You will see that the UI comprises a dialog box with
two buttons, one an owner-draw control button and one a normal push button (for
reference). Open up the resource script (OwnerDraw.rc) as text and see
that our owner-draw button is a normal system button but with the
BS_OWNERDRAW
button style flag set.
//////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_MAIN_DLG DIALOG DISCARDABLE 0, 0, 307, 122
STYLE DS_SETFOREGROUND | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION |
WS_SYSMENU
CAPTION "Owner Draw Sample"
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "Owner Draw",IDC_OWNERDRAW_BTN,"Button",BS_OWNERDRAW |
WS_TABSTOP,80,20,105,35
PUSHBUTTON "Normal",IDC_NORMAL_BTN,80,60,105,35
PUSHBUTTON "OK",IDOK,250,5,50,14
PUSHBUTTON "Cancel",IDCANCEL,250,22,50,14
END
With this flag set we are telling Windows that it is the responsibility of
the button's owner (the dialog) to draw the button. You will find the
implementation for the dialog in MainDlg.cpp.
What a control looks like on the screen is a reflection of its state. A
button, for example, could be pressed or focused. The pre-defined states that
Windows can tell us about are :-
ODS_CHECKED
The menu item is to be checked. This bit is used
only in a menu.
ODS_COMBOBOXEDIT
The drawing takes place in the selection
field (edit control) of an owner-drawn combo box.
ODS_DEFAULT
The item is the default item.
ODS_DISABLED
The item is to be drawn as disabled.
ODS_FOCUS
The item has the keyboard focus.
ODS_GRAYED
The item is to be grayed. This bit is used only in
a menu.
ODS_HOTLIGHT
Windows 98/Me, Windows 2000/XP: The item is
being hot-tracked, that is, the item will be highlighted when the mouse is on
the item.
ODS_INACTIVE
Windows 98/Me, Windows 2000/XP: The item is
inactive and the window associated with the menu is inactive.
ODS_NOACCEL
Windows 2000/XP: The control is drawn without the
keyboard accelerator cues.
ODS_NOFOCUSRECT
Windows 2000/XP: The control is drawn without
focus indicator cues.
ODS_SELECTED
The menu item's status is selected.
But obviously not all of these apply to our button. Although Windows will
notify us if any of the relevant states listed above change, in our example we
only redraw the button in the following situations :-
- It gains the focus
- It looses the focus
- It is pressed
- It is released
- (specially for XP) If it is "hot".
XP users may have noticed that system buttons are highlighted (or tracked)
when the mouse is over the button. However, we don't receive any special
notification from Windows when a system button is being hot-tracked. For that we
need to determine if the mouse has moved over the button. In our example, we
have used the old 16-bit technique of sub-classing - basically we have replaced
the windows procedure of the owner-draw button with our own procedure so that we
can listen out for WM_MOUSEMOVE
messages. When the mouse moves over
the button the procedure will receive the WM_MOUSEMOVE
message and
then we can redraw the button highlighted.
It is the WM_DRAWITEM
notification from the operating system
that will prompt us to redraw the button. See that provided with the
notification is information on the current state of the button according to
Windows. Coupled with the extra state information gleaned from sub-classing we
now have enough information at hand in order to render the button.
When the owner-draw button is double-clicked it sends out a
BN_DBLCLK
notification rather than responding as if it
had been pressed and released twice over. In order to change this behaviour
we can make our owner-draw button post the WM_LBUTTONDOWN
notification back to itself when it receives a WM_LBUTTONDBLCLK
message.
So far, so good. The next problem is deciding what the button should look
like. The first step is to determine if the operating system supports themes.
The function InitThemes()
attempts to load UXTHEME.DLL
dynamically. Don't try and statically link to the UXTHEME library as the
application will fail to run on systems that don't ship with the dll. We load
the following functions: - OpenThemeData,
GetThemeBackgroundContentRect, DrawThemeBackground, DrawThemeText,
and CloseThemeData
.
Drawing the button is then a four stage process: -
- Stage 1 - draw the background (including the frame).
- Stage 2 - draw the icon.
- Stage 3 - write on the text.
- Stage 4 - draw the focus rectangle.
When the user has changed the theme the operating system posts a
WM_THEMECHANGED
message. It is then a simple matter of
unloading and then reloading the theme library to ensure the button
is rendered correctly.
To add basic support of themes to your application copy lines 27 to 82
(inclusive) from MainDlg.cpp. You must include the headers
UXTHEME.H and TMSCHEMA.H. Remember also to include a manifest and
to call InitCommonControls
when your application is first launched.
The functions PrepareImageRect
and DrawTheIcon
are
used to put the icon on the button. The WM_DRAWITEM
handler
demonstrates how to draw a themed and non-themed button, depending on the
operating system.
The functions PrepareImageRect
and DrawTheIcon()
are taken from the CButtonST class by Davide Calabro.