用c#和vb.net实现vs.net或office xp风格的菜单
小气的神 2001.08.18
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用户则拿着一本说明书,一步一步按指示找到结果;结果可能一样,但两者的方式是截然不同的。好了,这些是题外话了。