我看到你已经得到了关于 WPF 中的一切是多么容易以及 WinForms 多么糟糕的 HighCore 处理。但是您可能有兴趣知道您也可以在 WinForms 中执行此操作。你只是做的有点不同。WinForms 和 WPF 中的标准设计习惯不同也就不足为奇了。这并不能证明一个比另一个“更好”是合理的,它只是意味着你需要学习如何使用你正在使用的那个。(虽然,诚然,使用 20 年前由 Windows 本身发明的 UI 框架来实现一些更花哨的东西有点困难。它确实具有相当出色的功能。)
格式化信息有两种基本方法:单行上的所有内容(我相信这是您在问题中所要求的)或两行上的信息片段,其中每个项目基本上是一个两行单元(这是什么HighCore 的 WPF 解决方案演示)。
单行格式
简单的方法
我们将首先考虑将所有内容放在一行上,这真的很简单。您不需要用于分隔的列,当您将项目添加到组合框中时,您可以使用某种独特的分隔符,例如问题中使用的垂直管道 ( |
) 或破折号 ( -
)。
这很好用,因为该ComboBox.Items.Add
方法接受一个 type 的参数Object
,它只是调用它ToString
来获取控件中显示的值。如果你给它传递一个字符串,它会显示那个字符串。
myComboBox.BeginUpdate()
For Each record In myRecordSet
myComboBox.Items.Add(String.Format("{0} | {1}", record.UniqueID, record.Name))
' or even...
myComboBox.Items.Add(String.Format("{0} ({1})", record.UniqueID, record.Name))
Next record
myComboBox.EndUpdate()
或者
通过 OOP 进行增量改进
您甚至可以将自定义类传递给Add
跟踪唯一 ID 和名称属性(以及您想要的任何其他内容)的方法并覆盖该ToString
方法以用于显示目的。
Public Class Record
Public Property UniqueID As Long ' maybe this should be a string too
Public Property Name As String
Public Overrides Function ToString() As String
' Generate the string that will be displayed in the combobox for this
' record, just like we did above when adding it directly to the combobox,
' except that in this case, it will be dynamically generated on the fly,
' allowing you to also track state information along with each item.
Return String.Format("{0} | {1}", Me.UniqueID, Me.Name)
End Function
End Class
' ...
' (somewhere else, when you add the items to the combobox:)
myComboBox.BeginUpdate()
For Each r In myRecordSet
' Create a Record object representing this item, and set its properties.
Dim newRecord As New Record
newRecord.UniqueID = r.UniqueID
newRecord.Name = r.Name
' ...etc.
' Then, add that object to the combobox.
myComboBox.Items.Add(newRecord)
Next r
myComboBox.EndUpdate()
修复锯齿
当然,如果每组中的第一项可以是可变长度的并且您使用的是可变宽度字体(即,除了代码编辑器之外,不是像地球上的每个 UI 那样等宽字体),分隔符不会对齐起来,你不会得到两个格式很好的列。相反,它看起来混乱而丑陋。
如果 ComboBox 控件支持的制表符可以自动为我们处理所有内容,那就太好了,但不幸的是它没有。遗憾的是,这是对底层 Win32 控件的硬限制。
修复这个不规则的边缘问题是可能的,但它确实有点复杂。它需要接管组合框中项目的绘制,称为“所有者绘制”。
为此,您将其DrawMode
属性设置为OwnerDrawFixed
并处理DrawItem
事件以手动绘制文本。您将使用该TextRenderer.DrawText
方法来绘制标题字符串(因为它与 WinForms 内部使用的匹配;避免使用Graphics.DrawString
),并TextRenderer.MeasureText
在必要时获得正确的间距。绘图代码可以(并且应该)使用DrawItemEventArgs
传递的 as提供的所有默认属性e
。您不需要OwnerDrawVariable
模式或处理MeasureItem
事件,因为在这种情况下每个项目的宽度和高度不能变化。
只是为了给你一个想法,这是一个快速而肮脏的实现,它只是将下拉列表垂直分成两半:
Private Sub myComboBox_DrawItem(sender As Object, e As DrawItemEventArgs) Handles myComboBox.DrawItem
' Fill the background.
e.DrawBackground()
' Extract the Record object corresponding to the combobox item to be drawn.
Dim record As Record = DirectCast(myComboBox.Items(e.Index), Record)
Dim id As String = record.UniqueID.ToString()
Dim name As String = record.Name
' Calculate important positions based on the area of the drop-down box.
Dim xLeft As Integer = e.Bounds.Location.X
Dim xRight As Integer = xLeft + e.Bounds.Width
Dim xMid As Integer = (xRight - xLeft) / 2
Dim yTop As Integer = e.Bounds.Location.Y
Dim yBottom As Integer = yTop + e.Bounds.Height
' Draw the first (Unique ID) string in the first half.
TextRenderer.DrawText(e.Graphics, id, e.Font, New Point(xLeft, yTop), e.ForeColor)
' Draw the column separator line right down the middle.
e.Graphics.DrawLine(SystemPens.ButtonFace, xMid, yTop, xMid, yBottom)
' Draw the second (Name) string in the second half, adding a bit of padding.
TextRenderer.DrawText(e.Graphics, name, e.Font, New Point(xMid + 5, yTop), e.ForeColor, TextFormatFlags.Left)
' Finally, draw the focus rectangle.
e.DrawFocusRectangle()
End Sub
现在,这看起来很不错。您当然可以改进DrawItem
事件处理程序方法使用的技术,但只要组合框的大小适合它将显示的值,它就可以很好地工作。
多行格式
定义自定义组合框类
第二种方法,其中每个项目是一个双行组,如 HighCore 的 WPF 示例,最好通过继承内置的 ComboBox 控件并完全控制其绘图例程来完成。但这没什么好害怕的,子类化控件是一种标准的 WinForms 习惯用法,用于获得对 UI 的额外控制。(当然,您可以通过像我上面所做的那样处理事件来实现所有这些,但我认为子类化是一种更简洁的方法,并且如果您希望拥有多个行为相似的组合框,也可以促进重用。)
同样,您不需要,OwnerDrawVariable
因为项目的高度不会改变。您将始终有两条线,因此固定高度可以正常工作。您只需要确保将ItemHeight
属性设置为其正常值的两倍,因为您将有两行。您可以使用 以复杂的方式执行此操作TextRenderer.MeasureText
,也可以通过将默认值乘以 2 来轻松执行此操作。我在此演示中选择了后者。
将此类添加到您的项目中,然后使用MultiLineComboBox
控件而不是内置的System.Windows.Forms.ComboBox
. 所有的属性和方法都是一样的。
Public Class MultiLineComboBox : Inherits ComboBox
Public Sub New()
' Call the base class.
MyBase.New()
' Typing a value into this combobox won't make sense, so make it impossible.
Me.DropDownStyle = ComboBoxStyle.DropDownList
' Set the height of each item to be twice its normal value
' (because we have two lines instead of one).
Me.ItemHeight *= 2
End Sub
Protected Overrides Sub OnDrawItem(e As DrawItemEventArgs)
' Call the base class.
MyBase.OnDrawItem(e)
' Fill the background.
e.DrawBackground()
' Extract the Record object corresponding to the combobox item to be drawn.
If (e.Index >= 0) Then
Dim record As Record = DirectCast(Me.Items(e.Index), Record)
' Format the item's caption string.
Dim caption As String = String.Format("ID: {0}{1}Name: {2}", record.UniqueID.ToString(), Environment.NewLine, record.Name)
' And then draw that string, left-aligned and vertically centered.
TextRenderer.DrawText(e.Graphics, caption, e.Font, e.Bounds, e.ForeColor, TextFormatFlags.Left Or TextFormatFlags.VerticalCenter)
End If
' Finally, draw the focus rectangle.
e.DrawFocusRectangle()
End Sub
End Class
添加幻想和繁荣
我们现在得到的还不错,但是通过在 中的绘图代码上花费更多的精力OnDrawItem
,我们可以添加一些额外的视觉幻想和华丽。
例如,如果没有选择矩形,就很难判断它们实际上是两行单元。这对于组合框控件来说是不寻常的,因此出于可用性原因,您的应用程序应该竭尽全力使这一点非常清晰。我们可以这样做的一种方法是缩进第二行。你还记得我说过内置的组合框控件不支持选项卡吗?好吧,这不再适用,因为我们现在正在自己绘制。我们可以通过在第二行的开头添加一些额外的填充来模拟选项卡。
如果您希望将标签(“ID:”和“名称:”)与实际值分开,您也可以这样做。也许你会让标签加粗并淡化文本颜色。
因此,您会看到,只需使用绘图代码,您就可以创建几乎任何您想要的效果。我们拥有完全的控制权,并且通过将其全部封装在一个MultiLineComboBox
可以在所有地方重用的类中,您的其余代码甚至不必知道发生了任何特殊情况。很酷,对吧?
避免所有的工作
最后,如果我没有指出您可以跳过所有这些工作并选择已经编写的各种自定义多行组合框控件,那将是我的疏忽。
这个很漂亮。当您单击组合框上的下拉箭头时,它实际上只是显示一个 ListView 控件。这样做,您可以免费获得 ListView 控件的所有格式设置细节,但它的行为就像一个常规的 ComboBox。