65.9K
CodeProject 正在变化。 阅读更多。
Home

Crumb - 类似面包屑的嵌套按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (29投票s)

2010 年 12 月 1 日

CPOL

5分钟阅读

viewsIcon

74072

downloadIcon

2151

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。该事件符合标准,并且有一个名为 CrumbClickEventArgsEventArgs 类,它具有以下属性

目录 单击的 Crumb 在嵌套中的索引。
Sender 实际被单击的 Crumb。
单击前 (Checked Before) 获取 Crumb 在单击 **之前** 是否被 Checked
单击后 (CheckedAfter) 获取或设置事件发生后 Crumb 的 Checked 状态。如果 ChecksOnClicktrue,则此设置无效。
点击切换 (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 种)可能的选择(取决于是否有复选框)

  1. 已单击 (Clicked)
  2. 悬停 (Hovered)
  3. Checked/Selected(如果没有复选框)(如果有,复选框将显示已选中/未选中)
  4. 正常

嵌套 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!另外,如果有什么地方看起来不对劲,请在差评之前问我!

© . All rights reserved.