Pull to refresh

WPF Binding: Мощь стилей и шаблонов в WPF.

Reading time9 min
Views38K
Original author: Beatriz Costa
В WPF существует очень четкое разделение между поведением Control'а и тем, как он выглядит. К примеру, поведение объекта класса Button состоит в том, чтобы реагировать на различные события по клику, но его вид может быть любым — вы можете сделать кнопку в виде стрелки, рыбы, или чего-либо еще, что подходит для вашего приложения. Переопределение отображения Control'а очень просто сделать при использовании VS со стилями и шаблонами, и даже еще проще, если у вас есть Microsoft Expression Blend. В этом примере я покажу вам, как переопределить отображение ListBox'а, который используется для отображения списка планет.

Я решила начать с создания источника данных с планетами и солнцем. Я определила класс «SolarSystemObject» с такими свойствами: Name, Orbit, Diameter, Image and Details. Я перегрузила метод ToString() в этом классе так, чтобы он возвращал название объекта солнечной системы. Потом я добавила класс «SolarSystem» со свойством «SolarSystemObjects» с типом ObservableCollection. В конструкторе класса «SolarSystem» я добавила солнце и девять планет в коллекцию «SolarSystemObjects».

Как только я определила источник данных, я была готова добавить на главное окно ListBox, который был связан с данной коллекцией:
  1. <Window.Resources>
  2.   <local:SolarSystem x:Key=”solarSystem/>
  3.   (…)
  4. </Window.Resources>
  5.   <ListBox ItemsSource=”{Binding Source={StaticResource solarSystem}, Path=SolarSystemObjects}” />
* This source code was highlighted with Source Code Highlighter.

И так, ListBox отображает планеты, но визуально это все еще выглядит несколько простовато:


На данном этапе я начала думать о том, как отобразить планеты наиболее реалистичным способом — моей целью было достигнуть отображения, сходного с диаграммами солнечной системы в школьных учебниках. Первым шагом стало изменения layout'а у ListBoxItem'ов. Стандартным layout'ом для ListBox'а является StackPanel, которая заставляет ListBoxItem'ы отображаться один за другим (если быть более точным, это VirtualizingStackPanel, которая добавляет виртуализацию к традиционному StackPanel). Для того чтобы отобразить планеты так, как я хочу, мне требуется Canvas, который позволяет мне позиционировать элементы в нем по определенному числу пикселей слева и сверху от границ этого Canvas'а. У ListBox'а есть свойство ItemsPanel с типом ItemsPanelTemplate, которое может быть использовано для изменения layout'а ListBox'а, как и делается в моем примере. Вот как я это сделала:
  1. <Style TargetType=”ListBox>
  2.   <Setter Property=”ItemsPanel>
  3.     <Setter.Value>
  4.       <ItemsPanelTemplate>
  5.         <Canvas Width=”590? Height=”590? Background=”Black/>
  6.       </ItemsPanelTemplate>
  7.     </Setter.Value>
  8.   </Setter>
  9. </Style>
* This source code was highlighted with Source Code Highlighter.

Моим следующим шагом было определение отображения каждой планеты. Я сделала это, используя DataTemplate. Я решила представлять каждую планету ее изображением и белым эллипсом, имитирующим ее орбиту вокруг солнца. Я также добавила подсказку с подробной информацией о планете, которая появляется, когда курсор находится над планетой.
  1. <DataTemplate DataType="{x:Type local:SolarSystemObject}">
  2.   <Canvas Width="20" Height="20" >
  3.     <Ellipse
  4.       Canvas.Left="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-1.707}"
  5.       Canvas.Top="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-0.293}"
  6.       Width="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
  7.       Height="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
  8.       Stroke="White"
  9.       StrokeThickness="1"/>
  10.     <Image Source="{Binding Path=Image}" Width="20" Height="20">
  11.       <Image.ToolTip>
  12.         <StackPanel Width="250" TextBlock.FontSize="12">
  13.           <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />
  14.           <StackPanel Orientation="Horizontal">
  15.             <TextBlock Text="Orbit: " />
  16.             <TextBlock Text="{Binding Path=Orbit}" />
  17.             <TextBlock Text=" AU" />
  18.           </StackPanel>
  19.           <TextBlock Text="{Binding Path=Details}" TextWrapping="Wrap"/>
  20.         </StackPanel>
  21.       </Image.ToolTip>
  22.     </Image>
  23.   </Canvas>
  24. </DataTemplate>
  25.   
  26. <Style TargetType="ListBoxItem">
  27.   <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  28.   <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  29.   (…)
  30. </Style>
* This source code was highlighted with Source Code Highlighter.

