Introduction
For such a long time, I have tried to find a breadcrumb control like the one in the Ubuntu Software Center, but I just couldn't find one, so I decided to put myself at work and make one. So, after a few hours of work, here it is!
Background
This image shows where I have the idea from.
Using the Code
The class inherits from the System.Windows.Forms.Control
class, not from System.Windows.Forms.Button
, but the behavior and most features are the same. The System.Windows.Forms.Vercas.Crumb
class features the following properties:
Name | Description |
---|
Text | The text displayed on the button. |
Image | The image displayed on the button. |
TextAlign | The alignment of the text on the button. |
ImageAlign | The alignment of the image on the button. |
TextImageAlign | Determines whether the image and the text will be both aligned by the text's alignment, and showed side-by-side. |
CheckBox | Determines whether a checkbox should be drawn on the button to show its Checked property. Otherwise, the button will appear focused or unfocused. |
CheckOnClick | Determines whether the button will change its Checked property when clicked. |
Checked | Determines whether the button appears focused or the checkbox displayed on the button is checked. |
Index | Gets the index of the crumb in the nest. |
Child | The crumb that will be right after the current. |
The nesting behavior allows maximum one crumb to be checked. In the sample form, there is a subscription to the CrumbClick
default event that will always keep a checked crumb in the nest. By the way, the blue-ish button in the screenshot is the checked crumb.
I must also add that the background image is drawn multiple times to achieve the background, because, as pointed below, it doesn't stretch normally, the right side is transparent.
And now the technical part... specifically, drawing the button. Well, because of my lack of drawing skills, I decided to use images.
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;
These static variables are placed so that you can give them another value. This is useful if you, for example, have the images in external files. By the way, these are the images:
They are made to take the least space (on disk). As I said, I have to draw the fill/body image multiple times because stretching it will put a transparent gradient above it.
Making so many settings brings lots of possibilities: a crumb may/may not have a child; may/may not have the checkbox; may/may not have an image; may/may not have text. For this, there is the DefaultSize
overridden property:
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);
}
}
As you might have noticed, I take every possibility in count. Most importantly, if the crumb has a child, it will be auto-sized because of some bugs I had. The text's font also counts. The text is measured and the size is calculated according to it, too.
There is an event in the control called CrumbClick
. This event redirects itself trough the nested crumbs. So any clicked crumb will launch the event of all crumbs. Subscribing to the first crumb's event will actually help you track all the crumbs. The event is by-the-standards, and features an EventArgs
class called CrumbClickEventArgs
, which has the following properties:
Index | The index of the clicked crumb in the nest. |
Sender | The crumb which was actually clicked. |
Checked Before | Gets whether the crumb was Checked or not before being clicked. |
CheckedAfter | Gets or sets the Checked state of the crumb after the event. This does not have any effect if ChecksOnClick is true . |
ChecksOnClick | Gets or sets whether the crumb is supposed to switch its Checked state on click. If this property is true , the crumb's Checked state will always shift to the other, no matter what you tell in the event argument. |
This event is passed to the parent through a subscription to the CrumbClick
event of a child. The code of the event looks like this:
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); }
}
The event is re-invoked, but on the parent crumb this time. The event arguments are preserved.
I have also mentioned that only one crumb in a nest can be checked. This is supposed to imitate a selection system.
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
is a static boolean to announce that other crumbs should not update their fellows in the nest, so an overflow exception will be dodged.
Now, the drawing part:
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;
}
Here I draw the button's component images and then the text/image. Don't cry, it is not that hard. It just requires a lot of checks to be made, to make sure everything is cool. In the last lines, there are three alien methods: dit
(draw image text), di
(draw image), and dt
(draw text). They are all static methods and require many variables to decide on where to draw. These are the codes:
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);
}
}
Here I draw the text according to the TextAlign
property:
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);
}
}
Here I draw the image according to the ImageAlign
property:
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);
}
}
Here I draw the images and text according to the TextAlign
property. Also, this mess is used on the OnPaint(...)
method of the control:
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);
}
Here I pass the arguments required by dc
to paint a crumb.
Also, the dit
method is used only if the TextImageAlign
property is set to true
.
While drawing the button (not the checkbox, text, or image), there are 4 (or 3) possible choices (depending on whether there is a checkbox or not):
Clicked
Hovered
Checked
/Selected
(If there is no checkbox) (If there is, the checkbox will show checked/unchecked)Normal
When nesting a crumb, the pointy (right) side of the parent is drawn above the child. This is found in a subscription to the Paint
event of the child. The code is:
PaintEventHandler childPaint = new PaintEventHandler(c_Paint);
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);
}
}
I am really looking for a faster way to draw, because, in the example, I get some kind of clipping when selecting a crumb.
Points of Interest
I have learned that Graphics.DrawImage(...);
sucks at stretching.
History
- 11/28/2010 - Initial release.
- 12/1/2010 - Updated article with code discussion.
Please tell me about any bugs you find! Also, if something doesn't seem alright, ask me before low-rating!