В WPF существует очень четкое разделение между поведением Control'а и тем, как он выглядит. К примеру, поведение объекта класса Button состоит в том, чтобы реагировать на различные события по клику, но его вид может быть любым — вы можете сделать кнопку в виде стрелки, рыбы, или чего-либо еще, что подходит для вашего приложения. Переопределение отображения Control'а очень просто сделать при использовании VS со стилями и шаблонами, и даже еще проще, если у вас есть Microsoft Expression Blend. В этом примере я покажу вам, как переопределить отображение ListBox'а, который используется для отображения списка планет.
Я решила начать с создания источника данных с планетами и солнцем. Я определила класс «SolarSystemObject» с такими свойствами: Name, Orbit, Diameter, Image and Details. Я перегрузила метод ToString() в этом классе так, чтобы он возвращал название объекта солнечной системы. Потом я добавила класс «SolarSystem» со свойством «SolarSystemObjects» с типом ObservableCollection. В конструкторе класса «SolarSystem» я добавила солнце и девять планет в коллекцию «SolarSystemObjects».
Как только я определила источник данных, я была готова добавить на главное окно ListBox, который был связан с данной коллекцией:
И так, ListBox отображает планеты, но визуально это все еще выглядит несколько простовато:

На данном этапе я начала думать о том, как отобразить планеты наиболее реалистичным способом — моей целью было достигнуть отображения, сходного с диаграммами солнечной системы в школьных учебниках. Первым шагом стало изменения layout'а у ListBoxItem'ов. Стандартным layout'ом для ListBox'а является StackPanel, которая заставляет ListBoxItem'ы отображаться один за другим (если быть более точным, это VirtualizingStackPanel, которая добавляет виртуализацию к традиционному StackPanel). Для того чтобы отобразить планеты так, как я хочу, мне требуется Canvas, который позволяет мне позиционировать элементы в нем по определенному числу пикселей слева и сверху от границ этого Canvas'а. У ListBox'а есть свойство ItemsPanel с типом ItemsPanelTemplate, которое может быть использовано для изменения layout'а ListBox'а, как и делается в моем примере. Вот как я это сделала:
Моим следующим шагом было определение отображения каждой планеты. Я сделала это, используя DataTemplate. Я решила представлять каждую планету ее изображением и белым эллипсом, имитирующим ее орбиту вокруг солнца. Я также добавила подсказку с подробной информацией о планете, которая появляется, когда курсор находится над планетой.
Как вы можете видеть в шаблоне и стиле выше, свойства, которые определяют положение ListBoxItem'а и положение и размер Ellips'а основываются на орбите планеты и все используют один и тот же конвертер, только с различными параметрами. Задача конвертера состоит в преобразовании расстояний между объектами солнечной системы в расстояния внутри Canvas'а в пикселях. Моя первая реализация этого конвертера просто перемножала значение орбиты на константу, но я обнаружила, что внутренние планеты были очень тесно расположены друг к другу. Поэтому я решила немного изменить расчет, чтобы сделать его нелинейным. Я так же решила, чтобы конвертер принимал некий параметр, который масштабировал бы конечный результат на некоторое значение так, чтобы я могла использовать эту логику множество раз.
Если вы запустите приложение сейчас, то вы увидите, что все планеты корректно расположены по отношению к солнцу. Если вы наведете на них мышь, то вы получите более детальную информацию о планете. Если вы кликнете по планете, то стандартный шаблон ListBoxItem'а назначит синий фон выбранному элементу, который выглядит как небольшая рамка вокруг элемента. Это не тот эффект, который я хотела бы видеть, поэтому я решила изменить представление выбранного элемента.
Чтобы изменить этот стиль, я думаю было бы проще использовать Microsoft Expression Blend, чтобы посмотреть на стандартный шаблон, а затем переделать его так, как вы хотите. Я начала с выбора ListBox'а в Blend, затем я проследовала в меню «Object», выбрала «Edit Other Styles», «Edit ItemContainerStyle» и затем «Edit a Copy». Затем я задала имя шаблону и нажала на «OK». Если на данном этапе вы проследуете на закладку «XAML», то вы увидете полный стандартный стиль для ListBoxItem'а, который включает в себя следующий шаблон:
Используя его за основу, я создала простой шаблон, который добавляет желтый эллипс вокруг выбранной планеты:
Следующий скриншот показывает конечную версию приложения. Если вы наведете мышью на картинку планеты, вы получите более подробную информацию о ней. Если вы кликните по планете, желтый эллипс окружит планету.