Как вы можете видеть в шаблоне и стиле выше, свойства, которые определяют положение ListBoxItem'а и положение и размер Ellips'а основываются на орбите планеты и все используют один и тот же конвертер, только с различными параметрами. Задача конвертера состоит в преобразовании расстояний между объектами солнечной системы в расстояния внутри Canvas'а в пикселях. Моя первая реализация этого конвертера просто перемножала значение орбиты на константу, но я обнаружила, что внутренние планеты были очень тесно расположены друг к другу. Поэтому я решила немного изменить расчет, чтобы сделать его нелинейным. Я так же решила, чтобы конвертер принимал некий параметр, который масштабировал бы конечный результат на некоторое значение так, чтобы я могла использовать эту логику множество раз.
  1. public class ConvertOrbit : IValueConverter
  2. {
  3.   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  4.   {
  5.     double orbit = (double)value;
  6.     double factor = System.Convert.ToDouble(parameter);
  7.     return Math.Pow(orbit / 40, 0.4) * 770 * factor;
  8.   }
  9.   
  10.   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  11.   {
  12.     throw new NotSupportedException(”This method should never be called”);
  13.   }
  14. }
* This source code was highlighted with Source Code Highlighter.

Если вы запустите приложение сейчас, то вы увидите, что все планеты корректно расположены по отношению к солнцу. Если вы наведете на них мышь, то вы получите более детальную информацию о планете. Если вы кликнете по планете, то стандартный шаблон ListBoxItem'а назначит синий фон выбранному элементу, который выглядит как небольшая рамка вокруг элемента. Это не тот эффект, который я хотела бы видеть, поэтому я решила изменить представление выбранного элемента.
Чтобы изменить этот стиль, я думаю было бы проще использовать Microsoft Expression Blend, чтобы посмотреть на стандартный шаблон, а затем переделать его так, как вы хотите. Я начала с выбора ListBox'а в Blend, затем я проследовала в меню «Object», выбрала «Edit Other Styles», «Edit ItemContainerStyle» и затем «Edit a Copy». Затем я задала имя шаблону и нажала на «OK». Если на данном этапе вы проследуете на закладку «XAML», то вы увидете полный стандартный стиль для ListBoxItem'а, который включает в себя следующий шаблон:
  1. <Setter Property=”Template>
  2.   <Setter.Value>
  3.     <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
  4.       <Border SnapsToDevicePixels=”truex:Name=”BdBackground=”{TemplateBinding Background}” BorderBrush=”{TemplateBinding BorderBrush}” BorderThickness=”{TemplateBinding BorderThickness}” Padding=”{TemplateBinding Padding}”>
  5.         <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}” HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}” VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
  6.       </Border>
  7.       <ControlTemplate.Triggers>
  8.         <Trigger Property=”IsSelectedValue=”true>
  9.           <Setter Property=”BackgroundTargetName=”BdValue=”{DynamicResource {x:Static SystemColors.HighlightBrushKey}}”/>
  10.           <Setter Property=”ForegroundValue=”{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}”/>
  11.         </Trigger>
  12.         <MultiTrigger>
  13.           <MultiTrigger.Conditions>
  14.             <Condition Property=”IsSelectedValue=”true/>
  15.             <Condition Property=”Selector.IsSelectionActiveValue=”false/>
  16.           </MultiTrigger.Conditions>
  17.           <Setter Property=”BackgroundTargetName=”BdValue=”{DynamicResource {x:Static SystemColors.ControlBrushKey}}”/>
  18.           <Setter Property=”ForegroundValue=”{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}”/>
  19.         </MultiTrigger>
  20.         <Trigger Property=”IsEnabledValue=”false>
  21.           <Setter Property=”ForegroundValue=”{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}”/>
  22.         </Trigger>
  23.       </ControlTemplate.Triggers>
  24.     </ControlTemplate>
  25.   </Setter.Value>
  26. </Setter>
* This source code was highlighted with Source Code Highlighter.

Используя его за основу, я создала простой шаблон, который добавляет желтый эллипс вокруг выбранной планеты:
  1. <Style TargetType=”ListBoxItem>
  2.   (…)
  3.   <Setter Property=”Template>
  4.     <Setter.Value>
  5.       <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
  6.         <Grid>
  7.           <Ellipse x:Name=”selectedPlanetMargin=”-10StrokeThickness=”2/>
  8.           <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}”
  9.             HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}”
  10.             VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
  11.         </Grid>
  12.         <ControlTemplate.Triggers>
  13.           <Trigger Property=”IsSelectedValue=”true>
  14.             <Setter Property=”StrokeTargetName=”selectedPlanetValue=”Yellow/>
  15.           </Trigger>
  16.         </ControlTemplate.Triggers>
  17.       </ControlTemplate>
  18.     </Setter.Value>
  19.   </Setter>
  20. </Style>
* This source code was highlighted with Source Code Highlighter.

Следующий скриншот показывает конечную версию приложения. Если вы наведете мышью на картинку планеты, вы получите более подробную информацию о ней. Если вы кликните по планете, желтый эллипс окружит планету.


Здесь вы можете найти проект для Visual Studio с кодом, который был использован в статье.
Tags:
Hubs:
+22
Comments23

Articles