VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)

ComboBox控件可以输入文本也可以提供列表来选择项,而且还自带有属性来实现自动匹配,但是它有一个弊端,只能从头开始匹配,例如"张三丰",只能输入“张”、"张三"或“张三丰”才能匹配出来,而输入"三"或“三丰”是匹配不了。演示代码如下:

Dim data() As String = {"张三丰|ZSF", "李四|LS", "王五|WW", "赵六|ZL", "田七|TQ"}
        With Me.ComboBox1
            .Items.AddRange(data)
            .AutoCompleteMode = AutoCompleteMode.SuggestAppend
            .AutoCompleteSource = AutoCompleteSource.ListItems
        End With

效果如图:
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第1张图片
而在项目想实现类似百度搜索框,输入文本时能够自动匹配,重要的是实现模糊匹配,也就是说在前台输入"三"或“三丰”也是有匹配项的。实现效果如图:
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第2张图片
于是就开始百度,在CSDN里查到《自动完成TextBox实现类似百度搜索框》一文,是通过扩展控件方法来实现的,改编了一下,用到ComboBox控件来。唉,不生产代码,只能做代码的搬运工啦,55……

Imports System.ComponentModel
Public Class ComboBoxEx
    Inherits ComboBox

#Region "字段"
    ''' 
    ''' 列表框
    ''' 
    Private listBox As ListBox
    ''' 
    ''' 记住前输入的字符串
    ''' 
    Private oldText As String
    ''' 
    ''' 显示面板
    ''' 
    Private panel As Panel
    ''' 
    ''' 进程锁
    ''' 
    Private _lockObj As Object = New Object()
#End Region

#Region "属性"
    ''' 
    ''' 在显示之前键入的最小字符
    ''' 
    ''' 
    
    Public Property MinTypedCharacters As Integer = 1

    ''' 
    ''' listBox选择索引值
    ''' 
    ''' 
    
    Public Property LstSelectedIndex As Integer
        Get
            Return listBox.SelectedIndex
        End Get
        Set(ByVal value As Integer)
            If listBox.Items.Count > 0 Then listBox.SelectedIndex = value
        End Set
    End Property

    ''' 
    ''' 当前显示的实际列表
    ''' 
    ''' 
    Private Property CurrentAutoCompleteList As List(Of String)

    ''' 
    ''' 该控件的父窗体
    ''' 
    ''' 
    Private ReadOnly Property ParentForm As Form
        Get
            Return Me.Parent.FindForm()
        End Get
    End Property
#End Region

#Region "构造函数"
    Public Sub New()
        '调用基类构造函数
        MyBase.New()

        '列表框
        Me.listBox = New ListBox()
        Me.listBox.Name = " SuggestionListBox"
        Me.listBox.Font = Me.Font
        Me.listBox.Visible = True

        '这个容器用来保持列表框所在位置
        Me.panel = New Panel()
        Me.panel.Visible = False
        Me.panel.Font = Me.Font
        '能够适应父窗体的大小更改
        Me.panel.AutoSizeMode = AutoSizeMode.GrowAndShrink
        '初始化最小尺寸以避免重叠或闪烁问题
        Me.panel.ClientSize = New Size(1, 1)
        Me.panel.Name = " SuggestionPanel"
        Me.panel.Padding = New Padding(0, 0, 0, 0)
        Me.panel.Margin = New Padding(0, 0, 0, 0)
        Me.panel.BackColor = Color.Transparent
        Me.panel.ForeColor = Color.Transparent
        Me.panel.Text = " "
        Me.panel.PerformLayout()
        '控件是否存在容器里
        If Not panel.Controls.Contains(listBox) Then Me.panel.Controls.Add(listBox)
        '让ListBox填充容器
        Me.listBox.Dock = DockStyle.Fill
        '只有一项可以选择
        Me.listBox.SelectionMode = SelectionMode.One
        '事件
        AddHandler Me.listBox.KeyDown, New KeyEventHandler(AddressOf ListBox_KeyDown)
        AddHandler Me.listBox.MouseClick, New MouseEventHandler(AddressOf ListBox_MouseClick)
        AddHandler Me.listBox.MouseDoubleClick, New MouseEventHandler(AddressOf ListBox_MouseDoubleClick)

#Region "备注: ArrayList与List"
        '令人惊奇的是ArrayList比List快一点
        '使用ArrayList而替换List”
        '使用List泛型类
#End Region
        Me.CurrentAutoCompleteList = New List(Of String)()

#Region "备注: DataSource 与 AddRange"
        '使用数据源比添加项目更快(见注释listbox.items.addrange方法如下)
#End Region
        'currentautocompletelist作为数据源列表
        listBox.DataSource = Nothing
        listBox.DataSource = CurrentAutoCompleteList
        '设置输入历史
        oldText = Me.Text
    End Sub
