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

向四个方向扩展的面板

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (39投票s)

2006 年 8 月 3 日

9分钟阅读

viewsIcon

227594

downloadIcon

8456

一个可扩展面板,您可以将其设置为从下到上、从上到下、从左到右或从右到左展开/折叠。

Sample Image - maximum width is 600 pixels

引言

我需要一个可以在 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 - 创建负责展开/折叠动画的后台工作线程的类。

Class diagram (main classes available)

默认情况下,每当引发 Paint 事件(发送 WM_PAINT 消息)时,控件会直接绘制到 Graphics 对象(熟悉 Win32 的人称之为设备上下文)。如果此过程重复执行,就会出现烦人的闪烁效果。双缓冲技术在 Windows 编程开发人员中很知名,它有助于我们减少闪烁。Microsoft 的人让 .NET 中的双缓冲使用起来非常简单,您只需为您的控件设置一些标志即可。对于大多数场景,这都能奏效,但在某些情况下需要手动控制该过程。要为此控件启用此功能,只需将以下样式设置为 true:ControlStyles.AllPaintingInWmPaintControlStyles.UserPaintControlStyles.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 设置为底部/右侧,则折叠控件将无法实现。
© . All rights reserved.