//
you're reading...
Articles, Technology

MenuExtenderComponent for .NET – Part 3

MenuExtenderComponent — Building an Even Better Mouse Trap

Part 3 of 4

Table of Contents

The MenuDrawer

All menu drawers to be used by the component must inherit from the abstract MenuDrawer class. This class provides two abstract methods as well as a few helper methods to facilitate the drawing and measuring of each MenuItem. In addition to the helper methods and two abstract methods there are several properties to tell you the state of the item being drawn without having to do the Boolean checks yourself.

Before a call to Measure or DrawMenu is made several properties will be set to give the MenuDrawer the information it needs to do the job.

Properties

The base MenuDrawer class has several properties to give the needed information to the Measure and DrawMenu methods. If you want you can also skip to the Methods or right to the quick implementation.

Graphics

public Graphics Graphics { get; set; }

Gets or sets the Graphics object to be used when drawing or measuring this MenuItem.

Bounds

public Rectangle Bounds { get; set; }

Gets or sets the bounds for the MenuItem. When measuring this is set to Rectangle.Empty and when drawing the value comes from the DrawItemEventArgs. Drawing outside of the boundary can corrupt other MenuItems if not using double buffering. If you are using double buffering the size of the underlying double-buffer is the same size as the bounds so nothing bad will happen.

Index

public int Index { get; set; }

Gets or sets the index telling where this MenuItem is positioned with its siblings as returned from the DrawItemEventArgs.

State

public DrawItemState State { get; set; }

Gets or sets the DrawItemState as returned from the DrawItemEventArgs.

Extender

public MenuExtenderComponent Extender { get; set; }

Gets or sets a reference to the MenuExtenderComponent which controls the MenuItem. This is used to get at the properties it has, such as the ImageLists.

MenuItem

public MenuItem MenuItem { get; set; }

Gets or sets a reference to the MenuItem that is being drawn or measured.

MenuInfo

public MenuInfo MenuInfo { get; set; }

Gets or sets a reference to the MenuInfo class; which contains all of the information from the extended-properties.

There are also a few helper properties to decode information from the State and MenuItem properties.

IsHighlighted

public bool IsHighlighted { get; }

Returns true if the MenuItem should be highlighted. This is determined by either the mouse hovering over the MenuItem or it is currently selected.

ShouldDrawAccelerator

public bool ShouldDrawAccelerator { get; }

Returns true if menu accelerators (the underlines underneath certain letters in the menu text) should drawn.

IsSeperator

public bool IsSeperator { get; }

Returns true if the MenuItem should be drawn as a horizontal separator. Separators are specified by setting the MenuItem's Text property to “-“.

IsTopLevel

public bool IsTopLevel { get; }

Returns true if this MenuItem's parent is a MainMenu. Having it return true for the parent being a ContextMenu doesn’t make sense in the current implementation.

IsHover

public bool IsHover { get; }

Returns true if the mouse is currently hovering over this MenuItem.

Methods

To make drawing and measuring of the MenuItems easier there are several helper methods, both static and instance depending on whether any state information is needed.

GetStringFormat

public StringFormat GetStringFormat()
{
  StringFormat sf = new StringFormat(StringFormatFlags.NoWrap);

  if( ShouldDrawAccelerator )
    sf.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Show;
  else
    sf.HotkeyPrefix = System.Drawing.Text.HotkeyPrefix.Hide;

  return sf;
}

The GetStringFormat method returns a new StringFormat suitable for drawing the text in a MenuItem. It sets the NoWrap flag and determines whether a menu accelerator should be drawn. Don’t forget to call Dispose on the StringFormat when you are done with it!

GetShortcutText

