向四个方向扩展的面板






4.70/5 (39投票s)
2006 年 8 月 3 日
9分钟阅读

227594

8456
一个可扩展面板,您可以将其设置为从下到上、从上到下、从左到右或从右到左展开/折叠。
引言
我需要一个可以在 X 和 Y 轴上展开/折叠的面板,但找不到可以实现 Y 轴的,于是决定自己写一个。这个控件的主要思想是继承 Panel 控件,并将一个“标题”控件停靠在左侧、右侧、上方或下方。
使用代码
要使用此控件,请在设计模式下转到工具箱窗口,选择要包含该控件的类别,右键单击它并选择“选择项...”,然后找到 ExtendedPanel.dll 文件。之后,您应该可以在您的窗体上看到 ExpandedPanel
作为可拖放的项之一。
控件属性
该控件提供以下属性:
State
- 返回对象的状态,可以是 collapsed(折叠)、collapsing(折叠中)、expanded(展开)或 expanded(展开中)Animation
- 指示此控件是否应动画化折叠/展开过程BorderColor
- 用于绘制边框线的颜色CornerStyle
- 边框的样式,可以是圆角或普通Moveable
- 指定通过鼠标拖动标题栏是否可以移动此控件CornerStyle
- 设置边框是普通角还是圆角AnimationStep
- 指定用于展开/折叠控件的大小CaptionAlign
- 设置此面板的标题控件的位置。可以是 left(左)、right(右)、up(上)或 down(下)CaptionBrush
- 设置标题栏的绘制方式。可以是 solid(纯色)或 gradient(渐变)CaptionColorOne
- 如果选择渐变选项,则为起始颜色;如果是纯色画笔样式,则为颜色CaptionColorTwo
- 如果选择渐变选项,则为起始颜色;如果是纯色画笔样式,则为颜色CaptionFont
- 绘制标题时使用的字体CaptionText
- 标题控件中显示的文本CaptionTextColor
- 绘制标题文本时使用的颜色CaptionSize
- 以像素为单位设置标题栏的大小CaptionImage
- 显示在标题栏中的图标
架构
此程序集中定义的主要类如下所示:BufferPaintingCtrl
- 继承自Panel
控件,增加了双缓冲支持。稍后会详细介绍。DirectionCtrl
- 定义显示箭头指示面板展开或折叠方向的控件。CornerCtrl
- 定义支持绘制普通角或圆角边框的控件。CaptionCtrl
- 定义面板的标题控件。ExpandedPanel
- 支持折叠/展开以及拖动的扩展面板。CollapseAnimation
- 创建负责展开/折叠动画的后台工作线程的类。
默认情况下,每当引发 Paint 事件(发送 WM_PAINT
消息)时,控件会直接绘制到 Graphics
对象(熟悉 Win32 的人称之为设备上下文)。如果此过程重复执行,就会出现烦人的闪烁效果。双缓冲技术在 Windows 编程开发人员中很知名,它有助于我们减少闪烁。Microsoft 的人让 .NET 中的双缓冲使用起来非常简单,您只需为您的控件设置一些标志即可。对于大多数场景,这都能奏效,但在某些情况下需要手动控制该过程。要为此控件启用此功能,只需将以下样式设置为 true:ControlStyles.AllPaintingInWmPaint
、ControlStyles.UserPaint
、ControlStyles.DoubleBuffer
。
完成此操作后,绘图过程会稍有不同。Paint 处理程序接收的不是屏幕的 Graphics
对象(设备上下文),而是用于内存位图的另一个 Graphics
对象。因此,控件会绘制到隐藏的图像中。绘图完成后,该图像会被复制到屏幕设备上下文中。由于只对屏幕图形执行一次图形操作,因此可以减少闪烁效果,甚至消除它。
因此,BufferPaintingCtrl
类背后的思想是在其构造函数中设置这些标志,因为程序集中定义的一些类会处理 Paint 事件。
protected BufferPaintingCtrl()
{
///set up the control styles so that it support double buffering painting
this.SetStyle( ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.DoubleBuffer,true);
UpdateStyles();
}
CornerCtrl
CornerCtrl
旨在支持绘制普通角或圆角边框。该控件有一个在 Paint 处理程序中使用的图形路径对象。下面的列表展示了实例化图形路径对象的方法;cornerSquare
用于定义包含椭圆的区域,该椭圆的弧线将被绘制,如果选择了圆角。此成员必须在子类中定义以控制圆角,否则我们将无法获得任何圆角。protected virtual void InitializeGraphicPath()
{
if (null != graphicPath)
{
graphicPath.Dispose();
graphicPath = null;
}
graphicPath = new GraphicsPath();
switch (cornerStyle)
{
case CornerStyle.Rounded:
graphicPath.AddArc(0, 0, cornerSquare, cornerSquare, 180, 90);
graphicPath.AddLine(cornerSquare - cornerSquare / 2, 0,
Width - cornerSquare + cornerSquare / 2 - 1, 0);
graphicPath.AddArc(Width - cornerSquare - 1, 0, cornerSquare,
cornerSquare, -90, 90);
graphicPath.AddLine(Width - 1, cornerSquare - cornerSquare / 2,
Width - 1, Height - cornerSquare + cornerSquare / 2);
graphicPath.AddArc(Width - cornerSquare - 1,
Height - 1 - cornerSquare, cornerSquare, cornerSquare, 0, 90);
graphicPath.AddLine(cornerSquare - cornerSquare / 2, Height - 1,
Width - cornerSquare + cornerSquare / 2, Height - 1);
graphicPath.AddArc(0, Height - cornerSquare - 1, cornerSquare,
cornerSquare, 90, 90);
graphicPath.AddLine(0, cornerSquare - cornerSquare / 2, 0,
Height - cornerSquare + cornerSquare / 2);
break;
case CornerStyle.Normal:
graphicPath.AddLine(0, 0, Width-1, 0);
graphicPath.AddLine(Width-1, 0, Width-1, Height-1);
graphicPath.AddLine(Width-1, Height-1, 0, Height-1);
graphicPath.AddLine(0, Height-1, 0, 0);
break;
default:
throw
new ApplicationException("Unrecognized style for rendering the corners");
break;
}
}
ExpandedPanel
如 UML 类图所示,此类嵌入了一个 CaptionCtrl
对象。根据选项,它将被停靠在四个方向中的一个:左、下、右或上。在所有组件初始化(InitializeComponent
方法)后,代码会设置两个事件处理程序。第一个用于处理用户单击方向控件时(导致面板折叠/展开)引发的事件,第二个用于处理拖动(如果启用了此选项)。这是我所说代码的快照:
//set handler for collapsing/expanding
captionCtrl.SetStyleChangedHandler(new
DirectionCtrlStyleChangedEvent(CollapsingHandler));
//set the handler for the dragging event
captionCtrl.Dragging += new CaptionDraggingEvent(CaptionDraggingEvent);
我不会谈论拖动部分,因为代码很直观。但我会专注于折叠/展开部分。如前所述,标题栏中的方向控件定义了单击事件的处理器。捕获到事件后,控件的样式会发生更改(基本上指向与单击前相反的方向),并引发事件,导致 ExtendedPanel
对象展开/折叠。面板对象将在其处理程序中准备动画所需的所有上下文,将实例化 CollapseAnimation
对象(如果需要),设置其属性,并启动后台线程以执行动画所需的后台步骤(我不会列出此方法,但您可以在源代码中找到它)。我们现在来处理编写此代码中最具挑战性的部分。在此方法中的几行代码中,您会找到对 ChangeCaptionParent
的调用。当标题被设置为停靠在底部或右侧时,就会出现这种情况。需要特别注意,我稍后会解释原因。您知道,任何控件都有一个渲染起点(位置)和一个大小(宽度和高度)。所有容器都相对于其左上角定义了子控件的位置。通过更改容器的宽度/高度,所有位置大于新尺寸的子控件都会变得不可见。在我提到的两种情况下也是如此;我们必须将标题控件移回到它应该在的位置,方法是更新其位置。这样做会引发 WM_PAINT
消息。更改面板大小也会为此引发绘图事件,并且我们会遇到标题栏可见与不可见之间非常快速地切换的情况,导致非常烦人的闪烁效果(我们不希望这样)。因此,我解决这个问题的办法是在动画期间暂时移除面板标题,但从用户的角度来看,将其保留在“相同”位置。因此,在动画期间,标题控件的父容器将与面板的父容器相同。
private void ChangeCaptionParent()
{
//take the caption out of the panel beacause of the flickering
this.captionCtrl.Parent = this.Parent;
this.captionCtrl.Location = new Point(this.Location.X + this.Width -
this.captionCtrl.Width, this.Location.Y + this.Height -
this.captionCtrl.Height);
Win32Wrapper.SetWindowPos(this.Handle, this.captionCtrl.Handle,
0, 0, 0, 0,
Win32Wrapper.FlagsSetWindowPos.SWP_NOMOVE |
Win32Wrapper.FlagsSetWindowPos.SWP_NOSIZE |
Win32Wrapper.FlagsSetWindowPos.SWP_NOREDRAW);
//disable moving
backupMoveable = moveable;
moveable = false;
}
在动画的每一步,面板都会收到更新其大小的通知,并在上述情况下更新其位置。动画的结束也需要特殊处理,因为在停靠在底部或右侧的情况下,必须将标题控件移回面板控件中。
private void OnNotifyAnimationFinished(object sender)
{
if (captionAlign == DirectionStyle.Down)
{
//set caption location (no redrawing) and hiding
Win32Wrapper.SetWindowPos(this.captionCtrl.Handle, IntPtr.Zero, 0,
this.Height - this.captionCtrl.Height, 0, 0,
Win32Wrapper.FlagsSetWindowPos.SWP_NOREDRAW |
Win32Wrapper.FlagsSetWindowPos.SWP_NOZORDER |
Win32Wrapper.FlagsSetWindowPos.SWP_NOSIZE |
Win32Wrapper.FlagsSetWindowPos.SWP_HIDEWINDOW );
//set back the parent
this.captionCtrl.Parent = this;
this.captionCtrl.Visible = true;
//set back the moveable property; during collapsing the movement
//is not allowed
moveable = backupMoveable;
}
else
{
if (captionAlign == DirectionStyle.Right)
{
//set caption location (no redrawing) and hiding
Win32Wrapper.SetWindowPos(this.captionCtrl.Handle, IntPtr.Zero,
this.Width - this.captionCtrl.Width, 0, 0, 0,
Win32Wrapper.FlagsSetWindowPos.SWP_NOREDRAW |
Win32Wrapper.FlagsSetWindowPos.SWP_NOZORDER |
Win32Wrapper.FlagsSetWindowPos.SWP_NOSIZE |
Win32Wrapper.FlagsSetWindowPos.SWP_HIDEWINDOW);
//set back the parent
this.captionCtrl.Parent = this;
this.captionCtrl.Visible = true;
//set back the moveable property; during collapsing the movement
//is not allowed
moveable = backupMoveable;
}
}
//set the state of the object expanded/collapsed
SetState();
}
将标题加回原位也有些棘手。我必须执行两个操作:将面板设置为其父容器,并更新其位置(当前在面板父容器内的位置很可能与我们需要的不同)。无论哪种方式,先设置父容器再更新位置,或者反之,都不是可靠的解决方案。如果我先设置父容器,由于实际的位置坐标,标题会在面板控件内移动(可能变得不可见),只有更新位置才能将其移回原位。最终会导致不希望的闪烁效果。如果我先设置与面板控件相关的(我们希望标题在底部或右侧)位置,则会引发 Paint 事件,导致控件在屏幕上的其他位置绘制。只有将标题的父容器再次设置为面板,才能恢复正常。但这也不是用户能接受的解决方案。因此,Win32 派上用场了。Windows API 的 SetWindowsPos
方法为我们提供了在不引发重绘消息的情况下设置新位置的选项。使用此方法可以避免标题出现在屏幕的其他位置。我现在可以安全地将父容器再次设置为面板,并且由于我之前隐藏了它,可以将其重新设置为可见。我知道我不能总是解释得很清楚(幸好我不是老师),但我希望您能理解我的意思。
结论
希望这个控件能对您有所帮助。我确信还有改进的空间,因此欢迎任何反馈。
历史
- 2006 年 7 月 - 版本 1.0.0
- 首次发布
- 2006 年 8 月 - 版本 1.2.0
- Bug 修复 - 使控件线程安全
- Bug 修复 - 在折叠模式下,有时可以单击标题栏将控件带到最前面
- 2006 年 8 月 - 版本 1.3.0
- Bug 修复 - 当控件的宽度/高度设置为零时出现错误
- 更改 -
CaptionPercent
已变为CaptionSize
,并且不再根据停靠方向是控件宽度/高度的百分比。 - 新增 -
Collapse/Expand
方法,无需再单击鼠标即可引发这些事件。 - 新增 - 停靠校正。更改标题栏大小有时会覆盖包含的控件。因此,每当更改标题栏大小时,内部控件都会相应移动。
- 新增 -
State
属性仅支持设计时访问器。将其设置为折叠时,控件将首先显示为折叠状态。
- 2006 年 10 月 - 版本 1.4.0
- Bug 修复 - 选择折叠方法时不会更新“>>”控件,导致控件卡住。
- Bug 修复 - 如果将标题设置为向下/向右停靠,并且面板的 Anchor 设置为底部/右侧,则折叠控件将无法实现。