Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

Crumb - A breadcrumb-like nesting button

4.82/5 (31 votes)
2 Dec 2010CPOL5 min read 72.6K   2.1K  
Crumb is a button that can hold a child button and paint itself accordingly.

Image 1

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:

NameDescription
TextThe text displayed on the button.
ImageThe image displayed on the button.
TextAlignThe alignment of the text on the button.
ImageAlignThe alignment of the image on the button.
TextImageAlignDetermines whether the image and the text will be both aligned by the text's alignment, and showed side-by-side.
CheckBoxDetermines whether a checkbox should be drawn on the button to show its Checked property. Otherwise, the button will appear focused or unfocused.
CheckOnClickDetermines whether the button will change its Checked property when clicked.
CheckedDetermines whether the button appears focused or the checkbox displayed on the button is checked.
IndexGets the index of the crumb in the nest.
ChildThe 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.

C#
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:

Image 2

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:

C#
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:

IndexThe index of the clicked crumb in the nest.
SenderThe crumb which was actually clicked.
Checked BeforeGets whether the crumb was Checked or not before being clicked.
CheckedAfterGets or sets the Checked state of the crumb after the event. This does not have any effect if ChecksOnClick is true.
ChecksOnClickGets 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:

C#
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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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):

  1. Clicked
  2. Hovered
  3. Checked/Selected (If there is no checkbox) (If there is, the checkbox will show checked/unchecked)
  4. 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:

C#
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);
    }
}

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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)