天天看點

WPF自定義搜尋控件

       原文連結:http://blog.csdn.net/zhuo_wp/article/details/78821711    轉載請注明出處

       在利用WPF開發桌面軟體時,我們常常會用到自動搜尋功能:在一個搜尋框中輸入搜尋關鍵字,得到搜尋結果,并可以選擇某一項搜尋結果。WPF中沒有這樣的預定義控件,不過這一搜尋功能可以簡單的利用TextBox來實作,我們隻需要監聽TextBox的TextChanged事件,在事件處理方法中查找搜尋源中與輸入關鍵字比對的項。但是,如果這樣的搜尋場景被大量使用的時候,最好的處理方式是封裝一個可專門用于搜尋功能的自定義控件。本文即是簡要的說明其思路與簡單實作的。

       一 設計思路

WPF自定義搜尋控件

       如圖所示,從界面上說,搜尋控件主要有三個部分:

       1 搜尋關鍵字輸入部分,主要用于搜尋關鍵字的輸入,可以用WPF中的TextBox來實作,需要監聽TextBox的TextChanged事件。

        2 清除搜尋關鍵字按鈕,用于一鍵清除輸入框中的關鍵字,可用Button來實作。

        3 搜尋結果展示部分,用于展示搜尋結果,并且隻有在有搜尋結果的時候才展示,可以用WPF中的Popup來實作。

       此外,還可以顯示搜尋提示語和搜尋圖示。

       在應用控件的時候,搜尋資料源是可以從控件外部提供的,是以需要依賴屬性來綁定搜尋源,同時搜尋提示語可以通過依賴屬性來提供自定義設定功能。

       二 代碼結構

       如圖所示,整個控件的代碼結構包括用于界面建構的SearchableTextBox.xaml檔案,用于搜尋邏輯控制的SearchableTextBox.cs檔案,用于将原始資料構造成可搜尋資料的SearchModel類,以及一些輔助方法類。

