Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Almost Office2003 - Getting rid of the margin in MenuItems

0.00/5 (No votes)
10 Mar 2006 1  
A tutorial on using the IExtenderProvider.

Disclaimer

At first, I have to say that this is my first article ever posted on a developer page. Furthermore, I'm a German and my English is not very good, hope you will see more clearly through the source code ;-)

Introduction

Screenshot

If you have ever tried to develop your own MenuItems or MenuItemExtender which mimic the appearance of Microsoft's Office 2003 menus, you might have noticed the two pixel margin around the edges where you can't paint on using the Graphics object.

It becomes clear that you must show some extra-effort if you want to get rid of these margins.

One possibility might be to draw the extra-info on the screen DC, but there are some problems in locating the menu. The second and more elegant way is to hook the creation event of any MenuItem in your application and process the NC_PAINT and WM_PRINT messages before the menu item itself receives the message.

Using SpecialMenuProvider

Using the component is very simple:

  • Drag a SpecialMenuProvider on your form.
  • If needed, select a 16x16 pixel image on the Glyph property.

About the code

This is the code to hook the creation message.

private Form _owner=null;
private IntPtr _hook=IntPtr.Zero;
private Win32.HookProc _hookprc=null;
false)>
public Form OwnerForm
{
    get{return _owner;}
    set
    {
        if (_hook!=IntPtr.Zero)//uninstall hook

        {
            Win32.UnhookWindowsHookEx(_hook);
            _hook=IntPtr.Zero;
        }
        _owner = value;
        if (_owner != null)
        {
            if (_hookprc == null)
            {
                _hookprc = new Win32.HookProc(OnHookProc);
            }
            _hook = Win32.SetWindowsHookEx(
               Win32.WH_CALLWNDPROC,//install hook

               _hookprc, IntPtr.Zero, 
                Win32.GetWindowThreadProcessId(_owner.Handle, 0));
        }
    }
}
internal abstract class Win32
{
    [DllImport("user32.dll", EntryPoint="SetWindowsHookExA",
         CharSet=CharSet.Ansi, 
         SetLastError=true, ExactSpelling=true)]
    public static extern IntPtr SetWindowsHookEx(int type,
         HookProc hook, IntPtr instance, int threadID);
 
    public delegate int HookProc(int code, 
      IntPtr wparam, ref Win32.CWPSTRUCT cwp); 
}

Then go forward and handle the hooks you get:

private int OnHookProc(int code, IntPtr wparam, 
                       ref Win32.CWPSTRUCT cwp)
{
    if (code == 0)
    {
        switch (cwp.message)
        {
            case Win32.WM_CREATE://a window is created

            {
                StringBuilder builder1 = 
                       new StringBuilder(0x40);
                int num2 = Win32.GetClassName(cwp.hwnd, 
                       builder1, builder1.Capacity);
                string text1 = builder1.ToString();
                if (string.Compare(text1,"#32768",false) == 0)
                //test if the class name

                //identifies the control as a MenuItem

                {
                    this.lastHook = new MenuHook(this,_lastwidth);
                    this.lastHook.AssignHandle(cwp.hwnd);
                    _lastwidth=0;
                    /*
                    * We don't use a local variable, because the GC
                    * would destroy it immediately afte leaving the
                    * function. Instead we use one private variable
                    * ,because there's always only one ContextMenu
                    * on the Desktop and the Hooker is destroyed
                    * when another ContextMenu lights up.
                    */
                }
                break;
            }
            case Win32.WM_DESTROY:
            //owner is destroyed, unhook all

            {
                if ((cwp.hwnd == _owner.Handle) && 
                     _hook!=IntPtr.Zero)
                {
                    Win32.UnhookWindowsHookEx(_hook);
                    _hook = IntPtr.Zero;
                }
                break;
            }
        }
    }
    return Win32.CallNextHookEx(_hook, code, wparam, ref cwp);
}

Every time a MenuItem is created by the form, the code generates an object which receives the messages posted to the WndProc before the MenuItem itself reads them.

