Crumb - 类似面包屑的嵌套按钮
Crumb 是一个可以包含子按钮并相应绘制自身的按钮。
引言
很长一段时间以来,我一直在寻找像 Ubuntu 软件中心那样式的面包屑控件,但我一直找不到,所以我决定自己动手做一个。所以,经过几个小时的工作,它来了!
背景
这张图片显示了我的灵感来源。
Using the Code
该类继承自 System.Windows.Forms.Control
类,而不是 System.Windows.Forms.Button
类,但其行为和大部分功能是相同的。System.Windows.Forms.Vercas.Crumb
类具有以下属性
名称 | 描述 |
---|---|
文本 |
按钮上显示的文本。 |
Image |
按钮上显示的图像。 |
TextAlign |
按钮上文本的对齐方式。 |
ImageAlign |
按钮上图像的对齐方式。 |
TextImage对齐 (TextImage对齐) |
确定图像和文本是否都根据文本的对齐方式进行对齐,并并排显示。 |
CheckBox |
确定是否在按钮上绘制一个复选框来显示其 Checked 属性。否则,按钮将显示为已聚焦或未聚焦。 |
点击切换 (CheckOnClick) |
确定按钮在单击时是否更改其 Checked 属性。 |
Checked |
确定按钮是否显示为已聚焦或按钮上显示的复选框是否已选中。 |
目录 |
获取 Crumb 在嵌套中的索引。 |
Child |
紧跟在当前 Crumb 后面的 Crumb。 |
嵌套行为最多只允许一个 Crumb 被选中。在示例窗体中,我们订阅了 CrumbClick
默认事件,该事件将始终保持嵌套中的 Crumb 被选中。顺便说一下,截图中的蓝灰色按钮就是被选中的 Crumb。
我还必须补充一点,背景图像被绘制了多次以实现背景效果,因为正如下面所指出的,它不会正常拉伸,右侧是透明的。
现在是技术部分... 具体来说,就是绘制按钮。好吧,由于我缺乏绘画技巧,我决定使用图像。
static Image Left_Edge = Properties.Resources.crumb_left_end;
static Image Body = Properties.Resources.crumb_body;
static Image Right_Edge = Properties.Resources.crumb_right_end;
static Image Right_Triangle = Properties.Resources.crumb_right_point;
static Image Selected_Left_Edge = Properties.Resources.selected_crumb_left_end;
static Image Selected_Body = Properties.Resources.selected_crumb_body;
static Image Selected_Right_Edge = Properties.Resources.selected_crumb_right_end;
static Image Selected_Right_Triangle = Properties.Resources.selected_crumb_right_point;
static Image Hovered_Left_Edge = Properties.Resources.hovered_crumb_left_end;
static Image Hovered_Body = Properties.Resources.hovered_crumb_body;
static Image Hovered_Right_Edge = Properties.Resources.hovered_crumb_right_end;
static Image Hovered_Right_Triangle = Properties.Resources.hovered_crumb_right_point;
static Image Clicked_Left_Edge = Properties.Resources.clicked_crumb_left_end;
static Image Clicked_Body = Properties.Resources.clicked_crumb_body;
static Image Clicked_Right_Edge = Properties.Resources.clicked_crumb_right_end;
static Image Clicked_Right_Triangle = Properties.Resources.clicked_crumb_right_point;
这些静态变量的放置是为了让你能够给它们赋予另一个值。如果你有外部文件的图像,这会很有用。顺便说一句,这些是图像
它们被设计成占用最少的空间(在磁盘上)。正如我所说,我必须多次绘制填充/主体图像,因为拉伸它会在上面覆盖一个透明渐变。
如此多的设置带来了很多可能性:一个 Crumb 可以/不可以有子项;可以/不可以有复选框;可以/不可以有图像;可以/不可以有文本。为此,有被重写的 DefaultSize
属性
protected override Size DefaultSize
{
get
{
var w = (c == null ? (this.Controls.Count == 0 ? 3 : 15) :
Math.Max(15, c.Width)) + (this.CheckBox ? 24 : 0) +
(this.img != null ? img.Width : 0) +
(!string.IsNullOrEmpty(this.Text) ?
TextRenderer.MeasureText(this.Text, this.Font).Width : 0) +
(this.Parent is Crumb ? 13 : 0);
return new Size(this.Controls.Count > 0 ? w :
Math.Max(w, this.Width), 24);
}
}
正如你可能已经注意到的,我考虑了各种可能性。最重要的是,如果 Crumb 有子项,由于我遇到的一些 bug,它会被自动调整大小。文本的字体也很重要。文本被测量,并且尺寸也是根据文本计算的。
控件中有一个名为 CrumbClick
的事件。此事件通过嵌套的 Crumb 进行重定向。因此,**任何被单击的 Crumb** 都会触发**所有 Crumb** 的事件。订阅第一个 Crumb 的事件实际上可以帮助你跟踪所有 Crumb。该事件符合标准,并且有一个名为 CrumbClickEventArgs
的 EventArgs
类,它具有以下属性
目录 |
单击的 Crumb 在嵌套中的索引。 |
Sender |
实际被单击的 Crumb。 |
单击前 (Checked Before) |
获取 Crumb 在单击 **之前** 是否被 Checked 。 |
单击后 (CheckedAfter) |
获取或设置事件发生后 Crumb 的 Checked 状态。如果 ChecksOnClick 为 true ,则此设置无效。 |
点击切换 (ChecksOnClick) |
获取或设置 Crumb 是否应该在单击时切换其 Checked 状态。如果此属性为 true ,则 Crumb 的 Checked 状态将始终切换到另一状态,无论你在事件参数中指定什么。 |
此事件通过订阅子项的 CrumbClick
事件传递给父项。事件代码如下:
EventHandler<crumbclickeventargs> childClick =
new EventHandler<crumbclickeventargs>(c_Click);
static void c_Click(object sender, CrumbClickEventArgs e)
{
if ((sender as Crumb).Parent is Crumb)
{ ((sender as Crumb).Parent as Crumb).OnCrumbClick(e); }
}
事件被重新调用,但这次是在父 Crumb 上。事件参数被保留。
我还提到,一个嵌套中只有一个 Crumb 可以被选中。这是为了模仿一个选择系统。
public Boolean Checked
{
get
{
return chk;
}
set
{
if (!nocc)
{
nocc = true;
Crumb cr = this.Child;
while (cr != null) { cr.Checked = false; cr = cr.Child; }
cr = this.Parent as Crumb;
while (cr != null && cr is Crumb)
{ cr.Checked = false; cr = cr.Parent as Crumb; }
nocc = false;
}
chk = value;
Refresh();
}
}
nocc
是一个静态布尔值,用于指示其他 Crumb 不应该更新它们在嵌套中的同伴,以便避免溢出异常。
现在,绘制部分
public static float dc(PaintEventArgs e, Color foreColor, float x = 0f,
string text = "", Image img = null, bool clicked = false,
bool hovered = false, bool chk = false, bool chkbox = false,
float width = 0f, Font font = null, bool tai = true,
ContentAlignment ta = ContentAlignment.MiddleCenter,
ContentAlignment ia = ContentAlignment.MiddleLeft,
bool pt = false, bool ch = true)
{
if (font == null) { font = SystemFonts.MessageBoxFont; }
width = Math.Max((ch ? 15 : 3) + (chkbox ? 24 : 0) +
(img != null ? img.Width : 0) + (!string.IsNullOrEmpty(text) ?
TextRenderer.MeasureText(text, font).Width : 0) + (pt ? 13 : 0), width);
if (clicked)
{
e.Graphics.DrawImage(Crumb.Clicked_Left_Edge, x, 0);
for (int i = (int)x + Crumb.Clicked_Left_Edge.Width; i <=
x + width - (ch ? Crumb.Clicked_Right_Triangle :
Crumb.Clicked_Right_Edge).Width; i++)
e.Graphics.DrawImage(Crumb.Clicked_Body, i, 0);
e.Graphics.DrawImage(ch ? Crumb.Clicked_Right_Triangle :
Crumb.Clicked_Right_Edge, x + width - (ch ?
Crumb.Clicked_Right_Triangle : Crumb.Clicked_Right_Edge).Width, 0);
}
else if (hovered)
{
e.Graphics.DrawImage(Crumb.Hovered_Left_Edge, x, 0);
for (int i = (int)x + Crumb.Hovered_Left_Edge.Width; i <=
x + width - (ch ? Crumb.Hovered_Right_Triangle :
Crumb.Hovered_Right_Edge).Width; i++)
e.Graphics.DrawImage(Crumb.Hovered_Body, i, 0);
e.Graphics.DrawImage((ch ? Crumb.Hovered_Right_Triangle :
Crumb.Hovered_Right_Edge), x + width - (ch ?
Crumb.Hovered_Right_Triangle : Crumb.Hovered_Right_Edge).Width, 0);
}
else if (chk && !chkbox)
{
e.Graphics.DrawImage(Crumb.Selected_Left_Edge, x, 0);
for (int i = (int)x + Crumb.Selected_Left_Edge.Width; i <=
x + width - (ch ? Crumb.Selected_Right_Triangle :
Crumb.Selected_Right_Edge).Width; i++)
e.Graphics.DrawImage(Crumb.Selected_Body, i, 0);
e.Graphics.DrawImage((ch ? Crumb.Selected_Right_Triangle :
Crumb.Selected_Right_Edge), x + width - (ch ?
Crumb.Selected_Right_Triangle :
Crumb.Selected_Right_Edge).Width, 0);
}
else
{
e.Graphics.DrawImage(Crumb.Left_Edge, x, 0);
for (int i = (int)x + Crumb.Left_Edge.Width; i <= x + width -
(ch ? Crumb.Right_Triangle : Crumb.Right_Edge).Width; i++)
e.Graphics.DrawImage(Crumb.Body, i, 0);
e.Graphics.DrawImage((ch ? Crumb.Right_Triangle : Crumb.Right_Edge),
x + width - (ch ? Crumb.Right_Triangle : Crumb.Right_Edge).Width, 0);
}
if (chkbox)
{
var st = chk ? (clicked ?
System.Windows.Forms.VisualStyles.CheckBoxState.CheckedPressed :
System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal) :
(clicked ? System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedPressed :
System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedNormal);
var sz = CheckBoxRenderer.GetGlyphSize(e.Graphics, st);
CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point((int)(x +
(pt ? 13 : 0) + (24 - sz.Height) / 2), (24 - sz.Height) / 2), st);
}
if (tai)
{
dit(e, foreColor, x + (pt ? 13 : 0), ta, text, font, chkbox, width, ia, img);
}
else
{
di(e, x, img, ia, chkbox, width);
dt(e, foreColor, x + (pt ? 13 : 0), ta, text, font, chkbox, width);
}
return width;
}
在这里,我绘制按钮的组件图像,然后绘制文本/图像。别哭,这并没有**那么**难。它只需要进行大量的检查,以确保一切都很好。在最后几行,有三个陌生的方法:dit
(绘制图像文本)、di
(绘制图像) 和 dt
(绘制文本)。它们都是静态方法,需要许多变量来决定在哪里绘制。这些是代码
private static void dt(PaintEventArgs e, Color foreColor, float x = 0f,
ContentAlignment txta = ContentAlignment.MiddleCenter,
string text = "", Font font = null,
bool chkbox = false, float width = 0f)
{
if (!string.IsNullOrEmpty(text))
{
PointF p = new PointF();
var s = e.Graphics.MeasureString(text, font);
switch (txta)
{
case ContentAlignment.BottomCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - s.Width) / 2, 21 - s.Height);
break;
case ContentAlignment.BottomLeft:
p = new PointF(x + (chkbox ? 24 : 3), 21 - s.Height);
break;
case ContentAlignment.BottomRight:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15) - s.Width, 21 - s.Height);
break;
case ContentAlignment.MiddleCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - s.Width) / 2, (24 - s.Height) / 2);
break;
case ContentAlignment.MiddleLeft:
p = new PointF(x + (chkbox ? 24 : 3), (24 - s.Height) / 2);
break;
case ContentAlignment.MiddleRight:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15) - s.Width, (24 - s.Height) / 2);
break;
case ContentAlignment.TopCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - s.Width) / 2, 3);
break;
case ContentAlignment.TopLeft:
p = new PointF(x + (chkbox ? 24 : 3), 3);
break;
case ContentAlignment.TopRight:
p = new PointF(x + ((chkbox ?
(width - 24) : width) - 15) - s.Width, 3);
break;
}
using (Brush b = new SolidBrush(foreColor))
e.Graphics.DrawString(text, font, b, p);
}
}
在这里,我根据 TextAlign
属性绘制文本
private static void di(PaintEventArgs e, float x = 0f, Image img = null,
ContentAlignment imga = ContentAlignment.MiddleLeft,
bool chkbox = false, float width = 0f)
{
if (img != null)
{
PointF p = new Point();
switch (imga)
{
case ContentAlignment.BottomCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - img.Width) / 2, 21 - img.Height);
break;
case ContentAlignment.BottomLeft:
p = new PointF(x + (chkbox ? 24 : 3), 21 - img.Height);
break;
case ContentAlignment.BottomRight:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15) - img.Width, 21 - img.Height);
break;
case ContentAlignment.MiddleCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - img.Width) / 2, (24 - img.Height) / 2);
break;
case ContentAlignment.MiddleLeft:
p = new PointF(x + (chkbox ? 24 : 3), (24 - img.Height) / 2);
break;
case ContentAlignment.MiddleRight:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15) - img.Width, (24 - img.Height) / 2);
break;
case ContentAlignment.TopCenter:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15 - img.Width) / 2, 3);
break;
case ContentAlignment.TopLeft:
p = new PointF(x + (chkbox ? 24 : 3), 3);
break;
case ContentAlignment.TopRight:
p = new PointF(x + ((chkbox ? (width - 24) :
width) - 15) - img.Width, 3);
break;
}
e.Graphics.DrawImage(img, p);
}
}
在这里,我根据 ImageAlign
属性绘制图像
private static void dit(PaintEventArgs e, Color foreColor, float x = 0f,
ContentAlignment txta = ContentAlignment.MiddleCenter,
string text = "", Font font = null, bool chkbox = false,
float width = 0f,
ContentAlignment imga = ContentAlignment.MiddleLeft, Image img = null)
{
if (!string.IsNullOrEmpty(text))
{
if (img != null)
{
if (!string.IsNullOrEmpty(text))
{
float w = 0, h = 0, ht = 0;
var s = e.Graphics.MeasureString(text, font);
switch (txta)
{
case ContentAlignment.BottomCenter:
w = ((chkbox ? (width - 24) : width) - 15 -
s.Width - img.Width) / 2; h = 21 -
img.Height; ht = 21 - s.Height;
break;
case ContentAlignment.BottomLeft:
w = chkbox ? 24 : 3; h = 21 - img.Height; ht = 21 - s.Height;
break;
case ContentAlignment.BottomRight:
w = ((chkbox ? (width - 24) : width) - 15) -
s.Width - img.Width; h = 21 -
img.Height; ht = 21 - s.Height;
break;
case ContentAlignment.MiddleCenter:
w = ((chkbox ? (width - 24) : width) - 15 -
s.Width - img.Width) / 2; h = (24 - img.Height) / 2;
ht = (24 - s.Height) / 2;
break;
case ContentAlignment.MiddleLeft:
w = chkbox ? 24 : 3; h = (24 - img.Height) / 2;
ht = (24 - s.Height) / 2;
break;
case ContentAlignment.MiddleRight:
w = ((chkbox ? (width - 24) : width) - 15) - s.Width -
img.Width; h = (24 - img.Height) / 2;
ht = (24 - s.Height) / 2;
break;
case ContentAlignment.TopCenter:
w = ((chkbox ? (width - 24) : width) - 15 -
s.Width - img.Width) / 2; h = ht = 3;
break;
case ContentAlignment.TopLeft:
w = chkbox ? 24 : 3; h = ht = 3;
break;
case ContentAlignment.TopRight:
w = ((chkbox ? (width - 24) : width) - 15) -
s.Width - img.Width; h = ht = 3;
break;
}
w += x;
e.Graphics.DrawImage(img, w, h);
using (Brush b = new SolidBrush(foreColor))
e.Graphics.DrawString(text, font, b, w + img.Width, ht);
}
}
else
{
dt(e, foreColor, x, txta, text, font, chkbox, width);
}
}
else
{
di(e, x, img, imga, chkbox, width);
}
}
在这里,我根据 TextAlign
属性绘制图像和文本。此外,这个混乱也在控件的 OnPaint(...)
方法中使用
protected override void OnPaint(PaintEventArgs e)
{
Crumb.dc(e, this.ForeColor, 0, Text, this.img, this.clicked,
this.hovered, this.chk, this.chkbox, this.c == null ?
this.Width : (this.Width - this.c.Width), this.Font,
this.tai, this.txta, this.imga, this.Parent is Crumb,
this.Controls.Count > 0);
base.OnPaint(e);
}
在这里,我传递 dc
绘制 Crumb 所需的参数。
另外,当 TextImageAlign
属性设置为 true
时,才使用 dit
方法。
绘制按钮时(不包括复选框、文本或图像),有 4 种(或 3 种)可能的选择(取决于是否有复选框)
已单击 (Clicked)
悬停 (Hovered)
Checked
/Selected
(如果没有复选框)(如果有,复选框将显示已选中/未选中)正常
嵌套 Crumb 时,父项的尖锐(右侧)端绘制在子项之上。这在订阅子项的 Paint
事件时找到。代码是
PaintEventHandler childPaint = new PaintEventHandler(c_Paint);
//Really, this is required for unsubscribing from the event.
//Does it sound right to unsubscribe a NEW DELEGATE from an event?
static void c_Paint(object sender, PaintEventArgs e)
{
var c = sender as Crumb;
if (c.Parent != null && c.Parent is Crumb)
{
var p = c.Parent as Crumb;
dc(e, Color.Black, -25f, width: 38f, hovered:
p.hovered, clicked: p.clicked, chk: p.chk, chkbox: p.chkbox);
}
}
我真的在寻找一种更快的绘图方法,因为在示例中,我选择 Crumb 时出现了一些剪裁效果。
关注点
我了解到 Graphics.DrawImage(...);
在拉伸方面很糟糕。
历史
- 2010/11/28 - 初始发布。
- 2010/12/1 - 更新文章并讨论代码。
请告诉我你发现的任何 bug!另外,如果有什么地方看起来不对劲,请在差评之前问我!