WPF的自动完成组合框

13

我需要一个适用于 WPF C# 的自动完成组合框。我尝试过几种方法,但都无效。例如,我尝试了组合框:

<ComboBox  Width="200"
      IsEditable="True"
      ItemsSource="{Binding Names}"
      IsTextSearchEnabled="True"
      HorizontalAlignment="Left"/>

Names 是一个字符串列表,其中包括 Peter John、John、John Doe、Cathy、Howard、John Richards 等等

如果您输入一个名字(例如 John),下拉框应该会展开,并且应该会看到以下内容:

  • John
  • John Doe
  • John Richards
  • Peter John

但这并不起作用。我该怎么做?


到目前为止,这是最好的解决方案。但不幸的是,它只查找列表中的第一个字母。例如:我正在搜索“John”,但没有“Peter John”。 - Struct
2
您可以配置“FilterMode”,例如使用“Contains”。 - Bolu
8个回答

12

经过许多调试,我终于找到了一个完整、可行的解决方案(至少看起来是这样的)。

步骤1. 修改XAML标记

您需要像下面这样修改ComboBox:

<ComboBox
    ...
    IsTextSearchEnabled="False"
    ...
    PreviewTextInput="PreviewTextInput_EnhanceComboSearch"
    PreviewKeyUp="PreviewKeyUp_EnhanceComboSearch"
    DataObject.Pasting="Pasting_EnhanceComboSearch" />

例如,要禁用默认文本搜索,并添加事件处理程序以处理用户添加、删除和粘贴文本。

步骤2. 添加一个辅助函数,用于获取ComboBox的内部TextBox(因为WPF)

为了使PreviewTextInput_EnhanceComboSearchPasting_EnhanceComboSearch正常工作,您需要访问ComboBox的插入符号。不幸的是,要做到这一点,您需要遍历视觉树(感谢Matt Hamilton)。您可以在扩展方法中执行此操作,但我在我的Page类中使用了静态方法:

public static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
    if (depObj == null) return null;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
    {
        var child = VisualTreeHelper.GetChild(depObj, i);

        var result = (child as T) ?? GetChildOfType<T>(child);
        if (result != null) return result;
    }
    return null;
}

步骤三. 实现事件处理程序

请注意,我使用了

s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1 

步骤三.a 在ComboBox内输入时触发搜索

PreviewTextInput处理程序运行时,ComboBox中的.Text属性包含修改之前的文本。因此,我们需要使用GetChildOfType方法获取ComboBox的内部TextBox,以便获取其插入光标的确切位置。

为了实现不区分大小写的检查,可以使用以下代码:s => s.ToLower().Contains(e.Text.ToLower())。记得根据需要更改这一部分内容。

