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

Автор оригинала: 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 с кодом, который был использован в статье.
Поделиться публикацией

Похожие публикации

Комментарии 23

    +3
    Спасибо. Отличная статья на отличном примере. Будем применять.
      +1
      Грамотно и понятно. Отличная работа. Ждём комментариев от любителей SVG :)
        0
        А в SVG что-то похожее на WPF-овый биндинг есть?
        (Просто интересно)
          +3
          Нет конечно. Но есть люди которые не перестают сравнивать эти технологии (в частности SVG и XAML).
            +1
            Есть в виде довольно грамотного расширения (Constraint SVG).
          0
          Я даже не представлял, что стандартный контрол можно изменять так гибко.
            0
            Приближаюсь к изучению WPF. Благодарю за помощь.
              0
              а плутон периодически бывает к солнцу ближе нептуна. но в данном случае это не важно, статья всё равно отличная! :)
                0
                Смотрю на любой язык программирования как на набор кубиков Lego. Чем язык мощнее, тем больше размер кубиков. Очень быстро можно построить небоскреб, быстрее чем из кубиков меньшего размера, но тем сложнее добавить миниатюрные детали, например, резной фасад. Потому кубиков меньшего размера в наборе нет, или же их количество ограничено или удобство их использования крайне посредственное.
                  0
                  В С++ какие кубики ?)
                    0
                    Там не кубики — там атомы и молекулы.
                      +1
                      Читайте выше: «Смотрю на любой язык программирования как на набор кубиков Lego.» )
                        0
                        Атомы и молекулы доступны программисту пишушему в машинных кодах в HEX редакторах.
                          0
                          Программист, пишущий в HEX редакторах, оперирует электронами, нейтронами и протонами :)
                            0
                            Элементарные частицы тоже состоят из частей меньших. Спор об уровне абстракции в HEX редакторах можно рподолжать довольно долго :)
                        0
                        В си с крестами кубики — прямая адресация памяти.
                      0
                      Да, я когда впервые это увидел, тоже офигел…
                      А для переводных статей может стоит указывать ссылку на оригинал?
                      А то за Беатрис обидно, я ее уважаю =)) Она действительно хорошо пишет, в основном ее стараниями в впфе и разобрался. =)
                        0
                        черт, не заметил, заранее извиняюсь ) но я ее все равно уважаю )
                          0
                          Ссылка на проект ошибочная, она ведет на другой пример. Вот правильная ссылка:
                          www.beacosta.com/Zips/37PlanetsListBox.zip
                            0
                            Довольно многословно (я об XML), как-то костыльно даже. В M$ явно не гонятся за минимальным концептуальным оверхедом %)

                            Статья сама по себе неплохая, но я бы немного рассказал о теории: что, зачем, откуда, почему.
                              0
                              Ссылка устарела, вот новая: bea.stollnitz.com/files/37/PlanetsListBox.zip
                                0
                                А можно ссылки на картинки поправить? Все куда-то исчезли…

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

                              Самое читаемое