Как стать автором
Обновить

Подсветка найденных элементов в ListBox'е WPF

Всем не раз встречались большие списки, в которых надо было найти и выбрать один или несколько элементов (например, поиск конкретного отеля в городе). Хорошо, если к такому списку прикручен поиск, но, к сожалению, так бывает далеко не всегда.

Разрабатывая один из проектов на .Net, столкнулся со следующей задачей — необходимо было в WPF реализовать поиск элементов (элемент представлял из себя строку) в списке ListBox с подсветкой введенного текста для поиска. После продолжительного поиска в гугле, готовых вариантов решения найдено не было, принялся за реализацию данной задачи.
Минимальные знания, которые потребуются: WPF, C#, LINQ.


Создание интерфейса

Первым делом — составим интерфейс будущего контролла, в данном случае — это будет TextBox, прямо под которым находится ListBox. Элементом ListBox'а будет являться CheckBox, в теле которого находится два TextBlock'а. Хотя их там будет даже 3, но об этом позже. CheckBox — потому что требуется выбрать несколько элементов, при этом выбранные элементы будем поднимать вверх списка (чтобы проще было их найти в огромном списке). Почему 2 TextBlock'a: т.к. стоит задача подсветить найденный кусочек текста во всей фразе, то нам придется разбивать один из TextBlock на Inlines — составные части, у одной из которых надо будет подсветить Background. Второй же TextBlock будет отображать полностью фразу, и будет находиться поверх первого TextBlock'a.

Для примера, в качестве элементов списка, возьмем системные шрифты — получается как раз достаточно большой список объектов (у меня — 257, но тестировал и на большем количестве). Чтобы увидеть, как шрифт применяется, дополним тело CheckBox'а ещё одним TextBlock'ом, в котором разместим фразу «ABCD АБВГ 1234» и будем применять к ней шрифт.

Код шаблона ListBoxItem
	 <DataTemplate x:Key="ListItemTemplate" >
	                <Grid x:Name="gridBasic" 
	                      Background="{Binding colorGround}" 
	                      Visibility="{Binding visibility}">
	                    <CheckBox Name="checkControl" 
	                              Margin="5" 
	                              IsChecked="{Binding isCheck}" 
	                              Checked="checkControl_Checked" 
	                              Unchecked="checkControl_Unchecked"
	                              HorizontalContentAlignment="Stretch">
	                        <Grid HorizontalAlignment="Stretch">
	                            <Grid.ColumnDefinitions>
	                                <ColumnDefinition Width="*"/>
	                                <ColumnDefinition Width="auto"/>
	                            </Grid.ColumnDefinitions>
	                            <Grid>
	                                <TextBlock Name="textBackground" 
	                                           Foreground="#00000000">
	                                    <TextBlock.Inlines >
	                                        <TextBlock Name="textBefore" 
	                                          Text="{Binding TextBefore}"/><TextBlock 
	                                            Name="textSelect" 
	                                            Text="{Binding TextSelect}" 
	                                            Background="LightBlue"/>
	                                    </TextBlock.Inlines>
	                                </TextBlock>
	                                <TextBlock Name="textContent" 
	                                           Text="{Binding Namefont}"/>
	                            </Grid>
	                            <TextBlock Grid.Column="1" 
	                                       Name="textInFont" 
	                                       FontFamily="{Binding Fontstyle}" 
	                                       Text="Abcd Абвг 123" 
	                                        HorizontalAlignment="Right"/>
	                        </Grid>
	                    </CheckBox>
	                </Grid>
	            </DataTemplate>

	


Как видно в коде, TextBlock с именем textBackground разбит на 2 части, на два Inlines: textBefore — текст, который будет перед выделением, и textSelect — текст, который будет выделен. Полностью текст шрифта выводится в TextBlock с именем textContent. К TextBlock'у c именем textInFont применяется начертание шрифта, чтобы можно было увидеть, как шрифт отобразится на русских и английских символах, а так же на цифрах.

ListBox будет описывается следующим образом
	  <Grid Grid.Row="2">
	            <Grid.RowDefinitions>
	                <RowDefinition Height="auto"/>
	                <RowDefinition Height="*"/>
	            </Grid.RowDefinitions>
	            <TextBox Grid.Row="0" 
	                     Name="textBoxSearch" 
	                     TextChanged="TextBox_TextChanged"/>
	            <ListBox Name="listBox" 
	                     ItemsSource="{Binding}" 
	                     Grid.Row="1" 
	                     HorizontalContentAlignment="Stretch"  
	                     ItemTemplate="{StaticResource ListItemTemplate}" 
	                     SelectionChanged="listBox_SelectionChanged"/>
	        </Grid>
	


Таким образом, разместили TextBox и под ним ListBox, применив к элементам ListBox шаблон, описанный выше. Подписавшись на необходимые события, переходим к следующей части.

Программная составляющая


