A Better Bitmap Button Class
I never much liked the CBitmapButton class. It has several major defects.
This class has too much generality and power for the "typical" case. So it makes the simple case as hard as the most general case. I don't need the most general case, and you probably don't either. You just want to display an interesting bitmap in a button, rather than some boring piece of text. The rest you'd like to work properly without any effort on your part.
The CBitmapButton class is well and truly weird in that it nominally requires you to create one-to-four images of the button, all nicely designed with the right kinds of edges and everything, and name them with funky names like "ACTIOND", "ACTIONU", "ACTIONF" and "ACTIONX". Of course, you can actually use IDs, and call the LoadBitmaps method yourself, but you still have to create those bitmaps.
Do you know how to create a really good-looking 3D edge? It's tricky. It is even more tricky when you have to create one that will work at any resolution. Do you know how to create a focus rectangle that looks good at any resolution? And what if you don't want your buttons to have to resize to the bitmap?
But why bother worrying about any of these issues? Mostly when I do a bitmap button I want a button that looks like an ordinary button. It has the appropriate edges, the appropriate edge changes on push-down, and a focus rectangle nicely displayed. The only thing I want different is the bitmap. And I'd like to draw only one, please.
The bitmap class described in this article solves all these problems. I've had code floating around for a while, but the number of recent bitmap questions prompted me to clean it up and get it into a publishable form.
CImageButton draws a button in a raised or depressed state and draws a focus rectangle around the image when the control has the focus.
This class requires only one bitmap, although you can supply up to three. You don't need the fourth (focus) bitmap because the focus rectangle is automatically drawn.
The bitmaps are specified solely by their resource IDs. No need to use nonstandard naming conventions. And if you want to use different bitmaps for different resolutions, you can do so just by calling LoadBitmaps with the desired bitmap IDs in your OnInitDialog handler.
If you supply only a single bitmap (the ideal situation), then the same bitmap is used for up and down, and will be grayed out with alternate-gray pixels when disabled. If you don't like the gray-out algorithm, you can supply an explicit gray-out image of your own devising for the disabled state.
Similarly, since the edges and focus are provided for you, you usually don't need to supply a special bitmap for the "down" state. The only real trick in doing the "down" state is that the focus rectangle and bitmap image shift right-and-down to preserve a visual illusion of a button push.
What about the problem of "dialog units", that nightmare that haunts everyone who ever tries to do something with a bitmap and dialog? If you don't know what this problem is, Microsoft cleverly decided that a dialog should resize itself based on screen resolution, so a dialog that looks reasonable at 640×480 will look equally reasonable at 1600×1200. Actually, this is a good idea, but its implementation leaves a lot to be desired. The most serious defect is that anything that uses a bitmap is in deep trouble, because bitmaps are always in pixel resolution. So your really nice-looking 16×16 bitmap at 640×480 is nearly invisible at 1600×1200. At 640×480 it is 1/40×1/30 of the screen. At 1600×1200 it is 1/100×1/75 of the screen. It has been reduced in size by a factor of 2.5.
I handle this by a provision to "fill" the button area with the bitmap image. This uses the StretchBlt function to expand the bitmap. For some images this works well; for others, it produces really ugly effects such as aliasing artifacts. But for a large number of simple images you use for your buttons, it is satisfactory, and saves having to create bitmaps at multiple resolutions.
What if you want an image that doesn't quite go from edge-to-edge in your button, but you want it to resize? Simply create a larger bitmap, and fill the bitmap in with the nominal 3D face color, which is RGB(192, 192, 192), "gray". I load the bitmap with LR_LOADMAP3DCOLORS, so instances of dark gray, gray, and light gray will all be replaced with the user's current display scheme settings. I use the COLOR_3DFACE to "gray out" the image if you haven't supplied a grayed-out image of your own, so the color scheme the user selects will be followed.
If you have chosen to not use the "fill" feature, this class honors the various button styles, such as BS_CENTER, BS_LEFT, and BS_RIGHT for horizontal alignment and BS_VCENTER, BS_TOP, and BS_BOTTOM for vertical alignment.
Actually, the only really tricky part of all this code was handling all the weird cases, such as what if the bitmap is larger than the button and the caller has specified a right-justified image? (Answer: the image is truncated on the left). You can read the code to discover all the other strange cases.
To create an image button, you create a button in the dialog editor, and mark it as owner-draw. Any caption you supply will be ignored. You then go into the Class Wizard and create a control variable for it, of type CImageButton. Note that you may have to rebuild the .clw file after adding CImageButton to your project. Delete the .clw file and invoke ClassWizard, then tell it to reconstruct the file from the existing project files.
What I provide in my CImageBitmap class is a simple set of methods that extended the basic CButton class.
void CImageBitmap::LoadBitmaps(UINT up, UINT down = 0, UINT disabled = 0)
You call this method to tell the button what images to use. Only the up image is required. The rest are optional, and are supplied implicitly at drawing time according to the table below.
-- LoadBitmaps parameters--
-- Button state --
void CImageBitmap::GetBitmaps(UINT &up, UINT &down, UINT &disabled)
This can be used to retrieve the resource IDs of the bitmaps set by the last LoadBitmaps call.
DWORD CImageButton::SetVPos(DWORD style)
This can be used to set the vertical position of the image. It can be one of BS_TOP, BS_BOTTOM, or BS_VCENTER. It returns the previous vertical style.
DWORD CImageButton::SetHPos(DWORD style)
This can be used to set the horizontal position of the image. It can be one of BS_LEFT, BS_RIGHT, or BS_CENTER. It returns the previous horizontal style.
This value is set to FALSE by the constructor. You can set it explicitly to TRUE if you want image-filling to occur.
Note that if you change the value of this variable after the OnInitDialog has completed, you should call InvaldiateRect on the CImageButton object to force it to redraw.
(Note: you may have read my diatribes about the silly m_ notation being used gratuitously; I've even seen people so misled by it that they declare local variables on the stack using this naming convention! But the one place I use it is to name member variables that are used to communicate information into a class instance from outside the class. This is one such instance.)
Just because there were several requests for how to make a button that "toggles", I've created a toggle bitmap class, which also downloads with this project. Look for the CToggleButton class in ToggleButton.cpp and ToggleButton.h. To create a toggle button, you create a button in the dialog editor, add a caption, and mark it as owner-draw. You then go into the Class Wizard and create a control variable for it, of type CToggleButton. Note that you may have to rebuild the .clw file after adding CToggleButton to your project. Delete the .clw file and invoke ClassWizard, then tell it to reconstruct the file from the existing project files.
This returns the current button-state, TRUE if the button is toggled on (image is depressed) and FALSE if the button is toggled off (image is not depressed).
BOOL CToggleButton::SetState(BOOL newState)
This sets the state. If newState is TRUE the state is "on" (depressed) and if newState is FALSE the state is "off" (not depressed). This returns the previous state of the button.
DWORD CToggleButton::SetVPos(DWORD style)
This can be used to set the vertical position of the text. It can be one of BS_TOP, BS_BOTTOM, or BS_VCENTER. It returns the previous vertical style.
DWORD CToggleButton::SetHPos(DWORD style)
This can be used to set the horizontal position of the text. It can be one of BS_LEFT, BS_RIGHT, or BS_CENTER. It returns the previous horizontal style.
The source file for these classes, and a project that I used for testing them, can be downloaded directly by clicking the button to the left.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.