internal class MenuHook:NativeWindow
{
    #region variablen
    private SpecialMenuProvider _parent=null;
    private int _lastwidth=0;
    #endregion

    public MenuHook(SpecialMenuProvider parent, 
                                 int lastwidth)
    {
        if (parent==null)
            //parent property mustn't be NULL

            throw new ArgumentNullException();
        //MenuExtender with drawing paramenters

        _parent=parent;
        // width of the topItem

        // unfolding the Menu or 0

        _lastwidth=lastwidth;
    }
    #region controller

    /// <summary>

    /// Hook window messages of a context/popup menu

    /// </summary>

    /// <param name="m">windows message</param>

    protected override void WndProc(ref Message m)
    {
        switch(m.Msg)
        {
            case Win32.WM_NCPAINT:
            //menu unfolding

            {
                IntPtr windc = Win32.GetWindowDC(m.HWnd);
                Graphics gr = Graphics.FromHdc(windc);
                this.DrawBorder(gr);
                Win32.ReleaseDC(m.HWnd, windc);
                gr.Dispose();
                m.Result = IntPtr.Zero;
                break;
            }
            case Win32.WM_PRINT:
            //user presses 'PRINT'

            {
                base.WndProc(ref m);
                IntPtr dc = m.WParam;
                Graphics gr = Graphics.FromHdc(dc);
                this.DrawBorder(gr);
                Win32.ReleaseDC(m.HWnd, dc);
                gr.Dispose();
                break;
            }
            default:
            {
                base.WndProc(ref m);
                break;
            }
        }
    }
    #endregion
    /// <summary>

    /// This draws the missing parts

    ///     in the margin of a menuitem

    /// </summary>

    /// <param name="gr">the graphics

    ///    surface to draw on</param>

    private void DrawBorder(Graphics gr)
    {
        //calculate the space of the context/popup menu

        Rectangle clip=Rectangle.Round(gr.VisibleClipBounds);
        clip.Width--; clip.Height--;

        int margin=_parent.MarginWidth;
        //fill the missing gradient parts 

        //using extender's brush

        gr.FillRectangle(_parent.MarginBrush,clip.X+1,
                            clip.Y+1,2,clip.Height-2);
        gr.FillRectangle(_parent.MarginBrush,
                 clip.X+1,clip.Y+1,margin,2);
        gr.FillRectangle(_parent.MarginBrush,
            clip.X+1,clip.Bottom-2,margin,2);

        //fill the other edges white, so using 

        //old windows style will not change the appearance

        gr.FillRectangle(Brushes.White,clip.X+margin+1, 
                       clip.Y+1,clip.Width-margin-1,2);
        gr.FillRectangle(Brushes.White,clip.X+margin+1,
                  clip.Bottom-2,clip.Width-margin-1,2);
        gr.FillRectangle(Brushes.White,clip.Right-2,
                            clip.Y+1,2,clip.Height);

        //draw the border with a little white line on the top,

        //then it looks like a tab unfolding.

        //in contextmenus: _lastwidth==0

        gr.DrawLine(Pens.White,clip.X+1,
                    clip.Y,clip.X+_lastwidth-2,clip.Y);
        gr.DrawLine(_parent.BorderPen,clip.X,
                    clip.Y,clip.X,clip.Bottom);
        gr.DrawLine(_parent.BorderPen,clip.X,
                    clip.Bottom,clip.Right,clip.Bottom);
        gr.DrawLine(_parent.BorderPen,clip.Right,
                    clip.Bottom,clip.Right,clip.Y);
        gr.DrawLine(_parent.BorderPen,clip.Right,
                    clip.Y,clip.X+_lastwidth-1,clip.Y);
    }
}

This does the drawing in the margin. To achieve the design of Office 2003, another aspect has to be mentioned: owner drawn Items. XPmenuItemExtender solves this by implementing the IExtenderProvider and providing two properties:

  • NewStyleActive - specifies, whether the item is owner-drawn; always TRUE.
  • Glyph - specifies the image displayed near the item.

While NewStyleActive is always true and can not be seen in the designer, it ensures that all design-time added controls implement the new style. For further explanation, see the excellent article MenuItems using IExtenderProvider - a better mousetrap.

