WPF 组合框中的自动建议

本文关键字:组合 WPF | 更新日期: 2023-09-27 18:31:57

My combobox 从 s 存储过程中返回一组值,如下所示

private void BindCombo()
{
    DataCombo.FillCombo(ComboDS(2313001), cmbClass, 0);
    DataCombo.FillCombo(DDCombo(5007), cmbGroup, 0);
}

我设法给出了一个基本的自动完成建议IsTextSearchenabled但无法获得我想要的自动建议箱。

我已经看到了很多自动完成/暗示性文本框的示例,但它们似乎都不适合我。

这段代码显然适合我。

但是我将如何使用这里的自动建议

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DotNetZen.AutoFilteredComboBox
{
    public class AutoFilteredComboBox : ComboBox
    {
        private int silenceEvents = 0;
    /// <summary>
    /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
    /// </summary>
    public AutoFilteredComboBox()
    {
        DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty(
            ComboBox.TextProperty, typeof(AutoFilteredComboBox));
        textProperty.AddValueChanged(this, this.OnTextChanged);
        this.RegisterIsCaseSensitiveChangeNotification();
    }
    #region IsCaseSensitive Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsCaseSensitiveProperty =
        DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
    /// <summary>
    /// Gets or sets the way the combo box treats the case sensitivity of typed text.
    /// </summary>
    /// <value>The way the combo box treats the case sensitivity of typed text.</value>
    [System.ComponentModel.Description("The way the combo box treats the case sensitivity of typed text.")]
    [System.ComponentModel.Category("AutoFiltered ComboBox")]
    [System.ComponentModel.DefaultValue(true)]
    public bool IsCaseSensitive
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(IsCaseSensitiveProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(IsCaseSensitiveProperty, value);
        }
    }
    protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e)
    {
        if (this.IsCaseSensitive)
            this.IsTextSearchEnabled = false;
        this.RefreshFilter();
    }
    private void RegisterIsCaseSensitiveChangeNotification()
    {
        System.ComponentModel.DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)).AddValueChanged(
            this, this.OnIsCaseSensitiveChanged);
    }
    #endregion
    #region DropDownOnFocus Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty DropDownOnFocusProperty =
        DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(true));
    /// <summary>
    /// Gets or sets the way the combo box behaves when it receives focus.
    /// </summary>
    /// <value>The way the combo box behaves when it receives focus.</value>
    [System.ComponentModel.Description("The way the combo box behaves when it receives focus.")]
    [System.ComponentModel.Category("AutoFiltered ComboBox")]
    [System.ComponentModel.DefaultValue(true)]
    public bool DropDownOnFocus
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(DropDownOnFocusProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(DropDownOnFocusProperty, value);
        }
    }
    #endregion
    #region | Handle selection |
    /// <summary>
    /// Called when <see cref="ComboBox.ApplyTemplate()"/> is called.
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.EditableTextBox.SelectionChanged += this.EditableTextBox_SelectionChanged;
    }
    /// <summary>
    /// Gets the text box in charge of the editable portion of the combo box.
    /// </summary>
    protected TextBox EditableTextBox
    {
        get
        {
            return ((TextBox)base.GetTemplateChild("PART_EditableTextBox"));
        }
    }
    private int start = 0, length = 0;
    private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e)
    {
        if (this.silenceEvents == 0)
        {
            this.start = ((TextBox)(e.OriginalSource)).SelectionStart;
            this.length = ((TextBox)(e.OriginalSource)).SelectionLength;
            this.RefreshFilter();
        }
    }
    #endregion
    #region | Handle focus |
    /// <summary>
    /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
    /// reaches this element in its route.
    /// </summary>
    /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);
        if (this.ItemsSource != null && this.DropDownOnFocus)
        {
            this.IsDropDownOpen = true;
        }
    }
    #endregion
    #region | Handle filtering |
    private void RefreshFilter()
    {
        if (this.ItemsSource != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
            view.Refresh();
            this.IsDropDownOpen = true;
        }
    }
    private bool FilterPredicate(object value)
    {
        // We don't like nulls.
        if (value == null)
            return false;
        // If there is no text, there's no reason to filter.
        if (this.Text.Length == 0)
            return true;
        string prefix = this.Text;
        // If the end of the text is selected, do not mind it.
        if (this.length > 0 && this.start + this.length == this.Text.Length)
        {
            prefix = prefix.Substring(0, this.start);
        }
        return value.ToString()
            .StartsWith(prefix, !this.IsCaseSensitive, CultureInfo.CurrentCulture);
    }
    #endregion
    /// <summary>
    /// Called when the source of an item in a selector changes.
    /// </summary>
    /// <param name="oldValue">Old value of the source.</param>
    /// <param name="newValue">New value of the source.</param>
    protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
    {
        if (newValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
            view.Filter += this.FilterPredicate;
        }
        if (oldValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
            view.Filter -= this.FilterPredicate;
        }
        base.OnItemsSourceChanged(oldValue, newValue);
    }
    private void OnTextChanged(object sender, EventArgs e)
    {
        if (!this.IsTextSearchEnabled && this.silenceEvents == 0)
        {
            this.RefreshFilter();
            // Manually simulate the automatic selection that would have been
            // available if the IsTextSearchEnabled dependency property was set.
            if (this.Text.Length > 0)
            {
                foreach (object item in CollectionViewSource.GetDefaultView(this.ItemsSource))
                {
                    int text = item.ToString().Length, prefix = this.Text.Length;
                    this.SelectedItem = item;
                    this.silenceEvents++;
                    this.EditableTextBox.Text = item.ToString();
                    this.EditableTextBox.Select(prefix, text - prefix);
                    this.silenceEvents--;
                    break;
                }
            }
        }
    }
}
}

