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; 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.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0; }; // 自动打开下拉框 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(); } } }