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

BarTender - 对您的内容进行分组

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (22投票s)

2006 年 4 月 16 日

CPOL

8分钟阅读

viewsIcon

133195

downloadIcon

3084

一个类似 Outlook 工具栏的控件,具有通用内容和动画效果。

Sample Image - BarTender.gif

目录

引言

每个人都知道 Outlook 的导航及其整洁的工具栏,并且已经存在许多不同的实现。其中一些非常整洁,但对于每个组可以插入什么类型的内容来说非常不灵活。我需要一种可能性,可以在所有可能的组合中快速显示和隐藏几个大型网格。我可以采用某种停靠和/或 MDI 样式来实现这一点,但出于多种原因,这在我的情况下并不好。此外,我认为我的解决方案比不得不通过拖放来处理停靠更方便、更快捷。

该组件的外观与普通的 Outlook 工具栏相似(我承认我没有坚持某种特定样式,而是根据我已看到的几种实现发明了自己的样式)。最大的区别在于,每个组中都可以放置任何控件,并且每个组不仅可以扩展或折叠,还可以由用户调整大小。为了使其看起来更时尚,我还为扩展/折叠过程添加了动画效果。

警告:如果您只想在组中插入链接/图标(如 Outlook 中),请不要使用此组件。市面上还有其他组件更适合处理此问题。

背景

要使用此组件,您应该对 Windows Forms 有一些经验。由于内置设计器支持不多,因此必须在运行时构建工具栏的内容。为了添加动画,我使用了自己的 Animations 组件。该组件仅作为编译后的程序集包含在下载文件中。如果您想进一步研究它,请看我的文章:Animating Windows Forms

使用代码

尽管没有广泛的设计器支持,但使用起来很简单。首先,将一个 GroupPaneBar 拖到您的 Form 上并进行编译。您会注意到其中已经显示了一些组。这些组仅用于对对其进行的设置进行视觉反馈,并且仅在设计模式下存在。现在,查看它的属性,并更改它们,直到您对外观和感觉满意为止。我稍后将更详细地讨论其中一些属性。

剩下的就是向工具栏添加组。这不能通过设计器完成。作为准备,您仍然可以提前将要添加的控件拖放到您的 Form 上进行调整。现在,在 Form 的构造函数中(在 InitializeComponent 之后),您可以将这些控件添加到工具栏中。

public MyForm() {
   InitializeComponent();
   _groupPaneBar.Add(new DataGrid(), "Bar 1", 
         null, false); //runtime created control
   _groupPaneBar.Add(_panelWithLinks, "Bar 2", 
         null, true); //design time created control
}

编译并启动,您将看到工具栏中有两个组。

示例

正如我一直所做的,我提供了一个示例应用程序,它应该展示该组件的大部分功能。它相当重量级,因为我将我能想到的所有东西都塞了进去,并且还将其用于最终测试。请注意,由于工具栏的嵌套和巨大的内容,可能会同时出现多个 ScrollBar

架构

结构相当简单。整个组件只有四个类,其中只有两个在做相关的工作。另外两个只是专门的事件类。

GroupPane

此类表示工具栏中的一个组。它负责绘制、事件管理、状态保持和动画。对于那些对编程自绘控件感兴趣的人来说,OnPaint 方法是最有趣的部分。

private void DrawArrow(Graphics graphics, Rectangle rect, 
                       Color color, bool isUp)
{
    int arrowHeight = rect.Height - 8;
    if (arrowHeight > 5)
        arrowHeight = 5;

    int halfLeftHeight = (rect.Height - arrowHeight) / 2;
    int halfWidth = (rect.Width / 2) - 1;
    using (SolidBrush brush = new SolidBrush(color))
    {
        int upwardsOffset = isUp ? 1 : -1;
        int curLine = 0;
        for (int i = (upwardsOffset < 0) ? 
            (arrowHeight - 1) : 0; (upwardsOffset < 0) ?
            (i >= 0) : (i < arrowHeight); i += upwardsOffset)
        {
            graphics.FillRectangle(brush, (rect.X + halfWidth) - i,
                (rect.Y + halfLeftHeight) + 
                 curLine, (i * 2) + 1, 1);
            curLine++;
        }
    }
}

