SizerPanel 和 CaptionPanel






4.44/5 (7投票s)
两个 WinForms 控件,用于开发紧凑且灵活的用户界面。
引言
在很多情况下,您需要在窗体中显示多个控件,例如编辑具有许多字段的数据记录。您希望允许用户根据显示内容的多少来调整这些编辑控件的大小。您可以使用 SplitContainer
来实现这一点,但 SplitContainer
只允许两个控件调整大小。要使多个编辑控件可调整大小,您可能需要使用许多 SplitContainer
并将它们嵌套在一起。但这会导致复杂的控件结构,并且 GUI 设计的更改会变得困难。
引言 II - SizerPanel / CaptionPanel - 图片和迷你网络讲座
该动画展示了一种非常紧凑的 GUI 可能性。一般的布局问题是:一些单行 TextBox
需要很长才能显示其全部内容,而另一些则可以短一些。因此,此布局将所有“单行”控件放在一个长行中,并插入换行符。一个更友好的选择是设置 FlowDirection.TopDown
,以便文本框排列在多列中。但是,例如,“Straße”(街道)文本框所在的列必须非常宽,并且该列中的所有控件(不需要那么宽)也必须如此——您看:所示的是最紧凑的选项。
……而视频由成千上万张图片组成;)。
如果您想初步了解如何使用 SizerPanel
和 CaptionPanel
,请在阅读我冗长的解释之前,先观看我的YouTube 迷你网络讲座。
SizerPanel
您可以将所有编辑控件放入 SizerPanel
中,每个控件都可以调整大小。根据您可以设置的某些属性,调整大小行为可能大不相同。
TopDown As Boolean
WrapContents As Boolean
IsFillControl As Boolean
AutoScroll As Boolean
简化了继承的 FlowDirection
属性。SizerPanel
只支持 TopDown
或不设置(“不 TopDown
”意味着:FlowDirection.LeftToRight
)。控件既可以水平调整大小,也可以垂直调整大小。SizerPanel
可以水平和垂直布局。这是什么意思?如果您的流布局是 TopDown
,并且您在水平方向上放大了一个控件,它会放大整个列。
如果为 True
,则换行内容是典型的 FlowlayoutPanel
任务:包含的控件排列成行(如果 FlowDirection.TopDown
则排列成列),行末有一个换行符。下一个控件放置在新行的第一个位置。一直这样做,直到 FlowlayoutPanel
完全填充(如果继续添加,可能会出现滚动条)。但是当 WrapContents
设置为 False
时会发生什么?那么,只有一行包含所有控件,没有换行符。其他行原本的空间会怎么样?
如果为 False
:填充模式。当 WrapContents
关闭时,SizerPanel
会自动切换到填充模式,并将控件的高度调整为其 ClientSize
的 Height
。
上面的填充模式可以得到改进:您可以将其中一个包含的控件设置为“FillControl
”。这意味着它将始终调整大小,以使 SizerPanel
完全填充。如果用户放大另一个控件,FillControl
会缩小,反之亦然。现在,SizerPanel
的行为类似于 SplitContainer
,但具有更多面板。(参见动画中上方面板的行为。)“IsFillControl
”属性实现为 ExtenderProvider
属性。因此,在属性网格中,此属性显示为包含控件的属性(尽管它在容器中实现)。
对 FillControl
的一个临时修复功能:如果您在 MultiColumn
模式下设置了 FillControl
,则该列会锚定到父控件的左侧和右侧。
如果为 True
:出现滚动条,当 SizerPanel
已填充并且您仍然添加控件或放大其中一个控件时。但是当 AutoScroll
关闭时会发生什么?添加的控件是否会放在可见范围之外,并且滚动条无法将其带回来?不!
如果为 False
:自动大小。SizerPanel
会简单地切换到 AutoSize
模式,并扩大自身以使新控件可访问。(参见动画中下方面板的行为。)
运行时调整大小
SizerPanel
的布局逻辑会考虑包含控件的 Margin
属性,而不是使用特殊的 SplitterControl
。这意味着,在两个具有 Margin = 3
的控件之间,会出现 6 像素的间距。在该间距内,MouseMove
事件会到达下面的 SizerPanel
,其 MouseMove
处理程序会检查附近是否有可调整大小的控件。如果有,则 Cursor
会设置为 Cursor.HSplit
/ .VSplit
,以告知用户大小调整选项。
CaptionPanel
假设您有许多控件(TextBox
、ComboBox
、DateTimePicker
等)来编辑数据记录的属性。那么,每个编辑控件都需要一个标题(caption),让用户知道它编辑的是哪个属性。通常,“标题任务”由 Label
完成,当您将数据源从 DataSource 窗口拖到 Form
的 Detail
模式时,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
?或者一个 Textbox
且 Multiline.Off
——您只能水平调整它的大小,而不能垂直调整。一个智能的 CaptionPanel
必须从其包含的 EditingControl
推断其大小调整能力,因为当您放大一个 CaptionPanel
时,包含的 Combobox
却保持 Height=20
,这看起来非常糟糕。
这是实现布局逻辑时会让你憎恨 WinForm.Control
类的一个任务:我没有找到可靠的方法来预测控件是否会接受大小更改。最终,您必须分配新大小,然后检查它是否被接受以及朝哪个方向!确实有一个 Control.GetPreferredSize
函数,但是——该死!——我无法弄清楚返回的 Size
的含义。例如:一个多行 TextBox
,MinimumSize
为 {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
事件会被一个处理程序订阅,该处理程序会删除该条目。因此,只有有效的 Component
s 会保留在该 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
结构。它的属性是 X
和 Y
(哇!)。它们可以通过索引访问。Vector(0)
表示 X
,Vector(1)
访问 Y
。我添加了一些运算符,特别是大量的扩展 CType()
运算符,以便轻松地将 Vector
与 Size
以及与 Point
进行转换。
甚至 Rectangle
也可以由一个 Vector
来建模,即由两个 Vector
:一个用于 Location
的 Vector
和一个用于 Size
的 Vector
。
现在我使用 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.Vector1
由 Padding.Left
/.Top
构建,Padding.Vector2
由 Padding.Right
/.Bottom
构建。AssignLineHeight
的前三个参数指定属于一行 Control
s 的 BoundsEx
,而 max
指定 notFlow
Direction
中所需的控件大小(如果 FlowDirection.LeftToRight
,max
指定所需的 Height
)。
我准备好了
如果您尝试使用我的控件,我祝您玩得开心,如果您发现错误或有(不太复杂的!)改进建议,请告知我。
历史
- 2009-12-28:首次发布。