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

在 ComboBox 中嵌入 ObjectListView

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016 年 11 月 8 日

CPOL

7分钟阅读

viewsIcon

16433

downloadIcon

628

一种将任何 ObjectListView 放入 ComboBox 的简单方法

引言

本文是我之前文章“将 DataGridView 嵌入 ComboBox”的扩展。

在我写完介绍如何在一个 ComboBox 控件中使用 DataGridView 的文章后,我发现了一个惊人的开源替代方案——ObjectListView。它有许多高级的内置功能,其中一个尤其有价值的功能是通用快速搜索和过滤。这个功能使其成为嵌入 ComboBox 的完美选择,结果如下所示:

背景

本文基于以下文章中提出的想法

首先,创建自定义的 ToolStripControlHost,然后用它来创建自定义 ComboBox。我没有创建自定义的 DataGridViewColumn,因为将 ObjectListView 这样出色的控件用在 DataGridView 中是一种浪费,而 ObjectListView 有一个通用的机制可以添加任何编辑控件,包括这个。

Using the Code

要使用提供的控件,您需要创建一个新的 UserControl,并继承自提供的 InfoListControl 类。该控件代表了您要在下拉列表中显示的对象视图(一些列显示对象的属性)。该控件的目的是允许开发人员在 VS 设计器中配置 ObjectListView(将列绑定到特定数据源类型的属性,设置各种视觉属性等)。因此,对于您将要使用的每种对象类型,您都需要一个 InfoListControl。如果您使用标准的 Visual Studio 方法创建继承的 UserControl,则无需编写一行代码。创建继承的控件后,您将看到一个 ObjectListView 设计器视图。在这里,您可以配置所有您需要的东西:设置数据绑定(列的 AspectName)、配置列标题、视觉外观等。您可以在 此处找到详细描述。

为您特定数据源类型创建 InfoListControl 后,您只需几行代码即可完成控件设置

Dim cntr As AccListComboBox
dim view As InfoListControl = new yourInfoListControlInstance
view.DataSource = GetDataSource(whatever)
' a name of the property that should be used to set SelectedValue
' an empty string if the SelectedValue should contain the object itself
view .ValueMember = ""
' whether to accept a single click to confirm a user selection
view .AcceptSingleClick = True
cntr.AddDataListView(view)
' e.g.. when you want to display integer value 0 as an empty string
cntr.EmptyValueString = "0"

InfoListControl 内部,我使用了一个 ObjectListView 的变体——DataListView,因为它开箱即用地支持数据绑定。但是,如果您需要非标准数据绑定(例如 DataTable、分层结构),您可以轻松实现自己的控件来支持它。

重要提示

ObjectListView 的当前版本有一个错误,当 ObjectListViewComboBox 用作 ObjectListView 本身的编辑控件时会导致异常。因此,您需要对 ObjectListView 源代码(CellEditKeyEngine 类中的 ItemBeingEdited 属性)进行以下更正,并自行编译(即不要使用预编译的二进制文件)。

/// <summary>
/// Gets the row of the cell that is currently being edited
/// </summary>
protected OLVListItem ItemBeingEdited {
get {
        OLVListItem olvi = (this.ListView == null ||
        this.ListView.CellEditEventArgs == null) ?
        null : this.ListView.CellEditEventArgs.ListViewItem;

        if (olvi != null && olvi.Index < 0)
        {
            if (olvi.RowObject == null)
                return olvi;
            for (int i = 0; i < this.ListView.Items.Count; i++)
            {
                if (this.ListView.GetItem(i).RowObject == olvi.RowObject)
                {
                    olvi = this.ListView.GetItem(i);
                    break;
                }
            }
        }

        return olvi;
    }
}

关注点

创建 InfoListControl

该控件的核心部分是 ObjectListViewToolStrip 类,它继承自 ToolStripControlHost。基本上,ToolStripControlHost 类可以自行处理任何控件,包括 ObjectListView,但在这种情况下,开发人员需要以编程方式创建 ObjectListView(不太用户友好)。因此,我决定创建一个专用的用户控件,一方面整洁地封装了所有 ObjectListView 特定的逻辑,另一方面提供了完整的设计器支持。

