Click here to Skip to main content
15,885,546 members
Articles / Desktop Programming / MFC
Article

Owner-draw icon buttons in plain C (no MFC)

Rate me:
Please Sign up or sign in to vote.
4.67/5 (29 votes)
7 Aug 2007CPOL7 min read 143.4K   4.6K   55   28
Use icons to draw buttons with owner-draw style.

Screenshot - odib_screenshot.png

Contents

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 as TCHAR (also use LPTSTR instead of LPSTR, LPCTSTR instead of LPCSTR, etc.). TCHARs are treated like wchar_t if Unicode (or char if ANSI). Next, Windows will pick wide character functions, e.g., SendMessageW() if Unicode (or default to SendMessageA() 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, here swprintf() (instead of their ANSI versions - in our example sprintf()).
  • We declare string constants like this: _T("string constant"), _T being a macro that, if Unicode is defined, turns an ANSI "string constant" made of char into a Unicode L"string constant" made of wchar_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 call CreateWindow() to create two buttons with style BS_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 process WM_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 call DestroyWindow() for the main window.
  • WM_DRAWITEM: on receiving this message, we retrieve a pointer to the DRAWITEMSTRUCT structure using lParam. The declaration lies in the WndProc() function: static DRAWITEMSTRUCT* pdis. Then, we call our custom function myManageOwnerDrawIconButton() with pdis 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
George Sand Hospital, Bourges
France France
Electronic health record, health classifications (diseases, procedures), database, diagnosis related groups.

Comments and Discussions

 
Questionpush button or radio button like owner draw button Pin
magicdirac29-Apr-17 14:29
magicdirac29-Apr-17 14:29 
GeneralMy vote of 5 Pin
PhilHart14-Aug-16 21:31
PhilHart14-Aug-16 21:31 
Questioncoloured button Pin
p3z3t7-Sep-13 2:50
p3z3t7-Sep-13 2:50 
AnswerRe: coloured button Pin
Bruno Challier10-Sep-13 8:07
Bruno Challier10-Sep-13 8:07 
GeneralRe: coloured button Pin
p3z3t20-Sep-13 6:44
p3z3t20-Sep-13 6:44 
GeneralMy vote of 5 Pin
Fletch5718-Sep-12 8:44
Fletch5718-Sep-12 8:44 
GeneralRe: My vote of 5 Pin
Bruno Challier25-Sep-12 21:07
Bruno Challier25-Sep-12 21:07 
QuestionOwner Draw Menus Pin
markrallyn7-Jan-12 2:24
markrallyn7-Jan-12 2:24 
AnswerRe: Owner Draw Menus Pin
Bruno Challier7-Jan-12 4:55
Bruno Challier7-Jan-12 4:55 
GeneralIcons get redrawn on top of each other. [modified] Pin
sight923-Mar-11 14:17
sight923-Mar-11 14:17 
GeneralRe: Icons get redrawn on top of each other. [modified] Pin
Bruno Challier7-Jan-12 4:33
Bruno Challier7-Jan-12 4:33 
GeneralMy vote of 5 Pin
jorgening13-Nov-10 7:23
jorgening13-Nov-10 7:23 
GeneralIcon On menu (.ico and not .bmp) Pin
Aabid24-Aug-09 21:42
Aabid24-Aug-09 21:42 
GeneralRe: Icon On menu (.ico and not .bmp) Pin
Bruno Challier25-Aug-09 3:39
Bruno Challier25-Aug-09 3:39 
GeneralYour code is not compiled under VS2005 Pin
Alex_II25-Oct-08 7:37
Alex_II25-Oct-08 7:37 
Generaldouble clicks of the buttons Pin
Zoltan Gaspar17-May-08 7:43
Zoltan Gaspar17-May-08 7:43 
GeneralRe: double clicks of the buttons Pin
Bruno Challier25-Aug-09 3:41
Bruno Challier25-Aug-09 3:41 
GeneralGood Article Pin
yiyang99913-Dec-07 10:04
yiyang99913-Dec-07 10:04 
GeneralThanks! Pin
Leslie Sanford18-Sep-07 10:43
Leslie Sanford18-Sep-07 10:43 
GeneralRe: Thanks! Pin
Bruno Challier18-Sep-07 18:06
Bruno Challier18-Sep-07 18:06 
Generalusing LoadImage Pin
Alex Cohn6-Sep-07 10:44
Alex Cohn6-Sep-07 10:44 
AnswerRe: using LoadImage Pin
Bruno Challier8-Sep-07 1:53
Bruno Challier8-Sep-07 1:53 
GeneralSome problems Pin
KarstenK7-Aug-07 22:36
mveKarstenK7-Aug-07 22:36 
AnswerRe: Some problems Pin
Bruno Challier8-Aug-07 3:52
Bruno Challier8-Aug-07 3:52 
Although I had no trouble building (VS2003 or gcc), you are right about myWriteToLog(), declarations have to come first. Also, the DrawInconEx() part is much simpler now. As for comments in French, those were inserted by VS2003.
Finally, processing ODS_HOTLIGHT may be what you are looking for, but I have not figured that out yet. Overall, thanks for your advice.

Bruno

GeneralTake a look at _tWinMain Pin
Mihai Nita7-Aug-07 6:31
Mihai Nita7-Aug-07 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.