酷炫的 Vista 风格菜单






4.92/5 (76投票s)
在您的 .NET 应用程序中使用酷炫的 Vista 风格菜单

引言
这是此控件的第二个版本,我之前的文章有点粗糙,所以我决定也更新一下。我基于 Vista 开始菜单开发了这个控件,但正如你在上面的图片中看到的,它可以很容易地定制以支持其他样式、主题等。更多详细信息请参见演示项目(mac 风格菜单,在运行时应用主题等)。
Using the Code
使用这个控件非常简单。只需将其添加到您的工具箱,然后拖放到您的窗体上。

接下来,填充菜单控件,添加菜单项。有几个重载的方法可以用来实现这个目的。
-
public VistaMenuItem Add(string sText);
-
public VistaMenuItem Add(string sText, string sDescription);
-
public VistaMenuItem Add(string sText, string sDescription, Image img);
-
public VistaMenuItem Add(string sText, Image img);
-
public void Add(VistaMenuItem btn);
所以,如果您想要丰富的菜单项,您应该使用第三种方法。这是一个简单的代码来添加一些项目
for(int idx = 0; idx < 5; idx++)
vmcMenu.MenuItems.Add(
"Item " + idx,
"Description " + idx,
img
);
结构

下图显示了菜单控件的不同元素。菜单项是丰富呈现的,这意味着它们具有图像表示、标识它们的标题和简短描述。每个菜单项都由分隔符分隔。它们可以是平面的、3D 的,并且在此控件的发布版本中,您还可以指定根本不呈现它们。此功能将在稍后介绍。使用菜单侧边栏,您可以使用不同的名称对不同的菜单进行分组,并且还可以提供一个图标,该图标将在侧边栏标题旁边呈现。

菜单面板使用渐变颜色渲染,如图所示。
该控件实现了许多与菜单外观相关的属性,例如起始和结束渐变颜色、内部/外部边框颜色、侧边栏渐变颜色。
-
public Color MenuStartColor
-
public Color MenuEndColor
-
public Color MenuInnerBorderColor
-
public Color MenuOuterBorderColor
-
public Color SideBarStartGradient
-
public Color SideBarEndGradient
Vista 菜单控件也能够渲染背景图像。您可以设置图像对齐方式,由ContentAlignment
枚举定义,然后调用DrawBackImage
来绘制图像。
private void DrawBackImage(
Graphics gfx,
Rectangle rc
)
{
if (m_bmpBackImage != null)
{
int lW = m_bmpBackImage.Width;
int lH = m_bmpBackImage.Height;
Rectangle rcImage = new Rectangle(
0,
0,
lW,
lH
);
switch (m_ImageAlign)
{
case ContentAlignment.BottomCenter:
rcImage.X = rc.Width / 2 - lW / 2;
rcImage.Y = rc.Height - lH - 2;
break;
case ContentAlignment.BottomLeft:
rcImage.X = rc.Left + 2;
rcImage.Y = rc.Height - lH - 2;
break;
case ContentAlignment.BottomRight:
rcImage.X = rc.Right - lW - 2;
rcImage.Y = rc.Height - lH - 2;
break;
case ContentAlignment.MiddleCenter:
rcImage.X = rc.Width / 2 - lW / 2;
rcImage.Y = rc.Height / 2 - lH / 2;
break;
case ContentAlignment.MiddleLeft:
rcImage.X = rc.Left + 2;
rcImage.Y = rc.Height / 2 - lH / 2;
break;
case ContentAlignment.MiddleRight:
rcImage.X = rc.Right - lW - 2;
rcImage.Y = rc.Height / 2 - lH / 2;
break;
case ContentAlignment.TopCenter:
rcImage.X = rc.Width / 2 - lW / 2;
rcImage.Y = rc.Top + 2;
break;
case ContentAlignment.TopLeft:
rcImage.X = rc.Left + 2;
rcImage.Y = rc.Top + 2;
break;
case ContentAlignment.TopRight:
rcImage.X = rc.Right - lW - 2;
rcImage.Y = rc.Top + 2;
break;
}
gfx.DrawImage(
m_bmpBackImage,
rcImage
);
}
}
实现

