29 October 2008

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

.NET
Translation
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. <Style TargetType="ListBoxItem">
  26. <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  27. <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  28. (…)
  29. </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. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  10. {
  11. throw new NotSupportedException(”This method should never be called”);
  12. }
  13. }
* 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:.netwpfbindingxaml
Hubs: .NET
+22
34.3k 49
Comments 23
Popular right now
Рефакторинг кода .NET
December 7, 202030,200 ₽Luxoft Training
Профессия iOS-разработчик
December 7, 202090,000 ₽SkillFactory
SEO-специалист
December 7, 202064,900 ₽Нетология
iOS-разработчик с нуля
December 7, 202070,740 ₽Нетология