Here is the function that paints the items. For top items, there is a decision to be made: if the item is the last top item in the line and has to paint the rest of the bar.

private void control_DrawItem(object sender, 
                     DrawItemEventArgs e)
{
    //collect the information used for drawing

    DrawItemInfo inf=new DrawItemInfo(sender,e,
    GetMenuGlyph((MenuItem)sender));

    if (inf.IsTopItem)//draw TopItem

    {
        #region draw Band
        Form frm=inf.MainMenu.GetForm();//owning form


        //width of the MainMenu + Width of one Form Border

        int width= frm.ClientSize.Width+ 
                   (frm.Width-frm.ClientSize.Width)/2;
    
        //use Band colors

        lnbrs.LinearColors=_cols[1];

        lnbrs.Transform=new Matrix(-(float)width,0f, 
              0f,1f,0f,0f);//scale the brush to the band


        if (e.Index==inf.MainMenu.MenuItems.Count-1)
        //item is last in line, draw the rest, too

            e.Graphics.FillRectangle(lnbrs,
                 inf.Rct.X,inf.Rct.Y,width-inf.Rct.X,
                 inf.Rct.Height);
        else//item is in line, just draw itself

            e.Graphics.FillRectangle(lnbrs,inf.Rct);
        #endregion

        #region layout
        //set the lastwidth field

        _lastwidth=0;
        if (inf.Selected)
            _lastwidth=e.Bounds.Width;
        #endregion

        #region draw TopItem
        inf.Rct.Width--;inf.Rct.Height--;//resize bounds


        lnbrs.Transform=new Matrix(0f,inf.Rct.Height, 
                   1f,0f,0f,inf.Rct.Y);//scale brush

    
        if (inf.Selected && !inf.Item.IsParent)
        //if the item has no subitems,

            //unfolding tab appearance is wrong, 

            //use hotlight appearance instead

            inf.HotLight=true;
    
        if (inf.HotLight && !inf.Disabled)
        //hot light appearance

        {
            //use hotlight colors

            lnbrs.LinearColors=_cols[2];
    
            //draw the background

            e.Graphics.FillRectangle(lnbrs,inf.Rct);
    
            //draw the border

            e.Graphics.DrawRectangle(border,inf.Rct);
        }
        else if (inf.Selected && !inf.Disabled)
        //unfolding tab appearance

        {
            lnbrs.LinearColors=_cols[0];
            //use band colors


            e.Graphics.FillRectangle(lnbrs,inf.Rct);
            //draw the background


            e.Graphics.DrawLines(border,new Point[]
            //draw a one-side-open reactangle

            {
                new Point(inf.Rct.X,inf.Rct.Bottom),
                new Point(inf.Rct.X,inf.Rct.Y),
                new Point(inf.Rct.Right,inf.Rct.Y),
                new Point(inf.Rct.Right,inf.Rct.Bottom)
            });
        }
        if (inf.Item.Text!="")//draw the text, no shortcut

        {
            SizeF sz;
            sz=e.Graphics.MeasureString(inf.Item.Text.Replace(@"&", 
                       ""),//use no DefaultItem property

                       e.Font);
    
            e.Graphics.DrawString(inf.Item.Text,
              //draw the text

              e.Font,
              //grayed if the Item is disabled

              inf.Disabled?Brushes.Gray:Brushes.Black,
              inf.Rct.X+(inf.Rct.Width-(int)sz.Width)/2,
              inf.Rct.Y+(inf.Rct.Height-(int)sz.Height)/2,fmt);
        }
        #endregion
    }
    else
    {
        #region draw background, margin and selection
        lnbrs.LinearColors=_cols[0];//use band colors

    
        lnbrs.Transform=new Matrix(_margin,0f,0f, 
                     1f,-1f,0f);//scale the brush

    
        e.Graphics.FillRectangle(lnbrs,0,inf.Rct.Y, 
          _margin-2,inf.Rct.Height);//draw the band

    
        e.Graphics.FillRectangle(Brushes.White,_margin-2, 
                   inf.Rct.Y,//fill the backspace white

                   2+inf.Rct.Width-_margin,inf.Rct.Height);

        if (inf.Item.Text=="-")//Item is a Separator

        {
            e.Graphics.DrawLine(new Pen(_cols[0][1]),
            //use the dark band color

            inf.Rct.X+_margin+2,inf.Rct.Y+inf.Rct.Height/2,
            inf.Rct.Right,inf.Rct.Y+inf.Rct.Height/2);
            return;
        }
        if (inf.Selected && !inf.Disabled)
        //item is hotlighted

        {
            hotbrs.Color=_cols[2][0];//use hotlight color

    
            e.Graphics.FillRectangle(hotbrs,//fill the background

            inf.Rct.X,inf.Rct.Y,inf.Rct.Width-1,inf.Rct.Height-1);
    
            e.Graphics.DrawRectangle(border,//draw the border

            inf.Rct.X,inf.Rct.Y,inf.Rct.Width-1,inf.Rct.Height-1);
        }
        #endregion
        #region draw chevron
        if (inf.Checked)//item is checked

        {
            hotbrs.Color=_cols[2][1];//use dark hot color

    
            e.Graphics.FillRectangle(hotbrs,
              //fill the background rect

              inf.Rct.X+1,inf.Rct.Y+1,inf.Rct.Height-3,
              inf.Rct.Height-3);
            e.Graphics.DrawRectangle(border,
              //draw the border

              inf.Rct.X+1,inf.Rct.Y+1,inf.Rct.Height-3,
              inf.Rct.Height-3);
    
            if (inf.Glyph==null)
            //if there is an image, 

            //no chevron will be drawed

            {
                e.Graphics.SmoothingMode= 
                      SmoothingMode.AntiAlias;
                      //for a smooth form

                e.Graphics.PixelOffsetMode=
                      PixelOffsetMode.HighQuality;
        
                if (!inf.Item.RadioCheck)//draw an check arrow

                {
                    e.Graphics.FillPolygon(Brushes.Black,new Point[]
                    {
                        new Point(inf.Rct.X+7,inf.Rct.Y+10),
                        new Point(inf.Rct.X+10,inf.Rct.Y+13),
                        new Point(inf.Rct.X+15,inf.Rct.Y+8),

                        new Point(inf.Rct.X+15,inf.Rct.Y+10),
                        new Point(inf.Rct.X+10,inf.Rct.Y+15),
                        new Point(inf.Rct.X+7,inf.Rct.Y+12)

                    });
                }
                else//draw a circle

                {
                    e.Graphics.FillEllipse(Brushes.Black,
                    inf.Rct.X+8,inf.Rct.Y+8,7,7);
                }
                e.Graphics.SmoothingMode=SmoothingMode.Default;
            }
        }
        #endregion

        #region draw image
        if (inf.Glyph!=null)
        {
            if (!inf.Disabled)//draw image grayed

                e.Graphics.DrawImageUnscaled(inf.Glyph,
                  inf.Rct.X+(inf.Rct.Height-inf.Glyph.Width)/2,
                  inf.Rct.Y+(inf.Rct.Height-inf.Glyph.Height)/2);
            else
                ControlPaint.DrawImageDisabled(e.Graphics,inf.Glyph,
                  inf.Rct.X+(inf.Rct.Height-inf.Glyph.Width)/2,
                  inf.Rct.Y+(inf.Rct.Height-inf.Glyph.Height)/2,
                  Color.Transparent);
        }
        #endregion

        #region draw text & shortcut
        SizeF sz;
        Font fnt= 
          inf.Item.DefaultItem?new Font(e.Font,
          FontStyle.Bold): 
          SystemInformation.MenuFont;
          //set font to BOLD if Item is a DefaultItem

        if (inf.Item.Text!="")
        {
            //draw text

            sz=e.Graphics.MeasureString(inf.Item.Text,fnt);
            e.Graphics.DrawString(inf.Item.Text,fnt,
              inf.Disabled?Brushes.Gray:Brushes.Black,
              inf.Rct.X+inf.Rct.Height+5,
              inf.Rct.Y+(inf.Rct.Height-(int)sz.Height)/2,fmt);
        } 
        if (inf.Item.Shortcut!=Shortcut.None && 
            inf.Item.ShowShortcut)
        {
            string shc=GetShortcutString((Keys)inf.Item.Shortcut);
        
            sz=e.Graphics.MeasureString(shc,fnt);//draw shortcut

            e.Graphics.DrawString(shc,fnt,
              inf.Disabled?Brushes.Gray:Brushes.Black,
              inf.Rct.Right-(int)sz.Width-16,
              inf.Rct.Y+(inf.Rct.Height-(int)sz.Height)/2);
        }
        #endregion
    }
}

