Author:unknown From:Internet
vs.net或office XP中的菜单都是非常漂亮的,反正我很喜欢。可惜VS.NET没有带制作这种菜单的控件或组件,不知正式版本会不会提供一个模板和向导。至今还记得刚学计算机语言时自己用Turbo C制作菜单的感受,那些矩形框函数和象素操作的确很迷人,况且那时是如此的流行菜单。
这篇文章中我会介绍有关在framework SDK Beta 2 中制作自己风格的菜单,所以你最好已安装了Framework SDK Beta 2,VS.NET Beta 2 不一定是必须的。附带的Zip包中的例子都是VS.NET Project的。
整个的文章包括三部分:
开始我会涉及到在winform中最基本的一些菜单的概念。
然后会有一个以前接触过的有关菜单的例子,它是for Beta 1的。老实说我没有想到Beta1 到Beta2有许多函数和命名空间发生了变化,以前我在Beta1中测试过这个例子,很顺利。这次在Beta2中会有许多错误,我提供了两个Project,一个是原来的Project的,一个是我修改后For Beta 2的。这种移植很枯燥,但可以很快熟悉新的Beta2的类库和函数,Show出来的菜单还不错,感觉是Office2000风格的菜单。如果你有兴趣可以试一试这个过程,会获益非浅的,这个例子还包括按钮的,原来的作者其实是在Demo控件的“Owner-drawn menus”技术;不过我只对菜单部分感兴趣。
最后一部分是制作vs.net或xp风格的例子,上面那个例子的效果不能使我完全满意,然后我重新写了另外一个,不过我对最后的结果还不是很满意,因为我的没有上一个例子那么完整,例子中我只显示了这个风格的菜单,对于事件响应、状态栏更新、tooltips、菜单的状态(enabled state)等处理都没有考虑,我把这些归结为时间问题,并承诺自己下次把它做得更好。
1.framework SDK Beta 2中菜单分成两类一类是普通的菜单叫:MainMenu,在VS.NET的Toolsbox中有这样一个对应的菜单控件,拖下它到你的窗体中,设置一下属性就可以所见所得了,这个版本的比VS.Studio98 系列的要好用和漂亮的多。另一类叫:ContextMenu菜单,也就是常用的弹出菜单。对于VB6来说所有的普通菜单在VS.NET中是可以兼容和自动升级成MainMenu类型的菜单,但对于PopMenu的菜单是不能转换成ContextMenu类型的菜单,你必须自己重新修改代码实现。这里我们主要是针对MainMenu的,其实原理一样。
最简单的菜单你可以这样做:
using System;
using System.Windows.Forms;
public class frmVB6 : Form {
private MainMenu muMain ; // MainMenu
public static int Main(string[] Args) {
application.run(new frmVB6());
return 0;
}
public frmVB6() {
// The following code sets up the form properties.
this.text = "Form1";
this.height = 213 + SystemInformation.CaptionHeight;
this.width = 312;
this.startposition = FormStartPosition.WindowsDefaultLocation;
menuitem mItemFile = new MenuItem() ;
mitemfile.text = "&File" ;
menuitem mItemExit = new MenuItem() ;
mitemexit.text = "E&xit" ;
mumain = new MainMenu() ;
mumain.menuitems.add( mItemFile ) ;
mumain.menuitems.add( mItemExit) ;
this.menu = muMain ;
}
}
手工方式保存它为一个.cs文件然后在编译它:
csc /t:winexe /r:System.dll /r:System.Windows.Forms.Dll /r:System.Drawing.Dll Form1Menu.cs
vs.net下只用new一个新的winform项目,然后在默认窗体中放入mainmenu控件,然后设置完属性,f5就可以了,完全不用一行代码。
如果要生成一个主菜单和一个菜单的子菜单项目,主要是menuitems.addrange的方法,看下面的代码:
this.mainmenu1 = new System.Windows.Forms.MainMenu();
this.menuitem1 = new System.Windows.Forms.MenuItem();
this.menuitem2 = new System.Windows.Forms.MenuItem();
this.menuitem3 = new System.Windows.Forms.MenuItem();
this.menuitem4 = new System.Windows.Forms.MenuItem();
// mainMenu1
this.mainmenu1.menuitems.addrange(new System.Windows.Forms.MenuItem[] {
this.menuitem1, this.menuItem2});
// menuItem1
this.menuitem1.index = 0;
this.menuitem1.menuitems.addrange(new System.Windows.Forms.MenuItem[] {
this.menuitem3, this.menuItem4});
this.menuitem1.index = 0 ;
this.menuitem1.text = "&File";
// menuItem2
this.menuitem2.index = 1;
this.menuitem2.text = "Help";
// menuItem3
this.menuitem3.index = 0;
this.menuitem3.text = "Open";
this.menuitem3.click += new System.EventHandler(this.menuItem3_Click);
// menuItem4
this.menuitem4.index = 1;
this.menuitem4.text = "Exit";
this.menuitem4.click += new System.EventHandler(this.menuItem4_Click);
this.menu = this.mainMenu1;
如代码所示menuitem1(file)和menuitem2(help)被addrange到mainmenu1中成为了顶级的菜单,menuitem3(open)和menuitem4(exit)被addrange到menuitem1(file)中成为了file菜单下的子菜单项。
this.menuitem3.click += new System.EventHandler(this.menuItem3_Click);表明MenuItem3点击时激发的事件处理程序,一般的事件处理程序象下面这样:
private void menuItem3_Click(object sender, System.EventArgs e)
{
messagebox.show ( " My Click Open" ) ;
}
我们关心的其实是menuitem,让它用我们的方式画出有vs.net或xp风格的菜单就可以了,更简单的说就是实现一个menuitem的继承类,扩展它draw的部分。好吧,让我们深入一点看看第二部分。
2.“Owner-drawn menus”技术
这个例子是vb.net语法的.我去掉了和menu无关的class,原因是错误太多,你会遇到类库和命名空间的移植性的问题:
最多的是beta1 System.WinForms 和Beta 2 的System.Windows.Froms的命名空间问题;
然后是beta1中的bitand 、BitOR等等Bitxxx的函数在Beta2中已去掉了Bit又和VB中一样了(据说Beta1的这项改动遭到了总多VB Fans的投诉,说不能把VB也C#化,Bit是什么东东),这样你需要把这类函数改掉;
然后是nameobjectcollectionbase从原来的system.collections中删除了,beta2放在system.collections.specialized 中,真的有些昏倒,开始我还以为Beta2中删除了这个类。
最后是一些overrides和 Overloads的问题,具体的看VS.NET或Framework SDK Beta 2编译时的提示就可以了,这方面MS做得不错,Task list中告诉你具体得建议,照做就是了。
具体一点你可以在framework SDK Beta 2安装目录的Doc目录中找到这两个文件,这是从Beta1移植到Beta2上不错的指导文件:APIChangesBeta1toBeta2.htm 和Change List - Beta1 to Beta2.doc 特别是这个doc文件洋洋洒洒90多页,但很有帮助。
希望你还能在排除所有的错误之后保持清醒,找到最核心有用的代码,来分析。主要是cactionmenu.vb,焦点在onmeasureitem和ondrawitem这两个函数或说事件处理程序上。onmeasureitem主要是处理menuitem的itemheight和itemwidth的,从它传的measureitemeventargs参数数就知道。ondrawitem主要是如何画菜单的问题。关键字overrides表明我们要在子类中重新定义menuitem中的这两个方法。
从56行到58行是onmeasureitem函数:
protected Overrides Sub OnMeasureItem(ByVal e As System.Windows.Forms.MeasureItemEventArgs)
if Me.Action.Caption = "-" Then
e.itemheight = 5
else
e.itemheight = 20
end If
dim fs As FontStyle
if Me.DefaultItem = True Then fs = fs Or FontStyle.Bold
dim fnt As New Font("Tahoma", 8, fs)
dim sf As SizeF = e.Graphics.MeasureString(Me.Action.Caption, fnt)
fnt.dispose()
e.itemwidth = CInt(sf.Width) + 20
end Sub
measureitemeventargs提供4个属性graphis、index、itemheight和itemwidth。me相当于c#或java的this关键字。fnt.dispose()中dispose是一个很有意思的函数调用,在以往的windows编程中象字体、画笔等许多资源都希望快使用快释放,这个语句是用来控制gc(garbage collection)的,意思是我已使用完了这个设备或资源,GC你可以收回了。
从70到146行是有关onitemdraw函数的:
protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
' colors, fonts
dim clrBgIcon, clrBgText, clrText As Color, fs As FontStyle, fnt As Font
dim b As SolidBrush, p As Pen
dim fEnabled As Boolean = Not CType(e.State And DrawItemState.Disabled, Boolean)
dim fSelected As Boolean = CType(e.State And DrawItemState.Selected, Boolean)
dim fDefault As Boolean = CType(e.State And DrawItemState.Default, Boolean)
dim fBreak As Boolean = (Me.Action.Caption = "-")
if fEnabled And fSelected And Not fBreak Then
clrbgicon = Color.Silver
clrbgtext = Color.White
clrtext = Color.Blue
fs = fs Or FontStyle.Underline
else
clrbgicon = Color.Gray
clrbgtext = Color.Silver
clrtext = Color.Black
end If
if Not fEnabled Then
clrtext = Color.White
end If
if fDefault Then
fs = fs Or FontStyle.Bold
end If
fnt = New Font("Tahoma", 8, fs)
' total background (partly to remain for icon)
b = New SolidBrush(clrBgIcon)
e.graphics.fillregion(b, New [Region](e.Bounds))
b.dispose()
' icon?
if Not Me.Action.ActionList Is Nothing Then
dim il As ImageList = Me.Action.ActionList.ImageList
if Not il Is Nothing Then
dim index As Integer = Me.Action.Image
if index > -1 And index < il.Images.Count Then
dim rect As Rectangle = e.Bounds
with rect
.x += 2
.y += 2
.width = 16
.height = 16
end With
e.graphics.drawimage(il.images.item(index), rect)
end If
end If
end If
' text background
dim rf As RectangleF
with rf
.x = 18
.y = e.Bounds.Y
.width = e.Bounds.Width - .X
.height = e.Bounds.Height
end With
b = New SolidBrush(clrBgText)
e.graphics.fillregion(b, New [Region](rf))
b.dispose()
' text/line
rf.y += 3 : rf.Height -= 3
if Not fBreak Then
b = New SolidBrush(clrText)
e.graphics.drawstring(me.action.caption, fnt, b, rf)
fnt.dispose()
b.dispose()
else
p = New Pen(Color.Black)
rf.y -= 1
e.graphics.drawline(p, rf.X, rf.Y, rf.Right, rf.Y)
p.dispose()
end If
' border
if fEnabled And fSelected And Not fBreak Then
p = New Pen(Color.Black)
e.graphics.drawrectangle(p, e.Bounds)
p.dispose()
end If
end Sub
drawitemeventargs参数给了你和菜单相关的所有环境和信息,它包括6个属性:bounds、font、forecolor、graphics、index、states。如果你以前用过windows下的gdi函数,那一定很熟悉这些函数,不是很复杂只需要你一点点算术知识和美术观点就可以了,如果你是第一次那么在纸上画几个矩形块就可以了理解和做的很好,比起以前tc下的菜单编程容易得多。主要是作者是如何把icon画在菜单上的,然后是根据不同的states表现一下菜单的forecolor, Bounds就是菜单项最前面的表示选中等等的小方块。
好了第二部分涉及到了大部分技术细节了,这里你需要关注的是,如何画出来,下一部分我们来看如何画的好看些,象vs.net或office XP那样子。
3. “MenuItemStyle”接口和VS.NET风格的菜单项
这个project又将切换到c#语言。我是这样想的:先针对普通菜单、office200风格、vs.net风格三种情况定义一个统一的接口(interface),其中包括画icon(drawicon)、画分割条(drawseparator)、画菜单背景(drawbackground)、写菜单项的文字(drawmenutext)等功能;普通、office2000和vs.net根据各自不同的情况实现这个接口的drawxxx的功能。然后从menuitem继承一个子类,象第二部分讲的那样overrides 菜单项的两个函数:OnMeasureItem和OnDrawItem,根据不同的风格调用上面实现的接口中的DrawXXX函数就可以了。最后我把这部分都分隔出来放在一个.CS文件中,单独编译成一个VSNET.Menu.DLL,你只用using VSNET.Menu ; 然后就可以象在第一部分那样象使用普通的MenuItem那样来用了,Demo源代码中你还可以看到我定义了IconMenuItem的类,它有一个方法:MenuItemCreator(VSNET.Menu.IconMenuStyle sType , String sText , Bitmap bmp , System.EventHandler eh)可以完成生成需要的MenuItem。本来我想用资源文件或将图片Icon等资源放在一个专门的文件中,然后由这个类来负责从资源文件或外部的类中获得资源CreateMenuItem。但是是第一版,你会看到例程中我仍然用原始的New Bitmap()的方式直接从硬盘拿资源。当我看到它show出来时,先是很开心,然后发现还有许多要改进,想想其实做一个专业的菜单也需要花许多心思。
好吧让我们看一下有关vs.net风格菜单项这部分主要的实现代码:
public class VSNetStyle : MenuItemStyleDrawer
{
static Color bgcolor = Color.FromArgb(246, 246, 246);
static Color ibgcolor = Color.FromArgb(202, 202, 202);
static Color sbcolor = Color.FromArgb(173, 173, 209);
static Color sbbcolor = Color.FromArgb( 0, 0, 128);
static int TEXTSTART = 20;
public void DrawCheckmark(Graphics g, Rectangle bounds, bool selected)
{
controlpaint.drawmenuglyph(g, new Rectangle(bounds.X + 2, bounds.Y + 2, 14, 14), MenuGlyph.Checkmark);
}
public void DrawIcon(Graphics g, Image icon, Rectangle bounds, bool selected, bool enabled, bool ischecked)
{
if (enabled)
{
if (selected)
{
controlpaint.drawimagedisabled(g, icon, bounds.Left + 2, bounds.Top + 2, Color.Black);
g.drawimage(icon, bounds.Left + 1, bounds.Top + 1);
}
else
{
g.drawimage(icon, bounds.Left + 2, bounds.Top + 2);
}
}
else
controlpaint.drawimagedisabled(g, icon, bounds.Left + 2, bounds.Top + 2, SystemColors.HighlightText);
}
public void DrawSeparator(Graphics g, Rectangle bounds)
{
int y = bounds.Y + bounds.Height / 2;
g.drawline(new Pen(SystemColors.ControlDark), bounds.X + SystemInformation.SmallIconSize.Width + 7, y, bounds.X + bounds.Width - 2, y);
}
public void DrawBackground(Graphics g, Rectangle bounds, DrawItemState state, bool toplevel, bool hasicon)
{
bool selected = (state & DrawItemState.Selected) > 0;
if (selected || ((state & DrawItemState.HotLight) > 0))
{
if (toplevel && selected)
{ // draw toplevel, selected menuitem
g.fillrectangle(new SolidBrush(ibgcolor), bounds);
controlpaint.drawborder3d(g, bounds.Left, bounds.Top, bounds.Width, bounds.Height, Border3DStyle.Flat, Border3DSide.Top | Border3DSide.Left | Border3DSide.Right);
}
else
{ // draw menuitem, selected OR toplevel, hotlighted
g.fillrectangle(new SolidBrush(sbcolor), bounds);
g.drawrectangle(new Pen(sbbcolor), bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1);
}
}
else
{
if (!toplevel)
{ // draw menuitem, unselected
g.fillrectangle(new SolidBrush(ibgcolor), bounds);
bounds.x += SystemInformation.SmallIconSize.Width + 5;
bounds.width -= SystemInformation.SmallIconSize.Width + 5;
g.fillrectangle(new SolidBrush(bgcolor), bounds);
}
else
{
// draw toplevel, unselected menuitem
g.fillrectangle(systembrushes.menu, bounds);
}
}
}
public void DrawMenuText(Graphics g, Rectangle bounds, string text, string shortcut, bool enabled, bool toplevel, DrawItemState state)
{
stringformat stringformat = new StringFormat();
stringformat.hotkeyprefix = ((state & DrawItemState.NoAccelerator) > 0) ? HotkeyPrefix.Hide : HotkeyPrefix.Show;
int textwidth = (int)(g.MeasureString(text, SystemInformation.MenuFont).Width);
int x = toplevel ? bounds.Left + (bounds.Width - textwidth) / 2: bounds.Left + TEXTSTART;
int y = bounds.Top + 2;
brush brush = null;
if (!enabled)
brush = new SolidBrush(Color.FromArgb(120, SystemColors.MenuText));
else
brush = new SolidBrush(Color.Black);
g.drawstring(text, SystemInformation.MenuFont, brush, x, y, stringformat);
g.drawstring(shortcut, SystemInformation.MenuFont, brush, bounds.Left + 130, bounds.Top + 2, stringformat);
}
}
menuitemstyledrawer就是那个公用的接口类,无论普通风格、office2000还是vs.net风格都要实现自己方式的接口,这个接口包括drawcheckmark、drawicon、drawmenutext、drawbackground、drawseparator等函数,可以实现菜单项需要的各种函数。完成这部分后可以从menuitem继承一个子类来象第二部分一样处理了。看下面的代码,具体考察一下熟悉的onmeasureitem和ondrawitem:
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
base.onmeasureitem(e);
// make shortcut text 省略这部分代码。
if (menustyle != IconMenuStyle.Standard)
{
if (Text == "-")
{
e.itemheight = 8;
e.itemwidth = 4;
return;
}
int textwidth = (int)(e.Graphics.MeasureString(Text + shortcuttext, SystemInformation.MenuFont).Width);
e.itemheight = SystemInformation.MenuHeight;
if (Parent == Parent.GetMainMenu())
e.itemwidth = textwidth - 5; // 5 is a magic number
else
e.itemwidth = Math.Max(160, textwidth + 50);
}
}
iconmenustyle.standard是个enum表明是普通风格、office2000或是vs。net的风格。这部分和我们第二部分看到的没有什么不同。
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.ondrawitem(e);
graphics g = e.Graphics;
rectangle bounds = e.Bounds;
bool selected = (e.State & DrawItemState.Selected) > 0;
bool toplevel = (Parent == Parent.GetMainMenu());
bool hasicon = Icon != null;
style.drawbackground(g, bounds, e.State, toplevel, hasicon);
if (hasicon)
style.drawicon(g, Icon, bounds, selected, Enabled, Checked);
else
if (Checked)
style.drawcheckmark(g, bounds, selected);
if (Text == "-")
{
style.drawseparator(g, bounds);
}
else
{
style.drawmenutext(g, bounds, Text, shortcuttext, Enabled, toplevel, e.State);
}
}
刚刚我们说的menuitemstyledrawer接口的好处在这里显示出来,整个过程显得简单明了,具体实现得代码不是很多。当这个类完成后,剩下来的就是使用了它了,这部分象第一部分所述,你可以在一个顶级菜单项的子菜单项声明成iconmenu类型的也就是我们实现的继承menuitem的类,简单的代码象下面这样:
private System.Windows.Forms.MenuItem mItems1 ; System.Drawing.Bitmap Bitmap1 = new Bitmap( BMPPATHSTR + "Open.bmp") ;
mitems1 = iMenuItem.MenuItemCreator( MenuStyle , "&Open" , Bitmap1,
这个mitem1就是一个vs.net风格的菜单项了。具体的可以看附带的project和屏幕截图。
至此我们完成了用vb.net或c#完成一个有vs.net或office XP风格的菜单。三个部分是渐进的,如果你以前进行或实验过第二部分讨论的问题,那么第三部分唯一让人感兴趣的是MenuItemStyleDrawer接口的思路。对于整个新的.NET的编程方式上说,原来的VB用户可能会经历一个痛苦的过程,他们的第一反应是Sub Class、Hook或是终极的API,而接触过C++、MFC、DELPHI甚至VJ++的用户会很容易想到继承,特别时DELPHI和VJ的用户入手应当最快了。想想会开始怀念以前VB的时光,因为对于这样的问题,VB用户总是拿着大锤,直接敲个大洞,然后拿到结果;而C++、MFC、DEPHI用户则拿着一本说明书,一步一步按指示找到结果;结果可能一样,但两者的方式是截然不同的。好了,这些是题外话了。
>>点这里下载源代码