Owner Drawn Menus - Update

By Clayton Todd - Contact Me.

This is an update to an article I wrote, "Adding vertical text and a color bar to a popup menu".  The previous mentioned article contained many hard coded items and was difficult to customize.  This update seeks to make the code easier to customize and better explain some of the possibilities that can be accomplished when you set the OwnerDraw property of a Menu to true.  This article does not include any code for drawing vertical text, you can refer to the previous article for that example, but the drawing routines in this article are better then the ones in the previous article.  

By setting the OwnerDraw property to true we are specifiy that we want to draw all the stuff that makes up the menu items.  If you set the OwnerDraw property to true, add some menu items and run your program, your menus will look a little strange.  So you need to provided your own code for the OnDrawItem event.  You can also provide your own code for the OnMeasureItem event if we want to change the sizes of the menu, and in this article we will be doing that.

Getting Started

Open up Builder and start a new application.

Add a TMainMenu Component and an TImageList Component. 

Hook up the ImageList to the Menu and the Menu to the Form.

Be sure to set the OwnerDraw property of the TMainMenu Component to true.

I have included an example project if you wish to use it.  The code in this article is based off of it.

 

Place the code below in the header file in its correct place


const AnsiString BLANK_LINE="-";

private:	// User declarations
   TColor MainMenuBackground;
   TColor MainMenuHighlightColor;
   TColor MainMenuTextColor;
   TColor MainMenuTextBackground;
   TColor MainMenuHighlightTextColor;
   TColor Custom;
   TColor VerticalColor;
   TColor MenuColor;
   TColor HighlightColor;
   TColor BorderColor;
   TColor BorderEraseColor;
   TColor NormalTextColor;
   TColor NormalTextBackground;
   TColor HighlightTextColor;
   TColor DisabledTextColor;
   int VerticalWidth;
   int FocusRectRightIndent;
   int FocusRectLeftIndent;
   int LeftTextPos;
   int SideBuffer;
   int MenuIncreaseWidth;
   int Offset;

   int MenuItemHeight;
   int ItemOffset;
   TIcon *Icon;
protected:
   void __fastcall MyExpandItemWidth(TObject *Sender, TCanvas *ACanvas,
      int &Width, int &Height);
   void __fastcall MyDrawItem(TObject* Sender, TCanvas* ACanvas,
   const TRect &ARect, bool Selected);


We need to make sure that all the menu items under a main menu item have their OnDrawItem, and OnMeasureItem customized.  We want to set it so when you click on the main menu item all the menu items under it have those two events set to our customized events.  This example will go one sub-menu deep in setting the events.  If you have more sub-menus you can easily adjusted it to go into deeper levels.  Recursion should also work and reduce manually handling more and more sub-menus.  You will have to share this OnClick with any other main menu bar items or write seperate OnClick handlers for each.  By writing seperate ones you could make the menu items of 2 different main menu items act completely different or as I do here just use the same code for all the menu items.

Example of sub-menus.

This will assign our customized event handlers to the OnMeasureItem and OnDrawItem of all the menu items. If you want to restore the default handlers during runtime you will have to save them which is done the same way that we reassign them. By saving them you can turn OwnerDraw on and off during runtime and restore the old event handlers.


void __fastcall TForm1::Example1Click(TObject *Sender)
{

   TMenuItem *MenuItem = ((TMenuItem*)Sender);
   if(MenuItem->Count > 0)
   {
      for(int i=0; i <= MenuItem->Count-1; i++)
      {
         MenuItem->Items[i]->OnMeasureItem = MyExpandItemWidth;
         MenuItem->Items[i]->OnDrawItem = MyDrawItem;
         if(MenuItem->Items[i]->Count > 0)
         {
            for(int x=0; x <= MenuItem->Items[i]->Count-1; x++)
            {
               MenuItem->Items[i]->Items[x]->OnMeasureItem = MyExpandItemWidth;
               MenuItem->Items[i]->Items[x]->OnDrawItem = MyDrawItem;
            }
         }
      }
   }
}


You can customize how the menu bar items are drawn by adding our own drawing code to the OnDrawItem event for the main menu items.  If you have other items on the menu bar you can set them to all share the same code if you want them to all be drawn the same or provide customized drawing code for each one based on how you want to draw them.  I usually share the event handler with all the main menu items so the drawing is consistant and code is simpler.


