Click here to Skip to main content
15,885,742 members
Articles / Programming Languages / XML

Almost Office2003 - Getting Rid of the Margin in MenuItems

Rate me:
Please Sign up or sign in to vote.
4.48/5 (15 votes)
10 Mar 20063 min read 92.9K   304   46   24
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.

C#
private Form _owner=null;
private IntPtr _hook=IntPtr.Zero;
private Win32.HookProc _hookprc=null;
C#
[Browsable(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));
        }
    }
}
C#
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:

C#
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 after 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.

C#
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 cannot 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.

C#
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:

C#
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.

C#
[ProvideProperty("NewStyleActive",typeof(MenuItem))]
[ProvideProperty("MenuGlyph",typeof(MenuItem))]
[ToolboxBitmap(typeof(SpecialMenuProvider),"images.SpecialMenuProvider.bmp")]
public class SpecialMenuProvider : Component,IExtenderProvider 
[Description("Specifies whether NewStyle-Drawing is enabled or not")]
[Browsable(false)]
public bool GetNewStyleActive(MenuItem control) 
{
    return true;//make sure every new item is selected
}
/// <summary>
/// Specifies whether 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.


Written By
Other VariSoft Industries
Germany Germany
my name is ramon van blech

Comments and Discussions

 
QuestionCustom drawing ALL the borders! Pin
aoverholtzer5-Jul-07 12:52
aoverholtzer5-Jul-07 12:52 
AnswerRe: Custom drawing ALL the borders! Pin
Julian Ott22-Jul-07 4:57
Julian Ott22-Jul-07 4:57 
GeneralProblem with NotiyIcon Pin
yousefk30-Mar-07 10:31
yousefk30-Mar-07 10:31 
GeneralRe: Problem with NotiyIcon Pin
Julian Ott30-Mar-07 14:06
Julian Ott30-Mar-07 14:06 
GeneralRe: Problem with NotiyIcon Pin
yousefk31-Mar-07 2:20
yousefk31-Mar-07 2:20 
GeneralNice Job Pin
Mike Hankey12-Mar-07 15:52
mveMike Hankey12-Mar-07 15:52 
GeneralWindow Startup Position Pin
lsoth8-Feb-07 9:45
lsoth8-Feb-07 9:45 
GeneralRe: Window Startup Position Pin
Julian Ott9-Feb-07 2:43
Julian Ott9-Feb-07 2:43 
GeneralRe: Window Startup Position Pin
lsoth13-Feb-07 10:12
lsoth13-Feb-07 10:12 
Generala poor component Pin
Mohmmad Rezazadeh20-May-06 5:55
professionalMohmmad Rezazadeh20-May-06 5:55 
GeneralRe: a poor component Pin
Julian Ott22-May-06 8:00
Julian Ott22-May-06 8:00 
QuestionA strange error!?? Pin
Mahesh Sapre4-May-06 19:10
Mahesh Sapre4-May-06 19:10 
AnswerRe: A strange error!?? Pin
Julian Ott5-May-06 9:36
Julian Ott5-May-06 9:36 
GeneralRe: A strange error!?? Pin
Mahesh Sapre6-May-06 1:56
Mahesh Sapre6-May-06 1:56 
GeneralA nice component Pin
Mahesh Sapre3-May-06 19:59
Mahesh Sapre3-May-06 19:59 
GeneralRe: A nice component Pin
Julian Ott4-May-06 9:00
Julian Ott4-May-06 9:00 
AnswerRe: A nice component Pin
Mahesh Sapre4-May-06 18:42
Mahesh Sapre4-May-06 18:42 
GeneralGood One! Pin
Infotech Belgaum3-May-06 18:41
Infotech Belgaum3-May-06 18:41 
GeneralRe: Good One! Pin
Julian Ott5-May-06 9:50
Julian Ott5-May-06 9:50 
GeneralSystem menu draw problem Pin
icerein30-Mar-06 15:24
icerein30-Mar-06 15:24 
GeneralRe: System menu draw problem Pin
Julian Ott30-Mar-06 20:48
Julian Ott30-Mar-06 20:48 
GeneralA minor problem! Pin
Mick Doherty14-Mar-06 0:26
Mick Doherty14-Mar-06 0:26 
I've started to do this myself, but never got round to finishing it as I was trying to get it to do MDI as well, but that's a major issue.
I just thought that I should point out one minor issue. Set the Break/BarBreak property of one of your menu items and the items will then be incorrectly drawn.
GeneralRe: A minor problem! Pin
Julian Ott19-Mar-06 0:59
Julian Ott19-Mar-06 0:59 
GeneralRe: A minor problem! Pin
Julian Ott20-Mar-06 11:02
Julian Ott20-Mar-06 11:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.