BarTender - 对您的内容进行分组






4.82/5 (22投票s)
一个类似 Outlook 工具栏的控件,具有通用内容和动画效果。
目录
引言
每个人都知道 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
属性来确定是否进行动画。要直接影响此行为,Expand
和Collapse
方法有一些重载,可以覆盖此行为。这在启动时展开/折叠某些组时可能很有用。ExpandedHeight
获取或设置组展开时的高度。如果设置此属性时组已展开,它将立即调整到新高度。请注意,此值还包括边框和组的标题。
IsAnimationRunning
获取组当前是否正在折叠或展开的过程中。
Control
获取或设置要在组内显示的控件。这使您能够交换组的完整内容。
事件
GroupPane
类总共有八个事件。其中四个在某些属性更改时触发 - 另外四个是关于通知组的折叠和展开的。由于这些属性也由 GroupPaneBar
引发,因此我将在那里详细解释它们。
GroupPaneBar
一个 GroupPaneBar
包含多个 GroupPane
实例。如前所述,GroupPane
不能没有 GroupPaneBar
而存在,因此它也充当 GroupPane
的工厂。由于其大小,我在这里不列出完整的公共接口 - 特别是关于视觉设置。请查看代码。所有属性都得到了很好的文档说明。
组组织
要为 GroupPaneBar
创建 GroupPane
,将调用重载函数 CreateGroupPane
来创建实例。这些实例不会立即添加到工具栏。因此,需要调用 Add
才能稍后将组添加到工具栏。此外,GroupPaneBar
还有几个类似的集合函数,如 Clear
、Remove
或 RemoveAt
,用于更改包含的组。为了更方便地添加组,有几个 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);
GroupPaneAdded
和 GroupPaneRemoved
事件可用于在组添加到工具栏或从工具栏中删除时获得通知。
视觉属性
除了 GroupPane
的 Image
和 Text
属性外,所有视觉设置都通过 GroupPaneBar
进行。正如我所说的,我希望它是集中化的。缺点是无法为每个组设置不同的样式。但这不是我的愿望清单上的内容,可能也不是您的,而且这样可以更容易地更改整个工具栏。如果这里有足够多的人大声疾呼,我可能会重新考虑这一点。请注意,每个属性都有一个事件,当它被更改时(并且仅当它被更改时才会触发 - 如果用完全相同的值设置属性,则不会触发任何事件)。
展开和折叠
除了 ExpandAll
和 CollapseAll
方法(它们类似于 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
中的Form
的TopLevel
属性设置为false
。这使得使用Form
与此组件更加容易。请注意,当Form
是 MDI 容器时,此操作会导致ArgumentException
。这基于 duffman071 的请求。 - 向
GroupPaneBar
添加了一个ShowExpandCollapseButton
属性。将其设置为false
将从组中删除展开/折叠按钮。然后,可以通过单击组标题中的任意位置来展开或折叠它们。我还向属性示例添加了一个新的CheckBox
来测试此新功能。这基于 duffman071 的请求。
- 自动将插入到