void __fastcall TForm1::Example1DrawItem(TObject *Sender, TCanvas *ACanvas,
      TRect &ARect, bool Selected)
{
   TRect FocusRectBorder;
   TRect FocusRectFill;
   TMenuItem *MenuItem = ((TMenuItem*)Sender);

   AnsiString Text = MenuItem->Caption;

   // Color the background behind the text
   ACanvas->Brush->Color = MainMenuBackground;
   ACanvas->FillRect(ARect);

   // For any blank items acting as seperators
   if(Text == "")
      return;

   if(Selected)
   {
      // Draw the outline of the selection box
      FocusRectBorder = ARect;
      ACanvas->Brush->Color = BorderColor;
      ACanvas->FrameRect(FocusRectBorder);


      // Draw the inside of the selection box
      // Make is a little smaller so it does not erase the outline
      FocusRectFill = ARect;
      FocusRectFill.Top += SideBuffer;
      FocusRectFill.Right -= SideBuffer;
      FocusRectFill.Left += SideBuffer;
      FocusRectFill.Bottom -= SideBuffer;
      ACanvas->Brush->Color = MainMenuHighlightColor;
      ACanvas->FillRect(FocusRectFill);

      // Set the color that we want our text drawn when the item is selected
      ACanvas->Font->Color = MainMenuHighlightTextColor;
   }
   else
   {
      ACanvas->Font->Color = MainMenuTextColor;
   }


   int TextLength;
   TRect TextRect;

   TextLength = Text.Length();
   TextRect = ARect;
   // This determines where the text is drawn.
   TextRect.Left += 5;
   TextRect.Top += 1;

   // Draw the text
   DrawText(ACanvas->Handle,Text.c_str(), TextLength, &TextRect, 0);

}


Inside our custom OnMeasureItem event handler we can change the Height and Width of the menu.


void __fastcall TForm1::MyExpandItemWidth(TObject *Sender,
   TCanvas *ACanvas, int &Width, int &Height)
{
   Width += MenuIncreaseWidth;
   Height += Offset;
   MenuItemHeight = Height;
   ItemOffset = Offset/2;
}


Now for each one of the Menu Items we are going to draw them how ever we wish.  The Canvas you get access is the area of the menu item you are currently drawing.  So you could set each menu item to have different backgrounds, vertical color bars, etc...

You could load a bitmap file and draw it to the canvas with either Draw or Stretch Draw. This way you could have large custom image backgrounds.


void __fastcall TForm1::MyDrawItem(TObject* Sender, TCanvas* ACanvas,
   const TRect &ARect, bool Selected)
{
   int TopPos, TextLength;
   AnsiString Text;
   TRect TempRect;
   TRect VerticalRect;
   TRect FocusRectBorder;
   TRect FocusRectFill;
   TRect TextRect;

   TMenuItem *MenuItem = ((TMenuItem*)Sender);

   Text = MenuItem->Caption;

   // Draw the Background erases anything that was there before.
   ACanvas->Brush->Color = MenuColor;
   ACanvas->FillRect(ARect);

   // Draw any seperator lines
   if(Text==BLANK_LINE)
   {
      // Draw the Vertical Bar
      VerticalRect = ARect;
      VerticalRect.Top -= SideBuffer;
      VerticalRect.Right = VerticalWidth;
      VerticalRect.Bottom += SideBuffer;
      ACanvas->Brush->Color = VerticalColor;
      ACanvas->FillRect(VerticalRect);

      // Draw the Blank Line
      ACanvas->MoveTo(VerticalWidth,ARect.Top+ARect.Height()/2);
      ACanvas->LineTo(ARect.Right,ARect.Top+ARect.Height()/2);
      return;
   }

   // This is for non seperator lines

   TextLength = Text.Length();

   if(Selected)
   {
      // Have to draw the vertical bar section to fill in the area that is not
      // covered by the selection rect
      VerticalRect = ARect;
      VerticalRect.Top -= SideBuffer;
      VerticalRect.Right = VerticalWidth;
      VerticalRect.Bottom += SideBuffer;
      ACanvas->Brush->Color = VerticalColor;
      ACanvas->FillRect(VerticalRect);

      if(MenuItem->Enabled)
      {
         // The item is selected and enabled

         // Draw the focus rect outline border
         FocusRectBorder = ARect;
         FocusRectBorder.Left += FocusRectLeftIndent - SideBuffer;
         FocusRectBorder.Right -= FocusRectRightIndent - SideBuffer;
         ACanvas->Brush->Color = BorderColor;
         ACanvas->FrameRect(FocusRectBorder);


         // Fill in the focus rect.  Making it a little smaller so as to not
         // draw over the outline
         FocusRectFill = ARect;
         FocusRectFill.Right -= FocusRectRightIndent;
         FocusRectFill.Left += FocusRectLeftIndent;
         FocusRectFill.Bottom -= SideBuffer;
         FocusRectFill.Top += SideBuffer;
         ACanvas->Brush->Color = HighlightColor;
         ACanvas->FillRect(FocusRectFill);

         // Set the way we want to draw our text
         ACanvas->Font->Color = HighlightTextColor;
         ACanvas->Font->Style = TFontStyles() << fsBold;
      }
      else
      {
         // Menu item is not enabled so do not draw any selection
         // rect and change the text color to reflect its unenabled state
         // NOTE: The icon is still drawn normally and not disabled
         ACanvas->Font->Style = TFontStyles();
         ACanvas->Brush->Color = NormalTextBackground;
         ACanvas->Font->Color = DisabledTextColor;
      }

   }
   else
   {
      // Fill in the vertical area
      VerticalRect = ARect;
      VerticalRect.Top -= SideBuffer;
      VerticalRect.Right = VerticalWidth;
      VerticalRect.Bottom += SideBuffer;
      ACanvas->Brush->Color = VerticalColor;
      ACanvas->FillRect(VerticalRect);

      // Set the text background color and font based on if it is enabled or not

      if(MenuItem->Enabled)
      {
         ACanvas->Brush->Color = NormalTextBackground;
         ACanvas->Font->Color = NormalTextColor;
      }
      else
      {
         ACanvas->Brush->Color = NormalTextBackground;
         ACanvas->Font->Color = DisabledTextColor;
      }

   }

   // Calculate out the Rect we want to draw our text in
   TextRect = ARect;
   TextRect.Left += LeftTextPos;
   if(Offset > 0)
      TextRect.Top += Offset/2 + SideBuffer;
   else
      TextRect.Top += 2 + SideBuffer;

   TextRect.Top += SideBuffer;

   // Draw any menu item icons
   if(Menu->Images != NULL)
   {
      Icon = new TIcon();
      Menu->Images->GetIcon(MenuItem->ImageIndex,Icon);
      ACanvas->Draw(5,ARect.Top+ItemOffset+1,Icon);
      delete Icon;
   }

   // Draw the text
   DrawText(ACanvas->Handle,Text.c_str(), TextLength, &TextRect, 0);
}


