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

SizerPanel 和 CaptionPanel

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (7投票s)

2009 年 12 月 20 日

CPOL

9分钟阅读

viewsIcon

37128

downloadIcon

2241

两个 WinForms 控件,用于开发紧凑且灵活的用户界面。

引言

在很多情况下,您需要在窗体中显示多个控件,例如编辑具有许多字段的数据记录。您希望允许用户根据显示内容的多少来调整这些编辑控件的大小。您可以使用 SplitContainer 来实现这一点,但 SplitContainer 只允许两个控件调整大小。要使多个编辑控件可调整大小,您可能需要使用许多 SplitContainer 并将它们嵌套在一起。但这会导致复杂的控件结构,并且 GUI 设计的更改会变得困难。

引言 II - SizerPanel / CaptionPanel - 图片和迷你网络讲座

一图胜千言……

SizerPanel.gif

该动画展示了一种非常紧凑的 GUI 可能性。一般的布局问题是:一些单行 TextBox 需要很长才能显示其全部内容,而另一些则可以短一些。因此,此布局将所有“单行”控件放在一个长行中,并插入换行符。一个更友好的选择是设置 FlowDirection.TopDown,以便文本框排列在多列中。但是,例如,“Straße”(街道)文本框所在的列必须非常宽,并且该列中的所有控件(不需要那么宽)也必须如此——您看:所示的是最紧凑的选项。

……而视频由成千上万张图片组成;)。

如果您想初步了解如何使用 SizerPanelCaptionPanel,请在阅读我冗长的解释之前,先观看我的YouTube 迷你网络讲座

SizerPanel

您可以将所有编辑控件放入 SizerPanel 中,每个控件都可以调整大小。根据您可以设置的某些属性,调整大小行为可能大不相同。

  • TopDown As Boolean
  • 简化了继承的 FlowDirection 属性SizerPanel 只支持 TopDown 或不设置(“不 TopDown”意味着:FlowDirection.LeftToRight)。控件既可以水平调整大小,也可以垂直调整大小。SizerPanel 可以水平和垂直布局。这是什么意思?如果您的流布局是 TopDown,并且您在水平方向上放大了一个控件,它会放大整个列。

  • WrapContents As Boolean
  • 如果为 True,则换行内容是典型的 FlowlayoutPanel 任务:包含的控件排列成行(如果 FlowDirection.TopDown 则排列成列),行末有一个换行符。下一个控件放置在新行的第一个位置。一直这样做,直到 FlowlayoutPanel 完全填充(如果继续添加,可能会出现滚动条)。但是当 WrapContents 设置为 False 时会发生什么?那么,只有一行包含所有控件,没有换行符。其他行原本的空间会怎么样?

    如果为 False:填充模式。WrapContents 关闭时,SizerPanel 会自动切换到填充模式,并将控件的高度调整为其 ClientSizeHeight

  • IsFillControl As Boolean
  • 上面的填充模式可以得到改进:您可以将其中一个包含的控件设置为“FillControl”。这意味着它将始终调整大小,以使 SizerPanel 完全填充。如果用户放大另一个控件,FillControl 会缩小,反之亦然。现在,SizerPanel 的行为类似于 SplitContainer,但具有更多面板。(参见动画中上方面板的行为。)“IsFillControl”属性实现为 ExtenderProvider 属性。因此,在属性网格中,此属性显示为包含控件的属性(尽管它在容器中实现)。

    FillControl 的一个临时修复功能:如果您在 MultiColumn 模式下设置了 FillControl,则该列会锚定到父控件的左侧和右侧。

  • AutoScroll As Boolean
  • 如果为 True:出现滚动条,当 SizerPanel 已填充并且您仍然添加控件或放大其中一个控件时。但是当 AutoScroll 关闭时会发生什么?添加的控件是否会放在可见范围之外,并且滚动条无法将其带回来?不!

    如果为 False:自动大小。SizerPanel 会简单地切换到 AutoSize 模式,并扩大自身以使新控件可访问。(参见动画中下方面板的行为。)

运行时调整大小

SizerPanel 的布局逻辑会考虑包含控件的 Margin 属性,而不是使用特殊的 SplitterControl。这意味着,在两个具有 Margin = 3 的控件之间,会出现 6 像素的间距。在该间距内,MouseMove 事件会到达下面的 SizerPanel,其 MouseMove 处理程序会检查附近是否有可调整大小的控件。如果有,则 Cursor 会设置为 Cursor.HSplit / .VSplit,以告知用户大小调整选项。

CaptionPanel