private void PreviewTextInput_EnhanceComboSearch(object sender, TextCompositionEventArgs e)
{
    ComboBox cmb = (ComboBox)sender;

    cmb.IsDropDownOpen = true;

    if (!string.IsNullOrEmpty(cmb.Text))
    {
        string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, e.Text);
        cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else if (!string.IsNullOrEmpty(e.Text))
    {
        cmb.ItemsSource = Names.Where(s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else
    {
        cmb.ItemsSource = Names;
    }
}

步骤 3.b:用户在组合框中粘贴时触发搜索

DataObject.Pasting 处理程序的行为类似于 PreviewTextInput 处理程序,因此我们需要再次使用插入符号。

private void Pasting_EnhanceComboSearch(object sender, DataObjectPastingEventArgs e)
{
    ComboBox cmb = (ComboBox)sender;

    cmb.IsDropDownOpen = true;

    string pastedText = (string)e.DataObject.GetData(typeof(string));
    string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, pastedText);

    if (!string.IsNullOrEmpty(fullText))
    {
        cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
    }
    else
    {
        cmb.ItemsSource = Names;
    }
}

第三步c:当用户删除ComboBox中的文本(以及按下空格,因为WPF)时触发搜索

当用户按下Delete或Backspace键时,将触发此操作。

同时也会触发Space键,因为PreviewTextInput会忽略它,所以在示例中过滤出“John Doe”和“John Richards”中的“John”会很困难。

private void PreviewKeyUp_EnhanceComboSearch(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Back || e.Key == Key.Delete)
    {
        ComboBox cmb = (ComboBox)sender;

        cmb.IsDropDownOpen = true;

        if (!string.IsNullOrEmpty(cmb.Text))
        {
            cmb.ItemsSource = Names.Where(s => s.IndexOf(cmb.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
        }
        else
        {
            cmb.ItemsSource = Names;
        }
    }
}

...那应该就足够了。


1
不错,但是如果您按几次Esc键,所有条目都会消失。 - Chris
@Chris...或者如果你选择之前输入的文本并键入一个字母。是的,还有更多的边缘情况需要考虑。我想我会在有时间的时候进行更多的研究,但目前看来,这确实是需要一个经过充分测试的库的事情。 - Dragomok
3
请参考以下链接:https://dev59.com/NG865IYBdhLWcg3wlvmq - ice1e0
1
我不知道它是如此直截了当 :-| - Vidar

6
使用PreviewTextInput事件进行筛选并显示下拉框,示例如下:
private void ComboBox_TextInput_1(object sender, TextCompositionEventArgs e)
    {           
        cmbperson.IsDropDownOpen = true;
        cmbperson.ItemsSource = DataBase.Persons.Where(p => p.Name.Contains(e.Text)).ToList();
    }

不幸的是,在PreviewTextInput中,e.Text将是用户输入的任何内容,即键盘输入的单个字符。此外,由于这是PreviewTextInput,所以ComboBox.Text将是用户键入字符之前在框中的内容。然而,关于IsDropDownOpen的注释帮助我找到了解决方案,所以我不会给出反对意见。 - Dragomok
返回用户输入的最后一项! - Muhannad
算了,这太乱了。 - Muhannad

3

我建议您使用一个专门用于自动完成的控件,而不是使用组合框。许多公司都提供此类控件,这个免费的控件被认为很好。


谢谢Moti,但我觉得你的建议不适用于C#,对吗? - Struct
1
@Struct是为WPF设计的,它与C#一起工作。控件本身是用VB.NET编写的,但一旦编译成MSIL,对于库用户而言就像C#一样好用。 - Moti Azu
1
现在已经有C#版本,如果需要自定义,这很不错。 - Chris

1
这是对我有效的实现:


<ComboBox
    Name="ItemsControl"
    IsEditable="True"
    KeyUp="OnItemsControlKeyUp"

我检查文本是否自上次应用过滤器以来已更改(以避免在按下非字母数字键时进行过滤)。

private string _textBeforeFilter;

private void OnItemsControlKeyUp(object sender, KeyEventArgs e)
{
    var arrowKey = e.Key >= Key.Left && e.Key <= Key.Down;

    // if arrow key (navigating) or the text hasn't changed, then a we don't need to filter
    if (arrowKey || ItemsControl.Text.EqualsIgnoreCase(_textBeforeFilter)) return;

    _textBeforeFilter = ItemsControl.Text;

    var textIsEmpty = string.IsNullOrWhiteSpace(ItemsControl.Text);

    var itemsViewOriginal = (CollectionView) CollectionViewSource.GetDefaultView(ItemsControl.ItemsSource);
    // if the text is empty, then we show everything, otherwise filter based on the text 
    itemsViewOriginal.Filter = o => textIsEmpty || ((string) o).ContainsIgnoreCase(ItemsControl.Text);
}

注意:EqualsIgnoreCaseContainsIgnoreCase是扩展方法:

public static bool EqualsIgnoreCase(this string source, string value)
{
    return source.Equals(value, StringComparison.OrdinalIgnoreCase);
}

public static bool ContainsIgnoreCase(this string source, string value)
{
    return source.Contains(value, StringComparison.OrdinalIgnoreCase);
}

0

由于先前答案中提到的自动完成控件链接已不再有效,而在我看来使用预定义控件比重新发明轮子更可行,因此这里提供一个漂亮控件的链接。 您可以按照以下步骤进行安装。

Install-Package AutoCompleteTextBox

这里有一个演示,展示如何在你的代码中使用它。


0

8
请确保在发布链接之前添加足够的内容描述。 - Nilesh Singh

0
在XAML中,您应该设置 IsEditable=True 并添加 PreviewKeyDown 事件的处理程序:
private void ComboBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        var cmb = (ComboBox)sender;
        cmb.IsDropDownOpen = true;
        var textbox = cmb.Template.FindName("PART_EditableTextBox", cmb) as TextBox;
        cmb.ItemsSource = CurrentStorage.Organisations.Where(p => string.IsNullOrEmpty(cmb.Text) || p.Name.ToLower().Contains(textbox.Text.ToLower())).ToList();
    }

2
你好,欢迎来到Stack Overflow。请编辑您的答案,提供一个解释,以更好地帮助那些寻求相关但不同问题建议的人们。有关更多建议,请参阅“如何撰写良好的答案”。 - Ruzihm

0
使用 ComboBox.Items.Filter 来显示符合文本框中输入的文本的项目。这是一个示例:
            If cmb.Text = "" Then
                cmb.Items.Filter = Nothing
            Else
                Dim T = cmb.Text
                cmb.Items.Filter = Nothing
                Dim I = cmb.Items.IndexOf(T)
                cmb.Items.Filter = Function(x As String)
                                       If x.StartsWith(T) Then Return True
                                       If x.Contains(" " & T) Then Return True
                                       Return False
                                   End Function

                If I = -1 Then
                    cmb.SelectedIndex = -1
                    cmb.Text = T
                    Dim txt As TextBox = cmb.Template.FindName("PART_EditableTextBox", cmb)
                    txt.SelectionStart = T.Length
                Else
                    cmb.SelectedIndex = 0
                End If

            End If

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接