This code will measure each MenuItem:

private void control_MeasureItem(object sender, 
                        MeasureItemEventArgs e)
{
    MenuItem mnu=(MenuItem)sender;

    if (mnu.Text=="-")
    {
        e.ItemHeight=3; return;
    }//MenuItem is Separator


    //dont measure '&' because it is replaced 

    //by an underline segment

    string txt=mnu.Text.Replace(@"&","");
    
    if (mnu.Shortcut!=Shortcut.None && mnu.ShowShortcut)
        txt+=GetShortcutString((Keys)mnu.Shortcut);
        //Get MenuShortcut, if visible


    int twidth=(int)e.Graphics.MeasureString(txt, 
      //Measure the string

      mnu.DefaultItem?
      //if the item is the DefaultItem, BOLD Font is used

      new Font(SystemInformation.MenuFont,FontStyle.Bold)
      :SystemInformation.MenuFont, 
      PointF.Empty,fmt).Width;

    if(mnu.Parent==mnu.Parent.GetMainMenu())
    //Item is in Top-Band of a MainMenu

    {
        e.ItemHeight=16;
        e.ItemWidth=twidth+2;
    }
    else//item is in a context/popup menu

    {
        e.ItemHeight=23;
        e.ItemWidth=twidth+45+_margin;
    }
}