private GraphicsPath CreateRoundedRectPath(Rectangle r, int radius)
{
    GraphicsPath path = new GraphicsPath();
    path.AddLine(r.Left + radius, r.Top, 
                (r.Left + r.Width) - (radius * 2), r.Top);
    path.AddArc((r.Left + r.Width) - (radius * 2), r.Top, 
                 radius * 2, radius * 2, 270f, 90f);
    path.AddLine((int) (r.Left + r.Width), 
                 (int) (r.Top + radius), (int) (r.Left + r.Width),
        (int) ((r.Top + r.Height) - (radius * 2)));
    path.AddArc((int) ((r.Left + r.Width) - (radius * 2)), 
                (int) ((r.Top + r.Height) - (radius * 2)),
                (int) (radius * 2), (int) (radius * 2), 
                (float) 0f, (float) 90f);
    path.AddLine((int) ((r.Left + r.Width) - (radius * 2)), 
                 (int) (r.Top + r.Height), 
                 (int) (r.Left + radius), (int) (r.Top + r.Height));
    path.AddArc(r.Left, (r.Top + r.Height) - (radius * 2), 
                radius * 2, radius * 2, 90f, 90f);
    path.AddLine(r.Left, (r.Top + r.Height) - 
                (radius * 2), r.Left, r.Top + radius);
    path.AddArc(r.Left, r.Top, radius * 2, radius * 2, 180f, 90f);
    path.CloseFigure();
    return path;
}

private Color GetColor(Color color)
{
    return GetColor(color, base.Enabled);
}

private Color GetColor(Color color, bool enabled)
{
    if (enabled)
        return color;
    return ControlPaint.LightLight(color);
}

protected override void OnPaint(PaintEventArgs e)
{            
    e.Graphics.SmoothingMode = SmoothingMode.HighQuality;

    e.Graphics.Clear(this.BackColor);

    Rectangle headerRectangle = GetHeaderRectangle();

    if (headerRectangle.Width == 0 || headerRectangle.Height == 0)
        return;

    //fill gradient backcolor of header
    using (LinearGradientBrush brush = 
                new LinearGradientBrush(headerRectangle, 
                GetColor(_parent.HeaderColor1), 
                GetColor(_parent.HeaderColor2), 
                _parent.HeaderGradientMode))
    {
        e.Graphics.FillRectangle(brush, headerRectangle);
    }

    //draw surrounding borders
    using (Pen pen = new Pen(GetColor(_parent.BorderColor), 
                     _parent.BorderWidth))
    {
        //top
        e.Graphics.DrawLine(pen, _parent.BorderWidth * 2, 
           (_parent.BorderWidth) / 2, 
            Width - _parent.BorderWidth * 2.5f, 
            (_parent.BorderWidth) / 2);

        //topleft
        e.Graphics.DrawArc(pen, _parent.BorderWidth / 2f, 
            _parent.BorderWidth / 2f, 
            _parent.BorderWidth * 4, 
            _parent.BorderWidth * 4, 180, 90);
    
        //topright
        e.Graphics.DrawArc(pen, Width - 1 - _parent.BorderWidth * 4.5f, 
            _parent.BorderWidth / 2f, _parent.BorderWidth * 4,
            _parent.BorderWidth * 4, 270, 90);

        //bottom
        e.Graphics.DrawLine(pen, 1, Height -  2 * 
            _parent.BorderWidth, Width - 2, 
            Height - 2 * _parent.BorderWidth);

        //left
        e.Graphics.DrawLine(pen, (_parent.BorderWidth) / 2, 
             _parent.BorderWidth * 2f, 
            (_parent.BorderWidth) / 2, Height - 1.5f * 
            _parent.BorderWidth - 1);
        
        //right
        e.Graphics.DrawLine(pen, Width - 1 - _parent.BorderWidth / 2, 
            _parent.BorderWidth * 2, 
            Width - 1 - _parent.BorderWidth / 2, 
            Height - 1.5f * _parent.BorderWidth - 1);

        //under header
        e.Graphics.DrawLine(pen, _parent.BorderWidth / 2f,
            _parent.BorderWidth + _parent.HeaderHeight, 
             Width - _parent.BorderWidth,
            _parent.BorderWidth  + _parent.HeaderHeight);
    }

    //draw expand/collapse button
    int buttonSize = (int)(headerRectangle.Height - 
                     _parent.BorderWidth - 5);
    _buttonRect = new Rectangle(this.Width - 
        _parent.BorderWidth - 5 - buttonSize, 
        _parent.BorderWidth + 2, buttonSize, buttonSize);
    using (Pen pen = new Pen(GetColor(_parent.BorderColor, 
           Enabled && _parent.CanExpandCollapse), 1))
    {
        e.Graphics.DrawPath(pen, 
                   CreateRoundedRectPath(_buttonRect, 1));
    }

    if (_buttonHighlighted && Enabled)
    {
        //draw button highlighting
        using (Pen pen = 
           new Pen(Color.FromArgb(_parent.ButtonHighlightAlpha, 
           _parent.ButtonHighlightColor), 4))
        {
            Rectangle highlightRect = _buttonRect;
            highlightRect.Inflate(-2, -2);
            e.Graphics.DrawPath(pen, 
              CreateRoundedRectPath(highlightRect, 3));
        }
    }
    
    //draw expand/collapse arrow
    Rectangle shapeRect = new Rectangle(_buttonRect.X + 1, 
        _buttonRect.Y + 1, _buttonRect.Width - 1, 
        _buttonRect.Height - 1);
    if (_heightAnimator.IsRunning || _expanded)
        DrawArrow(e.Graphics, shapeRect, 
            GetColor(_parent.ButtonArrowColor, 
            Enabled && _parent.CanExpandCollapse), true);
    if (_heightAnimator.IsRunning || !_expanded)
        DrawArrow(e.Graphics, shapeRect, 
            GetColor(_parent.ButtonArrowColor, 
            Enabled && _parent.CanExpandCollapse), false);            

    if (_image != null && _parent.ImagesEnabled)
    //draw image
    {
        int x = _parent.BorderWidth * 3;
        int y = (int)(_parent.BorderWidth + 
                 _parent.HeaderHeight / 2f - _image.Height / 2f);
        if (Enabled)
            e.Graphics.DrawImageUnscaled(_image, x, y);
        else
            ControlPaint.DrawImageDisabled(e.Graphics, _image, x, y,
                GetColor(_parent.HeaderColor1));
    }

    if (_text != null)
    {
        //draw text
        int textX = _parent.BorderWidth * 3 + 
                   (_image == null ? 0 : _image.Width);
        Rectangle textRect = new Rectangle(textX, _parent.BorderWidth, 
            _buttonRect.Left - textX - _parent.BorderWidth,
            _parent.HeaderHeight - _parent.BorderWidth);
        if (Enabled)
        {
            using (SolidBrush brush = new SolidBrush(base.ForeColor))
            {
                e.Graphics.DrawString(_text, base.Font, brush,
                    textRect, _parent.GetStringFormat());
            }
        } 
        else
        {
            ControlPaint.DrawStringDisabled(e.Graphics, _text, base.Font, 
                GetColor(base.ForeColor), textRect, 
                _parent.GetStringFormat());
        }
    }
}