实际上,在 InfoListControl 中,我没有使用通用的 ObjectListView,而是使用了它的子类——DataListView(因为我需要数据绑定支持)。对实际控件的封装提供了一种简单的方式来重写该控件以支持通用的 ObjectListViewTreeListViewObjectListView 的另一个子类)。

InfoListControl 包含一个由 VS 设计器添加的 DataListView 实例,并提供了一个简单的 public 构造函数以及四个几乎不言自明的属性及其后备字段。

Private _AcceptSingleClick As Boolean = False
Private _ValueMember As String = ""
Private _FilterString As String = ""


''' <summary>
''' Whether single click is sufficient to choose an item.
''' </summary>
''' <remarks></remarks>
Public Property AcceptSingleClick() As Boolean
    Get
        Return _AcceptSingleClick
    End Get
    Set(ByVal value As Boolean)
        _AcceptSingleClick = value
    End Set
End Property

''' <summary>
''' A value object property that holds the required value (if any).
''' </summary>
''' <remarks></remarks>
Public Property ValueMember() As String
    Get
        Return _ValueMember
    End Get
    Set(ByVal value As String)
        If value Is Nothing Then value = ""
        _ValueMember = value
    End Set
End Property

''' <summary>
''' A <see cref="BindingSource">BindingSource_
</see> that wraps a value object list.
''' </summary>
''' <remarks></remarks>
Public Property DataSource() As Object
    Get
        Return baseDataListView.DataSource
    End Get
    Set(ByVal value As Object)
        baseDataListView.DataSource = value
    End Set
End Property

''' <summary>
''' A string that is used to filter the value object list.
''' </summary>
''' <remarks></remarks>
Public Property FilterString() As String
    Get
        Return _FilterString
    End Get
    Set(ByVal value As String)
        If value Is Nothing Then value = ""
        If value <> _FilterString Then
            _FilterString = value
            baseDataListView.AdditionalFilter = _
                TextMatchFilter.Contains(baseDataListView, _FilterString)
        End If
    End Set
End Property

Public Sub New()
    ' This call is required by the Windows Form Designer.
    InitializeComponent()

    baseDataListView.DefaultRenderer = New HighlightTextRenderer( _
        TextMatchFilter.Contains(baseDataListView, New String() {""}))
    baseDataListView.SelectColumnsMenuStaysOpen = True
End Sub

这里唯一值得关注的是对 ObjectListView 方法的调用,这些方法用于设置过滤图形渲染器和过滤(string)本身。

接下来,InfoListControl 将实现事件以将用户选择传递给父 ToolStripControlHost

Friend Delegate Sub ValueSelectedEventHandler(ByVal sender As Object, ByVal e As ValueChangedEventArgs)
Friend Event ValueSelected As ValueSelectedEventHandler

Protected Sub OnValueSelected(ByVal e As ValueChangedEventArgs)
    RaiseEvent ValueSelected(Me, e)
End Sub

Protected Sub OnValueSelected(ByVal currentObject As Object, ByVal isCanceled As Boolean)

    If _ValueMember Is Nothing OrElse String.IsNullOrEmpty(_ValueMember) _
        OrElse currentObject Is Nothing Then

        RaiseEvent ValueSelected(Me, New ValueChangedEventArgs(currentObject, isCanceled))

    Else

        If baseDataListView.GetItemCount() < 1 OrElse baseDataListView.GetItem(0). _
            RowObject.GetType().GetProperty(_ValueMember.Trim, BindingFlags.Public _
            OrElse BindingFlags.Instance) Is Nothing Then

            RaiseEvent ValueSelected(Me, New ValueChangedEventArgs(Nothing, isCanceled))

        Else
            RaiseEvent ValueSelected(Me, New ValueChangedEventArgs( _
                GetValueMemberValue(currentObject), isCanceled))
        End If

    End If