DrawMenuItems
是菜单控件的核心方法。
private void DrawMenuItems(
Graphics gfx,
Rectangle rc,
float r
)
{
Rectangle rcItem = new Rectangle();
bool bVertical = (m_eMenuOrientation == VistaMenuOrientation.Vertical)
? true : false;
if (bVertical)
{
rcItem.X = 5;
rcItem.Y = 4;
rcItem.Width = rc.Width - 10;
rcItem.Height = m_lItemHeight;
}
else
{
rcItem.X = 5;
rcItem.Y = 4;
rcItem.Width = m_lItemWidth;
rcItem.Height = rc.Height - 7;
}
if (m_bDrawBar){
rcItem.X = m_lBarWidth;
rcItem.Width -= m_lBarWidth - 5;
}
Rectangle rcUpRect = rcItem;
Rectangle rcDownRect = rcItem;
rcUpRect.Height /= 2;
rcDownRect.Height /= 2;
rcDownRect.Y = rcUpRect.Height + 3;
if (items == null || items.Count == 0)
return;
gfx.SmoothingMode = SmoothingMode.HighQuality;
gfx.CompositingQuality = CompositingQuality.HighQuality;
gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
foreach (VistaMenuItem item in items) {
#region Draw selection / checked state
try
{
item.Left = rcItem.X;
item.Top = rcItem.Y;
Rectangle rcItemInner = rcItem;
if (item.Checked)
{
if (item.Hovering)
{
FillMenuItem(
gfx,
rcUpRect,
rcDownRect,
item.CheckedStartColor,
item.CheckedEndColor,
item.CheckedStartColorStart,
item.CheckedEndColorEnd,
r
);
DrawItemBorder(
gfx,
rcItemInner,
item.MouseDown,
item.InnerBorder,
item.OuterBorder,
r
);
}
else
{
FillMenuItem(
gfx,
rcUpRect,
rcDownRect,
item.CheckedStartColor,
item.CheckedEndColor,
item.CheckedStartColorStart,
item.CheckedEndColorEnd,
r
);
DrawItemBorder(
gfx,
rcItemInner,
item.MouseDown,
item.InnerBorder,
item.OuterBorder,
r
);
}
}
else
{
if (item.Hovering)
{
if (!item.Disabled)
{
FillMenuItem(
gfx,
rcUpRect,
rcDownRect,
item.SelectionStartColor,
item.SelectionEndColor,
item.SelectionStartColorStart,
item.SelectionEndColorEnd,
r
);
DrawItemBorder(
gfx,
rcItemInner,
item.MouseDown,
item.InnerBorder,
item.OuterBorder,
r
);
}
}
}
}
catch (Exception e)
{
MessageBox.Show(
e.ToString()
);
}
#endregion
#region Draw icons
if (item.Image != null)
{
Rectangle rcIcon = new Rectangle();
rcIcon.X = rcItem.X + 2;
rcIcon.Y = rcItem.Bottom - item.Image.Height;
rcIcon.Width = item.Image.Width;
rcIcon.Height = item.Image.Height;
if (item.Disabled)
{
ControlPaint.DrawImageDisabled(
gfx,
item.Image,
rcIcon.X,
rcIcon.Y,
Color.Transparent);
}
else
{
gfx.DrawImage(
item.Image,
rcIcon
);
}
}
#endregion
#region Draw separators
if (m_bSeparators)
{
Point pStart = new Point();
Point pEnd = new Point();
if (bVertical)
{
pStart = new Point(rcItem.Left + 3, rcItem.Bottom);
pEnd = new Point(rcItem.Right - 3, rcItem.Bottom);
}
else
{
pStart = new Point(rcItem.Right, rcItem.Top);
pEnd = new Point(rcItem.Right, rcItem.Bottom);
}
using (Pen pInner = new Pen(m_clrInnerBorder),
pOuter = new Pen(m_clrOuterBorder))
{
if (!m_bFlatSeparators)
{
// don't draw separator for last item:
if (items.IndexOf(item) < items.Count - 1)
{
if (bVertical)
{
gfx.DrawLine(pOuter, pStart, pEnd);
pStart.Y += 1; pEnd.Y += 1;
gfx.DrawLine(pInner, pStart, pEnd);
}
else
{
gfx.DrawLine(pOuter, pStart, pEnd);
pStart.X += 1; pEnd.X += 1;
gfx.DrawLine(pInner, pStart, pEnd);
}
}
}
else
{
Pen pFlat = new Pen(m_clrFlatSeparators);
// don't draw separator for last item:
pStart.Y += 1; pEnd.Y += 1;
if (items.IndexOf(item) < items.Count - 1)
gfx.DrawLine(pFlat, pStart, pEnd);
// clean up:
pFlat.Dispose();
}
}
}
#endregion
#region Draw item's text
StringFormat sf = new StringFormat();
StringFormat sfUpper = new StringFormat();
sfUpper.Trimming = StringTrimming.EllipsisCharacter;
sfUpper.FormatFlags = StringFormatFlags.LineLimit;
sf.Trimming = StringTrimming.EllipsisCharacter;
sf.FormatFlags = StringFormatFlags.LineLimit;
Rectangle rcCaption = rcUpRect;
Rectangle rcContent = rcDownRect;
if (item.Image != null)
{
sfUpper.Alignment = StringAlignment.Near;
sfUpper.LineAlignment = StringAlignment.Near;
sfUpper.LineAlignment = StringAlignment.Center;
sf.Alignment = StringAlignment.Near;
Rectangle rcImage = new Rectangle(
rcItem.X + 2,
rcItem.Y,
item.Image.Width,
item.Image.Height);
rcCaption.X = rcImage.Right + 2;
rcContent.X = rcImage.Right + 4;
rcCaption.Width -= rcImage.Width;
rcContent.Width -= rcImage.Width + 4;
}
else
{
sfUpper.Alignment = StringAlignment.Center;
sfUpper.LineAlignment = StringAlignment.Near;
sfUpper.LineAlignment = StringAlignment.Center;
sf.Alignment = StringAlignment.Center;
}
// draw text for item's caption / description:
SolidBrush sbCaption = new SolidBrush(Color.Empty);
SolidBrush sbContent = new SolidBrush(Color.Empty);
if (item.Checked)
{
sbCaption.Color = item.CheckedCaptionColor;
sbContent.Color = item.CheckedContentColor;
}
else
{
sbCaption.Color = item.CaptionColor;
sbContent.Color = item.ContentColor;
}
gfx.DrawString(item.Text, item.CaptionFont, sbCaption,
rcCaption, sfUpper);
gfx.DrawString(item.Description, item.ContentFont,
sbContent, rcContent, sf);
sfUpper.Dispose();
sf.Dispose();
sbCaption.Dispose();
sbCaption.Dispose();
#endregion
#region Update positions
if (bVertical)
{
rcUpRect.Y += rcItem.Height + 2;
rcDownRect.Y += rcItem.Height + 2;
rcItem.Y += rcItem.Height + 2;
}
else
{
rcUpRect.X += rcItem.Width + 2;
rcDownRect.X += rcItem.Width + 2;
rcItem.X += rcItem.Width + 2;
}
#endregion
}
}
它控制菜单项的渲染过程,具体取决于项目的状态(禁用、悬停、选中)。此外,它还指定如何根据菜单的方向渲染项目标题、描述、图像和分隔符。
每个菜单项都由VistaMenuItem
类表示。如图类图所示,有大量的属性可以自定义项目的外观。VistaMenuItems
是一个从CollectionBase
扩展的类,它保存所有菜单项,并控制添加/删除。这样,我们可以轻松地执行命中测试操作,以搜索悬停的项目或检测目标菜单项上的鼠标点击。
与菜单功能相关的其他重要方法是HitTestItem
和CalcMenuSize
。
private int HitTestItem(
int x,
int y
)
{
int code = -1;
VistaMenuItem item = null;
if ((x > m_lBarWidth) && (x <= this.ClientRectangle.Width))
{
if ((y >= 2) && (y <= this.ClientRectangle.Height))
{
for (int idx = 0; idx < items.Count; idx++)
{
item = items[idx];
if (y >= item.Top)
{
if (y < item.Top + m_lItemHeight)
{
code = idx;
break;
}
}
}
}
}
if (code == -1)
{
// cursor inside side bar:
for (int i = 0; i < items.Count; i++)
{
// unhover any hovering item:
items[i].Hovering = false;
Cursor = Cursors.Default;
Invalidate();
}
}
return code;
}
public void CalcMenuSize()
{
if (!this.DesignMode){
int lHeight = (items.Count ) * (m_lItemHeight + 3 ) + 1 ;
int lWidth = (items.Count) * (m_lItemWidth + 4) + 1;
if (m_eMenuOrientation == VistaMenuOrientation.Vertical)
{
this.MaximumSize = new Size(this.Width, lHeight);
this.Size = new Size(this.Width, lHeight);
}
else
{
this.MinimumSize = new Size(this.m_lItemWidth, this.m_lItemHeight);
this.MaximumSize = new Size(lWidth, this.m_lItemHeight + 5);
this.Size = new Size(lWidth, this.m_lItemHeight + 5);
}
Invalidate();
}
}
}
当渲染侧边栏时,第一个方法用于查找悬停的项目。它首先检查鼠标是否在菜单区域内,不包括侧边栏矩形的宽度。如果条件评估为true
,那么我们遍历项目集合,并找到正确的项目索引。第二个方法动态调整菜单的高度或宽度,具体取决于菜单的方向。
新功能

