Jan Schröder
IT-Dienstleistungen

Class MenuOwnerDraw - Menus with imgages in C#

The C# class MenuOwnerDraw transforms at runtime menus to owner drawn menus, with the possibility to display images, which symbolize the menu items functions. MenuOwnerDraw is a class, not a component, so no additional DLL has to be deployed. Look at a tiny example and the source code:

 
Download the source (50 KB)

 

using System;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Runtime.InteropServices;
namespace SI.Controls
{
  public class MenuOwnerDraw
  {
#region Description
    //
    //####################################################################################
    //
    // class     MenuOwnerDraw {
    // Author    Jan Schröder
    //           Schröder Informatik
    //           www.SchroederInformatik.de
    // Version   1.0.2
    // Date      05/05/23
    //
    // This class transforms menus to owner drawn menus, with the possibility to display
    // images, which symbolize the menu items functions.
    //
    // Use this class as follows:
    // 1. Add this Visual Basic source to your project.
    // 2. Create the form or context menu as usually. It's important to leave the
    //    properties "OwnerDraw" as "false".
    // 3. Add an image list to the form or use one, which is already linked to the
    //    toolbox.
    // 4. Append the indexes of the images, which should be displayed, left to the text
    //    of the appropriated menu items. for ( example: if ( you want to display the
    //    image with index 0 left to the menu item with the text "Save", append "_i_0"
    //    to the text. The text has to be "Save_i_0".
    // 5. Create instances of this class in the forms load event. for ( example:
    //    MenuOwnerDraw1 = new MenuOwnerDraw(this, ImageList1);
    //    MenuOwnerDraw2 = new MenuOwnerDraw(ContextMenu1, ImageList2);
    //
    // That's all.
    //
    // MenuOwnerDraw has only one public member: the new constructor with two
    // alternatives, one for the form menu and the other for context menus.
    //
    // The private methodes "MeasureItem" and "DrawItem" are doing the work. Both are
    // callback functions, used to handle the events "MeasureItem" and "DrawItem".
    //
    //
    // Versions:
    // 1.0.0 04/11/26    First Version
    // 1.0.1 05/01/29    DrawMenuCheck; new design (more XP-stylish);
    //                   better support of system colors with very much contrast;
    //                   new outline of the source code
    // 1.0.2 05/05/23    Translation from VB into C#
    //
    //####################################################################################
    //
#endregion
#region Constructors
    public MenuOwnerDraw(Form FormX, ImageList imgMenuImages)
    {
      InitializeFormMenu(ref FormX, ref imgMenuImages);
    }
    public MenuOwnerDraw(ContextMenu ContextMenu, ImageList imgMenuImages)
    {
      InitializeContextMenu(ref ContextMenu, ref imgMenuImages);
    }
#endregion
#region Data definition
    private ImageList imgMenuImagesForModule;
    private int nXimgLeft, nXimgRectRight, nXtextLeft;   // metrics
    [ StructLayout( LayoutKind.Sequential )]
      private class StringSize
    {
      public int cx, cy;
      public StringSize(){cx = 0; cy = 0;}
    }
#endregion
#region Event handling
    private void DrawSubMenuItem(object sender, DrawItemEventArgs e)
    {
      //
      // A sub menu item can be a seperator or text. So, a seperator can be detected
      // as an item, which text is empty.
      //
      MenuItem customItem = (MenuItem) sender;
      string sText = new string(' ',256);
      string sShortcut = new string(' ',256);
      int nImageIndex = 0;
      GetMenuTextImageShortcut(customItem.Text, ref sText, ref nImageIndex,
        ref sShortcut);
      if ( sText == "" )
      {
        DrawMenuSep(e);
      }
      else
      {
        DrawMenuTextImageShortcut(e, ref sText, ref nImageIndex, ref sShortcut);
      }
      //
    }
    private void MeasureItem(object sender, MeasureItemEventArgs e)
    {
      //
      // for ( each menu item, the height an the width of the drawing area have to be
      // evaluated.
      //
      MenuItem customItem = (MenuItem) sender;
      int nImageIndex = 0;
      Graphics grfx = Graphics.FromImage(imgMenuImagesForModule.Images[0]);
      string sText = new string(' ',256);
      string sShortcut = new string(' ',256);
      GetMenuTextImageShortcut(customItem.Text, ref sText, ref nImageIndex,
        ref sShortcut);
      SizeF stringSize = grfx.MeasureString(sText + sShortcut,
        SystemInformation.MenuFont);
      //
      // The width is determined by the width of the image area, given by nXtextLeft
      // (look at sub SetMetric above) and the width of the menu item string, which
      // is a combination of text and shortcut.
      //
      e.ItemWidth = (int)stringSize.Width + nXtextLeft + 7;
      //
      // The height is determined by the height of the image or the height of the
      // menu item string. for ( menu separators, the string is empty.
      //
      e.ItemHeight = Max((int)stringSize.Height + 7,
        imgMenuImagesForModule.Images[0].Height);
      if ( sText == "" ) {e.ItemHeight = (int)(e.ItemHeight / 2);}
      //
    }
#endregion
#region API stuff
    //
    private const int MF_BYPOSITION = 0x400;
    //
    private const int DST_ICON = 0x3;
    private const int DST_BITMAP = 0x4;
    private const int DSS_NORMAL = 0x0;
    private const int DSS_DISABLED = 0x20;
    //
    private const int COLOR_SCROLLBAR = 0;
    private const int COLOR_BACKGROUND = 1;
    private const int COLOR_ACTIVECAPTION = 2;
    private const int COLOR_INACTIVECAPTION = 3;
    private const int COLOR_MENU = 4;
    private const int COLOR_WINDOW = 5;
    private const int COLOR_WINDOWFRAME = 6;
    private const int COLOR_MENUTEXT = 7;
    private const int COLOR_WINDOWTEXT = 8;
    private const int COLOR_CAPTIONTEXT = 9;
    private const int COLOR_ACTIVEBORDER = 10;
    private const int COLOR_INACTIVEBORDER = 11;
    private const int COLOR_APPWORKSPACE = 12;
    private const int COLOR_HIGHLIGHT = 13;
    private const int COLOR_HIGHLIGHTTEXT = 14;
    private const int COLOR_BTNFACE = 15;
    private const int COLOR_BTNSHADOW = 16;
    private const int COLOR_GRAYTEXT = 17;
    private const int COLOR_BTNTEXT = 18;
    private const int COLOR_INACTIVECAPTIONTEXT = 19;
    private const int COLOR_BTNHIGHLIGHT = 20;
    //
    private const int TRANSPARENT = 1;
    //
    [DllImport("user32")] private static extern int DestroyIcon(int hIcon);
    [DllImport("user32", CharSet=CharSet.Auto)] private static extern int
      DrawState(int hdc, int hBrush, int lpDrawStateProc, int lParam, int wParam,
      int n1, int n2, int n3, int n4, int un);
    [DllImport("user32", CharSet=CharSet.Auto)] private static extern int
      GetMenuString(int hMenu, int wIDItem, StringBuilder lpString,
      int nMaxCount, int wFlag);
    [DllImport("gdi32", CharSet=CharSet.Auto)] private static extern int
      GetTextExtentPoint32(int hDC, string lpsz, int cbString,
      [ In, Out ] StringSize lpSize);
    [DllImport("user32")] private static extern int GetSysColor(int nIndex);
    [DllImport("COMCTL32")] private static extern int ImageList_GetIcon(int HIMAGELIST,
      int ImgIndex, int hbmMask);
    [DllImport("gdi32")] private static extern int SetBkMode(int hDC, int nBkMode);
    [DllImport("gdi32")] private static extern int SetTextColor(int hDC, int crColor);
    [DllImport("gdi32", CharSet=CharSet.Auto)] private static extern int
      TextOut(int hDC, int x, int y, string lpString, int nCount);
    //
#endregion
#region Subroutines and functions
    private int Max(int nA, int nB)
    {
      //
      // Gives back the maximum value of both.
      //
      if ( nA > = nB ) {return nA;}
      else {return nB;}
      //
    }
    private void DrawMenuCheck(DrawItemEventArgs e)
    {
      //
      // Draws a check mark, for checked menu items. The check mark is build of some
      // lines. The menu item could be disabled. In this case, the check mark has to
      // be drawn with a grayed pen.
      //
      int nOffsetX, nOffsetY, nQuarter1X, nHalfX, nQuarter3X, nQuarter1Y,
        nHalfY, nQuarter3Y;
      Pen penCheck, penRect;
      Color ColorRect = Color.FromArgb(28, 81, 128);
      Color ColorCheck = Color.FromArgb(33, 161, 33);
      int nWidth = nXimgRectRight - 9;
      if ( (e.State & DrawItemState.Grayed) == DrawItemState.Grayed )
      {
        penCheck = SystemPens.GrayText;
        penRect = SystemPens.GrayText;
      }
      else
      {
        penCheck = new Pen(ColorCheck);
        penRect = new Pen(ColorRect);
      }
      nOffsetX = e.Bounds.X + 4;
      nOffsetY = e.Bounds.Y + (e.Bounds.Height - nWidth) / 2 + 1;
      nQuarter1X = nOffsetX + nWidth / 4;
      nHalfX = nOffsetX + nWidth / 2;
      nQuarter3X = nOffsetX + (3 * nWidth) / 4;
      nQuarter1Y = nOffsetY + nWidth / 4;
      nHalfY = nOffsetY + nWidth / 2 + 1;
      nQuarter3Y = nOffsetY + (3 * nWidth) / 4;
      e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
      //
      // Rectangle with white background and shadow
      //
      e.Graphics.FillRectangle(Brushes.White,
        nOffsetX, nOffsetY, nWidth, nWidth);
      e.Graphics.DrawRectangle(SystemPens.Control,
        nOffsetX + 1, nOffsetY + 1, nWidth - 1, nWidth - 1);
      e.Graphics.DrawRectangle(SystemPens.ControlLight,
        nOffsetX + 2, nOffsetY + 2, nWidth - 4, nWidth - 4);
      e.Graphics.DrawRectangle(SystemPens.ControlLightLight,
        nOffsetX + 3, nOffsetY + 3, nWidth - 7, nWidth - 7);
      e.Graphics.DrawRectangle(penRect, nOffsetX, nOffsetY, nWidth, nWidth);
      //
      // Checkmark
      //
      e.Graphics.DrawLine(penCheck, nQuarter1X, nHalfY,
        nHalfX, nQuarter3Y);
      e.Graphics.DrawLine(penCheck, nQuarter1X, nHalfY - 1,
        nHalfX, nQuarter3Y - 1);
      e.Graphics.DrawLine(penCheck, nHalfX - 1, nQuarter3Y - 1,
        nQuarter3X, nQuarter1Y);
      e.Graphics.DrawLine(penCheck, nHalfX - 2, nQuarter3Y - 2,
        nQuarter3X, nQuarter1Y + 1);
      e.Graphics.DrawLine(penCheck, nHalfX - 2, nQuarter3Y - 2,
        nQuarter3X, nQuarter1Y + 2);
      //
      if ( (e.State & DrawItemState.Grayed) == DrawItemState.Grayed )
      {
      }
      else
      {
        penCheck.Dispose();
        penRect.Dispose();
      }
    }
    private void DrawMenuImage(DrawItemEventArgs e,
      int nImageIndex, int x, int y)
    {
      //
      // Draw the desired image. if ( the menu item is disabled, this state has to be
      // shown. Therefore the API function "DrawState" is used for drawing.
      //
      int hIcon, nResult, fuFlags;
      System.IntPtr hDc;
      if ( (e.State & DrawItemState.Grayed) == DrawItemState.Grayed )
      {
        fuFlags = DST_ICON + DSS_DISABLED;
      }
      else
      {
        fuFlags = DST_ICON + DSS_NORMAL;
      }
      hIcon = ImageList_GetIcon(imgMenuImagesForModule.Handle.ToInt32(),
        nImageIndex, 0);
      hDc = e.Graphics.GetHdc();
      nResult = DrawState(hDc.ToInt32(), 0, 0, hIcon, 0, x, y, 0, 0, fuFlags);
      DestroyIcon(hIcon);
      e.Graphics.ReleaseHdc(hDc);
      //
    }
    private void DrawMenuSep(DrawItemEventArgs e)
    {
      //
      // Draws a line to represent a menu separator.
      //
      int nYMiddle;
      nYMiddle = (int)(e.Bounds.Y + e.Bounds.Height / 2);
      e.Graphics.FillRectangle(SystemBrushes.Menu, e.Bounds);
      e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds.X, e.Bounds.Y,
        nXimgRectRight, e.Bounds.Height);
      e.Graphics.DrawLine(SystemPens.ControlDark, e.Bounds.X + nXtextLeft, nYMiddle,
        e.Bounds.Right, nYMiddle);
      //
    }
    private void DrawMenuTextImageShortcut(DrawItemEventArgs e,
      ref string  sText, ref int nImageIndex, ref string  sShortcut)
    {
      //
      // Draw all the stuff by using some subroutines.
      //
      // First of all, draw Text and Shortcut.
      //
      DrawStringUnderline(e, ref sText, ref sShortcut);
      //
      // Fill the background of the image area.
      //
      e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds.X, e.Bounds.Y,
        nXimgRectRight, e.Bounds.Height);
      //
      // Draw the image, if desired.
      //
      if ( nImageIndex > -1 )
      {
        DrawMenuImage(e, nImageIndex, e.Bounds.X + nXimgLeft,
          e.Bounds.Y + e.Bounds.Height / 2 -
          imgMenuImagesForModule.Images[nImageIndex].Height / 2);
      }
      //
      // Draw a check mark for checked items.
      //
      if ( (e.State & DrawItemState.Checked) == DrawItemState.Checked )
      {
        DrawMenuCheck(e);
      }
      //
    }
    private void DrawStringUnderline(DrawItemEventArgs e,
      ref string sText, ref string sShortcut)
    {
      //
      // Most of the work will be done here:
      // - The Background off the text area is to be drawn
      // - The text and the shortcut are to be drawn
      // - The accelerator is to be drawn
      //
      // The API functions "TextOut" and "GetTextExtentPoint32" are used, because
      // Graphics.DrawString in combination with Graphics.MeasureString is not exact
      // enough for drawing the accelerator by underlining the appropriate character.
      // Especially if ClearType is active, the result would not be acceptable.
      //
      bool bAccelerator, bNoAccelerator;
      string sWithoutAmpersand, sBeforAmpersand, sUnderlineChar;
      int nTextHeight, nOffsetY, nPos, nUnderlineWidth, nBeforAmpersandWidth;
      int nTextColor;
      Brush brushRect;
      Pen penMenuText;
      int x1, y1, x2, y2;
      int hDc; System.IntPtr hDcIntPtr;
      StringSize szTest = new StringSize();
      //
      // Evaluate the background brush and the text color
      //
      nOffsetY = e.Bounds.Y + e.Bounds.Height / 2;
      if ( (e.State & DrawItemState.Grayed) == DrawItemState.Grayed )
      {
        brushRect = SystemBrushes.Menu;
        nTextColor = GetSysColor(COLOR_GRAYTEXT);
      }
      else
      {
        if ( (e.State & DrawItemState.Selected) == DrawItemState.Selected )
        {
          brushRect = SystemBrushes.Highlight;
          nTextColor = GetSysColor(COLOR_HIGHLIGHTTEXT);
        }
        else
        {
          brushRect = SystemBrushes.Menu;
          nTextColor = GetSysColor(COLOR_MENUTEXT);
        }
      }
      //
      // Draw the Background off the text area
      //
      e.Graphics.FillRectangle(brushRect, nXimgRectRight, e.Bounds.Y,
        e.Bounds.Width, e.Bounds.Height);
      //
      // The menus device context is needed for the use of some GDI functions.
      // Background mode "Transparent" and TextColor has to be set, the menu font
      // is already selected.
      //
      hDcIntPtr = e.Graphics.GetHdc();
      hDc = hDcIntPtr.ToInt32();
      SetBkMode(hDc, TRANSPARENT);
      SetTextColor(hDc, nTextColor);
      //
      // Evaluate the text height.
      //
      GetTextExtentPoint32(hDc, "A", 1, szTest);
      nTextHeight = szTest.cy;
      //
      // The accelerator is defined by the character in the menu text, which follows the
      // ampersand character. First draw the text without an ampersand.
      //
      sWithoutAmpersand = sText.Replace("&", "");
      if ( sWithoutAmpersand != null )
      {
        TextOut(hDc, e.Bounds.X + nXtextLeft, nOffsetY - nTextHeight / 2,
          sWithoutAmpersand, sWithoutAmpersand.Length);
      }
      //
      // if a shortcut is defined, draw it right aligned.
      //
      if ( sShortcut != null )
      {
        GetTextExtentPoint32(hDc, sShortcut, sShortcut.Length, szTest);
        TextOut(hDc, e.Bounds.Width - szTest.cx - 4,
          nOffsetY - nTextHeight / 2, sShortcut, sShortcut.Length);
      }
      //
      // In menus, drawn by Windows 2000 or later, accelerators wont be shown by
      // default. Only if the user is poping up a menu by keyboard, the accelerators
      // will be shown.
      //
      bNoAccelerator = (bool)
        ((e.State & DrawItemState.NoAccelerator) == DrawItemState.NoAccelerator);
      bAccelerator = ! bNoAccelerator;
      if ( bAccelerator )
      {
        //
        // Evaluate the character, which shall be underlined.
        //
        nPos = sText.IndexOf("&");
        if ( nPos > 0 )
        {
          sBeforAmpersand = sText.Substring(0, nPos - 1);
          sUnderlineChar = sText.Substring(nPos, 1);
        }
        else
        {
          sBeforAmpersand = sText;
          sUnderlineChar = null;
        }
        //
        // if ( there is a character to underline, make is so.
        //
        if ( sUnderlineChar != null )
        {
          //
          // get { the width of the text before the ampersand and the width of the
          // character, which has to be underlined.
          //
          GetTextExtentPoint32(hDc, sBeforAmpersand,
            sBeforAmpersand.Length, szTest);
          nBeforAmpersandWidth = szTest.cx;
          GetTextExtentPoint32(hDc, sUnderlineChar, 1, szTest);
          nUnderlineWidth = szTest.cx - 1;
          e.Graphics.ReleaseHdc(hDcIntPtr);
          //
          // Setting the coordinates for drawing the underline
          //
          x1 = e.Bounds.X + nXtextLeft + nBeforAmpersandWidth;
          x2 = x1 + nUnderlineWidth;
          y1 = e.Bounds.Y + e.Bounds.Height / 2 + nTextHeight / 2 - 1;
          y2 = y1;
          //
          // The menu item could be disabled. In this case, the underline has to  
          // be drawn with a grayed pen. if ( it is selected, a highlighted pen is
          // to be used.
          //
          if ( (e.State & DrawItemState.Grayed) == DrawItemState.Grayed )
          {
            penMenuText = SystemPens.GrayText;
          }
          else
          {
            if ( (e.State & DrawItemState.Selected) == DrawItemState.Selected)
            {
              penMenuText = SystemPens.HighlightText;
            }
            else
            {
              penMenuText = SystemPens.MenuText;
            }
          }
          e.Graphics.DrawLine(penMenuText, x1, y1, x2, y2);
        }
        else
        {
          e.Graphics.ReleaseHdc(hDcIntPtr);
        }
      }
      else
      {
        e.Graphics.ReleaseHdc(hDcIntPtr);
      }
      //
    }
    private void GetMenuTextImageShortcut( string sMenuText, ref string sText,
      ref int nImage, ref string  sShortcut)
    {
      //
      // Separates the originally text, an image index and the "cultured" shortcut
      // from the given string.
      //
      string sImageIndex;
      int nIdx;
      //
      // The shortcut is separated by a tab character.
      //
      nIdx = sMenuText.IndexOf("\t");
      if ( nIdx > -1 )
      {
        sText = sMenuText.Substring(0, nIdx);
        sShortcut = sMenuText.Substring(nIdx + 1, sMenuText.Length - nIdx -1);
      }
      else
      {
        sText = sMenuText;
        sShortcut = null;
      }
      //
      // The index of the image is separated by the string "_i_".
      //
      nIdx = sText.IndexOf("_i_");
      if ( nIdx > -1 )
      {
        sImageIndex = sText.Substring(nIdx + 3, sText.Length - nIdx - 3);
        sText = sText.Substring(0, nIdx);
        nImage = Convert.ToInt32(sImageIndex);
      }
      else
      {
        nImage = -1;
      }
      //
    }
    private void InitializeContextMenu(ref ContextMenu ContextMenu,
      ref ImageList imgMenuImages)
    {
      //
      // Initializing each menu item of a context menu
      //
      imgMenuImagesForModule = imgMenuImages;
      foreach (MenuItem SubMenuItem in ContextMenu.MenuItems )
      {
        InitializeSubMenu(SubMenuItem);
      } //
      SetMetric(ref imgMenuImages);
      //
    }
    private void InitializeFormMenu(ref Form FormX, ref ImageList imgMenuImages)
    {
      //
      // Initializing each menu item of the main menu
      //
      imgMenuImagesForModule = imgMenuImages;
      foreach (MenuItem FormMenuItem in FormX.Menu.MenuItems)
      {
        InitializeMainMenu(FormMenuItem);
      } //
      SetMetric(ref imgMenuImages);
      //
    }
    private void InitializeMainMenu(MenuItem FormMenuItem)
    {
      //
      // Initializing each sub menu item of a menu item
      //
      foreach (MenuItem SubMenuItem in FormMenuItem.MenuItems)
      {
        InitializeSubMenu(SubMenuItem);
      } //
      //
    }
    private void InitializeSubMenu(MenuItem SubMenuItem)
    {
      //
      // Initializing a sub menu item
      //
      long nResult;
      StringBuilder sBuffer = new StringBuilder(256);
      //
      // The menu string is read before the property "OwnerDraw" is set to "true",
      // because it contains the short cut corresponding to the users keyboard. for
      // (example: in Germany "Strg" is the name of the Key "Ctrl". The Text of the
      // menu item is used to store the original text, an image index and the
      // "cultured" shortcut. The shortcut is separated by a tab character.
      //
      nResult = GetMenuString(SubMenuItem.Parent.Handle.ToInt32(), SubMenuItem.Index,
        sBuffer, sBuffer.Capacity, MF_BYPOSITION);
      SubMenuItem.Text = sBuffer.ToString().Substring(0, (int)nResult);
      SubMenuItem.OwnerDraw = true;
      //
      // Add handlers to the events "MeasureItem" and "DrawItem". The work has to
      // be done there.
      //
      SubMenuItem.MeasureItem += new MeasureItemEventHandler(MeasureItem);
      SubMenuItem.DrawItem += new DrawItemEventHandler(DrawSubMenuItem);
      //
      // if ( the sub menu has one or more sub menus, they have to be initialized too.
      //
      foreach (MenuItem SubSubMenuItem in SubMenuItem.MenuItems)
      {
        InitializeSubMenu(SubSubMenuItem);
      } //
      //
    }
    private void SetMetric(ref ImageList imgMenuImages)
    {
      //
      // Calculation of some metrics, so it has to be done only one time.
      //
      nXimgLeft = (int)(imgMenuImages.Images[0].Width / 4 + 0.5);
      nXimgRectRight = (int)(3 * imgMenuImages.Images[0].Width / 2 + 0.5);
      nXtextLeft = imgMenuImages.Images[0].Width * 2;
      //
    }
#endregion
  }
}

Home   |  Fähigkeiten  |  Projekte  |  Zeitachse   |  Downloads   |  Kontakt