using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Melskin.Controls { /// /// 搜索过滤的下拉框控件 /// 自定义了ItemTemplate的时候,SearchableComboBox 设置 TextSearch.TextPath 附加属性,这个属性告诉 ComboBox:“虽然我的下拉列表长得很复杂(有颜色、有图标),但当你需要把我当成‘文本’处理(比如显示在输入框里、或者搜索时),请使用这个属性” /// public class SearchableComboBox : ComboBox { private TextBox? editableTextBox; private bool isInternalOperation; static SearchableComboBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchableComboBox), new FrameworkPropertyMetadata(typeof(SearchableComboBox))); } public SearchableComboBox() { IsEditable = true; IsTextSearchEnabled = false; StaysOpenOnEdit = true; } public override void OnApplyTemplate() { base.OnApplyTemplate(); editableTextBox = GetTemplateChild("PART_EditableTextBox") as TextBox; if (editableTextBox != null) { editableTextBox.TextChanged += OnEditableTextBoxTextChanged; // 点击时全选 editableTextBox.GotFocus += (s, e) => editableTextBox.SelectAll(); } } protected override void OnSelectionChanged(SelectionChangedEventArgs e) { if (isInternalOperation) return; isInternalOperation = true; base.OnSelectionChanged(e); // 重点 1:选中项改变后,必须彻底清空过滤器,否则下次打开下拉框只剩下一项 this.Items.Filter = null; isInternalOperation = false; } private void OnEditableTextBoxTextChanged(object sender, TextChangedEventArgs e) { if (isInternalOperation) return; if (editableTextBox == null) return; string searchText = editableTextBox.Text; string selectedText = GetItemDisplayText(SelectedItem); // 重点 2:断开锁定逻辑 // 如果文本框的内容和当前选中项的文本不一致,说明用户正在打字替换内容 if (SelectedItem != null && searchText != selectedText) { isInternalOperation = true; SelectedItem = null; // 必须将 SelectedItem 设为 null,否则 WPF 会强行还原文本 isInternalOperation = false; } // 重点 3:执行过滤 this.Items.Filter = item => { if (string.IsNullOrEmpty(searchText)) return true; string itemText = GetItemDisplayText(item); // 只要包含关键字就显示(忽略大小写) return itemText.Contains(searchText); }; // 自动打开下拉框 if (!IsDropDownOpen && editableTextBox.IsFocused && !string.IsNullOrEmpty(searchText)) { IsDropDownOpen = true; } } private string GetItemDisplayText(object item) { if (item == null) return string.Empty; if (item is ComboBoxItem cbi) return cbi.Content?.ToString() ?? string.Empty; // 兼容 DisplayMemberPath 或 TextSearch.TextPath string path = DisplayMemberPath; if (string.IsNullOrEmpty(path)) path = TextSearch.GetTextPath(this); if (!string.IsNullOrEmpty(path)) { var prop = item.GetType().GetProperty(path); if (prop != null) return prop.GetValue(item)?.ToString() ?? string.Empty; } return item.ToString()!; } } }