此更新中新的酷炫功能之一是能够水平显示菜单。此功能由MenuOrientation
属性控制。我发现此功能特别有趣,可以在主应用程序窗口中使用,您可以在其中引入应用程序的各个部分。例如,Nero StartSmart Essentials。请注意,当菜单水平显示时,无法渲染侧边栏。
实现的另一个有用的功能是选中状态。当项目被选中时,您可以指定项目的渐变颜色,以及标题/描述字体颜色。要激活此功能,您应该将CheckOnClick
功能设置为true
。

您可以使用SelectedItem
属性以编程方式获取/选择菜单项。
public int SelectedItem
{
get
{
int idx = -1;
for (int i = 0; i < items.Count; i++) {
if (items[i].Checked)
idx = items.IndexOf(items[i]);
}
return idx;
}
set
{
if (value < 0 || value > items.Count)
return;
for (int i = 0; i < items.Count; i++){
if (items[value].Disabled)
return;
items[value].Checked = true;
}
Invalidate();
}
}
此版本中提供的最后一个功能是禁用分隔符渲染的可能性。

演示项目

请确保在运行示例项目之前,将 Visitor TT2 BRK 字体复制到您的字体文件夹。
历史
- 2008年11月7日:初始版本
- 2008年11月21日:修复了重绘问题
- 2008年11月25日:添加了新功能
- 2008年12月28日:第二个版本