Owner-Draw Buttons
Modern graphical interfaces make extensive use of buttons that are painted with pictures rather than text. Also menu items may use pictures or non-standard fonts. These buttons and menu items are called “owner-draw” in Windows. You can take complete control of the appearance of a button, list box, combo box, or menu item. Here's a sample of some owner-draw buttons from MathXpert:
In this picture, the borders around the buttons are drawn by GDI graphics--they are just done by LineTo with a pen in shades of light gray or dark gray.
The background color of the button has been set to exactly match the background color of the bitmap. (You can't see that in black and white very well.) That way, the button does not have to be the exact size of the bitmap. It just has to be larger. Since the bitmap is measured in pixels, and buttons (at least in dialog boxes) are not, this is important if we want our program to look good at various resolutions.
Since painting buttons with bitmaps is so common a requirement, and so tricky to program all by yourself, there are some shortcuts in MFC. But they all have some shortcomings.
First let us discuss what happens without these shortcuts. Then your choice is binary: take a standard button as Windows draws it, or draw the entire button yourself. Drawing it yourself means that you are responsible for the shading effects when the button is pressed. The same goes for menus: if you declare an item to be owner-draw, you will be responsible for its appearance when it is highlighted (as the mouse cursor moves over it).
It is this fact that makes matters complicated.
A button, list box, combo box, or menu item that has had its style set to owner-draw will receive WM_MEASUREITEM and WM_DRAWITEM messages.
It has to respond to the WM_MEASUREITEM by telling Windows the dimensions of the item. Windows can then create a device context the right size. This device context (for the button, menu, or list box) will be passed with the WM_DRAWITEM message. Your program responds to this message by painting the button or menu or list box.
The parameters of the OnDrawItem handler will tell you whether the button has to be painted in the normal state, or the depressed state, or the disabled state. You can either use a different bitmap for each state, or you can use one bitmap and handle the shading effects with GDI graphics, drawing gray lines or white lines along some edges of the button. That is what was done in the picture on the first slide.
Be sure to leave the DC in the same condition you got it, that is, with the original brushes, pens, etc.
Button Size and Bitmap Size
Button sizes are specified in the .rc file in “dialog base units”. These are a fixed fraction of the dialog font. Bitmap sizes are always in pixels. At a different resolution, your bitmap will have the same size in pixels, but your button may not. So, at some resolutions, your bitmap may be small and at other resolutions it may not even entirely fit in the button. Of course in that case it will be clipped, but that may make the button look wrong when depressed (or when not depressed for that matter) because the shading effects at the edge get clipped. There are two solutions to this problem.
One method (which is the one used by the CBitmap class) is to resize the button at run time, to just fit the bitmaps. Let’s hope there’s always room in the dialog box to do that without messing up the appearance of your dialog.
Another method, which I have used in MathXpert, is to just be sure the buttons are big enough to hold the bitmaps, and color the buttons to match the background of the bitmap. This requires that the boundary of the bitmaps be all the same color. Then the bitmaps merge imperceptibly into the background. The shading effects are done with GDI graphics, so only one bitmap per button is needed.
However, the bitmaps that are used to make buttons at low resolutions shrink at high resolutions to be quite small. Therefore, most modern applications must maintain two sets of bitmaps for their buttons. The large bitmaps (large in pixels) are used at high resolutions, and the small bitmaps are used at low resolutions (where they don't appear so small). This means the application must detect the resolution using GetDeviceCaps, and select the bitmaps to be used accordingly. This detection has to be done carefully as too early a call to GetDC can cause a crash.
Easy Bitmap Buttons with MFC
- Apply the Bitmap style on the button's property sheet.
- Declare a variable of type CButton that will last as long as the button; for example by using Class Wizard to associate a control variable to the button.
- In OnInitDialog, or in OnInitialUpdate (depending whether this is a modal dialog or a CFormView application) call the SetBitmap member function of this variable. This function needs an HBITMAP as argument--you cannot avoid the Win32 API here. HBITMAP is the Win32 way of manipulating a bitmap--it is a "handle to a bitmap". This is the Win32 object which is "wrapped" by the CBitmap class. The CBitmap class contains a member function to return a handle to the wrapped bitmap. This function is named GetSafeHandle. The Safe part means it works even if called from another thread than the one owning the bitmap (I think).
In this method, you need only one bitmap. MFC will use shading effects done with GDI graphics to draw the button in the depressed state. Clearly what happens here is that MFC makes this an owner-draw button but supplies the code for drawing it. So as far as the underlying Win32 API goes, this is the same as if you wrote the code to paint the button. But from the MFC programmer's point of view, it’s easier. You don't have to write OnMeasureItem and OnDrawItem.
An Example Program--OwnerDrawDemo.
Make an SDI application based on CFormView. Add a button to the form and set its property sheet as shown:
Note that the text disappears from the button when you set the Bitmap style.Now, right-click your button, choose Add Variable, and define a CButton variable to go with this button:
Now, we will need a bitmap to use to paint the button. Therefore we add one, as we have done before, using Project | Add Resource | Bitmap | Import. Also add a member variable m_MonaLisa of type CBitmap.
I inserted monalisa.bmp, which we used in a previous example. While you have the bitmap editor open, you can right-click any blank area to bring up the property sheet of the bitmap. There you can change the ID from IDB_BITMAP1 to IDB_MONALISA. That’s an important thing to do if you will have several bitmaps in your program, to avoid confusion about which identifier goes with which picture.
Next we go to OnInitialUpdate and initialize the button as follows. If this had been in a dialog instead of a CFormView application you would put this code in OnInitDialog.
void COwnerDrawDemoView::OnInitialUpdate()
{ CFormView::OnInitialUpdate();
GetParentFrame()->RecalcLayout();
ResizeParentToFit();
m_MonaLisa.LoadBitmap(IDB_MONALISA);
HBITMAP hBitmap= (HBITMAP) m_MonaLisa.GetSafeHandle();
m_Button1.SetBitmap(hBitmap);
}
Now run the program. You don't see the whole bitmap, because we never resized the button.
Now, if we were to resize the button manually to the correct size, that would be difficult, and wrong anyway, since at a different resolution it will be a different size measured in pixels. Instead, go back to OnInitDialog and use the code we used before to get the size of a bitmap.
BITMAP bm;
m_MonaLisa.GetBitmap(&bm);
int width = bm.bmWidth;
int height = bm.bmHeight;
CRect r;
m_theButton.GetWindowRect(&r); // r comes out in screen coordinates
ScreenToClient(&r); // MoveWindow needs coordinates in parent window
r.right = r.left + width;
r.bottom = r.top + height;
m_theButton.MoveWindow(&r);
This code goes in OnInitialUpdate just after the previous code. Now the button fits the bitmap perfectly:
What if I want both a picture and text on my button?
Then you have two choices:
- have the text included as part of the bitmap (use a text tool in your picture-editing software)
- don’t use the “easy” method above; write your own OnMeasureItem and OnDrawItem. In OnDrawItem use DisplayBitmap2 as we have done before to display your bitmap, and then use TextOut to display your text.
This is what I did in MathXpert, so that the text on each button can be displayed in English, German, or French (whichever is in use at the moment) without having to supply a new set of bitmaps for each language, and switch the bitmaps dynamically when the language switches.
Using CBitmapButton and AutoLoad
There is another way to put pictures on buttons in MFC, using the class CBitmapButton. In my opinion it’s not very useful, but I will discuss it so that you know its limitations, which are not quite apparent until you try it. The advantages of using it are
- You don’t have to write OnMeasureItem or OnDrawItem
- You don’t have to size the button yourself to fit the bitmap
Sounds great, but:
- You’ll need to supply at least two bitmaps, one for when the button is up and one for when it is depressed. Different bitmaps are used for this rather than GDI graphics for borders.
- CBitmapButton doesn’t support palettes, so if your application must support 256 color monitors, this method is out of the question, unless you are content to use only 16-color bitmaps on your buttons.
The manual does not make the first important limitation quite clear. It seems to imply that you could get by with only one bitmap. But that is not the case. If you supply only one bitmap, you can compile and run, but you don’t see anything happen when the button is clicked. If you want to see a visual effect when the button is clicked (which of course you do) then you must supply at least two bitmaps.
You still set the button’s owner-draw property and supply bitmaps for both normal and depressed (up and down) states of the button. You have to call AutoLoad and you have to be careful to identify your bitmap resources by name instead of by ID number. Note that this method will not support palettes; so it will work OK for many-color bitmaps in true-color modes, but not in 256-color mode.
If you should want to try this method (sometime in the future when you’re working for a company whose graphic artists can produce the required bitmaps) , then read the manual entries for CBitmapButton, then for its member function AutoLoad, and finally click the link in that entry for the comments on the CBitmapButton class.