几乎是 Office2003 - 摆脱 MenuItem 中的边距






4.48/5 (13投票s)
2006年3月10日
3分钟阅读

94638

323
关于使用 IExtenderProvider 的教程
免责声明
首先,我必须说这是我第一次在开发者页面上发表文章。此外,我是德国人,我的英语不太好,希望您能更清楚地理解源代码。 ;-)
引言
如果您曾经尝试开发自己的MenuItem
或MenuItemExtender
来模拟 Microsoft Office 2003 菜单的外观,您可能已经注意到边缘周围有两像素的边距,您无法使用Graphics
对象在其上绘制。
很明显,如果您想摆脱这些边距,您必须付出额外的努力。
一种可能性是绘制屏幕 DC 上的额外信息,但在定位菜单时存在一些问题。 第二种更优雅的方法是钩住应用程序中任何MenuItem
的创建事件,并在菜单项本身收到消息之前处理NC_PAINT
和WM_PRINT
消息。
使用 SpecialMenuProvider
使用组件非常简单
- 在您的窗体上拖动一个
SpecialMenuProvider
。 - 如果需要,请在
Glyph
属性上选择一个 16x16 像素的图像。
关于代码
这是钩住创建消息的代码。
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);
}
然后继续并处理您获得的钩子
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);
}
每次窗体创建一个MenuItem
时,代码都会生成一个对象,该对象在MenuItem
本身读取消息之前接收发送到WndProc
的消息。
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);
}
}
这会在边距中进行绘制。 为了实现 Office 2003 的设计,还必须提到另一个方面:所有者绘制的 Items。 XPmenuItemExtender
通过实现IExtenderProvider
并提供两个属性来解决这个问题
NewStyleActive
- 指定该项目是否为所有者绘制; 始终为TRUE
Glyph
- 指定在项目附近显示的图像
虽然NewStyleActive
始终为 true
且无法在设计器中看到,但它确保所有在设计时添加的控件都实现新的样式。 有关更多说明,请参阅优秀文章 使用 IExtenderProvider 的菜单项 - 更好的捕鼠器。
这是绘制项目的函数。 对于顶部项目,必须做出一个决定:如果该项目是行中的最后一个顶部项目并且必须绘制该栏的其余部分。
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
}
}
此代码将测量每个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;
}
}
最后,为了提供一个美观、简单的设计实现,请使用IExtender
接口并使您的组件扩展所有MenuItem
。
[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);
}
}
总结
不幸的是,Office 2003 的完整实现需要一些额外的研究。 例如,您必须评估菜单是显示在顶部项目的左侧还是下方,并调整白线。 但是,请随意修改代码并实现更多功能。 但如果您有一个好主意,我很高兴您能告诉我 ;-)
我希望这篇文章对您有所帮助; 如果您想使用一些示例,只需下载演示项目。 代码会自行解释。 您也可以访问 我的主页 以下载该项目。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。