假设您有许多控件(TextBoxComboBoxDateTimePicker 等)来编辑数据记录的属性。那么,每个编辑控件都需要一个标题(caption),让用户知道它编辑的是哪个属性。通常,“标题任务”由 Label 完成,当您将数据源从 DataSource 窗口拖到 FormDetail 模式时,Designer 会相应地生成 Label 和编辑控件。不幸的是,在流式布局中,这没有用。因为换行符可能会出现在标题标签和其关联的编辑控件之间,标题会显示在 Panel 的右侧,而编辑控件在下一行的左侧。所以您看,有必要将标题和编辑控件放在同一个 Panel 上。CaptionPanel 的概念由此诞生。

但我添加的 CaptionPanel 比简单地将其显示为布局引擎的一个单元具有更多的功能:它可以从包含的控件推断出标题。我惊讶于这可以做得如此简单。

''' <summary>examine the childControl and propose a caption</summary>
Private Function InferCaption() As String
   If Me.Controls.Count = 0 Then Return AddColon(Name)
   With Controls(0)
      If .DataBindings.Count = 0 Then
         Return AddColon(.Name)
      Else
         Dim bnd = .DataBindings(0)
         Return AddColon(bnd.BindingMemberInfo.BindingMember)
      End If
   End With
End Function

'append/remove ':', depending wether the Caption is on the left side or on Top
Private Function AddColon(ByVal s As String) As String
   s = s.TrimEnd(":"c, " "c)
   Return If(_CaptionOnTop, s, s & ":")
End Function

如果可用,它会将数据绑定的属性作为标题。当然,您也可以手动设置标题。

出现问题

我的 CaptionPanel 方法很简单:继承自 Panel,通过所有者绘制(owner-drawing)显示标题,并将包含的编辑控件居中放置在剩余空间中。但是,对于不可调整大小的编辑控件怎么办?例如 MonthCalendar?或者一个 TextboxMultiline.Off——您只能水平调整它的大小,而不能垂直调整。一个智能的 CaptionPanel 必须从其包含的 EditingControl 推断其大小调整能力,因为当您放大一个 CaptionPanel 时,包含的 Combobox 却保持 Height=20,这看起来非常糟糕。

这是实现布局逻辑时会让你憎恨 WinForm.Control 类的一个任务:我没有找到可靠的方法来预测控件是否会接受大小更改。最终,您必须分配新大小,然后检查它是否被接受以及朝哪个方向!确实有一个 Control.GetPreferredSize 函数,但是——该死!——我无法弄清楚返回的 Size 的含义。例如:一个多行 TextBoxMinimumSize 为 {10; 0},当前大小为 {100; 15},放置在一个 Panel 上。调用 Textbox1.GetPreferredSize(New Size(50, 50)) 返回 {10; 20}!!有人能向我解释这个值的含义吗?

Control.AutoSize 属性在 Label 中可以看到,默认情况下为 True,因此默认情况下,您无法在 Designer 中调整 Label 的大小。但您仍然可以通过代码调整它们的大小!换句话说:Label.AutoSize 在运行时是谎言。

在实现布局逻辑时,我真正缺少的是一个可靠的 Control.CanSizeWidth/CanSizeHeight 属性。

但是,让我们停止谈论问题,开始谈论……

……解决方案

BoundsEx……

……是我的一种布局辅助“发明”。它封装了一些数据和代码来检查控件的大小调整能力。例如,它提供(在某种程度上)缺失的 CanSize 属性。这很有用,但没有那么有创意。我们如何使用 BoundsEx 属性来扩展给定的 Control?答案是:使用……

……ComponentProperty

ComponentProperty 继承自 Dictionary(Of Tcomponent, T),其中 T 是属性的数据类型。Dictionary.Item(key As Tcomponent) 的实现方式是:如果您尝试访问一个新条目,它会在缺失时生成一个新条目。在生成新条目时,键 IComponent.Disposed 事件会被一个处理程序订阅,该处理程序会删除该条目。因此,只有有效的 Components 会保留在该 Dictionary 中,无论您将哪个 Component 作为键传入——都会有一个对应的条目。

为了扩展 Control 类(当然,它实现了 IComponent)并为其添加 BoundsEx 属性,我实例化了一个全局可访问的 ComponentProperty(Of Control, BoundsEx),现在世界上的每个 Control 都关联着一个 BoundsEx 条目。为了将这种方法推向极致,我实现了一个扩展函数 Control.BoundsX() As BoundsEx,其效果是 BoundsEx 的访问代码看起来和感觉就像访问真实的 Control 属性。

Namespace System.ComponentModel

   Public Class ComponentProperty(Of Tcomp As IComponent, T)
      Inherits Dictionary(Of Tcomp, T)
      Private ReadOnly _Init As Func(Of Tcomp, T)

      Private ReadOnly cmp_Disposed As eventhandler = _
         Function(s, e) MyBase.Remove(DirectCast(s, Tcomp))

      Public Sub New(Optional ByVal init As Func(Of Tcomp, T) = Nothing)
         _Init = init
      End Sub

      Default Public Shadows Property Item(ByVal cmp As Tcomp) As T
         Get
            Dim ret As T = Nothing
            If Not MyBase.TryGetValue(cmp, ret) Then
               If _Init.NotNull Then ret = _Init(cmp)
               MyBase.Add(cmp, ret)
               AddHandler cmp.Disposed, cmp_Disposed
            End If
            Return ret
         End Get
         Set(ByVal value As T)
            If Not MyBase.ContainsKey(cmp) Then _
               AddHandler cmp.Disposed, cmp_Disposed
            MyBase.Item(cmp) = value
         End Set
      End Property

   End Class

End Namespace

Namespace System.Windows.Forms

   Public Module modBounds

      Private ReadOnly Boundses As _
         New ComponentProperty(Of Control, BoundsEx) _
         (Function(ctl) New BoundsEx(ctl, True))

      <System.Runtime.CompilerServices.Extension()> _
      Public Function BoundsX(ByVal ctl As Control) As BoundsEx
         Return Boundses(ctl)
      End Function

      Public Class BoundsEx
         '...
      End Class

   End Module

End Namespace

这是我们访问 BoundsEx 的方式

Dim bnd = TextBox1.BoundsX

向量

您是否听说过“DRY”(“Don't Repeat Yourself”)这个简洁代码原则?我真的不希望我不得不创建布局代码来正确对齐一行中的所有控件,然后我必须再次编写完全相同的代码来支持 FlowDirection.TopDown。而且,我必须将所有 Size.Width 改为 Size.Height,所有 Height 改为 Width,所有 X 改为 Y,所有 Top 改为 Left,所有 Vertical 改为 Horizontal………………

所以我发明了 Vector 结构。它的属性是 XY(哇!)。它们可以通过索引访问。Vector(0) 表示 XVector(1) 访问 Y。我添加了一些运算符,特别是大量的扩展 CType() 运算符,以便轻松地将 VectorSize 以及与 Point 进行转换。

甚至 Rectangle 也可以由一个 Vector 来建模,即由两个 Vector:一个用于 LocationVector 和一个用于 SizeVector

现在我使用 Vector 编写布局代码,使用索引 0 或 1。为了使其在 FlowDirection.TopDown 中工作,我只需交换索引,任务就完成了。

将水平作为索引 0,垂直作为索引 1 非常方便。例如,我可以简单地将我的(上面提到的)Control.CanSize 属性公开为一个布尔数组,CanSize(0) 表示水平可调大小,CanSize(1) 表示垂直可调大小。此外,FlowDirection 枚举成员也完美地契合此概念(FlowDirection.LeftToRight = 0, .TopDown = 1)。

我希望您已经理解了我 SizerPanel 布局算法的总体方法。

Private Sub LayoutWrapping(ByVal bnds As List(Of BoundsEx))
   Dim iFlow = MyBase.FlowDirection
   Dim notFlow = iFlow Xor 1
   Dim ubound = bnds.Count - 1
   Dim curr As Vector = Padding.Vector1 + AutoScrollPosition
   Dim available As Vector = ClientSize - Padding.Vector2
   Dim ground = curr(iFlow)
   Dim iLineStart As Integer = 0
   Dim max As Integer = 0
   For iBnd = 0 To ubound
      Dim bnd = bnds(iBnd)
      If curr(iFlow) + bnd.Fullsize(iFlow) > available(iFlow) Then
         AssignLineHeight(iLineStart, iBnd - 1, bnds, max)
         curr(iFlow) = ground
         curr(notFlow) += max
         iLineStart = iBnd
         max = 0
      End If
      max.Maximize(bnd.Fullsize(notFlow))
      bnd.TopLeft = curr
      curr(iFlow) += bnd.Fullsize(iFlow)
   Next
   AssignLineHeight(iLineStart, ubound, bnds, bndSizing, max)
End Sub

Padding.Vector1Padding.Left/.Top 构建,Padding.Vector2Padding.Right/.Bottom 构建。AssignLineHeight 的前三个参数指定属于一行 Controls 的 BoundsEx,而 max 指定 notFlow Direction 中所需的控件大小(如果 FlowDirection.LeftToRightmax 指定所需的 Height)。

我准备好了

如果您尝试使用我的控件,我祝您玩得开心,如果您发现错误或有(不太复杂的!)改进建议,请告知我。

历史

  • 2009-12-28:首次发布。
© . All rights reserved.