Во первых, необходим класс, который будет содержать все необходимые элементы для привязки данных. Для примера, назовем его Cust

	public class Cust
	        {
	            /// <summary>
	            /// название шрифта
	            /// </summary>
	            public string Namefont { get; set; }
	            /// <summary>
	            /// начертание шрифта
	            /// </summary>
	            public FontFamily Fontstyle { get; set; }
	            /// <summary>
	            /// выбран шрифт или нет
	            /// </summary>
	            public bool isCheck { get; set; }       
	            /// <summary>
	            /// текст перед выделением
	            /// </summary>
	            public String TextBefore { get; set; }
	            /// <summary>
	            /// выделенный текст
	            /// </summary>
	            public String TextSelect { get; set; }
	            /// <summary>
	            /// подсветка выбранного элемента
	            /// </summary>
	            public Brush colorGround
	            {
	                get
	                {
	                    if (isCheck) return Brushes.LightSkyBlue;
	                    else return Brushes.White;
	                }
	            }


	            public Cust(String newFonts, bool check)
	            {
	                Namefont = newFonts;
	                Fontstyle = new FontFamily(Namefont);
	                isCheck = check;
	            }
	        }
	


Теперь необходимо создать две коллекции — в первой будем хранить полный список объектов, во втором — только отфильтрованные элементы. И сразу же напишем функцию инициализации контролла

	        /// <summary>
	        /// Коллекция всех элементов
	        /// </summary>
	        private List<Cust> _collectionSource = new ObservableCollection<Cust>();

	        /// <summary>
	        /// коллекция фильтрованных элементов
	        /// </summary>
	        public List<Cust> Collection = new ObservableCollection<Cust>();

	 public void Init()
	        {
	            //достаем системные шрифты, список сразу делаем сортированным по названию
	            List<String> fontsList = (Fonts.SystemFontFamilies.OrderBy(f => f.Source).Select(f => f.Source)).ToList();

	            _collectionSource.Clear();
	            Collection.Clear();

	            textCol.Text = fontsList.Count.ToString();

	            for (int i = 0; i < fontsList.Count; i++)
	            {
	                _collectionSource.Add(new Cust(fontsList[i], false));
	                Collection.Add(_collectionSource[i]);
	            }

	            listBox.DataContext = Collection;

	        }
	


Теперь мы можем наблюдать вот такой контролл:
image

Теперь самое интересное — фильтрация и подсветка. Для фильтрации напишем следующую функцию:
	 /// <summary>
	        /// фильтрация коллекции
	        /// </summary>
	        /// <param name="s"></param>
	        private void FilterItems(string s)
	        {
	            
	            if (string.IsNullOrEmpty(textBoxSearch.Text))
	            {
	                Collection = new List<Cust>(_collectionSource.OrderBy(i => !i.isCheck));
	                foreach (Cust item in Collection)
	                {
	                    item.TextSelect = String.Empty;
	                }
	            }
	            else 
	            {
	                Collection = new List<Cust>(_collectionSource.OrderBy(i => !i.Namefont.ToLower().StartsWith(s)).
	                    Where(item => item.Namefont.ToLower().Contains(s)));
	                foreach (Cust item in Collection)
	                {
	                    item.TextBefore = item.Namefont.Substring(0, item.Namefont.ToLower().IndexOf(s));
	                    item.TextSelect = item.Namefont.Substring(item.Namefont.ToLower().IndexOf(s), s.Length);                    
	                }
	            }
	             
	            listBox.DataContext = Collection;
	        }
	

В случае, если в TextBox'e не было ничего введено, то выбранные элементы автоматически поднимаются в вверх, если же пользователь начал вводить, то после каждого изменения текста в TextBox вызывается функция FilterItems, в которую передается текущей текст в TextBox'е, после чего происходит фильтрация по заданному условию основной коллекции, результат записывается во вторую коллекцию, с которой уже и связывается DataContext ListBox'a. Так же, во второй коллекции для каждого элемента задается TextBefore и TextSelect, к которым как раз привязаны соответствующие по названиям TextBlock'и в шаблоне ListBoxItem.
Сортировка второй коллекции производится сначала по первому вхождению найденного текста, и только потом — вхождение этого текста в остальной части фразы.
При выборе шрифта, либо отмене выбора — происходит событие либо checkControl_Checked, либо checkControl_Unchecked соответственно.


	        private void checkControl_Checked(object sender, RoutedEventArgs e)
	        {
	            FilterItems(textBoxSearch.Text.ToLower());
	            textCol.Text = _collectionSource.Count(i => i.isCheck).ToString();      
	            
	        }

	        private void checkControl_Unchecked(object sender, RoutedEventArgs e)
	        {
	            FilterItems(textBoxSearch.Text.ToLower());
	            textCol.Text = _collectionSource.Count(i => i.isCheck).ToString();
	           
	        }
	


Для случая, если понадобится выбрать все элементы — реализовал возможность выделения всех элементов

	private void checkAll_Checked(object sender, RoutedEventArgs e)
	        {
	            foreach (Cust t in _collectionSource)
	            {
	                t.isCheck = true;
	            }
	            FilterItems(textBoxSearch.Text.ToLower());
	            textCol.Text = _collectionSource.Count.ToString();
	            
	        }

	        private void checkAll_Unchecked(object sender, RoutedEventArgs e)
	        {
	            foreach (Cust t in _collectionSource)
	            {
	                t.isCheck = false;
	            }
	            FilterItems(textBoxSearch.Text.ToLower());
	            textCol.Text = "0";
	            
	        }
	


Итого

В итоге — получился вот такой контролл, в котором быстро можно найти один или несколько элементов. При этом — подсвечивается найденный текст в элементе.
image
Спасибо за внимание. Надеюсь, статья была полезна и интересна.
P.S. исходники можно взять тут narod.ru либо dropbox
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.