65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (13投票s)

2006年3月10日

3分钟阅读

viewsIcon

94638

downloadIcon

323

关于使用 IExtenderProvider 的教程

免责声明

首先,我必须说这是我第一次在开发者页面上发表文章。此外,我是德国人,我的英语不太好,希望您能更清楚地理解源代码。 ;-)

引言

Screenshot

如果您曾经尝试开发自己的MenuItemMenuItemExtender来模拟 Microsoft Office 2003 菜单的外观,您可能已经注意到边缘周围有两像素的边距,您无法使用Graphics对象在其上绘制。

很明显,如果您想摆脱这些边距,您必须付出额外的努力。

一种可能性是绘制屏幕 DC 上的额外信息,但在定位菜单时存在一些问题。 第二种更优雅的方法是钩住应用程序中任何MenuItem的创建事件,并在菜单项本身收到消息之前处理NC_PAINTWM_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 的完整实现需要一些额外的研究。 例如,您必须评估菜单是显示在顶部项目的左侧还是下方,并调整白线。 但是,请随意修改代码并实现更多功能。 但如果您有一个好主意,我很高兴您能告诉我 ;-)

我希望这篇文章对您有所帮助; 如果您想使用一些示例,只需下载演示项目。 代码会自行解释。 您也可以访问 我的主页 以下载该项目。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.