// TODO: Support localization of the keyboard modifiers
// Ctrl, Shift, Alt
public string GetShortcutText()
{
  string text = "";

  Shortcut c = MenuItem.Shortcut;

  Keys k = (Keys) c;

  if( (k & Keys.Control) != 0 )
  {
    text += "Ctrl";

    k &= ~Keys.Control;
  }

  if( (k & Keys.Shift) != 0 )
  {
    if( text != "" )
      text += "+";

    text += "Shift";

    k &= ~Keys.Shift;
  }

  if( (k & Keys.Alt) != 0 )
  {
    if( text != "" )
      text += "+";

    text += "Alt";

    k &= ~Keys.Alt;
  }

  if( text != "" )
    text += "+";

  if( k >= Keys.D0 && k 

The GetShortcutText method converts the Shortcut value from the MenuItem over to a text representation suitable for displaying.  This is done by determining which keys make up the Shortcut value.  In version 1.0 and 1.1 of the framework the values in the Shortcut enum is simply the bitwise or of values from the Keys enum.  Once it finds a key modifier (Alt, Shift, or Control) it appends the text for the modifier to the value to return, then removes that modifier from the value by using a bitwise and of the bitwise negation of the modifier.

DrawMenuGlyph & DrawMenuGlyphHighlight

It seems like a bug, but ControlPaint.DrawMenuGlyph does not draw the glyph transparently so if you try to use it as-is you will see a white block surrounding the glyph when you hover over a checked MenuItem. To correct this, the MenuDrawer does some work behind the scenes and provides its own DrawMenuGlyph method that can be called. The glyph drawn by ControlPaint is also always drawn in black, while it is a common color for menu text it isn't used on every system. To get around that, the drawing is modified by the use of the ImageAttributes class.
public static void DrawMenuGlyph( Graphics graphics, 
                                  int x, int y, MenuGlyph glyph )
{
  DrawMenuGlyph( graphics, x, y, 16, 16, glyph );
}

public static void DrawMenuGlyph( Graphics graphics, 
                                  int x, int y, int w, int h, MenuGlyph glyph )
{
  ImageAttributes ia = new ImageAttributes();
  
  ColorMap [] colorMaps = new ColorMap[1];
  colorMaps[0] = new ColorMap();
  colorMaps[0].OldColor = Color.Black;
  colorMaps[0].NewColor = SystemColors.MenuText;

  ia.SetRemapTable( colorMaps );

  Rectangle dest = new Rectangle( x, y, w, h );

  graphics.DrawImage( menuGlyphs, dest, 
                      16 * (int) glyph, 0, 16, 16, 
                      GraphicsUnit.Pixel, ia );
}

Here you see a new variables introduced called menuGlyphs, the type initializer for the MenuDrawer creates this Bitmap and is then populated with each of the glyphs at a 16×16 size which is commonly used.

private static Bitmap menuGlyphs;

static MenuDrawer()
{
  menuGlyphs = null;
  MakeMenuGlyphsBitmap();
}

private static void MakeMenuGlyphsBitmap()
{
  menuGlyphs = new Bitmap( 16 * 3, 16 );

  using( Graphics g = Graphics.FromImage( menuGlyphs ) )
  {
    g.Clear( Color.White );
    int x = 0;

    for( MenuGlyph glyph = MenuGlyph.Min; glyph 

At this point you may be asking why I made yet another method when this code is only called once.  To tell you the truth, until this very moment that method was in the type initializer method; but something occurred to me.  What would happen if the user changed their system colors while an application using the MenuExtenderComponent was running?  The menus should draw correctly, for the most part since they all use the SystemColors class for getting colors to draw.  But the MenuGlyphs are only drawn this one time, so they won't be updated - time to introduce some complexity.  By adding the following class to the MenuDrawer code, the type initializer can be changed to automatically update itself.

private class MessageFilter : IMessageFilter
{
  const int WM_SYSCOLORCHANGE = 0x0015;

  public MessageFilter() {}

  bool IMessageFilter.PreFilterMessage(ref Message m)
  {
    if( m.Msg == WM_SYSCOLORCHANGE )
    {
      MenuDrawer.MakeMenuGlyphsBitmap();
    }

    return false;
  }
}

static MenuDrawer()
{
  menuGlyphs = null;
  MakeMenuGlyphsBitmap();

  Application.AddFilter( new MessageFilter() );
}

Now by using this, the checkmarks and bullets will look correctly on any system but what about the highlighted state? By making use of the ImageAttributes class again we can make use of the same bitmap to do the drawing and just change the color from Color.Black to SystemColors.HighlightText to correspond with the text color.

public static void DrawMenuGlyphHighlighted( Graphics graphics, 
                           int x, int y, int w, int h, MenuGlyph glyph )
{
  ImageAttributes ia = new ImageAttributes();
  
  ColorMap [] colorMaps = new ColorMap[1];
  colorMaps[0] = new ColorMap();
  colorMaps[0].OldColor = Color.Black;
  colorMaps[0].NewColor = SystemColors.HighlightText;

  ia.SetRemapTable( colorMaps );

  Rectangle dest = new Rectangle( x, y, w, h );

  graphics.DrawImage( menuGlyphs, dest, 
                      16 * (int) glyph, 0, 16, 16, 
                      GraphicsUnit.Pixel, ia );
}

It beats me why Graphics.DrawImage doesn’t have an overload to let me pass in the following parameters: Image img, Rectangle dest, Rectangle src, GraphicsUnit unit, ImageAttributes ia. All I know is that it doesn’t so I had to convert the source Rectangle to the x, y, width, and height values.

License

The code and binary component follows the FreeBSD license:

MenuExtenderComponent Copyright © 2004 James T. Johnson. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution

THIS SOFTWARE IS PROVIDED BY JAMES T. JOHNSON “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

This article is Copyright © 2004 James T. Johnson and requires my written permission to be reproduced elsewhere.

Continue reading the next part

Article History

  • March 18, 2004
    • Initial Version
Advertisements

About James

I am a Senior Developer/Consultant for InfoPlanIT, LLC. I previously spent over 7 years as a Product Manager for what eventually became ComponentOne, a division of GrapeCity. While there, I helped to create ActiveReports 7, GrapeCity ActiveAnalysis, and Data Dynamics Reports.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archive

%d bloggers like this: