using System.Collections; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; namespace VariaStudio.Controls { /// /// AutoComplete 控件提供了一个文本框,用户可以在其中输入文本,并从一个下拉列表中选择建议项。该控件适用于需要快速完成或查找功能的场景。 /// /// /// 通过设置 ItemsSource 属性,可以为 AutoComplete 控件提供数据源,用于填充建议列表。当用户在文本框中输入时,会根据输入的内容显示匹配的建议项。 /// [TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))] [TemplatePart(Name = "PART_Popup", Type = typeof(Popup))] [TemplatePart(Name = "PART_ListBox", Type = typeof(ListBox))] public class AutoComplete : Control { private TextBox? textBox; private Popup? popup; private ListBox? listBox; private bool isSelectionChanging = false; private const int filterDelay = 200; private CancellationTokenSource? cts; #region Dependency Properties /// /// 用于设置或获取 AutoComplete 控件的数据源。该属性接受实现了 IEnumerable 接口的任何集合,这些数据将作为建议项显示给用户。 /// public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(AutoComplete), new PropertyMetadata(null, OnItemsSourceChanged)); /// /// 用于设置或获取 AutoComplete 控件的数据源。该属性接受实现了 IEnumerable 接口的任何集合,这些数据将作为建议项显示给用户。 /// public IEnumerable ItemsSource { get => (IEnumerable)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } /// /// 用于设置或获取 AutoComplete 控件中文本框显示的文本。此属性支持双向数据绑定,允许在控件与数据源之间同步文本内容。 /// public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(AutoComplete), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnTextChanged)); /// /// 用于获取或设置 AutoComplete 控件中显示的文本。此属性支持双向数据绑定,允许用户输入的文本与外部数据源之间进行同步。 /// public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// 用于设置或获取 AutoComplete 控件中当前选中的项。该属性支持双向数据绑定,可以用来反映用户从建议列表中选择的项目。 /// public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(AutoComplete), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); /// /// 获取或设置当前在 AutoComplete 控件中选中的项。此属性支持双向数据绑定,允许用户选择的项能够被外部逻辑访问和修改。 /// public object? SelectedItem { get => GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } /// /// 用于设置或获取 AutoComplete 控件的下拉列表是否展开的状态。该属性为布尔值,当设置为 true 时,表示下拉列表处于打开状态;反之则关闭。 /// public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register(nameof(IsDropDownOpen), typeof(bool), typeof(AutoComplete), new PropertyMetadata(false)); /// /// /// public bool IsDropDownOpen { get => (bool)GetValue(IsDropDownOpenProperty); set => SetValue(IsDropDownOpenProperty, value); } /// /// /// public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(AutoComplete), new PropertyMetadata(string.Empty)); /// /// /// public string PlaceholderText { get => (string)GetValue(PlaceholderTextProperty); set => SetValue(PlaceholderTextProperty, value); } #endregion static AutoComplete() { DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoComplete), new FrameworkPropertyMetadata(typeof(AutoComplete))); } /// public override void OnApplyTemplate() { base.OnApplyTemplate(); if (textBox != null) { textBox.TextChanged -= OnTextBoxTextChanged; textBox.PreviewKeyDown -= OnTextBoxPreviewKeyDown; } if (listBox != null) { listBox.SelectionChanged -= OnListBoxSelectionChanged; } textBox = GetTemplateChild("PART_TextBox") as TextBox; popup = GetTemplateChild("PART_Popup") as Popup; listBox = GetTemplateChild("PART_ListBox") as ListBox; if (textBox != null) { textBox.TextChanged += OnTextBoxTextChanged; textBox.PreviewKeyDown += OnTextBoxPreviewKeyDown; } if (listBox != null) { listBox.SelectionChanged += OnListBoxSelectionChanged; } } private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (AutoComplete)d; if (control.listBox != null) { control.listBox.ItemsSource = e.NewValue as IEnumerable; } } private static async void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (AutoComplete)d; if (control.isSelectionChanging) return; if (control.textBox != null && control.textBox.Text != (string)e.NewValue) { control.textBox.Text = (string)e.NewValue; } control.cts?.Cancel(); control.cts = new CancellationTokenSource(); try { await Task.Delay(filterDelay, control.cts.Token).ConfigureAwait(true); control.FilterItemsSource(); } catch (TaskCanceledException) { // Ignore cancellation } } private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e) { if (isSelectionChanging) return; // 同步Text属性 if (textBox != null && Text != textBox.Text) { Text = textBox.Text; } // 清空我们自己控件的选中项 SelectedItem = null; // 同时清空ListBox内部的选中项 if (listBox is { SelectedItem: not null }) { listBox.SelectedItem = null; } } private void OnTextBoxPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Down && listBox is { Items.Count: > 0 }) { listBox.Focus(); listBox.SelectedIndex = 0; } else if (e.Key == Key.Escape) { IsDropDownOpen = false; } } private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.AddedItems.Count == 0 || listBox?.SelectedItem == null) return; isSelectionChanging = true; SelectedItem = listBox.SelectedItem; Text = SelectedItem?.ToString() ?? string.Empty; IsDropDownOpen = false; textBox?.Focus(); textBox?.Select(textBox.Text.Length, 0); isSelectionChanging = false; } private void FilterItemsSource() { if (ItemsSource == null) return; var view = CollectionViewSource.GetDefaultView(ItemsSource); if (string.IsNullOrEmpty(Text)) { view.Filter = null; } else { view.Filter = item => item.ToString()?.IndexOf(Text, StringComparison.OrdinalIgnoreCase) >= 0; } view.Refresh(); // !string.IsNullOrEmpty(Text) 的判断 if (!string.IsNullOrEmpty(Text) && !view.IsEmpty && textBox is { IsKeyboardFocused: true }) { IsDropDownOpen = true; } else { IsDropDownOpen = false; } } } }