Finally, to give a nice, easy design implementation, use the IExtender interface and make your component extend all MenuItems.

[ProvideProperty("NewStyleActive",typeof(MenuItem))]
[ProvideProperty("MenuGlyph",typeof(MenuItem))]
[ToolboxBitmap(typeof(SpecialMenuProvider),"images.SpecialMenuProvider.bmp")]
public class SpecialMenuProvider : Component,IExtenderProvider 
[Description("Specifies wheter NewStyle-Drawing is enabled or not")]
false)>
public bool GetNewStyleActive(MenuItem control) 
{
    return true;//make sure every new item is selected

}
/// <summary>

/// Specifies wheter NewStyle-Drawing is enabled or not

/// </summary>

public void SetNewStyleActive(MenuItem control, bool value) 
{
    if (!value) 
    {
        if (_menuitems.Contains(control))
        //remove it from the collection

        {
            _menuitems.Remove(control);
        }
        //reset to system drawing

        control.OwnerDraw=false;
        control.MeasureItem-=new 
          MeasureItemEventHandler(control_MeasureItem);
        control.DrawItem-=new 
          DrawItemEventHandler(control_DrawItem);
    }
    else 
    {
        //add it or change the value

        if (!_menuitems.Contains(control))
            _menuitems.Add(control,
               new MenuItemInfo(true,null));
        else
        ((MenuItemInfo)_menuitems[control]).NewStyle=true;
        //set to owner drawing

        control.OwnerDraw=true;
        control.MeasureItem+=new 
          MeasureItemEventHandler(control_MeasureItem);
        control.DrawItem+=new 
          DrawItemEventHandler(control_DrawItem);
    }
}

Concluding

Unfortunately, the complete implementation of Office 2003 requires some extra-research. For example, you have to evaluate if the menu is displayed left of the top item or beneath it, and adjust the white line. However, feel free to modify the code and implement more features. But I would be glad if you'd tell me if you have a great idea ;-)

I hope this article is useful to you; and if you want to use some samples, just download the demo project. The code will explain itself. You can also visit my homepage to download the project.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here