#End Region

#Region "隐藏ListBox"
    ''' 
    ''' 隐藏ListBox
    ''' 
    Private Sub HideSuggestionListBox()
        If (ParentForm IsNot Nothing) Then
            panel.Hide()
            If Me.ParentForm.Controls.Contains(panel) Then Me.ParentForm.Controls.Remove(panel)
        End If
    End Sub
#End Region

#Region "重载键盘响应事件"
    ''' 
    ''' 重载键盘响应事件
    ''' 
    ''' 
    Protected Overrides Sub OnKeyDown(ByVal args As KeyEventArgs)
        If (args.KeyCode = Keys.Up) Then
            MoveSelectionInListBox((LstSelectedIndex - 1))
            args.Handled = True
        ElseIf (args.KeyCode = Keys.Down) Then
            MoveSelectionInListBox((LstSelectedIndex + 1))
            args.Handled = True
        ElseIf (args.KeyCode = Keys.PageUp) Then
            MoveSelectionInListBox((LstSelectedIndex - 10))
            args.Handled = True
        ElseIf (args.KeyCode = Keys.PageDown) Then
            MoveSelectionInListBox((LstSelectedIndex + 10))
            args.Handled = True
        ElseIf (args.KeyCode = Keys.Enter) Then
            SelectItem()
            args.Handled = True
        Else
            MyBase.OnKeyDown(args)
        End If
    End Sub
#End Region

#Region "重载光标离开事件"
    ''' 
    ''' 重载光标离开事件
    ''' 
    ''' 
    Protected Overrides Sub OnLeave(e As EventArgs)
        If Not panel.ContainsFocus Then
            Me.HideSuggestionListBox()
        End If

        MyBase.OnLeave(e)
    End Sub
#End Region

#Region "重载文本改变事件"
    ''' 
    ''' 重载文本改变事件
    ''' 
    ''' 
    Protected Overrides Sub OnTextChanged(ByVal args As EventArgs)
        '避免崩溃
        If Not Me.DesignMode Then ShowSuggests()
        MyBase.OnTextChanged(args)
        '记录输入
        oldText = Me.Text
    End Sub
#End Region

#Region "重载索引改变事件"
    ''' 
    ''' 重载索引改变事件
    ''' 
    ''' 
    Protected Overrides Sub OnSelectedIndexChanged(ByVal e As EventArgs)
        Me.HideSuggestionListBox()
        MyBase.OnSelectedIndexChanged(e)
    End Sub
#End Region

#Region "ListBox按键事件"
    ''' 
    ''' ListBox按键事件
    ''' 
    ''' 
    ''' 
    Private Sub ListBox_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
        If e.KeyCode = Keys.Enter Then
            '选择当前项目
            SelectItem()
            e.Handled = True
        End If
    End Sub
#End Region

#Region "ListBox鼠标单击事件"
    ''' 
    ''' ListBox鼠标单击事件
    ''' 
    ''' 
    ''' 
    Private Sub ListBox_MouseClick(ByVal sender As Object, ByVal e As MouseEventArgs)
        SelectItem()
    End Sub
#End Region

#Region "ListBox鼠标双击事件"
    ''' 
    ''' ListBox鼠标双击事件
    ''' 
    ''' 
    ''' 
    Private Sub ListBox_MouseDoubleClick(ByVal sender As Object, ByVal e As MouseEventArgs)
        SelectItem()
    End Sub
#End Region

#Region "移动ListBox项选择索引"
    ''' 
    ''' 移动ListBox项选择索引
    ''' 
    ''' 
    Private Sub MoveSelectionInListBox(ByVal Index As Integer)
        If Index <= -1 Then
            Me.LstSelectedIndex = 0
        Else

            If Index > (listBox.Items.Count - 1) Then
                LstSelectedIndex = (listBox.Items.Count - 1)
            Else
                LstSelectedIndex = Index
            End If
        End If
    End Sub
#End Region

#Region "选择项"
    ''' 
    ''' 选择项
    ''' 
    ''' 
    Private Function SelectItem() As Boolean
        '如果列表不为空
        If ((Me.listBox.Items.Count > 0) AndAlso (Me.LstSelectedIndex > -1)) Then
            '精确匹配项
            Dim intSel As Integer = Me.FindStringExact(Me.listBox.SelectedItem.ToString())
            '有匹配项则显示
            If intSel <> -1 Then
                Me.SelectedIndex = intSel
            End If
            '隐藏
            Me.HideSuggestionListBox()
        End If

        Return True
    End Function
#End Region