WPF自定義搜尋控件

       三 模闆代碼

       如下所示,搜尋控件的預設模闆包括:

       1 一個Path元素,用于顯示搜尋圖示。

       2 一個TextBlock控件,用于顯示搜尋提示語。

       3 一個TextBox控件, 用于資料搜尋關鍵字。

       4 一個Button控件,用于一鍵清除輸入框中的内容。

       5 一個Popup和一個ListBox,用于顯示搜尋結果。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SearchableTextBox"
    xmlns:cvt="clr-namespace:SearchableTextBox.Converters">
    
    <cvt:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
    <cvt:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter"/>

    <Style TargetType="{x:Type local:SearchableTextBox}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:SearchableTextBox}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}" 
                            CornerRadius="{TemplateBinding  CornerRadius}"
                            Background="{TemplateBinding Background}"
                            Padding="{TemplateBinding Padding}">
                        <Grid x:Name="PART_Root">

                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="auto"/>
                                <ColumnDefinition Width="auto"/>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="auto" />
                                <ColumnDefinition Width="auto"/>
                            </Grid.ColumnDefinitions>

                            <Grid Grid.Column="0" Width="{TemplateBinding CornerRadius, Converter={StaticResource CornerRadiusToDoubleConverter}}"/>

                            <Path Grid.Column="1" Width="{TemplateBinding SearchIconWidth}"
                                  Height="{TemplateBinding SearchIconHeight}" 
                                  Visibility="{TemplateBinding IsShowSearchIcon, Converter={StaticResource BoolToVisibilityConverter}}" 
                                  Stretch="Fill" Fill="{TemplateBinding SearchIconForeground}"
                                  Data="{TemplateBinding SearchIcon}">
                            </Path>

                            <TextBlock x:Name="PART_TextBlockTips" Grid.Column="2" Text="{TemplateBinding SearchTips}" Margin="15,0,0,0"
                                       Foreground="{TemplateBinding SearchTipsForeground}"
                                       HorizontalAlignment="Left" VerticalAlignment="Center"/>

                            <TextBox x:Name="PART_TextBoxInput" Grid.Column="2" 
                                     Text="{Binding SearchText,Delay=500, Mode=TwoWay,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:SearchableTextBox}, UpdateSourceTrigger=PropertyChanged}"
                                     BorderBrush="Transparent" BorderThickness="0" Background="Transparent"
                                     HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                                     HorizontalContentAlignment="Left" VerticalContentAlignment="Center"/>

                            <Button x:Name="PART_ButtonClear" Grid.Column="3" Visibility="Collapsed">
                                <Button.Template>
                                    <ControlTemplate>
                                        <Grid>
                                            <Label Content="x"/>
                                        </Grid>
                                    </ControlTemplate>
                                </Button.Template>
                            </Button>

                            <Grid Grid.Column="4" Width="{TemplateBinding CornerRadius, Converter={StaticResource CornerRadiusToDoubleConverter}}"/>

                            <Popup x:Name="PART_Popup"
                                   AllowsTransparency="true"
                                   PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
                                   Placement="Bottom"
                                   StaysOpen="False">
                                <Border Padding="0,0,4,0">
                                    <Border x:Name="Shdw"
                                            Background="Transparent"
                                            MaxWidth="252"
                                            MinWidth="{Binding ActualWidth, ElementName=PART_Root}">
                                        <Border x:Name="DropDownBorder"
                                                Margin="0,4,0,0"
                                                BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
                                                BorderThickness="1"
                                                Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
                                            <Grid RenderOptions.ClearTypeHint="Enabled">
                                                <Border Padding="14"
                                                        Background="White"
                                                        Name="NoResultBorder">
                                                    <TextBlock Text="無比對結果"
                                                               Foreground="Black" />
                                                </Border>
                                                <ListBox x:Name="PART_ListBoxSearchResult"
                                                         ItemsSource="{TemplateBinding SearchResultCollection}"
                                                         Foreground="Black"
                                                         Margin="0,10"
                                                         Padding="0"
                                                         Background="White"
                                                         ScrollViewer.HorizontalScrollBarVisibility="Auto"
                                                         MaxHeight="215"
                                                         VirtualizingStackPanel.IsVirtualizing="True"
                                                         BorderBrush="{DynamicResource Colorb9b9b9}"
                                                         BorderThickness="0">
                                                    <ListBox.ItemTemplate>
                                                        <DataTemplate>
                                                            <Label Content="{Binding Name}"/>
                                                        </DataTemplate>
                                                    </ListBox.ItemTemplate>
                                                    <ListBox.ItemContainerStyle>
                                                        <Style TargetType="{x:Type ListBoxItem}">
                                                            <Setter Property="Background"
                                                                    Value="Transparent" />
                                                            <Setter Property="Padding"
                                                                    Value="0" />
                                                            <Setter Property="BorderThickness"
                                                                    Value="0" />
                                                            <Setter Property="Template">
                                                                <Setter.Value>
                                                                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                                                        <Border x:Name="Bd"
                                                                                Height="38"
                                                                                Margin="0,0,0,4"
                                                                                BorderThickness="0"
                                                                                Background="{TemplateBinding Background}"
                                                                                SnapsToDevicePixels="true">
                                                                            <ContentPresenter HorizontalAlignment="Stretch"
                                                                                              Margin="14,0,0,0"
                                                                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                                                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                                                                        </Border>
                                                                        <ControlTemplate.Triggers>

                                                                            <Trigger Property="IsMouseOver"
                                                                                     Value="True">
                                                                                <Setter TargetName="Bd"
                                                                                        Property="BorderBrush"
                                                                                        Value="Chartreuse" />
                                                                                <Setter TargetName="Bd"
                                                                                        Property="Background"
                                                                                        Value="Chartreuse" />
                                                                                <Setter Property="Foreground"
                                                                                        Value="White" />
                                                                            </Trigger>
                                                                            <Trigger Property="IsSelected"
                                                                                     Value="true">
                                                                                <Setter Property="Background"
                                                                                        TargetName="Bd"
                                                                                        Value="Blue" />
                                                                                <Setter Property="Foreground"
                                                                                        Value="White" />
                                                                            </Trigger>
                                                                            <MultiTrigger>
                                                                                <MultiTrigger.Conditions>
                                                                                    <Condition Property="IsSelected"
                                                                                               Value="true" />
                                                                                    <Condition Property="Selector.IsSelectionActive"
                                                                                               Value="false" />
                                                                                </MultiTrigger.Conditions>
                                                                                <Setter Property="Background"
                                                                                        TargetName="Bd"
                                                                                        Value="{DynamicResource BgColorLeftTreeRowSelected}" />
                                                                                <Setter Property="Foreground"
                                                                                        Value="{DynamicResource ForegroundSelect}" />
                                                                            </MultiTrigger>
                                                                            <Trigger Property="IsEnabled"
                                                                                     Value="false">
                                                                                <Setter Property="Foreground"
                                                                                        Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                                                                            </Trigger>
                                                                        </ControlTemplate.Triggers>
                                                                    </ControlTemplate>
                                                                </Setter.Value>
                                                            </Setter>
                                                        </Style>
                                                    </ListBox.ItemContainerStyle>
                                                </ListBox>
                                            </Grid>
                                        </Border>
                                    </Border>

                                </Border>
                            </Popup>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
           

       四 搜尋模型

       搜尋模型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<string, IList<SearchModel>, IList<SearchModel>> SearchMethod { get; set; }

        #endregion

        #region Dependency Properties

        /// <summary>
        /// 控件圓角半徑
        /// </summary>
        public static readonly DependencyProperty CornerRadiusProperty
            = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(SearchableTextBox), new PropertyMetadata(new CornerRadius(0), null));
        /// <summary>
        /// 是否顯示搜尋圖示
        /// </summary>
        public static readonly DependencyProperty IsShowSearchIconProperty
            = DependencyProperty.Register("IsShowSearchIcon", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
        /// <summary>
        /// 搜尋圖示前景色
        /// </summary>
        public static readonly DependencyProperty SearchIconForegroundProperty
            = DependencyProperty.Register("SearchIconForeground", typeof(Brush), typeof(SearchableTextBox), new PropertyMetadata(new SolidColorBrush(Colors.Gray), null));
        /// <summary>
        /// 搜尋圖示
        /// </summary>
        public static readonly DependencyProperty SearchIconProperty
            = DependencyProperty.Register("SearchIcon", typeof(Geometry), typeof(SearchableTextBox), new PropertyMetadata(Geometry.Parse(SEARCH_ICON_STRING), null));
        /// <summary>
        /// 搜尋圖示高度
        /// </summary>
        public static readonly DependencyProperty SearchIconHeightProperty
            = DependencyProperty.Register("SearchIconHeight", typeof(double), typeof(SearchableTextBox), new PropertyMetadata(16.0, null));
        /// <summary>
        /// 搜尋圖示寬度
        /// </summary>
        public static readonly DependencyProperty SearchIconWidthProperty
            = DependencyProperty.Register("SearchIconWidth", typeof(double), typeof(SearchableTextBox), new PropertyMetadata(16.0, null));
        /// <summary>
        /// 是否顯示搜尋提示文字
        /// </summary>
        public static readonly DependencyProperty IsShowSearchTipsProperty
            = DependencyProperty.Register("IsShowSearchTips", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
        /// <summary>
        /// 隻是是否可以顯示搜尋框
        /// </summary>
        public static readonly DependencyProperty CanShowSearchTipsProperty
            = DependencyProperty.Register("CanShowSearchTips", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
        /// <summary>
        /// 搜尋提示文字
        /// </summary>
        public static readonly DependencyProperty SearchTipsProperty
            = DependencyProperty.Register("SearchTips", typeof(string), typeof(SearchableTextBox), new PropertyMetadata("請輸入搜尋條件", null));
        /// <summary>
        /// 搜尋提示文字顔色
        /// </summary>
        public static readonly DependencyProperty SearchTipsForegroundProperty
            = DependencyProperty.Register("SearchTipsForeground", typeof(Brush), typeof(SearchableTextBox), new PropertyMetadata(new SolidColorBrush(Colors.Gray), null));
        /// <summary>
        /// 是否顯示全部搜尋結果
        /// </summary>
        public static readonly DependencyProperty IsShowAllSearchResultProperty
            = DependencyProperty.Register("IsShowAllSearchResult", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, null));
        /// <summary>
        /// (在不顯示全部搜尋結果的條件下)可顯示的搜尋結果的最大資料量
        /// </summary>
        public static readonly DependencyProperty MaxShownResultCountProperty
            = DependencyProperty.Register("MaxShownResultCount", typeof(int), typeof(SearchableTextBox), new PropertyMetadata(10, null));
        /// <summary>
        /// 傳入的搜尋源
        /// </summary>
        public static readonly DependencyProperty SearchItemsSourceProperty
            = DependencyProperty.Register("SearchItemsSource", typeof(IList<SearchModel>), typeof(SearchableTextBox), new PropertyMetadata(null, null));
        /// <summary>
        /// 搜尋結果集合(用于綁定到ListBox)
        /// </summary>
        public static readonly DependencyProperty SearchResultCollectionProperty
            = DependencyProperty.Register("SearchResultCollection", typeof(ObservableCollection<SearchModel>), typeof(SearchableTextBox), new PropertyMetadata(new ObservableCollection<SearchModel>(), null));
        /// <summary>
        /// 是否允許顯示清除按鈕
        /// </summary>
        public static readonly DependencyProperty CanShowClearButtonProperty
            = DependencyProperty.Register("CanShowClearButton", typeof(bool), typeof(SearchableTextBox), new PropertyMetadata(true, OnCanShowClearButtonPropertyChanged));
        /// <summary>
        /// 輸入的搜尋條件字元串
        /// </summary>
        public static readonly DependencyProperty SearchTextProperty
            = DependencyProperty.Register("SearchText", typeof(string), typeof(SearchableTextBox), new PropertyMetadata("", OnSearchTextPropretyChanged));
        /// <summary>
        /// 選中的搜尋結果項
        /// </summary>
        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<SearchModel> SearchItemsSource
        {
            get { return (IList<SearchModel>)GetValue(SearchItemsSourceProperty); }
            set { SetValue(SearchItemsSourceProperty, value); }
        }
        public ObservableCollection<SearchModel> SearchResultCollection
        {
            get { return (ObservableCollection<SearchModel>)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

        /// <summary>
        /// 搜尋結果選擇改變事件
        /// </summary>
        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

        /// <summary>
        /// 是否可顯示清除按鈕屬性更改回調
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        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;
            }
        }

        /// <summary>
        /// 搜尋條件更改回調
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        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<SearchModel> 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<ListBoxItem>((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

        /// <summary>
        /// 預設搜尋方法
        /// </summary>
        /// <param name="searchKey"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        private List<SearchModel> DefaultSearch(string searchKey, IList<SearchModel> source)
        {
            List<SearchModel> searchResultList = new List<SearchModel>();
            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;
        }

        /// <summary>
        /// 設定選中的搜尋結果項
        /// </summary>
        /// <param name="selectedItem"></param>
        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
    }
           

       六 控件的使用

       可通過綁定的方式指定搜尋源資料

<UserControl x:Class="SearchableTextBoxExample.View.SearchableTextBoxExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:SearchableTextBoxExample.View"
             xmlns:controls="clr-namespace:SearchableTextBox;assembly=SearchableTextBox"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <controls:SearchableTextBox x:Name="stbTest" Grid.Row="0" Width="200" Height="30"
                                   HorizontalAlignment="Center" VerticalAlignment="Center"
                                   BorderBrush="Red" BorderThickness="1" Background="Transparent" CornerRadius="12"
                                   IsShowSearchIcon="True"
                                   SearchItemsSource="{Binding SearchItemsSourceCollection}"
                                   SelectedSearchItemChanged="stbTest_SelectedSearchItemChanged"/>

        <StackPanel Orientation="Horizontal" Grid.Row="1" VerticalAlignment="Top" HorizontalAlignment="Center">
            <TextBlock Text="搜尋選中結果:" VerticalAlignment="Center"/>
            <TextBox  Text="{Binding SelectedSearchItem.Name, ElementName=stbTest}"
                      Width="100" IsEnabled="False" VerticalAlignment="Center"/>
        </StackPanel>
    </Grid>
</UserControl>
           

       效果圖:

       未搜尋狀态:

WPF自定義搜尋控件

       無搜尋結果:

WPF自定義搜尋控件

       有搜尋結果:

WPF自定義搜尋控件

源代碼