在组合框中嵌入一个DataGridView

介绍 多栏组合框在W inForms应用程序中非常常见。然而,还没有开源的解决方案能够完全支持数据绑定,并且像DataGridView控件那样可定制。本文的目的是展示如何用相对较少的代码轻松地创建一个。 满足需求的最明显、最直接和最简单的方法是在一个组合框中托管一个DataGridView。这似乎不是一个简单的任务,但实际上它是惊人的容易做(至少在做了它之后)。 本文和提供的源代码更多的是“概念验证”类型,而不是完成的控件。有许多细节不是很“技术”和美学。另一方面,我在我的一个开源程序中使用它(DevExpress是不可能的),它对我的目的来说工作得很好。 这也是我的第一篇关于编程的文章,所以请原谅我糟糕的风格 Background  本文基于以下几篇文章提出的想法: 灵活的组合框和编辑控件使用ToolStripControlHost。MSDN文章如何:主机控件在Windows窗体DataGridView单元格-创建自定义的DataGridViewColumn。 首先创建一个自定义ToolStripControlHost,然后使用它来创建自定义组合框,该组合框又用于创建IDataGridViewEditingControl、自定义DataGridViewCell和自定义DataGridViewColumn。 使用code  使用提供的自定义AccGridComboBox和DataGridViewAccGridComboBoxColumn类就像使用ComboBox和DataGridViewColumn本身一样简单。 您所需要的是添加一个AccGridComboBox或一个datgridcomboboxcolumn到一个表单,就像您将添加一个ComboBox或一个DataGridViewColumn并分配一个相应的DataGridView而不是datasource:

' 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 控件的基本部分是继承ToolStripControlHost的ToolStripDataGridView类。ToolStripDataGridView类提供了4个新的自解释属性:CloseOnSingleClick、DataGridViewControl、MinDropDownWidth和DropDownHeight。目前我把MinDropDownWidth和DropDownHeight属性设置为只读。它们的值在构造函数中由对应的DataGridView属性的宽度和高度设置,以便在网格创建代码中限制所有网格区域定制代码。尽管这只是个人偏好的问题。 ToolStripDataGridView类订阅和取消订阅子DataGridView事件使用ToolStripControlHost保护覆盖子OnSubscribeControlEvents和OnUnsubscribeControlEvents:

' 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)

当父工具stripdatagridview被调整大小时,OnBoundsChanged和Dispose subs被覆盖以调整子DataGridView的大小,当父工具stripdatagridview被释放时,将释放子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行代码。 创建自定义组合框 控件的下一个重要部分是AccGridComboBox类本身,它显然继承自ComboBox。 AccGridComboBox类有一个私有变量myDropDown作为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文章Flexible ComboBox和EditingControl中复制的,如果需要手动输入支持,应该对其进行更改,因为它捕获了ComboBox所有区域的单击,从而阻止了文本输入。

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逻辑),并提供定制的setter方法来允许通过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实例的er不能跨不同表单重用。另一方面,如果您有一个AccGridComboBox实例作为DataGridView列的一部分,那么您需要为列的生存期而不是组合生存期保留DataGridView实例(组合实例在列生存期期间被释放)。要实现这两种受保护的可重写行为,使用了DisposeToolStripDataGridView属性。此属性指示Dispose方法是否也应该释放ToolStripDataGridView和DataGridView实例。它总是返回true,除非被重写。它在AccGridComboBoxEditingControl类中被重写,这个类被自定义DataGridViewCell使用。 创建自定义的IDataGridViewEditingControl、DataGridViewCell和DataGridViewColumn 创建自定义DataGridViewColumn的过程在MSDN文章“如何:宿主Windows Forms DataGridView单元格中的控件”中有详细介绍。所以我将只讨论特定于AccGridComboBox实现的代码部分。 在AccGridComboBoxEditingControl类的实现中,与前面提到的MSDN文章中描述的实现相比,只有少数几种具体的方法。这个类需要像前面讨论的那样重写DisposeToolStripDataGridView属性,以防止处理DataGridView。这个类还需要处理SelectedValueChanged事件,并通知DataGridView基础设施的更改。最终值到/从文本的转换是由基类AccGridComboBox处理的,因此GetEditingControlFormattedValue的实现只包含一个对文本属性的引用。

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

本文转载于:http://www.diyabc.com/frontweb/news386.html

原文地址:https://www.cnblogs.com/Dincat/p/13443972.html