#Region "显示模糊匹配词汇"
    ''' 
    ''' 显示模糊匹配词汇
    ''' 
    Private Sub ShowSuggests()
        '如果输入框文字长度大于限制长度
        If Me.Text.Length >= MinTypedCharacters Then
            '防止与其他控件重叠的问题
            '在加载数据时没有绘制,所以挂起布局
            panel.SuspendLayout()

            '修改当前数据源
            UpdateCurrentAutoCompleteList()

            If ((CurrentAutoCompleteList IsNot Nothing) AndAlso CurrentAutoCompleteList.Count > 0) Then
                '最后显示面板和列表框
                '刷新以防止绘制空矩形
                panel.Show()
                '设置在所有控件的顶部
                panel.BringToFront()

                '然后把焦点回到组合框
                Me.Focus()
                '这句很重要,注释掉,文本变化时会全选文本
                '设置选定文本的开始索引,即将光标定位到文本末尾
                Me.SelectionStart = Me.Text.Length
            Else
                Me.HideSuggestionListBox()
            End If
            '防止与其他控件重叠的问题
            panel.ResumeLayout(True)
        Else
            Me.HideSuggestionListBox()
        End If
    End Sub
#End Region

#Region "修改当前数据源"
    ''' 
    ''' 修改当前数据源
    ''' 
    Private Sub UpdateCurrentAutoCompleteList()
        '清除列表项
        CurrentAutoCompleteList.Clear()
        '获取数据源,如果数据源不是DataTable怎么处理?
        '期待你来解答
        Dim dt As DataTable = TryCast(Me.DataSource, DataTable)

        Dim strDisplayMember As String = Me.DisplayMember
        '如果没有绑定数据源及DisplayMember则结束
        If dt Is Nothing AndAlso String.IsNullOrWhiteSpace(strDisplayMember) Then Return

        Dim strSelItem As String = String.Empty
        For Each row As DataRow In dt.Rows
            strSelItem = row.Item(strDisplayMember).ToString
            '查找子串
            If (strSelItem.IndexOf(Me.Text) > -1) Then
                CurrentAutoCompleteList.Add(strSelItem)
            ElseIf (strSelItem.ToLower().IndexOf(Me.Text.ToLower()) > -1) Then
                CurrentAutoCompleteList.Add(strSelItem)
            End If
        Next

        '绑定数据源后,Obj.ToString是类名,得不到文本
        'For Each Obj As Object In Me.Items
        '    If (Obj.ToString.IndexOf(Me.Text) > -1) Then
        '        CurrentAutoCompleteList.Add(Obj.ToString)
        '    ElseIf (Obj.ToString.ToLower().IndexOf(Me.Text.ToLower()) > -1) Then
        '        CurrentAutoCompleteList.Add(Obj.ToString)
        '    End If
        'Next

        '继续更新列表框的UI部分
        UpdateListBoxItems()
    End Sub
#End Region