End Sub

Public Class ValueChangedEventArgs
    Inherits EventArgs

    Private _SelectedValue As Object = Nothing
    Private _SelectionCanceled As Boolean = False

    Public ReadOnly Property SelectedValue() As Object
        Get
            Return _SelectedValue
        End Get
    End Property

    Public ReadOnly Property SelectionCanceled() As Boolean
        Get
            Return _SelectionCanceled
        End Get
    End Property

    Friend Sub New(ByVal newValue As Object, ByVal isCanceled As Boolean)
        _SelectedValue = newValue
        _SelectionCanceled = isCanceled
    End Sub

End Class

这里值得关注的是 OnValueSelected 方法重载,它会引发事件并将选定的对象传递给事件参数。该方法的作用是过滤用户输入的选择,并(如果设置了 ValueMember 属性)返回不是对象本身,而是所需属性的值。所需属性的值是使用一个简单的帮助方法 GetValueMemberValue 获取的,该方法在 Try...Catch 块中使用简单的反射来避免运行时异常。

最后,InfoListControl 需要处理用户输入(通过鼠标或键盘)。

Private Sub baseDataListView_CellClick(ByVal sender As Object, _
    ByVal e As CellClickEventArgs) Handles baseDataListView.CellClick

    If Not e.Model Is Nothing AndAlso (e.ClickCount = 2 OrElse _
        (e.ClickCount = 1 AndAlso _AcceptSingleClick)) Then

        OnValueSelected(e.Model, False)

    End If

End Sub

Private Sub baseDataListView_KeyDown(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyEventArgs) Handles baseDataListView.KeyDown

    If e.KeyData = Keys.Enter AndAlso Not baseDataListView.SelectedItem Is Nothing _
        AndAlso Not baseDataListView.SelectedItem.RowObject Is Nothing Then

        OnValueSelected(baseDataListView.SelectedItem.RowObject, False)
        e.Handled = True

    ElseIf e.KeyData = Keys.Back Then

        If _FilterString <> "" Then
            _FilterString = _FilterString.Substring(0, _FilterString.Length - 1)
            baseDataListView.AdditionalFilter = _
                TextMatchFilter.Contains(baseDataListView, _FilterString)
        End If
        e.Handled = True

    ElseIf e.KeyData = Keys.Delete Then

        If _FilterString <> "" Then
            _FilterString = ""
            baseDataListView.AdditionalFilter = _
                TextMatchFilter.Contains(baseDataListView, _FilterString)
        End If
        e.Handled = True

    ElseIf e.KeyData = Keys.Escape Then

        OnValueSelected(Nothing, True)
        e.Handled = True

    End If

End Sub

