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;
false)>
public Form OwnerForm
{
get{return _owner;}
set
{
if (_hook!=IntPtr.Zero)
{
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,
_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:
{
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)
{
this.lastHook = new MenuHook(this,_lastwidth);
this.lastHook.AssignHandle(cwp.hwnd);
_lastwidth=0;
}
break;
}
case Win32.WM_DESTROY:
{
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)
throw new ArgumentNullException();
_parent=parent;
_lastwidth=lastwidth;
}
#region controller
protected override void WndProc(ref Message m)
{
switch(m.Msg)
{
case Win32.WM_NCPAINT:
{
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:
{
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
private void DrawBorder(Graphics gr)
{
Rectangle clip=Rectangle.Round(gr.VisibleClipBounds);
clip.Width--; clip.Height--;
int margin=_parent.MarginWidth;
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);
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);
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)
{
DrawItemInfo inf=new DrawItemInfo(sender,e,
GetMenuGlyph((MenuItem)sender));
if (inf.IsTopItem)
{
#region draw Band
Form frm=inf.MainMenu.GetForm();
int width= frm.ClientSize.Width+
(frm.Width-frm.ClientSize.Width)/2;
lnbrs.LinearColors=_cols[1];
lnbrs.Transform=new Matrix(-(float)width,0f,
0f,1f,0f,0f);
if (e.Index==inf.MainMenu.MenuItems.Count-1)
e.Graphics.FillRectangle(lnbrs,
inf.Rct.X,inf.Rct.Y,width-inf.Rct.X,
inf.Rct.Height);
else
e.Graphics.FillRectangle(lnbrs,inf.Rct);
#endregion
#region layout
_lastwidth=0;
if (inf.Selected)
_lastwidth=e.Bounds.Width;
#endregion
#region draw TopItem
inf.Rct.Width--;inf.Rct.Height--;
lnbrs.Transform=new Matrix(0f,inf.Rct.Height,
1f,0f,0f,inf.Rct.Y);
if (inf.Selected && !inf.Item.IsParent)
inf.HotLight=true;
if (inf.HotLight && !inf.Disabled)
{
lnbrs.LinearColors=_cols[2];
e.Graphics.FillRectangle(lnbrs,inf.Rct);
e.Graphics.DrawRectangle(border,inf.Rct);
}
else if (inf.Selected && !inf.Disabled)
{
lnbrs.LinearColors=_cols[0];
e.Graphics.FillRectangle(lnbrs,inf.Rct);
e.Graphics.DrawLines(border,new Point[]
{
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!="")
{
SizeF sz;
sz=e.Graphics.MeasureString(inf.Item.Text.Replace(@"&",
""),
e.Font);
e.Graphics.DrawString(inf.Item.Text,
e.Font,
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];
lnbrs.Transform=new Matrix(_margin,0f,0f,
1f,-1f,0f);
e.Graphics.FillRectangle(lnbrs,0,inf.Rct.Y,
_margin-2,inf.Rct.Height);
e.Graphics.FillRectangle(Brushes.White,_margin-2,
inf.Rct.Y,
2+inf.Rct.Width-_margin,inf.Rct.Height);
if (inf.Item.Text=="-")
{
e.Graphics.DrawLine(new Pen(_cols[0][1]),
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)
{
hotbrs.Color=_cols[2][0];
e.Graphics.FillRectangle(hotbrs,
inf.Rct.X,inf.Rct.Y,inf.Rct.Width-1,inf.Rct.Height-1);
e.Graphics.DrawRectangle(border,
inf.Rct.X,inf.Rct.Y,inf.Rct.Width-1,inf.Rct.Height-1);
}
#endregion
#region draw chevron
if (inf.Checked)
{
hotbrs.Color=_cols[2][1];
e.Graphics.FillRectangle(hotbrs,
inf.Rct.X+1,inf.Rct.Y+1,inf.Rct.Height-3,
inf.Rct.Height-3);
e.Graphics.DrawRectangle(border,
inf.Rct.X+1,inf.Rct.Y+1,inf.Rct.Height-3,
inf.Rct.Height-3);
if (inf.Glyph==null)
{
e.Graphics.SmoothingMode=
SmoothingMode.AntiAlias;
e.Graphics.PixelOffsetMode=
PixelOffsetMode.HighQuality;
if (!inf.Item.RadioCheck)
{
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
{
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)
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;
if (inf.Item.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);
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;
}
string txt=mnu.Text.Replace(@"&","");
if (mnu.Shortcut!=Shortcut.None && mnu.ShowShortcut)
txt+=GetShortcutString((Keys)mnu.Shortcut);
int twidth=(int)e.Graphics.MeasureString(txt,
mnu.DefaultItem?
new Font(SystemInformation.MenuFont,FontStyle.Bold)
:SystemInformation.MenuFont,
PointF.Empty,fmt).Width;
if(mnu.Parent==mnu.Parent.GetMainMenu())
{
e.ItemHeight=16;
e.ItemWidth=twidth+2;
}
else
{
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 wheter NewStyle-Drawing is enabled or not")]
false)>
public bool GetNewStyleActive(MenuItem control)
{
return true;
}
public void SetNewStyleActive(MenuItem control, bool value)
{
if (!value)
{
if (_menuitems.Contains(control))
{
_menuitems.Remove(control);
}
control.OwnerDraw=false;
control.MeasureItem-=new
MeasureItemEventHandler(control_MeasureItem);
control.DrawItem-=new
DrawItemEventHandler(control_DrawItem);
}
else
{
if (!_menuitems.Contains(control))
_menuitems.Add(control,
new MenuItemInfo(true,null));
else
((MenuItemInfo)_menuitems[control]).NewStyle=true;
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.