在 ComboBox 中嵌入 DataGridView
一种将任何 DataGridView 放入 ComboBox 的简单方法。
引言
多列组合框在 WinForms 应用程序中非常常见。然而,目前还没有开源解决方案能够完全支持数据绑定并且像 DataGridView
控件那样可定制。本文的目的是展示如何用相对少量代码轻松创建一个。
满足需求最明显、最直接、最简单的方法是将 DataGridView
托管在 ComboBox
中。这似乎是一项不简单的任务,但实际上(至少在完成之后)它非常容易实现。
本文及提供的源代码更多是“概念验证”类型,而非成品控件。有许多细节并非完全“技术性”或美观。另一方面,我在一个开源程序中使用它(DevExpress 不适用),对我来说效果很好。
这也是我第一篇编程文章,请原谅我的糟糕风格
背景
本文基于以下文章中的创意
- 灵活的 ComboBox 和 EditingControl - 利用
ToolStripControlHost
。 - MSDN 文章如何:在 Windows Forms DataGridView 单元格中托管控件 - 创建自定义
DataGridViewColumn
。
ToolStripControlHost
,然后用它来创建一个自定义 ComboBox
,再用它来创建一个 IDataGridViewEditingControl
、一个自定义 DataGridViewCell
和一个自定义 DataGridViewColumn
。使用代码
使用提供的自定义 AccGridComboBox
和 DataGridViewAccGridComboBoxColumn
类就像使用 ComboBox
和 DataGridViewColumn
本身一样简单。
您只需要像添加 ComboBox
或 DataGridViewColumn
一样,将 AccGridComboBox
或 DataGridViewAccGridComboBoxColumn
添加到窗体中,并为其指定一个相应的 DataGridView
来代替数据源。
' for columns
DataGridViewAccGridComboBoxColumn1.ComboDataGridView = ProgramaticalyCreatedDataGridView
' selection is done by single click, i.e. not double click
DataGridViewAccGridComboBoxColumn1.CloseOnSingleClick = True
' binding is trigered on value change, i.e. not on validating
DataGridViewAccGridComboBoxColumn1.InstantBinding = True
' for comboboxes (second param is CloseOnSingleClick property setter)
AccGridComboBox1.AddDataGridView(ProgramaticalyCreatedDataGridView, True)
AccGridComboBox1.InstantBinding = True
一个快速、自明的示例,演示如何以编程方式创建 DataGridView
:
Public Function CreateDataGridViewForPersonInfo(ByVal TargetForm As Form, _
ByVal ListBindingSource As BindingSource) As DataGridView
' create the resulting grid and it's columns
Dim result As New DataGridView
Dim DataGridViewTextBoxColumn1 As New System.Windows.Forms.DataGridViewTextBoxColumn
Dim DataGridViewTextBoxColumn2 As New System.Windows.Forms.DataGridViewTextBoxColumn
' begin initialization (to minimize events)
CType(result, System.ComponentModel.ISupportInitialize).BeginInit()
' setup grid properties as you need
result.AllowUserToAddRows = False
result.AllowUserToDeleteRows = False
result.AutoGenerateColumns = False
result.AllowUserToResizeRows = False
result.ColumnHeadersVisible = False
result.RowHeadersVisible = False
result.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells
result.ReadOnly = True
result.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect
result.Size = New System.Drawing.Size(300, 220)
result.AutoSize = False
' add datasource
result.DataSource = ListBindingSource
' add columns
result.Columns.AddRange(New System.Windows.Forms.DataGridViewColumn() _
{DataGridViewTextBoxColumn1, DataGridViewTextBoxColumn2})
' setup columns as you need
DataGridViewTextBoxColumn1.AutoSizeMode = _
System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill
DataGridViewTextBoxColumn1.DataPropertyName = "Name"
DataGridViewTextBoxColumn1.HeaderText = "Name"
DataGridViewTextBoxColumn1.Name = ""
DataGridViewTextBoxColumn1.ReadOnly = True
DataGridViewTextBoxColumn2.DataPropertyName = "Code"
DataGridViewTextBoxColumn2.HeaderText = "Code"
DataGridViewTextBoxColumn2.Name = ""
DataGridViewTextBoxColumn2.ReadOnly = True
DataGridViewTextBoxColumn2.AutoSizeMode = DataGridViewAutoSizeColumnMode.NotSet
' assign binding context of the form that hosts
' the control in order to enable databinding
result.BindingContext = TargetForm.BindingContext
' end initialization
CType(result, System.ComponentModel.ISupportInitialize).EndInit()
Return result
End Function
关注点
创建自定义 ToolStripControlHost
控件的核心是一个名为 ToolStripDataGridView
的类,它继承自 ToolStripControlHost
。ToolStripDataGridView
类提供了 4 个新的、自明的属性:CloseOnSingleClick
、DataGridViewControl
、MinDropDownWidth
和 DropDownHeight
。目前,我将 MinDropDownWidth
和 DropDownHeight
属性设为只读。它们的值在构造函数中由相应的 DataGridView
属性 Width
和 Height
设置,以将所有网格区域自定义代码限制在网格创建代码内部。尽管这仅仅是偏好问题。
ToolStripDataGridView
类使用 ToolStripControlHost
受保护的可覆盖子程序 OnSubscribeControlEvents
和 OnUnsubscribeControlEvents
来订阅和取消订阅子 DataGridView
事件。
' Subscribe and unsubscribe the control events you wish to expose.
Protected Overrides Sub OnSubscribeControlEvents(ByVal c As Control)
' Call the base so the base events are connected.
MyBase.OnSubscribeControlEvents(c)
Dim nDataGridView As DataGridView = DirectCast(c, DataGridView)
' Add the events:
' to highlight the item that is currently under the mouse pointer
AddHandler nDataGridView.CellMouseEnter, AddressOf OnDataGridViewCellMouseEnter
' to accept selection by enter key
AddHandler nDataGridView.KeyDown, AddressOf OnDataGridViewKeyDown
' to accept selection by double clicking
AddHandler nDataGridView.CellDoubleClick, AddressOf myDataGridView_DoubleClick
' to accept selection by single click (if CloseOnSingleClick is set tor TRUE)
AddHandler nDataGridView.CellClick, AddressOf myDataGridView_Click
End Sub
Protected Overrides Sub OnUnsubscribeControlEvents(ByVal c As Control)
' Call the base method so the basic events are unsubscribed.
MyBase.OnUnsubscribeControlEvents(c)
Dim nDataGridView As DataGridView = DirectCast(c, DataGridView)
' Remove the events.
RemoveHandler nDataGridView.CellMouseEnter, AddressOf OnDataGridViewCellMouseEnter
RemoveHandler nDataGridView.KeyDown, AddressOf OnDataGridViewKeyDown
RemoveHandler nDataGridView.CellDoubleClick, AddressOf myDataGridView_DoubleClick
RemoveHandler nDataGridView.CellClick, AddressOf myDataGridView_Click
End Sub
这些事件非常简单且自明。通过调用来选择一个项目。
DirectCast(Me.Owner, ToolStripDropDown).Close(ToolStripDropDownCloseReason.ItemClicked)
重写 OnBoundsChanged
和 Dispose
子程序,以便在父 ToolStripDataGridView
调整大小时调整子 DataGridView
的大小,并在父 ToolStripDataGridView
处置时处置子 DataGridView
。
Protected Overrides Sub OnBoundsChanged()
MyBase.OnBoundsChanged()
If Not Me.Control Is Nothing Then
DirectCast(Control, DataGridView).Size = Me.Size
DirectCast(Control, DataGridView).AutoResizeColumns()
End If
End Sub
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
MyBase.Dispose(disposing)
If Not Me.Control Is Nothing AndAlso Not _
DirectCast(Control, DataGridView).IsDisposed Then Control.Dispose()
End Sub
以上就是 ToolStripDataGridView
类的大部分内容:一个构造函数、四个简单的属性、四个简单的事件处理程序和四个简单的覆盖。总共 109 行代码(包括空格)。
创建自定义 ComboBox
控件的下一个核心部分是 AccGridComboBox
类本身,它显然继承自 ComboBox
。
AccGridComboBox
类有一个私有变量 myDropDown As ToolStripDropDown
,它在类构造函数中实例化,并用作 ToolStripDataGridView
的容器。ToolStripDataGridView
实例本身由 AddDataGridView
子程序设置。
Public Sub AddDataGridView(ByVal nDataGridView As DataGridView, ByVal nCloseOnSingleClick As Boolean)
If Not myDataGridView Is Nothing Then Throw New Exception( _
"Error. DataGridView is already assigned to the AccGridComboBox.")
myDataGridView = New ToolStripDataGridView(nDataGridView, nCloseOnSingleClick)
myDropDown.Width = Math.Max(Me.Width, myDataGridView.MinDropDownWidth)
myDropDown.Height = nDataGridView.Height
myDropDown.Items.Clear()
myDropDown.Items.Add(Me.myDataGridView)
End Sub
AccGridComboBox
通过重写 WndProc
并拦截消息来处理下拉列表的显示。此方法的当前实现是从 CodeProject 文章 灵活的 ComboBox 和 EditingControl 中复制的,如果需要手动输入支持,则应进行更改,因为它会捕获组合框所有区域的点击,从而阻止文本输入。
Private Const WM_LBUTTONDOWN As UInt32 = &H201
Private Const WM_LBUTTONDBLCLK As UInt32 = &H203
Private Const WM_KEYF4 As UInt32 = &H134
Protected Overrides Sub WndProc(ByRef m As Message)
'#Region "WM_KEYF4"
If m.Msg = WM_KEYF4 Then
Me.Focus()
Me.myDropDown.Refresh()
If Not Me.myDropDown.Visible Then
ShowDropDown()
Else
myDropDown.Close()
End If
Return
End If
'#End Region
'#Region "WM_LBUTTONDBLCLK"
If m.Msg = WM_LBUTTONDBLCLK OrElse m.Msg = WM_LBUTTONDOWN Then
If Not Me.myDropDown.Visible Then
ShowDropDown()
Else
myDropDown.Close()
End If
Return
End If
'#End Region
MyBase.WndProc(m)
End Sub
AccGridComboBox
中实际显示下拉列表的方法主要处理下拉列表的大小调整和选择合适的 DataGridView
行(包含当前 SelectedValue
)。
Private Sub ShowDropDown()
' if a DataGridView is assigned
If Not Me.myDataGridView Is Nothing Then
' just in case, though such situation is not supposed to happen
If Not myDropDown.Items.Contains(Me.myDataGridView) Then
myDropDown.Items.Clear()
myDropDown.Items.Add(Me.myDataGridView)
End If
' do sizing
myDropDown.Width = Math.Max(Me.Width, Me.myDataGridView.MinDropDownWidth)
myDataGridView.Size = myDropDown.Size
myDataGridView.DataGridViewControl.Size = myDropDown.Size
myDataGridView.DataGridViewControl.AutoResizeColumns()
' select DataGridViewRow that holds the currently selected value
If _SelectedValue Is Nothing OrElse IsDBNull(_SelectedValue) Then
myDataGridView.DataGridViewControl.CurrentCell = Nothing
ElseIf Not Me.ValueMember Is Nothing AndAlso _
Not String.IsNullOrEmpty(Me.ValueMember.Trim) Then
' If ValueMember is set, look for the value by reflection
If myDataGridView.DataGridViewControl.Rows.Count < 1 OrElse _
myDataGridView.DataGridViewControl.Rows(0).DataBoundItem Is Nothing OrElse _
myDataGridView.DataGridViewControl.Rows(0).DataBoundItem.GetType. _
GetProperty(Me.ValueMember.Trim, _
BindingFlags.Public OrElse BindingFlags.Instance) Is Nothing Then
myDataGridView.DataGridViewControl.CurrentCell = Nothing
Else
Dim CurrentValue As Object
For Each r As DataGridViewRow In myDataGridView.DataGridViewControl.Rows
If Not r.DataBoundItem Is Nothing Then
CurrentValue = GetValueMemberValue(r.DataBoundItem)
If _SelectedValue = CurrentValue Then
myDataGridView.DataGridViewControl.CurrentCell = _
myDataGridView.DataGridViewControl.Item(0, r.Index)
Exit For
End If
End If
Next
End If
Else
' If ValueMember is NOT set, look for the value by value or
Dim SelectionFound As Boolean = False
For Each r As DataGridViewRow In myDataGridView.DataGridViewControl.Rows
Try
' try by value because it's faster and lookup
' objects usualy implement equal operators
If _SelectedValue = r.DataBoundItem Then
myDataGridView.DataGridViewControl.CurrentCell = _
myDataGridView.DataGridViewControl.Item(0, r.Index)
SelectionFound = True
Exit For
End If
Catch ex As Exception
Try
If _SelectedValue Is r.DataBoundItem Then
myDataGridView.DataGridViewControl.CurrentCell = _
myDataGridView.DataGridViewControl.Item(0, r.Index)
SelectionFound = True
Exit For
End If
Catch e As Exception
End Try
End Try
Next
If Not SelectionFound Then _
myDataGridView.DataGridViewControl.CurrentCell = Nothing
End If
myDropDown.Show(Me, CalculatePoz)
End If
End Sub
' Helper method, tries geting ValueMember property value by reflection
Private Function GetValueMemberValue(ByVal DataboundItem As Object) As Object
Dim newValue As Object = Nothing
Try
newValue = DataboundItem.GetType.GetProperty(Me.ValueMember.Trim, BindingFlags.Public _
OrElse BindingFlags.Instance).GetValue(DataboundItem, Nothing)
Catch ex As Exception
End Try
Return newValue
End Function
' Helper method, takes care of dropdown fitting the window
Private Function CalculatePoz() As Point
Dim point As New Point(0, Me.Height)
If (Me.PointToScreen(New Point(0, 0)).Y + Me.Height + Me.myDataGridView.Height) _
> Screen.PrimaryScreen.WorkingArea.Height Then
point.Y = -Me.myDataGridView.Height - 7
End If
Return point
End Function
AccGridComboBox
通过重载 SelectedValue
属性(绕过原生 ComboBox 逻辑)并提供自定义设置器方法,该方法允许通过 ValueMember
设置值对象,来处理设置当前值。
Private Sub SetValue(ByVal value As Object, ByVal IsValueMemberValue As Boolean)
If value Is Nothing Then
Me.Text = ""
_SelectedValue = Nothing
Else
If Me.ValueMember Is Nothing OrElse String.IsNullOrEmpty(Me.ValueMember.Trim) _
OrElse IsValueMemberValue Then
Me.Text = value.ToString
_SelectedValue = value
Else
Dim newValue As Object = GetValueMemberValue(value)
' If getting the ValueMember property value fails, try setting the object itself
If newValue Is Nothing Then
Me.Text = value.ToString
_SelectedValue = value
Else
Me.Text = newValue.ToString
_SelectedValue = newValue
End If
End If
End If
End Sub
Private Sub ToolStripDropDown_Closed(ByVal sender As Object, _
ByVal e As ToolStripDropDownClosedEventArgs)
If e.CloseReason = ToolStripDropDownCloseReason.ItemClicked Then
If Not MyBase.Focused Then MyBase.Focus()
If myDataGridView.DataGridViewControl.CurrentRow Is Nothing Then
SetValue(Nothing, False)
Else
SetValue(myDataGridView.DataGridViewControl.CurrentRow.DataBoundItem, False)
End If
MyBase.OnSelectedValueChanged(New EventArgs)
' If InstantBinding property is set to TRUE, force binding.
If _InstantBinding Then
For Each b As Binding In MyBase.DataBindings
b.WriteValue()
Next
End If
End If
End Sub
从上面的代码可以看出,AccGridComboBox
还实现了一个自定义属性 InstantBinding
。它本身不是必需的,但在某些情况下,最好在值更改时而不是在验证后更新绑定。
以上是组合框控件本身所需的全部代码,但要使其准备好用作 IDataGridViewEditingControl
,您还需要实现一些其他方法。
Protected Overridable ReadOnly Property DisposeToolStripDataGridView() As Boolean
Get
Return True
End Get
End Property
Friend Sub AddToolStripDataGridView(ByVal nToolStripDataGridView As ToolStripDataGridView)
If nToolStripDataGridView Is Nothing OrElse (Not myDataGridView Is Nothing _
AndAlso myDataGridView Is nToolStripDataGridView) Then Exit Sub
myDataGridView = nToolStripDataGridView
myDropDown.Width = Math.Max(Me.Width, myDataGridView.MinDropDownWidth)
myDropDown.Height = myDataGridView.DropDownHeight
myDropDown.Items.Clear()
myDropDown.Items.Add(Me.myDataGridView)
End Sub
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If components IsNot Nothing Then components.Dispose()
If DisposeToolStripDataGridView Then
If Not myDropDown Is Nothing AndAlso Not _
myDropDown.IsDisposed Then myDropDown.Dispose()
If Not myDataGridView Is Nothing AndAlso _
Not myDataGridView.DataGridViewControl Is Nothing AndAlso _
Not myDataGridView.DataGridViewControl.IsDisposed Then _
myDataGridView.DataGridViewControl.Dispose()
If Not myDataGridView Is Nothing AndAlso Not myDataGridView.IsDisposed Then _
myDataGridView.Dispose()
ElseIf Not DisposeToolStripDataGridView AndAlso Not myDropDown Is Nothing _
AndAlso Not myDropDown.IsDisposed Then
If Not myDataGridView Is Nothing Then myDropDown.Items.Remove(myDataGridView)
myDropDown.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
如果您有一个独立的 AccGridComboBox
,那么在处置组合框本身时,最好同时处置托管的 ToolStripDropDown
、ToolStripDataGridView
和 DataGridView
实例,因为 DataGridView
实例不能在不同窗体之间重用。另一方面,如果您有一个 AccGridComboBox
实例作为 DataGridView 列的一部分,您需要为该列的生命周期保留 DataGridView 实例,而不是组合框的生命周期(组合框实例在列的生命周期内被处置)。为了实现这两种行为,使用了受保护的可覆盖属性 DisposeToolStripDataGridView
。此属性指示 Dispose 方法是否还应处置 ToolStripDataGridView
和 DataGridView
实例。除非被覆盖,否则它始终返回 true。它在 AccGridComboBoxEditingControl
类中被覆盖,而该类又被自定义 DataGridViewCell 使用。
创建自定义 IDataGridViewEditingControl、DataGridViewCell 和 DataGridViewColumn
MSDN 文章 如何:在 Windows Forms DataGridView 单元格中托管控件 详细介绍了创建自定义 DataGridViewColumn
的过程。因此,我只讨论特定于 AccGridComboBox
实现的代码部分。
在 AccGridComboBoxEditingControl
类的实现中,与上述 MSDN 文章中描述的实现相比,只有少数几个特定的方法。该类需要覆盖 DisposeToolStripDataGridView
属性(如前所述),以防止处置 DataGridView
。该类还需要处理 SelectedValueChanged
事件,并向 DataGridView 基础结构通知更改。最后,值到文本的转换由基类 AccGridComboBox
处理,因此 GetEditingControlFormattedValue
的实现仅包含对 Text
属性的引用。
Protected Overrides ReadOnly Property DisposeToolStripDataGridView() As Boolean
Get
Return False
End Get
End Property
Private Sub SelectedValueChangedHandler(ByVal sender As Object, _
ByVal e As EventArgs) Handles Me.SelectedValueChanged
If Not _hasValueChanged Then
_hasValueChanged = True
_dataGridView.NotifyCurrentCellDirty(True)
End If
End Sub
Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) _
As Object Implements _
System.Windows.Forms.IDataGridViewEditingControl.GetEditingControlFormattedValue
Return Me.Text
End Function
在 AccGridComboBoxDataGridViewCell
类的实现中,与上述 MSDN 文章中描述的实现相比,只有少数几个特定的方法。由于此单元格将处理不同的对象类型,因此 ValueType
属性返回最通用的类型——Object
。另外两个方法是自明的,负责初始化 AccGridComboBox
编辑控件,以及获取和设置单元格值。
Public Overrides ReadOnly Property ValueType() As Type
Get
Return GetType(Object)
End Get
End Property
Public Overrides Sub InitializeEditingControl(ByVal nRowIndex As Integer, _
ByVal nInitialFormattedValue As Object, ByVal nDataGridViewCellStyle As DataGridViewCellStyle)
MyBase.InitializeEditingControl(nRowIndex, nInitialFormattedValue, nDataGridViewCellStyle)
Dim cEditBox As AccGridComboBox = TryCast(Me.DataGridView.EditingControl, AccGridComboBox)
If cEditBox IsNot Nothing Then
If Not MyBase.OwningColumn Is Nothing AndAlso Not DirectCast(MyBase.OwningColumn, _
DataGridViewAccGridComboBoxColumn).ComboDataGridView Is Nothing Then
' Add the common column ToolStripDataGridView and set common properties
cEditBox.AddToolStripDataGridView(DirectCast(MyBase.OwningColumn, _
DataGridViewAccGridComboBoxColumn).GetToolStripDataGridView)
cEditBox.ValueMember = DirectCast(MyBase.OwningColumn, _
DataGridViewAccGridComboBoxColumn).ValueMember
cEditBox.InstantBinding = DirectCast(MyBase.OwningColumn, _
DataGridViewAccGridComboBoxColumn).InstantBinding
End If
' try to set current value
Try
cEditBox.SelectedValue = Value
Catch ex As Exception
cEditBox.SelectedValue = Nothing
End Try
End If
End Sub
Protected Overrides Function SetValue(ByVal rowIndex As Integer, ByVal value As Object) As Boolean
If Not Me.DataGridView Is Nothing AndAlso Not Me.DataGridView.EditingControl Is Nothing _
AndAlso TypeOf Me.DataGridView.EditingControl Is AccGridComboBox Then
Return MyBase.SetValue(rowIndex, DirectCast(Me.DataGridView.EditingControl, _
AccGridComboBox).SelectedValue)
Else
Return MyBase.SetValue(rowIndex, value)
End If
End Function
最后,DataGridViewAccGridComboBoxColumn
类仅实现镜像 AccGridComboBox
属性的属性,并负责处置关联的网格。
Private myDataGridView As ToolStripDataGridView = Nothing
Public Property ComboDataGridView() As DataGridView
Get
If Not myDataGridView Is Nothing Then Return myDataGridView.DataGridViewControl
Return Nothing
End Get
Set(ByVal value As DataGridView)
If Not value Is Nothing Then
myDataGridView = New ToolStripDataGridView(value, _CloseOnSingleClick)
Else
myDataGridView = Nothing
End If
End Set
End Property
Private _ValueMember As String = ""
Public Property ValueMember() As String
Get
Return _ValueMember
End Get
Set(ByVal value As String)
_ValueMember = value
End Set
End Property
Private _CloseOnSingleClick As Boolean = True
Public Property CloseOnSingleClick() As Boolean
Get
Return _CloseOnSingleClick
End Get
Set(ByVal value As Boolean)
_CloseOnSingleClick = value
If Not myDataGridView Is Nothing Then _
myDataGridView.CloseOnSingleClick = value
End Set
End Property
Private _InstantBinding As Boolean = True
Public Property InstantBinding() As Boolean
Get
Return _InstantBinding
End Get
Set(ByVal value As Boolean)
_InstantBinding = value
End Set
End Property
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not myDataGridView Is Nothing _
AndAlso Not myDataGridView.DataGridViewControl Is Nothing _
AndAlso Not myDataGridView.DataGridViewControl.IsDisposed Then _
myDataGridView.DataGridViewControl.Dispose()
If Not myDataGridView Is Nothing AndAlso Not myDataGridView.IsDisposed Then _
myDataGridView.Dispose()
End If
MyBase.Dispose(disposing)
End Sub