原文链接:http://blog.csdn.net/zhuo_wp/article/details/78821711 转载请注明出处
在利用WPF开发桌面软件时,我们常常会用到自动搜索功能:在一个搜索框中输入搜索关键字,得到搜索结果,并可以选择某一项搜索结果。WPF中没有这样的预定义控件,不过这一搜索功能可以简单的利用TextBox来实现,我们只需要监听TextBox的TextChanged事件,在事件处理方法中查找搜索源中与输入关键字匹配的项。但是,如果这样的搜索场景被大量使用的时候,最好的处理方式是封装一个可专门用于搜索功能的自定义控件。本文即是简要的说明其思路与简单实现的。
一 设计思路
如图所示,从界面上说,搜索控件主要有三个部分:
1 搜索关键字输入部分,主要用于搜索关键字的输入,可以用WPF中的TextBox来实现,需要监听TextBox的TextChanged事件。
2 清除搜索关键字按钮,用于一键清除输入框中的关键字,可用Button来实现。
3 搜索结果展示部分,用于展示搜索结果,并且只有在有搜索结果的时候才展示,可以用WPF中的Popup来实现。
此外,还可以显示搜索提示语和搜索图标。
在应用控件的时候,搜索数据源是可以从控件外部提供的,所以需要依赖属性来绑定搜索源,同时搜索提示语可以通过依赖属性来提供自定义设置功能。
二 代码结构
如图所示,整个控件的代码结构包括用于界面构建的SearchableTextBox.xaml文件,用于搜索逻辑控制的SearchableTextBox.cs文件,用于将原始数据构造成可搜索数据的SearchModel类,以及一些辅助方法类。
三 模板代码
如下所示,搜索控件的默认模板包括:
1 一个Path元素,用于显示搜索图标。
2 一个TextBlock控件,用于显示搜索提示语。
3 一个TextBox控件, 用于数据搜索关键字。
4 一个Button控件,用于一键清除输入框中的内容。
5 一个Popup和一个ListBox,用于显示搜索结果。
搜索模型SearchModel用于将需要被搜索的原始数据构造成可用于控件搜索的数据。模型中的SearchField字段用于搜索匹配,Name用于搜索结果展示,Tag字段可用于存放原始数据。其中SearchField和Name为必需字段,ID和Tag可根据实际需要选择赋值。
public class SearchModel
{
#region Fields
private string _id = string.Empty;
private string _name = string.Empty;
private string _searchField = string.Empty;
private object _tag = null;
#endregion
#region Properties
public string Id { get => _id; set => _id = value; }
public string Name { get => _name; set => _name = value; }
public string SearchField { get => _searchField; set => _searchField = value; }
public object Tag { get => _tag; set => _tag = value; }
#endregion
}
以下代码为控件的逻辑控制代码。其中需要注意的要点有:
1 当与模板中的TextBox的Text属性绑定的SearchText依赖属性的值发生改变时进行搜索。之所以不是监听TextBox的TextChanged事件进行搜索,是因为这样可以利用TextBox控件的延迟响应功能,避免短时间内向搜索框输入多个字符时没输入一个字符都会进行一次搜索。
2 代码中提供了一个默认的搜索方法,只是简单的利用字符串的Contains方法进行匹配。同时可以为控件的SearchMethod委托赋值传入自定义的搜索方法。
3 仍然监听TextBox的TextChanged事件是为了实时判断是否需要显示搜索提示语和清除按钮。
4 搜索结果可以通过鼠标或者方向键来选中。
5 当选中搜索结果中的一个项时,会触发SelectedSearchModelChanged事件
public class SearchableTextBox : Control
{
#region Consts
private const string SEARCH_ICON_STRING = "F1 M 14.8076,31.1139L 20.0677,25.9957C 19.3886,24.8199 19.25,23.4554 19.25,22C 19.25,17.5817 22.5817,14 27,14C 31.4183,14 35,17.5817 35,22C 35,26.4183 31.4183,29.75 27,29.75C 25.6193,29.75 24.3204,29.6502 23.1868,29.0345L 17.8861,34.1924C 17.105,34.9734 15.5887,34.9734 14.8076,34.1924C 14.0266,33.4113 14.0266,31.895 14.8076,31.1139 Z M 27,17C 24.2386,17 22,19.2386 22,22C 22,24.7614 24.2386,27 27,27C 29.7614,27 32,24.7614 32,22C 32,19.2386 29.7614,17 27,17 Z";
#endregion
#region Fields
private TextBlock _ttbSearchTips = null;
private TextBox _ttbInput = null;
private Button _btnClear = null;
private Popup _popup = null;
private ListBox _lstSearchResult = null;
private bool _canSearching = true;
#endregion
#region Propreties
public Func, IList> SearchMethod { get; set; }
#endregion
#region Dependency Properties
///
/// 控件圆角半径
///
public static readonly DependencyProperty CornerRadiusProperty
= DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(SearchableTextBox), new PropertyMetadata(new CornerRadius(0), null));
///
/// 是否显示搜索图标
///
public static readonly DependencyProperty IsShowSearchIconProperty
= DependencyProperty.Register("IsShowSearchIcon", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
///
/// 搜索图标前景色
///
public static readonly DependencyProperty SearchIconForegroundProperty
= DependencyProperty.Register("SearchIconForeground", typeof(Brush), typeof(SearchableTextBox), new PropertyMetadata(new SolidColorBrush(Colors.Gray), null));
///
/// 搜索图标
///
public static readonly DependencyProperty SearchIconProperty
= DependencyProperty.Register("SearchIcon", typeof(Geometry), typeof(SearchableTextBox), new PropertyMetadata(Geometry.Parse(SEARCH_ICON_STRING), null));
///
/// 搜索图标高度
///
public static readonly DependencyProperty SearchIconHeightProperty
= DependencyProperty.Register("SearchIconHeight", typeof(double), typeof(SearchableTextBox), new PropertyMetadata(16.0, null));
///
/// 搜索图标宽度
///
public static readonly DependencyProperty SearchIconWidthProperty
= DependencyProperty.Register("SearchIconWidth", typeof(double), typeof(SearchableTextBox), new PropertyMetadata(16.0, null));
///
/// 是否显示搜索提示文字
///
public static readonly DependencyProperty IsShowSearchTipsProperty
= DependencyProperty.Register("IsShowSearchTips", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
///
/// 只是是否可以显示搜索框
///
public static readonly DependencyProperty CanShowSearchTipsProperty
= DependencyProperty.Register("CanShowSearchTips", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
///
/// 搜索提示文字
///
public static readonly DependencyProperty SearchTipsProperty
= DependencyProperty.Register("SearchTips", typeof(string), typeof(SearchableTextBox), new PropertyMetadata("请输入搜索条件", null));
///
/// 搜索提示文字颜色
///
public static readonly DependencyProperty SearchTipsForegroundProperty
= DependencyProperty.Register("SearchTipsForeground", typeof(Brush), typeof(SearchableTextBox), new PropertyMetadata(new SolidColorBrush(Colors.Gray), null));
///
/// 是否显示全部搜索结果
///
public static readonly DependencyProperty IsShowAllSearchResultProperty
= DependencyProperty.Register("IsShowAllSearchResult", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
///
/// (在不显示全部搜索结果的条件下)可显示的搜索结果的最大数据量
///
public static readonly DependencyProperty MaxShownResultCountProperty
= DependencyProperty.Register("MaxShownResultCount", typeof(int), typeof(SearchableTextBox), new PropertyMetadata(10, null));
///
/// 传入的搜索源
///
public static readonly DependencyProperty SearchItemsSourceProperty
= DependencyProperty.Register("SearchItemsSource", typeof(IList), typeof(SearchableTextBox), new PropertyMetadata(null, null));
///
/// 搜索结果集合(用于绑定到ListBox)
///
public static readonly DependencyProperty SearchResultCollectionProperty
= DependencyProperty.Register("SearchResultCollection", typeof(ObservableCollection), typeof(SearchableTextBox), new PropertyMetadata(new ObservableCollection(), null));
///
/// 是否允许显示清除按钮
///
public static readonly DependencyProperty CanShowClearButtonProperty
= DependencyProperty.Register("CanShowClearButton", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, OnCanShowClearButtonPropertyChanged));
///
/// 输入的搜索条件字符串
///
public static readonly DependencyProperty SearchTextProperty
= DependencyProperty.Register("SearchText", typeof(string), typeof(SearchableTextBox), new PropertyMetadata("", OnSearchTextPropretyChanged));
///
/// 选中的搜索结果项
///
public static readonly DependencyProperty SelectedSearchItemProperty
= DependencyProperty.Register("SelectedSearchItem", typeof(SearchModel), typeof(SearchableTextBox), new PropertyMetadata(null));
#endregion
#region Dependency Property Wrappers
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public bool IsShowSearchIcon
{
get { return (bool)GetValue(IsShowSearchIconProperty); }
set { SetValue(IsShowSearchIconProperty, value); }
}
public Brush SearchIconForeground
{
get { return (Brush)GetValue(SearchIconForegroundProperty); }
set { SetValue(SearchIconForegroundProperty, value); }
}
public Geometry SearchIcon
{
get { return (Geometry)GetValue(SearchIconProperty); }
set { SetValue(SearchIconProperty, value); }
}
public double SearchIconHeight
{
get { return (double)GetValue(SearchIconHeightProperty); }
set { SetValue(SearchIconHeightProperty, value); }
}
public double SearchIconWidth
{
get { return (double)GetValue(SearchIconWidthProperty); }
set { SetValue(SearchIconWidthProperty, value); }
}
public bool IsShowSearchTips
{
get { return (bool)GetValue(IsShowSearchTipsProperty); }
set { SetValue(IsShowSearchTipsProperty, value); }
}
public bool CanShowSearchTips
{
get { return (bool)GetValue(CanShowSearchTipsProperty); }
set { SetValue(CanShowSearchTipsProperty, value); }
}
public string SearchTips
{
get { return (string)GetValue(SearchTipsProperty); }
set { SetValue(SearchTipsProperty, value); }
}
public Brush SearchTipsForeground
{
get { return (Brush)GetValue(SearchTipsForegroundProperty); }
set { SetValue(SearchTipsForegroundProperty, value); }
}
public bool IsShowAllSearchResult
{
get { return (bool)GetValue(IsShowAllSearchResultProperty); }
set { SetValue(IsShowAllSearchResultProperty, value); }
}
public int MaxShownResultCount
{
get { return (int)GetValue(MaxShownResultCountProperty); }
set { SetValue(MaxShownResultCountProperty, value); }
}
public IList SearchItemsSource
{
get { return (IList)GetValue(SearchItemsSourceProperty); }
set { SetValue(SearchItemsSourceProperty, value); }
}
public ObservableCollection SearchResultCollection
{
get { return (ObservableCollection)GetValue(SearchResultCollectionProperty); }
set { SetValue(SearchResultCollectionProperty, value); }
}
public bool CanShowClearButton
{
get { return (bool)GetValue(CanShowClearButtonProperty); }
set { SetValue(CanShowClearButtonProperty, value); }
}
public string SearchText
{
get { return (string)GetValue(SearchTextProperty); }
set { SetValue(SearchTextProperty, value); }
}
public SearchModel SelectedSearchItem
{
get { return (SearchModel)GetValue(SelectedSearchItemProperty); }
set { SetValue(SelectedSearchItemProperty, value); }
}
#endregion
#region Routed Events
///
/// 搜索结果选择改变事件
///
public static readonly RoutedEvent SelectedSearchItemChangedEvent
= EventManager.RegisterRoutedEvent("SelectedSearchItemChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SearchableTextBox));
#endregion
#region Event Wrappers
public event RoutedEventHandler SelectedSearchItemChanged
{
add
{
base.AddHandler(SelectedSearchItemChangedEvent, value);
}
remove
{
base.RemoveHandler(SelectedSearchItemChangedEvent, value);
}
}
#endregion
#region Constructors
static SearchableTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchableTextBox), new FrameworkPropertyMetadata(typeof(SearchableTextBox)));
}
#endregion
#region Override Methods
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PreviewKeyDown += SearchableTextBox_PreviewKeyDown;
_ttbSearchTips = GetTemplateChild("PART_TextBlockTips") as TextBlock;
_ttbInput = GetTemplateChild("PART_TextBoxInput") as TextBox;
if (_ttbInput != null)
{
_ttbInput.TextChanged += _ttbInput_TextChanged;
}
_popup = GetTemplateChild("PART_Popup") as Popup;
_lstSearchResult = GetTemplateChild("PART_ListBoxSearchResult") as ListBox;
if (_lstSearchResult != null)
{
_lstSearchResult.PreviewMouseLeftButtonDown += _lstSearchResult_PreviewMouseLeftButtonDown;
}
_btnClear = GetTemplateChild("PART_ButtonClear") as Button;
if (_btnClear != null)
{
_btnClear.Click += _btnClear_Click;
}
}
#endregion
#region Property Changed Callbacks
///
/// 是否可显示清除按钮属性更改回调
///
///
///
private static void OnCanShowClearButtonPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SearchableTextBox stb = d as SearchableTextBox;
if (stb == null || stb._btnClear == null)
{
return;
}
bool newValue = (bool)e.NewValue;
if (!newValue)
{
stb._btnClear.Visibility = Visibility.Collapsed;
}
else if (stb._ttbInput != null && !string.IsNullOrEmpty(stb._ttbInput.Text))
{
stb._btnClear.Visibility = Visibility.Visible;
}
}
///
/// 搜索条件更改回调
///
///
///
private static void OnSearchTextPropretyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SearchableTextBox stb = d as SearchableTextBox;
if (stb == null || !stb._canSearching)
{
return;
}
string searchKey = e.NewValue as string;
if (string.IsNullOrEmpty(searchKey))//搜索关键字为空,不展示搜索popup
{
if (stb._popup != null)
{
stb._popup.IsOpen = false;
}
return;
}
if (stb.SearchItemsSource == null || stb.SearchItemsSource.Count == 0)//搜索源为空,显示无搜索结果
{
if (stb._popup != null)
{
stb._popup.IsOpen = true;
}
if (stb._lstSearchResult != null)
{
stb._lstSearchResult.Visibility = Visibility.Collapsed;
}
return;
}
//根据关键字搜索匹配
IList searchResultList = null;
if (stb.SearchMethod != null)
{
searchResultList = stb.SearchMethod(searchKey, stb.SearchItemsSource);
}
else
{
searchResultList = stb.DefaultSearch(searchKey, stb.SearchItemsSource);
}
lock (stb)
{
stb.SearchResultCollection.Clear();
if (searchResultList != null && searchResultList.Count > 0)
{
foreach (SearchModel sm in searchResultList)
{
stb.SearchResultCollection.Add(sm);
}
}
if (stb._popup != null)
{
stb._popup.IsOpen = true;
}
if (stb._lstSearchResult != null)
{
if (stb.SearchResultCollection.Count != 0)
{
stb._lstSearchResult.Visibility = Visibility.Visible;
//每次重新搜索,从头开始展示
VirtualizingPanel virtualizingPanel = stb._lstSearchResult.GetItemsHost() as VirtualizingPanel;
if (virtualizingPanel != null)
{
virtualizingPanel.CallEnsureGenerator();
virtualizingPanel.CallBringIndexIntoView(0);
}
ListBoxItem firstItem = (ListBoxItem)stb._lstSearchResult.ItemContainerGenerator.ContainerFromIndex(0);
if (null != firstItem)
{
firstItem.UpdateLayout();
firstItem.BringIntoView();
}
}
else
{
stb._lstSearchResult.Visibility = Visibility.Collapsed;
}
}
}
}
#endregion
#region Event Methods
private void _ttbInput_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(_ttbInput.Text))//输入为空,显示Tips,隐藏清除按钮
{
if (CanShowSearchTips && _ttbSearchTips != null)
{
_ttbSearchTips.Visibility = Visibility;
}
if (CanShowClearButton && _btnClear != null)
{
//是Hidden而不是Collapsed,因为需要控件占位
_btnClear.Visibility = Visibility.Hidden;
}
}
else//输入不为空,隐藏Tips,显示清除按钮(如果允许)
{
if (_ttbSearchTips != null)
{
_ttbSearchTips.Visibility = Visibility.Collapsed;
}
if (CanShowClearButton && _btnClear != null)
{
_btnClear.Visibility = Visibility.Visible;
}
}
}
private void _btnClear_Click(object sender, RoutedEventArgs e)
{
if (_ttbInput != null)
{
_ttbInput.Text = "";
}
}
private void _lstSearchResult_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ListBoxItem item = LVTreeHelper.GetAncestor((DependencyObject)e.OriginalSource);
if (item == null)
{
return;
}
SetSelectedSearchItem(item.DataContext as SearchModel);
_popup.IsOpen = false;
}
private void SearchableTextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (_popup == null || !_popup.IsOpen
|| _lstSearchResult == null || _lstSearchResult.Items.Count == 0)
{
return;
}
if (e.Key == Key.Down)
{
if (_lstSearchResult.SelectedIndex < _lstSearchResult.Items.Count - 1)
{
_lstSearchResult.SelectedIndex = _lstSearchResult.SelectedIndex == -1 ? 0 : _lstSearchResult.SelectedIndex + 1;
_lstSearchResult.ScrollIntoView(_lstSearchResult.SelectedItem);
}
}
if (e.Key == Key.Up)
{
if (_lstSearchResult.SelectedIndex == -1 || _lstSearchResult.SelectedIndex == 0)
{
_lstSearchResult.SelectedIndex = 0;
}
else
{
_lstSearchResult.SelectedIndex = _lstSearchResult.SelectedIndex - 1;
_lstSearchResult.ScrollIntoView(_lstSearchResult.SelectedItem);
}
}
if (e.Key == Key.Enter)
{
SetSelectedSearchItem(_lstSearchResult.SelectedItem as SearchModel);
_popup.IsOpen = false;
}
e.Handled = true;
}
#endregion
#region Private Methods
///
/// 默认搜索方法
///
///
///
///
private List DefaultSearch(string searchKey, IList source)
{
List searchResultList = new List();
if (string.IsNullOrEmpty(searchKey) || source == null || source.Count == 0)
{
return searchResultList;
}
foreach (SearchModel sm in source)
{
if (sm.SearchField.Contains(searchKey))
{
searchResultList.Add(sm);
}
}
return searchResultList;
}
///
/// 设置选中的搜索结果项
///
///
private void SetSelectedSearchItem(SearchModel selectedSearchModel)
{
SelectedSearchItem = selectedSearchModel;
RoutedEventArgs args = new RoutedEventArgs(SelectedSearchItemChangedEvent, this);
RaiseEvent(args);
if (selectedSearchModel == null)
{
return;
}
//更新搜索框显示内容为选中项
_canSearching = false;
SearchText = selectedSearchModel.Name;
_canSearching = true;
}
#endregion
#region Public Methods
#endregion
}
六 控件的使用
可通过绑定的方式指定搜索源数据
未搜索状态:
无搜索结果:
有搜索结果:
源代码