Owner-draw icon buttons in plain C (no MFC)
Contents
- Introduction
- Background
- Owner-draw
- DRAWITEMSTRUCT
- Inside WndProc()
- The function to handle WM_DRAWITEM
- Additional information
- History
Introduction
Although Windows has a really cool GUI, sometimes the way standard buttons look quite does not meet our requirements. We may then choose buttons with a style called "owner-draw". The owner has to draw the button in every aspect (background, border, text, image) of every situation that requires drawing (click, enable, focus, etc.). There are a lot of classes in MFC/C++ (many of which can be found in the CodeProject) to help us deal with buttons we feel we should design from the ground up. But, if we use the Win32 API and plain C, there is not much to read on the subject. It took me days to come up with something functional, thanks to the guys who know about Windows programming (Ch. Petzold, J. Newcomer, B. Rector). Now, here is some simple, reliable code: if it proves useful, I will write something about owner-draw buttons with text (dealing with fonts, shapes, edges, colorrefs, brushes, and pens).
Background
This code uses the Win32 API, so you should be familiar with message loop, Windows messages, notifications, handles, etc. We will be using CreateWindow()
to create controls at run-time, MoveWindow()
to resize them, SendMessage()
to communicate with them, etc. Some GDI functions will be called to set colors (SetBkColor()
, SetTextColor()
) in the read-only edit control. To allow for keyboard shortcuts, we will also load accelerators.
This (tiny) application works either with the ANSI character set or Unicode. First, we define two almost identical macros (UNICODE
and _UNICODE
) in our main header file, at the very top of the source code (or turn them into /* comments */
to stick with the ANSI character set). Then, we include the <tchar.h> header file and use generic types and functions. Here is a short explanation:
- As far as Windows is concerned, we
#define UNICODE
(this one has no underscore) and declare all char asTCHAR
(also useLPTSTR
instead ofLPSTR
,LPCTSTR
instead ofLPCSTR
, etc.).TCHAR
s are treated likewchar_t
if Unicode (orchar
if ANSI). Next, Windows will pick wide character functions, e.g.,SendMessageW()
if Unicode (or default toSendMessageA()
if ANSI) - see the winuser.h header file. - As for C, we
#define _UNICODE
(this one has an underscore) and use generic functions (e.g.,_tsprintf()
) which are turned into their wide character versions, hereswprintf()
(instead of their ANSI versions - in our examplesprintf()
). - We declare string constants like this:
_T("string constant")
,_T
being a macro that, if Unicode is defined, turns an ANSI"string constant"
made ofchar
into a UnicodeL"string constant"
made ofwchar_t
.
Owner-draw. Ahem. Well?
No, the "owner" in "owner-draw" is not you or me: it is the parent window whose child window is the owner-draw button. That defines which callback function will have to deal with the message (WM_DRAWITEM
) that the parent window will be sent to by the Windows operating system. In this article, the function that handles this message (myManageOwnerDrawIconButton()
) is called by the default window callback procedure (WndProc()
). This is the simple way, and it is enough for the explanation this article is about.
Now, if you want to write a self-sufficient custom control environment, you will need to forward the WM_DRAWITEM
message to an additional callback function, which will be designed to handle the messages to (and notifications from) your control, using, e.g., the macro FORWARD_WM_DRAWITEM
(see the header file windowsx.h). If you use MFC, you are familiar with this mechanism, because in order to deal with it, you will add member functions to an instance of the button class, not that of its parent window.
Here, we talk about a button, an owner-draw control with type ODT_BUTTON
, but other controls are eligible: ODT_LISTBOX
, ODT_COMBOBOX
, and ODT_STATIC
. There is an exception to the issue of forwarding WM_DRAWITEM
, that is a menu item (with type ODT_MENU
) whose processing should be done by the parent window. Note that the type is determined by Windows at run-time, whereas the style is ours to define at compile-time.
The DRAWITEMSTRUCT structure
Throughout the code, we use a struct that contains all the information we need to handle the WM_DRAWITEM
message. Here is a brief description (MSDN reference):
typedef struct tagDRAWITEMSTRUCT { UINT CtlType; /* ODT_BUTTON, etc. */ UINT CtlID; /* The control's specific constant */ UINT itemID; /* Same as above, but for a menu item */ UINT itemAction; /* Job to do: ODA_DRAWENTIRE, etc. */ UINT itemState; /* Checked, focus, selected, etc. */ HWND hwndItem; /* The control's handle */ HDC hDC; /* The device context (to draw with) */ RECT rcItem; /* The control's rectangle boundaries */ ULONG_PTR itemData; /* For menu items */ } DRAWITEMSTRUCT;
Inside WndProc()
Windows sends a wide range of messages to an application, which will process them one by one through its callback function(s); here, WndProc()
is the only callback function. In this program, we have:
WM_CREATE
: here we callCreateWindow()
to create two buttons with styleBS_OWNERDRAW
, plus one edit control (readonly), and one static control which will be our background. All controls have width and height equal to zero at this point.WM_SIZE
: this happens as soon as the windows are created, and as often as the user resizes the main window. Coordinates x and y, width and height, are relative to the client area and depend on constants defined in the header file. Maintenance is easier, and we state the (re)size instructions only once.WM_COMMAND
: this accounts for whatever the user does to the controls and/or to the menu items. E.g.,BN_CLICKED
is what happens when we click a button. Clicking a button, selecting a View menu item, or pressing F3 or F4 sends a message to the edit control, telling it to display a string constant.WM_CTLCOLORSTATIC
: we process this message to select colors in a static control as well as a disabled or readonly edit control. Now, if the edit control is neither disabled nor readonly, we have to processWM_CTLCOLOREDIT
instead.WM_CLOSE
: received when the user selects some File Quit menu item, clicks the upper-right little red box, or presses Alt+F4.WM_DESTROY
: triggered when we callDestroyWindow()
for the main window.WM_DRAWITEM
: on receiving this message, we retrieve a pointer to theDRAWITEMSTRUCT
structure usinglParam
. The declaration lies in theWndProc()
function:static DRAWITEMSTRUCT* pdis
. Then, we call our custom functionmyManageOwnerDrawIconButton()
withpdis
as one of the parameters.
Here is the relevant code:
// ... switch(message) { case WM_DRAWITEM: pdis = (DRAWITEMSTRUCT*) lParam; switch(pdis->CtlID) { case IDC_LEVELUPBUTTON: // Fall through (you would use a "break" otherwise): case IDC_LEVELDNBUTTON: iResult = myManageOwnerDrawIconButton(pdis, hInst); if (RET_OK != iResult) return(FALSE); break; default: break; } return(TRUE); // ...
The function to handle WM_DRAWITEM
Four calls to LoadIcon()
return handles to four icons, two for each button - one active (pressed), one inactive (waiting). A static counter is incremented at first call, so that the icons are only loaded once. Now, this function actually loads once, and subsequent calls only retrieve the handle to the existing icon. According to MSDN, LoadIcon()
is superseded by LoadImage()
, but LoadImage()
loads at each call, and requires the application to call some destroy function each time it has loaded something. So, we use LoadIcon()
- but if you prefer the other, you will need a counter, so here it is. Oh, and static means the variable is created once, so its value is retained from call to call - typically, what you need for counters.
Once we have handles to the four icons we need, the rest is pretty straightforward: DrawIconEx()
will draw an icon using the device context, at the places determined by the x, y coordinates of the upper-left corner of the icon, with certain width and height. Each icon is centered in the DRAWITEMSTRUCT
's rectangle. The icons that we use to draw the buttons are chosen according to the control's identifier (IDC_LEVELUPBUTTON
, IDC_LEVELUPBUTTON
) and its current state (ODS_SELECTED
). DrawIconEx()
is more interesting than DrawIcon()
in that it lets you choose the icon's size - whereas DrawIcon()
only draws an icon with fixed width GetSystemMetrics(SM_CXICON)
and height GetSystemMetrics(SM_CYICON)
, that is to say 32x32 pixels. Sure enough, this is what we did, but if you need flexibility, pick DrawIconEx()
so Windows will resize the icon view the way you want it.
Here is the relevant code:
// Declaration: int myManageOwnerDrawIconButton(DRAWITEMSTRUCT* pdis, HINSTANCE hInstance); // Load an icon handle: hIcon = (HICON) LoadIcon(hInstance, MAKEINTRESOURCE(ID_ICON)); // And draw the icon into the device context: DrawIconEx( pdis->hDC, (int) 0.5 * (rect.right - rect.left - ICON_WIDTH), (int) 0.5 * (rect.bottom - rect.top - ICON_HEIGHT), (HICON) hIcon, ICON_WIDTH, ICON_HEIGHT, 0, NULL, DI_NORMAL); // ...
Additional information
Voilà. If you like this sample program, if you think you can use it, please log in and rate the article. And of course, let me know if you think it can be improved.
I include two zip archives: a Visual Studio project and a MinGW (GCC-based) set of files, so choose the solution you are comfortable with.
Last, there is a function (myWriteToLog()
) that writes a string to a log file (odib_log.txt), which is created if need be (same directory as the exe), then removed as soon as the program quits. The program calls this function only when an error happens. Turn all myWriteToLog()
calls into comments if you do not want this to happen.
History
- August 8, 2007: Corrected HTML and source code.
- August 7, 2007: First version.