Стилизация списков
122WPF --- Привязка, команды и стили WPF --- Стилизация списков
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
ItemContainerStyle
Стили позволяют повторно использовать форматирование с похожими элементами в разных местах. Почти ту же роль играют стили списков — они позволяют применить набор характеристик форматирования к каждому из индивидуальных элементов списка. Это важно, потому что система привязки данных WPF генерирует объекты-элементы списка автоматически.
В результате не так легко применить нужное форматирование к индивидуальным элементам. Решение обеспечивает свойство ItemsContainerStyle. Если свойство ItemsContainerStyle установлено, списочный элемент управления передаст его каждому своему элементу при его создании. В случае элемента управления ListBox каждый элемент представлен объектом ListBoxItem. (В ComboBox это ComboBoxItem и т.д.) Таким образом, любой стиль, который применяется посредством свойства ListBox.ItemContainerStyle, используется для установки свойств каждого объекта ListBoxItem.
Данное свойство мы уже применяли, когда демонстрировали конвертер CostToBackgroundConverter, подсвечивающий элементы списка с дорогими машинами.
Ниже показан один из простейших эффектов, которые можно реализовать с помощью ListBoxItem. Он применяет серо-голубой фон к каждому элементу. Для отделения индивидуальных элементов друг от друга (вместо общего сливающегося фона) стиль также добавляет некоторое пространство под поля:
<ListBox Name="lstCars" Margin="5">
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="ListBoxItem.Background" Value="LightSteelBlue"/>
<Setter Property="ListBoxItem.Margin" Value="5"/>
<Setter Property="ListBoxItem.Padding" Value="5"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Само по себе это не особенно интересно. Однако стиль становится немного более интересным с добавлением к нему триггеров. В следующем примере триггеры свойства изменяют цвет фона и добавляют сплошную рамку, когда свойство ListBoxItem.IsSelected получает значение true:
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="LightSteelBlue"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Padding" Value="5"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Для более ясной разметки в этом стиле используется свойство Style.TargetType, так что он может устанавливать свойства без включения имени класса в каждое средство установки.
Такое применение триггеров особенно удобно, потому что в ListBox не предусмотрено никакого другого способа применить нужное форматирование к выбранному элементу. Другими словами, если не использовать стиль, останется стандартная синяя подсветка выбранного элемента.
Элемент ListBox с флажками или переключателями
Стиль ItemsContainerStyle также важен, если нужно глубоко проникнуть в списочный элемент управления и изменить шаблон, используемый содержащимися в нем элементами. Например, этот прием можно использовать, чтобы заставить каждый ListBoxItem отображать переключатель или флажок рядом с текстом элемента списка.
Базовая техника состоит в замене шаблона элемента управления, используемого в качестве контейнера для каждого элемента списка. Модифицировать свойство ListBox.Template не понадобится, поскольку оно обеспечивает шаблон для ListBox. Вместо этого нужно модифицировать свойство ListBoxItem.Template. Ниже показан шаблон, который необходим для помещения каждого элемента списка в оболочку RadioButton:
<Stylе ...
<Setter Property="Margin" Value="5"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="Background" Value="LightSteelBlue"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="brd" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<RadioButton Focusable="False" Margin="{TemplateBinding Padding}" Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}">
<ContentPresenter/>
</RadioButton>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="LimeGreen"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Это работает, потому что RadioButton — элемент с содержимым, и может включать в себя любое содержимое. Хотя для получения содержимого можно было бы применить выражение привязки, намного более гибкое решение дает использование элемента ContentPresenter, как показано здесь. ContentPresenter охватывает то, что обычно появляется в элементе — текстовое свойство (если применяется ListBox.DisplayMemberPath) или более сложное представление данных (если используется свойство ListBox.ItemTemplate).
Настоящий трюк — привязка выражения к свойству RadioButton.IsChecked. Это выражение извлекает значение свойства ListBoxItem.IsSelected, используя свойство Binding.RelativeSource. Таким образом, в результате щелчка на RadioButton с целью его выбора соответствующий ListBoxItem помечается как выбранный. В то же время все остальные становятся невыбранными. Это выражение привязки также работает и в противоположном направлении, а это означает, что установка выбора в коде приводит к тому, что соответствующий RadioButton будет помечен.
Для завершения этого шаблона потребуется установить свойство RadioButton.Focusable в false. В противном случае будет возможен переход клавишей <Tab> к текущему выбранному ListBoxItem, а затем — к самому RadioButton, в чем нет смысла.
Создать ListBox, который содержит флажки (CheckBox), столь же легко. Фактически понадобится внести всего два изменения. Во-первых, заменить элемент RadioButton идентичным элементом CheckBox.
Во-вторых, изменить свойство ListBox.SelectionMode, чтобы разрешить множественный выбор. После этого пользователь сможет помечать столько элементов, сколько захочет. Вот правило стиля, которое превращает обычный ListBox в список флажков:
<ListBox Name="lstCars" Margin="5" SelectionMode="Multiple">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Margin" Value="5"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="Background" Value="LightSteelBlue"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="brd" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<CheckBox Focusable="False" Margin="{TemplateBinding Padding}" Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}">
<ContentPresenter/>
</CheckBox>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="LimeGreen"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Стиль чередующихся элементов
Один распространенный способ форматирования списка состоит в использовании чередующегося форматирования строк, другими словами, набора характеристик форматирования, которые отличаются в каждом втором элементе списка. Часто им задается слегка отличающийся цвет фона, чтобы строки были четко различимы.
WPF предлагает встроенную поддержку чередующихся элементов через два свойства: AlternationCount и AlternationIndex. Свойство AlternationCount — это количество элементов, формирующих последовательность, после которой стиль переключается. По умолчанию свойство AlternationCount установлено в 0, и чередующееся форматирование не используется.
Если вы установите AlternationCount в 1, стиль будет меняться после каждой строки, что позволит применить шаблон форматирования "четный-нечетный".
Если свойство AlternationCount установлено в 2, то первый элемент ListBoxItem получает AlternationIndex, равный 0, второй — 1, третий — снова 0, четвертый — 1, и т.д. Трюк состоит в использовании в стиле ItemContainerStyle триггера, который проверяет значение AlternationIndex и соответственно варьирует форматирование (вернем стандартный шаблон для ListBoxItem):
<ListBox Name="lstCars" Margin="5" AlternationCount="2">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Margin" Value="5,3"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="Background" Value="LightSteelBlue"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="brd" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="Orange"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="LimeGreen"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Обратите внимание, что AlternationIndex — присоединенное свойство, которое определено в классе ListBox (формально — в классе ItemsControl, от которого он унаследован). Это свойство не определено в классе ListBoxItem, поэтому при использовании триггера стиля должно указываться имя класса.
Интересно, что элемент с чередующимся стилем не обязательно должен быть каждым вторым. Вместо этого можно создавать более сложные варианты чередующегося форматирования, состоящие из последовательности в три и более элементов. Например, чтобы использовать три группы, установите для свойства AlternationCount значение 3 и напишите триггеры для любого из трех возможных значений AlternationIndex (0, 1 или 2). В списке элементы 1, 4, 7, 10 и т.д. получат значение AlternationIndex, равное 0. Элементы 2, 5, 8, 11 и т.д. — значение AlternationIndex, равное 1. И, наконец, элементы 3, 6, 9, 12 и т.д. — значение AlternationIndex, равное 2.
Селекторы стиля
Вы уже видели, как варьировать стиль на основе выбранного состояния элемента или его позиции в списке. Однако может понадобиться учесть множество других условий — критериев, зависящих от данных, а не от содержащего их контейнера ListBoxItem.
Чтобы справиться с такой ситуацией, нужен способ указания для различных элементов совершенно разных стилей. К сожалению, это невозможно делать декларативно. Взамен придется построить специализированный класс, унаследованный от StyleSelector. Этот класс отвечает за исследование каждого элемента данных и выбор соответствующего стиля. Эта работа выполняется в методе SelectStyle(), который допускается переопределять.
Вот как выглядит простейший селектор, который подсвечивает названия машин, в зависимости от того к какой категории они относятся (эконом-, средний-, бизнес- или премиум-класс):
public class CategoryHighlightStyleSelector : StyleSelector
{
public Style EconomyClassStyle { get; set; }
public Style MiddleClassStyle { get; set; }
public Style BuisnessClassStyle { get; set; }
public Style PremiumClassStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
CarTable car = (CarTable)item;
switch (car.CategoryID)
{
case 1:
return EconomyClassStyle;
case 2:
return MiddleClassStyle;
case 3:
return BuisnessClassStyle;
case 4:
return PremiumClassStyle;
default:
return null;
}
}
}
Чтобы это работало, понадобится создать четыре стиля, которые будут использоваться, а также создать и инициализировать экземпляр CategoryHighlightStyleSelector:
<Window.Resources>
<Style x:Key="myLBIStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="{Binding Path=Cost, Converter={StaticResource bgConverter}}"/>
</Style>
<Style x:Key="EconomyClassStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="#6eb1f5"/>
</Style>
<Style x:Key="MiddleClassStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="#c76ef5"/>
</Style>
<Style x:Key="BuisnessClassStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="#f7d034"/>
</Style>
<Style x:Key="PremiumClassStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="#74f734"/>
</Style>
</Window.Resources>
...
<ListBox Name="lstCars" Margin="5">
<ListBox.ItemContainerStyleSelector>
<databinding:CategoryHighlightStyleSelector EconomyClassStyle="{StaticResource EconomyClassStyle}"
MiddleClassStyle="{StaticResource MiddleClassStyle}" BuisnessClassStyle="{StaticResource BuisnessClassStyle}"
PremiumClassStyle="{StaticResource PremiumClassStyle}"/>
</ListBox.ItemContainerStyleSelector>
</ListBox>
Процесс выбора стилей выполняется один раз, когда список привязывается в первый раз. Это становится проблемой при отображении редактируемых данных, когда в результате редактирования какой-то элемент данных перемещается из одной категории стиля в другую. В такой ситуации потребуется заставить WPF заново применить стили, а простого способа сделать это не существует. Подход на основе грубой силы состоит в удалении селектора стиля установкой свойства ItemContainerStyleSelector в null, с последующим его переназначением:
StyleSelector selector = lstCars.ItemContainerStyleSelector;
lstCars.ItemContainerStyleSelector = null;
lstCars.ItemContainerStyleSelector = selector;
Можно организовать запуск этого кода автоматически в ответ на определенные изменения. При повторном присваивании селектора стиля WPF проверяет и обновляет каждый элемент в списке — процесс, не занимающий много времени в списках малых и средних размеров: