Pull to refresh

Подсветка найденных элементов в 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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.
Change theme settings