它使用许多不同种类的绘制操作,并确保 GroupPane 在禁用模式下被清晰地绘制。我认为,这是许多控件程序员在开发新控件时常常忽略的事情之一。所有视觉设置都从相应的 GroupPaneBar 获取,这意味着 GroupPane 不能没有它而存在。主要原因是,我希望所有设置都在一个集中式位置完成,而不是在每个组中单独完成。通常,在使用此组件时,您甚至不必考虑此类,但如果您想以编程方式扩展/折叠或更改特定组的属性,那么您可以在此处进行设置。

以下是公共接口中最重要的成员

  • ParentBar

    简单地获取包含该组的 GroupPaneBar

  • 文本

    获取或设置要在组顶部显示的文本。

  • Image

    获取或设置要在组顶部显示的 Image

  • 展开后

    获取或设置该组是否应展开或折叠。设置此属性时,它将使用 GroupPaneBar.AnimationEnabled 属性来确定是否进行动画。要直接影响此行为,ExpandCollapse 方法有一些重载,可以覆盖此行为。这在启动时展开/折叠某些组时可能很有用。

  • ExpandedHeight

    获取或设置组展开时的高度。如果设置此属性时组已展开,它将立即调整到新高度。请注意,此值还包括边框和组的标题。

  • IsAnimationRunning

    获取组当前是否正在折叠或展开的过程中。

  • Control

    获取或设置要在组内显示的控件。这使您能够交换组的完整内容。

事件