#Region "修改ListBox数据源"
    ''' 
    ''' 修改ListBox数据源
    ''' 
    Private Sub UpdateListBoxItems()
        SyncLock _lockObj
            If CurrentAutoCompleteList.Count <= 0 Then Return
            '如果父窗体不为空
            If (ParentForm IsNot Nothing) Then
                '获取宽度
                panel.Width = Me.Width
                If CurrentAutoCompleteList.Count >= 10 Then
                    '容器高度
                    panel.Height = Me.listBox.ItemHeight * 10
                ElseIf CurrentAutoCompleteList.Count <= 3 Then
                    panel.Height = Me.listBox.ItemHeight * CurrentAutoCompleteList.Count + Me.listBox.ItemHeight
                Else
                    panel.Height = Me.listBox.ItemHeight * CurrentAutoCompleteList.Count
                End If
                '容器显示位置
                panel.Location = New Point(Me.Location.X, Me.Location.Y + Me.Height)
                '这里偶尔会报错,没找到原因
                If Not Me.ParentForm.Controls.Contains(panel) Then
                    Me.ParentForm.Controls.Add(panel)
                End If
                '更新列表
                Me.listBox.DataSource = Nothing
                Me.listBox.DataSource = CurrentAutoCompleteList
            End If
        End SyncLock
    End Sub
#End Region
End Class

窗体调用代码:

Public Class Form1
    Dim dt As New DataTable("MyTable")
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        DataGridView1.DataSource = dt

        dt.Columns.Add("MyID", Type.GetType("System.Int32")).Caption = "ID"
        dt.Columns.Add("Myname", Type.GetType("System.String")).Caption = "name"

        Dim row As DataRow
        For i As Integer = 1 To 100
            row = dt.NewRow
            row("MyID") = i
            row("Myname") = "No" & i.ToString
            dt.Rows.Add(row)
        Next
        
        row = dt.NewRow
        row("MyID") = 0
        row("Myname") = "张三丰|ZSF"
        dt.Rows.Add(row)

        With ComboBoxEx1
            '这几句的顺序很有讲究
            .DisplayMember = "Myname"
            .ValueMember = "MyID"
            .DataSource = dt
        End With
    End Sub
End Class

在测试过程中发现的一些问题:
1、光标定位问题。

'这句很重要,注释掉,文本变化时会全选文本
                '设置选定文本的开始索引,即将光标定位到文本末尾
                Me.SelectionStart = Me.Text.Length

如果把这句代码注释掉,每当文本发生改变自动全选所有文本,影响输入,如图:
在这里插入图片描述
2、DataSource绑定数据源问题
用Items获取项时,其ToString方法得到是一个“System.Data.DataRowView”类的name文本,也就是说绑定数据源后无法通过Items正确获取文本,如图:
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第3张图片
解决办法:
可以参考《c#(winform)中ComboBox和ListBox添加项和设定预选项完全解决》
我是直接操作数据源来解决。

'获取数据源,如果数据源不是DataTable怎么处理?
        '期待你来解答
        Dim dt As DataTable = TryCast(Me.DataSource, DataTable)

        Dim strDisplayMember As String = Me.DisplayMember
        '如果没有绑定数据源及DisplayMember则结束
        If dt Is Nothing AndAlso String.IsNullOrWhiteSpace(strDisplayMember) Then Return

        Dim strSelItem As String = String.Empty
        For Each row As DataRow In dt.Rows
            strSelItem = row.Item(strDisplayMember).ToString
            '查找子串
            If (strSelItem.IndexOf(Me.Text) > -1) Then
                CurrentAutoCompleteList.Add(strSelItem)
            ElseIf (strSelItem.ToLower().IndexOf(Me.Text.ToLower()) > -1) Then
                CurrentAutoCompleteList.Add(strSelItem)
            End If
        Next

但这里又有个问题,如果数据源不是DataTable怎么处理?
另外,绑定数据源时,下面三句代码的顺序很有讲究:

With ComboBoxEx1
            '这几句的顺序很有讲究
            .DisplayMember = "Myname"
            .ValueMember = "MyID"
            .DataSource = dt
        End With

具体原因可以查看《winform Combobox出现System.Data.DataRowView的解决办法》
3、时不时会在下面代码蹦出一个错误:System.ArgumentOutOfRangeException:“InvalidArgument=“2”的值对于“SelectedIndex”无效。

'这里偶尔会报错,没找到原因
                If Not Me.ParentForm.Controls.Contains(panel) Then
                    Me.ParentForm.Controls.Add(panel)
                End If

如图:
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第4张图片
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第5张图片
是什么原因出错,目前还没有找到,如果你知道,敬请赐教!
4、在显示匹配列表框时,单击组合框下三角又会弹出一个列表框
我想实现当显示匹配列表时不再显示组合框原有列表,目前没找到方法。

相关资源可以到我的下载资源里下载:https://download.csdn.net/user/zyjq52uys/uploads

2019.06.12后续
又进行了长时间的调试,发现输入10,按向下键选择No100,按回车确认选择,然后输入“张”就会弹出错误,如图:
VB.NET学习笔记:WinForm扩展ComboBox控件——仿百度搜索框(输入文本智能模糊提示说明、自动匹配过滤)_第6张图片
终于找到如何引发错误了,但始终找不出到底在哪出错,猜想应该与listBox控件有关。于是单独抽出ListBox控件进行测试,发现数据源为List集合时,更新List集合时ListBox控件的数据源并没有跟着更新,很是奇怪,于是在CSDN搜,发现了《如何解决List集合类数据源变更UI不能自动刷新的问题》一文,把此文的解决办法, 改用BindingSource 或者 BindingList 泛型类来实施绑定,问题成功解决。

另外,在为ComboBox控件绑定数据源时的顺序问题,会多次引发SelectedIndexChanged和SelectedValueChanged事件,可能会造成报错,在《C#–SelectedIndexChanged事件, SelectedValueChanged事件和SelectionChangeCommitted事件的区别及联系》一文中作者进行了详细的测试,我很懒去研究到底该用怎样的顺序为好,所以想到一个一劳永逸的方法,就是在绑定数据源时暂时移除SelectedIndexChanged和SelectedValueChanged事件,绑定结束后再重新绑定SelectedIndexChanged和SelectedValueChanged事件。

对于ComboBox控件需要绑定不同的数据源时,可以通过事件为ListBox控件的数据源CurrentAutoCompleteList赋值。即声明一个事件,然后在UpdateCurrentAutoCompleteList方法中引发事件,然后把原来在UpdateCurrentAutoCompleteList方法中的代码写到事件函数中。

你可能感兴趣的:(VB.NET扩展控件,百度搜索,模糊匹配,智能过滤,扩展ComboBox)