WPF 组合框中的自动建议

还发现AutoFilteredComboBox使用起来非常简单。虽然我做了一些更改:

  • 删除了 DependencyPropertyDescriptor 的使用,以避免组合框对象的内存泄漏
  • 引入了 FilterItem-event 和 FilterList-event,以允许自定义过滤
  • 将默认筛选从字符串开头更改为包含字符串
  • 删除了对启用 IsTextSearchEnabled 的支持
  • 一旦更改搜索字符串,就会显示下拉列表,因此会显示搜索结果

如何使用它的示例:

<Controls:AutoFilteredComboBox ItemsSource="{Binding ViewModel.AvailableItems}"
        SelectedValue="{Binding ViewModel.SelectedItem, Mode=TwoWay}"
        IsEditable="True" IsTextSearchEnabled="False"/>

AutoFilteredComboBox 的改进版本:

public class AutoFilteredComboBox : ComboBox
{
    bool _ignoreTextChanged;
    string _currentText;
    /// <summary>
    /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
    /// </summary>
    public AutoFilteredComboBox()
    {
        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) return;
    }
    public event Func<object, string, bool> FilterItem;
    public event Action<string> FilterList;
    #region IsCaseSensitive Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsCaseSensitiveProperty =
        DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
    /// <summary>
    /// Gets or sets the way the combo box treats the case sensitivity of typed text.
    /// </summary>
    /// <value>The way the combo box treats the case sensitivity of typed text.</value>
    [Description("The way the combo box treats the case sensitivity of typed text.")]
    [Category("AutoFiltered ComboBox")]
    [DefaultValue(true)]
    public bool IsCaseSensitive
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(IsCaseSensitiveProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(IsCaseSensitiveProperty, value);
        }
    }
    #endregion
    #region DropDownOnFocus Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty DropDownOnFocusProperty =
        DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
    /// <summary>
    /// Gets or sets the way the combo box behaves when it receives focus.
    /// </summary>
    /// <value>The way the combo box behaves when it receives focus.</value>
    [Description("The way the combo box behaves when it receives focus.")]
    [Category("AutoFiltered ComboBox")]
    [DefaultValue(false)]
    public bool DropDownOnFocus
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(DropDownOnFocusProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(DropDownOnFocusProperty, value);
        }
    }
    #endregion
    #region | Handle focus |
    /// <summary>
    /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
    /// reaches this element in its route.
    /// </summary>
    /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);
        if (this.ItemsSource != null && this.DropDownOnFocus)
        {
            this.IsDropDownOpen = true;
        }
    }
    #endregion
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        AddHandler(TextBox.TextChangedEvent, new TextChangedEventHandler(OnTextChanged));
        KeyUp += AutoFilteredComboBox_KeyUp;
        this.IsTextSearchEnabled = false;
    }
    void AutoFilteredComboBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Down)
        {
            if (this.IsDropDownOpen == true)
            {
                // Ensure that focus is given to the dropdown list
                if (Keyboard.FocusedElement is TextBox)
                {
                    Keyboard.Focus(this);
                    if (this.Items.Count > 0)
                    {
                        if (this.SelectedIndex == -1 || this.SelectedIndex==0)
                            this.SelectedIndex = 0;
                    }
                }
            }
        }
        if (Keyboard.FocusedElement is TextBox)
        {
            if (e.OriginalSource is TextBox)
            {
                // Avoid the automatic selection of the first letter (As next letter will cause overwrite)
                TextBox textBox = e.OriginalSource as TextBox;
                if (textBox.Text.Length == 1 && textBox.SelectionLength == 1)
                {
                    textBox.SelectionLength = 0;
                    textBox.SelectionStart = 1;
                }
            }
        }
    }
    #region | Handle filtering |
    private void RefreshFilter()
    {
        if (this.ItemsSource != null)
        {
            Action<string> filterList = FilterList;
            if (filterList != null)
            {
                filterList(_currentText);
            }
            else
            {
                ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
                view.Refresh();
            }
            this.SelectedIndex = -1;    // Prepare so arrow down selects first
            this.IsDropDownOpen = true;
        }
    }
    private bool FilterPredicate(object value)
    {
        // We don't like nulls.
        if (value == null)
            return false;
        // If there is no text, there's no reason to filter.
        if (string.IsNullOrEmpty(_currentText))
            return true;
        Func<object, string, bool> filterItem = FilterItem;
        if (filterItem != null)
            return filterItem(value, _currentText);
        if (IsCaseSensitive)
            return value.ToString().Contains(_currentText);
        else
            return value.ToString().ToUpper().Contains(_currentText.ToUpper());                
    }
    #endregion
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        try
        {
            _ignoreTextChanged = true;  // Ignore the following TextChanged
            base.OnSelectionChanged(e);
        }
        finally
        {
            _ignoreTextChanged = false;
        }
    }
    /// <summary>
    /// Called when the source of an item in a selector changes.
    /// </summary>
    /// <param name="oldValue">Old value of the source.</param>
    /// <param name="newValue">New value of the source.</param>
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (newValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
            if (FilterList == null)
                view.Filter += this.FilterPredicate;
        }
        if (oldValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
            view.Filter -= this.FilterPredicate;
        }
        base.OnItemsSourceChanged(oldValue, newValue);
    }
    private void OnTextChanged(object sender, TextChangedEventArgs e)
    {
        if (_ignoreTextChanged)
            return;
        _currentText = Text;
        if (!this.IsTextSearchEnabled)
        {
            this.RefreshFilter();
        }
    }

我找到了一个超级简单的解决方法来解决我的问题。

我创建了组合框的预览文本输入事件。

然后我就写了

Combobox.IsDropDownOpen = true

可能不是最优雅的,但在我的情况下有效