How To: Using bitmaps in menus
Posted: (EET/GMT+2)
How To: Using bitmaps in menus
Level: Intermediate
If you have had the time to take a look at the latest Microsoft products,
for example the members of the Office '97 family, you can see that the
menus in the applications use bitmaps. When I first saw the menus, the
words "wanna do that too" instanly popped into my mind. Fortunately, using
bitmaps is actually very easy, you just have to make a bit more work than
with the Delphi's TMenu component.
First, a bit background about menu creation. If you have programmed Windows while there was no such thing as Delphi around, you know that menus were created using plain API functions. The functions used in this How To are CreateMenu, CreatePopupMenu, InsertMenu, and SetMenu.
CreateMenu is used to create the menu bar, which is also called the "top-level" menu. Because there wouldn't be much use with only a menu bar, we also need to create few popup menus using the (you guessed it) CreatePopupMenu call.
Initially, all menus are empty. Thus, we must insert some menu items into the menus. Because top-level menu creation calls require a handle to the popup menu they work with, it is easiest to create the popup menu before creating the top-level item.
For this How To, we use InsertMenu to create the items. The call looks something like this:
MainMenu := CreateMenu; { create the top-level menu }
PopupMenu := CreatePopupMenu; { create a popup menu for the File menu }
InsertMenu(PopupMenu,-1,mf_ByPosition+mf_OwnerDraw,1,PChar(MenuItemTexts[1]));
After the menu is created and associated with the form using the SetMenu
call, we are ready to do the actual work. Because Windows now knows that we
are going to draw the menu items ourselves, it has to query us how large
the items are going to be. For this reason, Windows sends the
WM_MEASUREITEM message to the window associated with the menu.
Handling the message is just a matter of setting few dimensions. Of course,
this example is trivial here, but in an actual application you would
include a bit more complex calculations.
Of course, the color depends on the selection status. If we have a
selection, we use by default the blue color, othwerise light gray. Then we
simply draw the rectangle, which, by the way, wouldn't be absolutely
necessary when there is no selection, because Windows does this for use.
After the background, we draw the bitmap, if any. Because we numbered the
menu items similarly to the TImage components which store the bitmaps,
finding the correct bitmap is easy using the FindComponent call. If no
bitmap is found (like in the case of the File|Exit command), we simply draw
nothing. Otherwise, we draw the bitmap. Finally, we draw the menu text.
As you can see, using bitmaps in menus is easy, if not trivial. However,
creating menus this way can be a waste of time, even more if you compare it
with the amount of time needed to create a normal menu. For this reason,
I'm on the quest to write a TBitmapMenu component. Stay tuned, it might
become available some day... ;-)
Here, we use the mf_OwnerDraw flag to tell Windows that we want to draw the
menu item ourselves. Actually, this is the key to using bitmaps in menus:
because we draw the items ourselves, we can display anything, including
bitmaps. (BTW, it isn't necessary to include any text when creating the
menu item because we draw the text, but I've included the text just in
case you happen to remove the mf_OwnerDraw flag.)
Procedure TForm1.WMMeasureItem(Var Msg : TWMMeasureItem);
Begin
With Msg.MeasureItemStruct^do Begin
ItemWidth := 100;
ItemHeight := 21;
End;
End;
After this, we must create an event handler to actually draw the items.
Windows send the form a WM_DRAWITEM message for every owner drawn menu item
we've created. The actual handler looks more or less like this:
Procedure TForm1.WMDrawItem(Var Msg : TWMDrawItem);
Var
Image : TImage;
Canvas : TCanvas;
S : String;
Begin
With Msg.DrawItemStruct^ do Begin
Canvas := TCanvas.Create;
With Canvas do Begin
Handle := hDC;
If ((ItemState And ods_Selected) <> 0) Then Begin
Pen.Color := clHighlight;
Brush.Color := clHighlight;
Font.Color := clHighlightText;
End
Else Begin
Pen.Color := clBtnFace;
Brush.Color := clBtnFace;
End;
With RCItem do Begin
Rectangle(Left,Top,Right,Bottom);
Image := TImage(FindComponent('Image'+IntToStr(ItemID)));
If (Image <> nil) Then Draw(Left+3,Top+2,Image.Picture.Graphic);
If (ItemID = 100) Then S := '&Exit'
Else S := MenuItemTexts[ItemID];
Left := Left+30;
Top := Top+3;
Windows.DrawText(Handle,PChar(S),Length(S),RCItem,dt_Left);
End;
End;
End;
End;
The first thing to do in the handler is to create a canvas for the menu
item. Of course, we could make the handler a bit faster by elimiating the
TCanvas object, but this method is easier. After the canvas has been
created and associated with the menu's device context, we are ready to draw
the background.
Code for this How To
Questions? Comments? Give feedback.
Or just fill in a form.