Здесь вы можете найти проект для Visual Studio с кодом, который был использован в статье.
Я решила начать с создания источника данных с планетами и солнцем. Я определила класс «SolarSystemObject» с такими свойствами: Name, Orbit, Diameter, Image and Details. Я перегрузила метод ToString() в этом классе так, чтобы он возвращал название объекта солнечной системы. Потом я добавила класс «SolarSystem» со свойством «SolarSystemObjects» с типом ObservableCollection. В конструкторе класса «SolarSystem» я добавила солнце и девять планет в коллекцию «SolarSystemObjects».
Как только я определила источник данных, я была готова добавить на главное окно ListBox, который был связан с данной коллекцией:
- <Window.Resources>
- <local:SolarSystem x:Key=”solarSystem” />
- (…)
- </Window.Resources>
- <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'а, как и делается в моем примере. Вот как я это сделала:
- <Style TargetType=”ListBox”>
- <Setter Property=”ItemsPanel”>
- <Setter.Value>
- <ItemsPanelTemplate>
- <Canvas Width=”590? Height=”590? Background=”Black” />
- </ItemsPanelTemplate>
- </Setter.Value>
- </Setter>
- </Style>
* This source code was highlighted with Source Code Highlighter.
Моим следующим шагом было определение отображения каждой планеты. Я сделала это, используя DataTemplate. Я решила представлять каждую планету ее изображением и белым эллипсом, имитирующим ее орбиту вокруг солнца. Я также добавила подсказку с подробной информацией о планете, которая появляется, когда курсор находится над планетой.
- <DataTemplate DataType="{x:Type local:SolarSystemObject}">
- <Canvas Width="20" Height="20" >
- <Ellipse
- Canvas.Left="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-1.707}"
- Canvas.Top="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-0.293}"
- Width="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
- Height="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
- Stroke="White"
- StrokeThickness="1"/>
- <Image Source="{Binding Path=Image}" Width="20" Height="20">
- <Image.ToolTip>
- <StackPanel Width="250" TextBlock.FontSize="12">
- <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />
- <StackPanel Orientation="Horizontal">
- <TextBlock Text="Orbit: " />
- <TextBlock Text="{Binding Path=Orbit}" />
- <TextBlock Text=" AU" />
- </StackPanel>
- <TextBlock Text="{Binding Path=Details}" TextWrapping="Wrap"/>
- </StackPanel>
- </Image.ToolTip>
- </Image>
- </Canvas>
- </DataTemplate>
-
- <Style TargetType="ListBoxItem">
- <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
- <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
- (…)
- </Style>
* This source code was highlighted with Source Code Highlighter.
Как вы можете видеть в шаблоне и стиле выше, свойства, которые определяют положение ListBoxItem'а и положение и размер Ellips'а основываются на орбите планеты и все используют один и тот же конвертер, только с различными параметрами. Задача конвертера состоит в преобразовании расстояний между объектами солнечной системы в расстояния внутри Canvas'а в пикселях. Моя первая реализация этого конвертера просто перемножала значение орбиты на константу, но я обнаружила, что внутренние планеты были очень тесно расположены друг к другу. Поэтому я решила немного изменить расчет, чтобы сделать его нелинейным. Я так же решила, чтобы конвертер принимал некий параметр, который масштабировал бы конечный результат на некоторое значение так, чтобы я могла использовать эту логику множество раз.
- public class ConvertOrbit : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- double orbit = (double)value;
- double factor = System.Convert.ToDouble(parameter);
- return Math.Pow(orbit / 40, 0.4) * 770 * factor;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotSupportedException(”This method should never be called”);
- }
- }
* 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'а, который включает в себя следующий шаблон:
- <Setter Property=”Template”>
- <Setter.Value>
- <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
- <Border SnapsToDevicePixels=”true” x:Name=”Bd” Background=”{TemplateBinding Background}” BorderBrush=”{TemplateBinding BorderBrush}” BorderThickness=”{TemplateBinding BorderThickness}” Padding=”{TemplateBinding Padding}”>
- <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}” HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}” VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
- </Border>
- <ControlTemplate.Triggers>
- <Trigger Property=”IsSelected” Value=”true”>
- <Setter Property=”Background” TargetName=”Bd” Value=”{DynamicResource {x:Static SystemColors.HighlightBrushKey}}”/>
- <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}”/>
- </Trigger>
- <MultiTrigger>
- <MultiTrigger.Conditions>
- <Condition Property=”IsSelected” Value=”true”/>
- <Condition Property=”Selector.IsSelectionActive” Value=”false”/>
- </MultiTrigger.Conditions>
- <Setter Property=”Background” TargetName=”Bd” Value=”{DynamicResource {x:Static SystemColors.ControlBrushKey}}”/>
- <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}”/>
- </MultiTrigger>
- <Trigger Property=”IsEnabled” Value=”false”>
- <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}”/>
- </Trigger>
- </ControlTemplate.Triggers>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
* This source code was highlighted with Source Code Highlighter.
Используя его за основу, я создала простой шаблон, который добавляет желтый эллипс вокруг выбранной планеты:
- <Style TargetType=”ListBoxItem”>
- (…)
- <Setter Property=”Template”>
- <Setter.Value>
- <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
- <Grid>
- <Ellipse x:Name=”selectedPlanet” Margin=”-10” StrokeThickness=”2”/>
- <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}”
- HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}”
- VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
- </Grid>
- <ControlTemplate.Triggers>
- <Trigger Property=”IsSelected” Value=”true”>
- <Setter Property=”Stroke” TargetName=”selectedPlanet” Value=”Yellow”/>
- </Trigger>
- </ControlTemplate.Triggers>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
* This source code was highlighted with Source Code Highlighter.
Следующий скриншот показывает конечную версию приложения. Если вы наведете мышью на картинку планеты, вы получите более подробную информацию о ней. Если вы кликните по планете, желтый эллипс окружит планету.
Здесь вы можете найти проект для Visual Studio с кодом, который был использован в статье.