Private Sub baseDataListView_KeyPress(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles baseDataListView.KeyPress

    If Not Char.IsControl(e.KeyChar) AndAlso (Char.IsLetterOrDigit(e.KeyChar) _
        OrElse Char.IsPunctuation(e.KeyChar)) Then

        _FilterString = _FilterString & e.KeyChar
        baseDataListView.AdditionalFilter = _
            TextMatchFilter.Contains(baseDataListView, _FilterString)

    End If

End Sub

代码本身具有解释性。标准的事件处理程序评估用户输入,并引发 ValueSelected 事件来指示用户选择,或修改过滤 string

  • 字母、数字或标点符号键 会添加到过滤 string 中。
  • 退格键 会删除过滤 string 中的最后一个 char
  • Delete 键 会清除过滤 string
  • Enter 键 会使用当前选定的对象引发 ValueSelected 事件。
  • Escape 键 会引发 ValueSelected 事件,并将 null 对象和取消标志设置为 true
  • 单击鼠标 如果 AcceptSingleClick 设置为 TRUE,则会引发 ValueSelected 事件并带有被单击的对象。
  • 双击鼠标 会引发 ValueSelected 事件并带有被单击的对象。

从外部视角(父 ToolStripControlHost 控件的视角)来看,InfoListControl 是一个 Control,它公开了 4 个自定义属性和 1 个自定义事件。包含的 DataListView 控件也可以从外部对象访问,但通常只通过 VS 设计器进行配置。

创建 ObjectListViewToolStrip

该控件的核心部分是 ObjectListViewToolStrip 类,它继承自 ToolStripControlHost。它是一个内部类(仅由 ObjectListViewComboBox 内部使用),并处理 InfoListControl

  • 提供对所封装 InfoListControlFilterString 属性的代理属性。
  • 提供对所封装 InfoListControlDataSource 属性的代理访问方法 GetDataSource
  • 处理所封装 InfoListControlValueSelected 事件,并封装当前选定的值(SelectedValue)和取消标志(SelectionCanceled)。
  • 提供下拉列表大小的标准属性(MinDropDownWidthDropDownHeight)。

特定的 ObjectListViewToolStrip 方法是构造函数以及连接所封装 InfoListControl 事件的方法。

Public Sub New(ByVal listView As InfoListControl)
    MyBase.New(listView)
    Me.AutoSize = False
    Me._MinDropDownWidth = listView.Width
    Me._DropDownHeight = listView.Height
End Sub

Private Sub OnDataListViewValueSelected(ByVal sender As Object, _
    ByVal e As InfoListControl.ValueChangedEventArgs)
    _SelectedValue = e.SelectedValue
    _SelectionCanceled = e.SelectionCanceled
    DirectCast(Me.Owner, ToolStripDropDown).Close(ToolStripDropDownCloseReason.ItemClicked)
End Sub

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

    ' Cast the control to a InfoListControl control.
    Dim nDataListView As InfoListControl = DirectCast(c, InfoListControl)

    ' Add the event.
    AddHandler nDataListView.ValueSelected, AddressOf OnDataListViewValueSelected

End Sub

Protected Overrides Sub OnUnsubscribeControlEvents(ByVal c As Control)
    ' Call the base method so the basic events are unsubscribed.
    MyBase.OnUnsubscribeControlEvents(c)

    ' Cast the control to a InfoListControl control.
    Dim nDataListView As InfoListControl = DirectCast(c, InfoListControl)

    ' Remove the event.
    RemoveHandler nDataListView.ValueSelected, AddressOf OnDataListViewValueSelected

End Sub

Protected Overrides Sub OnBoundsChanged()
    MyBase.OnBoundsChanged()
    If Not Control Is Nothing Then
        DirectCast(Control, InfoListControl).Size = Me.Size
    End If
End Sub

构造函数设置所封装的 InfoListControl 并关闭自动调整大小,将大小控制留给重写的 OnBoundsChanged 方法。出于未知原因,内置的自动调整大小方法在 ToolStripControlHost 中使用时会失败。

OnSubscribeControlEventsOnUnsubscribeControlEvents 方法将 ValueSelected 事件连接到 OnDataListViewValueSelected 处理程序。该处理程序将 ValueSelected 事件参数持久化到 SelectedValueSelectionCanceled 属性中,并关闭选择下拉列表。

创建 ObjectListViewComboBox

控件本身继承自 ComboBox。它公开以下 public 属性和方法:

  • HasAttachedInfoList - 指示是否已将 InfoListControl 分配给控件。
  • InstantBinding - 是否在用户选择值时立即更新数据源(与验证时相对)。
  • SelectedValue - 当前选定的值对象,或者 InfoListControl.ValueMember 属性(如果已设置)的值对象的值。
  • EmptyValueString - 一个 SelectedValue string 表达式(ToString),应显示为空 string(例如,如果您设置 EmptyValueString="0",则整数值 0 将显示为空 string)。
  • FilterString - 当前应用的过滤字符串(代理属性)。
  • InfoListControlDataSource - 嵌套 InfoListControl 的数据源(代理属性)。
  • AddDataListView - 向控件添加一个新的 InfoListControl,即初始化控件。之后无法替换 InfoListControl

ObjectListViewComboBox 类有一个 private 变量 myDropDown As ToolStripDropDown,它充当 ObjectListViewToolStrip 的容器。ObjectListViewToolStrip 本身的实例由 AddDataListView 方法创建。

 Public Sub AddDataListView(ByVal dataView As InfoListControl)

    If Not myListView Is Nothing Then Throw New InvalidOperationException( _
        "Error. DataListView is already assigned to the ObjectListViewComboBox.")

    myListView = New ObjectListViewToolStrip(dataView)

    If myDropDown Is Nothing OrElse myDropDown.IsDisposed Then

        myDropDown = New ToolStripDropDown()
        myDropDown.AutoSize = False
        myDropDown.GripStyle = ToolStripGripStyle.Visible
        AddHandler myDropDown.Closed, AddressOf ToolStripDropDown_Closed

    Else

        myDropDown.Items.Clear()

    End If

    myDropDown.Items.Add(myListView)
    myDropDown.Width = Math.Max(Me.Width, myListView.MinDropDownWidth)
    myDropDown.Height = myListView.Height

End Sub

ObjectListViewComboBox 通过重写 WndProc 和拦截消息来处理显示下拉列表。此方法的当前实现是从 CodeProject 文章 灵活的 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

ObjectListViewComboBox 实际上显示下拉列表的方法主要处理下拉列表的大小、定位以及选择合适的 DataListView 行(其中包含当前 SelectedValue)。

    Private Sub ShowDropDown()
        If Not myDropDown Is Nothing AndAlso Not Me.myListView Is Nothing Then

            If Not myDropDown.Items.Contains(Me.myListView) Then
                myDropDown.Items.Clear()
                myDropDown.Items.Add(Me.myListView)
            End If

            myDropDown.Width = Math.Max(Me.Width, Me.myListView.MinDropDownWidth)
            myListView.Size = myDropDown.Size

            myListView.SetSelectedValue(_SelectedValue)

            myDropDown.Show(Me, CalculatePoz)

            SendKeys.Send("{down}")

        End If

    End Sub

    Private Function CalculatePoz() As Point

        Dim point As New Point(0, Me.Height)

        If (Me.PointToScreen(New Point(0, 0)).Y + Me.Height + Me.myListView.Height) _
            > Screen.PrimaryScreen.WorkingArea.Height Then
            point.Y = -Me.myListView.Height - 7
        End If

        Return point

    End Function

ObjectListViewComboBox 通过重载 SelectedValue 属性(以绕过本机 ComboBox 逻辑)并提供自定义 setter 方法来实现设置当前值,该方法可以通过 ValueMember 设置值对象。

    Private Sub ToolStripDropDown_Closed(ByVal sender As Object, _
        ByVal e As ToolStripDropDownClosedEventArgs)

        If e.CloseReason = ToolStripDropDownCloseReason.ItemClicked _
            AndAlso Not myListView Is Nothing AndAlso Not myListView.SelectionCanceled Then

            If Not MyBase.Focused Then MyBase.Focus()

            SetValue(myListView.SelectedValue)

            If _InstantBinding Then
                For Each b As Binding In MyBase.DataBindings
                    b.WriteValue()
                Next
            End If

        End If

    End Sub

    Private Sub SetValue(ByVal value As Object)

        If value Is Nothing Then
            Me.Text = ""
        ElseIf Not _EmptyValueString Is Nothing AndAlso _
            Not String.IsNullOrEmpty(_EmptyValueString.Trim) AndAlso _
            value.ToString.ToLower.Trim = _EmptyValueString.Trim.ToLower Then
            Me.Text = ""
        Else
            Me.Text = value.ToString
        End If

        _SelectedValue = value

        MyBase.OnSelectedValueChanged(New EventArgs)

    End Sub

从上面的代码可以看出,ObjectListViewComboBox 还有一个自定义属性 InstantBinding。它本身不是必需的,但在某些情况下,最好在实际值更改时更新绑定,而不是在验证时更新。

差不多就这些了,尽情享受 ObjectListView 吧!

© . All rights reserved.