Almost Office2003 - Getting Rid of the Margin in MenuItems






4.48/5 (13 votes)
Mar 10, 2006
3 min read

94114

305
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
If you have ever tried to develop your own MenuItem
s 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;
[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));
}
}
}
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 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.
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; alwaysTRUE
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.
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 MenuItem
s.
[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.