Now we have the menus drawing, but what if we want the width of the menu to larger, or the height, or different colors.  Unlike the first article all the variables have been setup so that you can change them in one place and it works for the whole program.

Example of Offset = 25;


void __fastcall TForm1::FormCreate(TObject *Sender)
{
   // Custom light blue color
   Custom=TColor(RGB(185,239,245));

   // Used to draw and highlight the main menu items
   MainMenuBackground = clSilver;
   MainMenuHighlightColor = Custom;
   MainMenuTextColor = clBlack;
   MainMenuTextBackground = clSilver;
   MainMenuHighlightTextColor = clRed;

   // Used to draw and color the menu items
   VerticalColor = clSilver;
   MenuColor = clWhite;
   HighlightColor = Custom;
   BorderColor = clBlack;
   NormalTextColor = clBlack;
   NormalTextBackground = clWhite;
   HighlightTextColor = clBlack;
   DisabledTextColor = clSilver;

   // Width of the vertical bar on the right
   VerticalWidth = 26;

   // Space buffer between the sides of the menu and the sides of the focus rect
   FocusRectRightIndent = 3;
   FocusRectLeftIndent = 3;

   // Position to draw the text
   LeftTextPos = 35;

   // Space between the focus rect outline and the focus rect
   SideBuffer = 1;

   // Assign an image list makes the width wider, so we need to adjust when you do and don't have one.
   if(Menu->Images == NULL)
      MenuIncreaseWidth = 100;
   else
      MenuIncreaseWidth = 50;

   // Offset will increase the Height of the menu items.  Also used to make sure the icons are aligned correctly.
   Offset = 5;
}


I mentioned earlier about drawing larger bitmaps to the menu items canvas. In the OnMeasureItem you could have something like the following. I have seen some commercial menu components that allow bitmap backgrounds, the code below shows how easy you can do it also.


   if(Image == NULL)
      Image = new Graphics::TBitmap();
   Image->LoadFromFile("menupic.bmp");

   // We need to subtract the width of the images in an ImageList if one is being used.
   Width = Image->Width-Menu->Images->Width;
   Height = Image->Height;

Then in the OnDrawItem you can simply draw the image to the canvas.


 ACanvas->Draw(0,0,Image);

Doesn't cover everything, but should get you started.