GroupPane 类总共有八个事件。其中四个在某些属性更改时触发 - 另外四个是关于通知组的折叠和展开的。由于这些属性也由 GroupPaneBar 引发,因此我将在那里详细解释它们。

GroupPaneBar

一个 GroupPaneBar 包含多个 GroupPane 实例。如前所述,GroupPane 不能没有 GroupPaneBar 而存在,因此它也充当 GroupPane 的工厂。由于其大小,我在这里不列出完整的公共接口 - 特别是关于视觉设置。请查看代码。所有属性都得到了很好的文档说明。

组组织

要为 GroupPaneBar 创建 GroupPane,将调用重载函数 CreateGroupPane 来创建实例。这些实例不会立即添加到工具栏。因此,需要调用 Add 才能稍后将组添加到工具栏。此外,GroupPaneBar 还有几个类似的集合函数,如 ClearRemoveRemoveAt,用于更改包含的组。为了更方便地添加组,有几个 Add 重载,它们不仅创建组,而且直接将它们添加到工具栏。

/// <summary>
/// Adds a new <see cref="GroupPane"/> to the end of the list.
/// </summary>
/// <param name="control">Element which should
/// initially beend placed in the new group.</param>
/// <param name="text">Initial text of the new group.</param>
/// <param name="image">Initial image of the new group.</param>
/// <param name="adjustGroupPaneHeightToControlheight">
///    Sets whether the expanded height of 
///    the resulting group pane should match the height
///    of the given control.</param>
/// <returns>The newly created <see cref="GroupPane"/>.</returns>
public GroupPane Add(Control control, string text, Image image, 
       bool adjustGroupPaneHeightToControlheight);

GroupPaneAddedGroupPaneRemoved 事件可用于在组添加到工具栏或从工具栏中删除时获得通知。

视觉属性

除了 GroupPaneImageText 属性外,所有视觉设置都通过 GroupPaneBar 进行。正如我所说的,我希望它是集中化的。缺点是无法为每个组设置不同的样式。但这不是我的愿望清单上的内容,可能也不是您的,而且这样可以更容易地更改整个工具栏。如果这里有足够多的人大声疾呼,我可能会重新考虑这一点。请注意,每个属性都有一个事件,当它被更改时(并且仅当它被更改时才会触发 - 如果用完全相同的值设置属性,则不会触发任何事件)。

展开和折叠

除了 ExpandAllCollapseAll 方法(它们类似于 GroupPane 中的 Expand/Collapse 方法,只是它们自然会展开/折叠所有包含的组)之外,还有四个与展开和折叠相关的事件。它们都提供事件参数,其中包含受影响的 GroupPane。其中两个在组展开或折叠后触发,更有趣的是,另外两个在发生这种情况之前触发。它们提供了一个事件参数中的 Cancel 属性,可用于阻止用户展开/折叠某些组,同时仍然允许修改其他组。

待办事项

  • TabControl 那样的设计器支持应该非常酷。
  • 组的内容目前非常通用。使用此组件构建一个真正的 Outlook 式工具栏不像使用其他组件那样容易,但这是可能的。一些更具体的实现(使用或继承现有类)可以提高易用性。
  • 任何您喜欢的 :)。请随时发布请求。

历史

  • 2006 年 4 月 16 日 - 版本 1.0
    • 初始发布。
  • 2006 年 4 月 17 日 - 版本 1.0.1
    • 通过重写 AdjustFormScrollbars 并继承自 ScrollableControl 而不是 Panel,修复了嵌套多个 GroupPaneBar 时出现的闪烁问题。感谢 Josh Smith 给我提供了一些关于此的思路。
    • 由于基类已更改,我不得不自己实现 BorderStyle 属性,并为其添加了一个 Changed 事件,并在属性示例中将其设置为可配置。
  • 2006 年 4 月 22 日 - 版本 1.1
    • 自动将插入到 GroupPane 中的 FormTopLevel 属性设置为 false。这使得使用 Form 与此组件更加容易。请注意,当 Form 是 MDI 容器时,此操作会导致 ArgumentException。这基于 duffman071 的请求。
    • GroupPaneBar 添加了一个 ShowExpandCollapseButton 属性。将其设置为 false 将从组中删除展开/折叠按钮。然后,可以通过单击组标题中的任意位置来展开或折叠它们。我还向属性示例添加了一个新的 CheckBox 来测试此新功能。这基于 duffman071 的请求。
© . All rights reserved.