3D галерея используя шаблоны WPF

WPF Важно
  1. 4 года назад

    Alexandr_Erohin

    Aug 13 Администратор
    Добавлено 4 года назад Alexandr_Erohin

    В данном небольшом посте я бы хотел описать мощь использования шаблонов в WPF на примере создания интерактивной галереи изображений:

    -image-

    СКАЧАТЬ ДЕМО-ПРОЕКТ (Visual Studio 11)

    Итак в качестве каркаса для данной галереи будет использоваться элемент ListBox, шаблон которого мы и будем изменять. Но для начала давайте выполним следующие простые шаги, необходимые для работоспособности данного примера:

    1) Создайте новый проект WPF в среде Visual Studio и присвойте ему имя, допустим, 3D Gallery.

    2) Добавьте в проект новый словарь ресурсов (Project --> Add New Item --> Resource Dictionary) и присвойте ему имя, допустим, ListBoxGalleryTemplate.xaml.

    3) Далее необходимо добавить ссылку на этот словарь в проект. Для этого откройте файл App.xaml и добавьте следующий код:

    <Application.Resources>
    
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="ListBoxGalleryTemplate.xaml"></ResourceDictionary>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
    
    </Application.Resources>

    (если вы используете Visual Studio 11, то этот код добавиться автоматически при создании словаря).

    4) Добавим теперь в проект несколько изображений (Project --> Add Existing Item). Для изображений я обычно использую папку Images, но вы можете добавить их в произвольное место в проекте.

    5) Добавим XML-файл для перелинковки изображений в приложении (я назвал его gallery.xml). Структура этого файла выглядит так:

    <?xml version="1.0" encoding="utf-8" ?>
    <Gallery>
      <Image>
        <Source>Images/img1.jpg</Source>
        <Description>Auto №1</Description>
        <Rotation>-40</Rotation>
      </Image>
      <Image>
        <Source>Images/img2.jpg</Source>
        <Description>Auto №2</Description>
        <Rotation>-30</Rotation>
      </Image>
        ...
    </Gallery>

    Как видите в тэге Source задается путь к картинке, в Description ее описание, в тэге Rotation - угол, на который будем поворачивать картинку. Конечно, в реальном приложении подобная коллекция будет получаться скорее всего динамически, например из б/д или из коллекции изображений, хранящихся на компьютере пользователя, но здесь я ограничился простым XML-файлом, созданным для демонстрации.

    6) Теперь добавим новый источник данных XmlDataProvider для этой коллекции в файле главного окна (MainWindow.xaml) в ресурсах Window, чтобы затем использовать его в качестве контекста данных для ListBox. Разметка главного окна на данном этапе должна выглядеть примерно следующим образом:

    <Window x:Class="_3D_Gallery.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="3D Gallery" Height="800" Width="1200" WindowStartupLocation="CenterScreen">
        <Window.Resources>
            <XmlDataProvider x:Key="DataDS" Source="gallery.xml" ></XmlDataProvider>
        </Window.Resources>
        <Grid Background="#252525">
            
        </Grid>
    </Window>

    Элемент ListBox добавим внутрь Grid чуть позже.

    7) Для работоспособности описанного ниже шаблона, добавим файл отделенного кода к словарю ресурсов ListBoxGalleryTemplate.xaml, созданному во втором пункте.

    Для этого добавьте новый файл класса в проект (Project --> Add Class) и укажите название файла идентичное названию словаря ресурсов, т.е. ListBoxGalleryTemplate.cs.

    После создания этого файла, среда Visual Studio добавит новый класс ListBoxGalleryTemplate внутри этого файла. Для реализации модели CodeBehind нужно сделать следующее:

    • Добавить модификатор partial к созданному классу в коде
    • Унаследовать класс от ResourceDictionary
    • Добавить к классу конструктор по умолчанию, вызывающий метод InitializeComponent();
    • В файле разметке (в нашем случае ListBoxGalleryTemplate.xaml) добавить ссылку на этот класс через расширение x:Class

    Так же потребуется добавить ссылки на пространства имен, используемых в проектах WPF по умолчанию (понадобиться в коде). Результирующий файл ListBoxGalleryTemplate.cs должен выглядеть следующим образом:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Globalization;
    using System.Windows.Media.Animation;
    
    namespace _3D_Gallery
    {
        public partial class ListBoxGalleryTemplate : ResourceDictionary
        {
            public ListBoxGalleryTemplate()
            {
                InitializeComponent();
            }
        }
    }

    Соответственно ListBoxGalleryTemplate.xaml:

    <ResourceDictionary x:Class="_3D_Gallery.ListBoxGalleryTemplate"
                        x:ClassModifier="public"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    </ResourceDictionary>

    Хочу обратить ваше внимание что использование этого и 2го пункта необязательно, ведь мы можем указать шаблон ListBox в ресурсах самого окна, как сделали это для элемента XmlDataProvider в предыдущем пункте. Да, такой вариант будет тоже работать корректно, но поверьте моему опыту, лучше указывать для каждого шаблона свой отдельный словарь ресурсов! Это поможет не только не засорять код проекта, но и позволит легко переносить указанную галерею в другие приложения.

    8) С этого шага начинается все самое интересное.

    Итак, предыдущие шаги можно назвать подготовительными перед созданием самой галереи. Откройте словарь ресурсов ListBoxGalleryTemplate.xaml и определите новый шаблон для элемента ListBox:

    <Style x:Key="ListBoxGallery" TargetType="{x:Type ListBox}">
            <Setter Property="ItemsPanel" Value="{DynamicResource ItemsPanelTemplate}"></Setter>
            <Setter Property="ItemContainerStyle" Value="{DynamicResource ListBoxItemStyle}"></Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <!-- Структура ListBox -->
                        <Border x:Name="Bd" SnapsToDevicePixels="True" Background="Transparent">
                            <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="False">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"></ItemsPresenter>
                            </ScrollViewer>
                        </Border>
                        <!-- Триггеры -->
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"></Setter>
                            </Trigger>
                            <Trigger Property="IsGrouping" Value="True">
                                <Setter Property="ScrollViewer.CanContentScroll" Value="False"></Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
    </Style>
  2. Alexandr_Erohin

    Aug 13 Администратор

    В самом шаблоне нет ничего интересного (он практически идентичен стандартному), но обратите внимание, что в стиле задаются два свойства:

    • ItemsPanel - изменяет стандартный внутренний контейнер элемента ListBoxItem. По умолчанию используется (как и в большинстве других элементов) ScrollViewer, нам же нужен Grid.
    • ItemContainerStyle - задает новый стиль (в котором мы и передадим шаблон) элемента ListBoxItem.

    Давайте создадим два этих шаблона, ниже стиля ListBoxGallery:

    <!-- Изменяем контейнер ListBoxItem на Grid -->
        <ItemsPanelTemplate x:Key="ItemsPanelTemplate">
            <Grid  IsItemsHost="True"/>
        </ItemsPanelTemplate>
        
        <!-- Стиль и шаблон для элементов ListBoxItem -->
        <Style x:Key="ListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Padding" Value="2,0,0,0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border  x:Name="Bd" SnapsToDevicePixels="True"  Background="#EEFFFFFF" BorderBrush="#FFCCCCCC"  RenderTransformOrigin="1,1" 
                                 HorizontalAlignment="Center" VerticalAlignment="Center" BorderThickness="1" Padding="2" Margin="5,5,5,5">
                            <Border.RenderTransform>
                                <TransformGroup>
                                    <RotateTransform Angle="{Binding XPath=Rotation}" x:Name="transRotation"/>
                                </TransformGroup>
                            </Border.RenderTransform>
                            <Grid SnapsToDevicePixels="True">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <Image x:Name="img" Source="{Binding XPath=Source}" Height="200" Stretch="Uniform" RenderTransformOrigin="0,0" SnapsToDevicePixels="True">
                                    <Image.RenderTransform>
                                        <TransformGroup>
                                            <ScaleTransform ScaleX="1" ScaleY="1" x:Name="scaleTrans"/>
                                        </TransformGroup>
                                    </Image.RenderTransform>
                                </Image>
                                <TextBlock Text="{Binding XPath=Rotation}" Visibility="Collapsed" Loaded="TextBlock_Loaded"/>
                                <Button Visibility="Collapsed" Style="{DynamicResource closeButtonStyle}" x:Name="closeButton" Click="closeButton_Click"  VerticalAlignment="Top" Width="20" Height="20" HorizontalAlignment="Right" Margin="0,5,5,0" Content="X"/>
                                <Border Grid.Row="1" Height="30" VerticalAlignment="Bottom" x:Name="txtBorder"  BorderThickness="0,2,0,0" Background="#22FFFFFF">
                                    <TextBlock x:Name="desc" Margin="5,0,0,0" VerticalAlignment="Center" FontWeight="Bold" Text="{Binding XPath=Description}" Foreground="#FF1C1C1C">
                                            <TextBlock.RenderTransform>
                                                <TransformGroup>
                                                    <TranslateTransform X="0" Y="0" x:Name="transX"/>
                                                </TransformGroup>
                                            </TextBlock.RenderTransform>
                                    </TextBlock>
                                </Border>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Trigger.EnterActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="transX" Storyboard.TargetProperty="X" To="10" Duration="00:00:00.3"/>
                                            <ColorAnimation Storyboard.TargetName="desc" Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)"
                                                            To="#FF32EBFB" Duration="00:00:00.3"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                                <Trigger.ExitActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="transX" Storyboard.TargetProperty="X" To="0" Duration="00:00:00.3"/>
                                            <ColorAnimation Storyboard.TargetName="desc" Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)"
                                                            To="#FF1C1C1C" Duration="00:00:00.3"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.ExitActions>
                            </Trigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Visibility" TargetName="closeButton" Value="Visible"/>
                                <Setter Property="Panel.ZIndex" Value="1"/>
                                <Trigger.EnterActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
                                                                 To="400" Duration="00:00:00.3"/>
                                            <DoubleAnimation Storyboard.TargetName="transRotation" Storyboard.TargetProperty="Angle" 
                                                                 To="0" Duration="00:00:00.3"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                                <Trigger.ExitActions>
                                    <BeginStoryboard >
                                        <Storyboard >
                                            <DoubleAnimation FillBehavior="HoldEnd" Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
                                                                 To="200" Duration="00:00:00.3"/>
                                            <DoubleAnimation Changed="DoubleAnimation_Changed"  FillBehavior="Stop" Storyboard.TargetName="transRotation" Storyboard.TargetProperty="Angle" 
                                                                Duration="00:00:00.3"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.ExitActions>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
        <!-- Дополнительная кисть -->
        <LinearGradientBrush x:Key="ControlBackgroundOver" EndPoint="0,1" StartPoint="0,0">
            <GradientStop Color="#FF605F5F" Offset="0"/>
            <GradientStop Color="#FF030303" Offset="1"/>
        </LinearGradientBrush>
        
        <!-- Кнопка "Закрыть" на картинке -->
        <Style x:Key="closeButtonStyle" TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Border  Background="{StaticResource ControlBackgroundOver}" BorderBrush="#FFCCCCCC" BorderThickness="1" CornerRadius="2">
                            <TextBlock x:Name="textX" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Text="X" 
                                       Foreground="#FFEEEFFF" FontWeight="Bold"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter TargetName="textX" Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX=".8" ScaleY=".8"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
  3. Alexandr_Erohin

    Aug 13 Администратор

    Давайте последовательно разберем этот код. Во-первых, создается элемент ItemsPanelTemplate, который передает новый контейнер в качестве используемого внутри ListBoxItem.

    Затем идет сам шаблон ListBoxItem. В данном шаблоне создается структура самой галереи:

    * Элемент Border с Name="Bd" - основной каркас галереи. Обратите внимание, что в свойстве RenderTransform задается трансформация RotateTransform, которая привязывается к значению Angle соответствующего элемента из XmlDataProvider. В результате каждая картинка поворачивается на нужный угол при загрузке галереи.

    * Внутри этого элемента Border создается элемент Grid, содержащий две строки (указано в коллекции Grid.RowDefinitions). В первой строке содержится само изображение (элемент Image), кнопка "Закрыть" (элемент Button c Name="closeButton") и скрытое поле, содержащее обработчик события Loaded, используемое для синхронизации анимации.

    Во второй строке находится текстовая надпись, содержащая описание картинки. Обратите внимание, что в свойстве RenderTransform данного элемента TextBlock содержится трансформация TranslateTransform. Она используется в анимации.

    После структуры самой галереи идут триггеры запускающие анимации.

    Первый триггер проверяет свойство IsMouseOver и запускает две анимации - DoubleAnimation (при наведении на картинку надпись немного смещается) и ColorAnimation (при наведении на картинку цвет надписи также меняется на голубой). После того как пользователь отвел мышку с картинки запускаются две обратные анимации, возвращающие текстовые элементы в исходное состояние.

    Второй триггер проверяет свойство IsSelected. Сначала он устанавливает видимым кнопку closeButton. Затем меняет свойство Panel.ZIndex на 1, чтобы выбираемый элемент ListBoxItem оказался поверх других. И наконец запускает две анимации, одна из которых изменяет свойство Height контейнера, увеличивая его при открытии картинки, вторая изменяет угол трансформации контейнера, в результате чего создается красивая анимация разворачивания картинки.

    Последним в приведенном выше коде является шаблон кнопки закрыть, который довольно простой и смысла разбирать его нет.

    Последним важным моментом является обработка клика по кнопке закрыть. Обработчик события closeButton_Click указывается в файле отделенного кода, который мы создали в 7 пункте:

    private void closeButton_Click(object sender, RoutedEventArgs e)
            {
                DependencyObject dobj = VisualTreeHelper.GetParent(
                    (ListBoxItem)((FrameworkElement)sender).TemplatedParent);
    
                while ((dobj as ListBox) == null)
                {
                    dobj = VisualTreeHelper.GetParent(dobj);
                }
    
                if (dobj != null)
                    (dobj as ListBox).SelectedIndex = -1;
    }

    Данный обработчик нужен, чтобы при закрытии картинки свойство SelectedIndex элемента ListBox возвращалось в -1 (иначе анимация закрытия просто не будет работать). Как видите приведенный код не так прост, как может показаться. Сначала создается объект dobj типа DependencyObjec, который ищет наследника кнопки типа LisBoxItem (фактически получается, что мы получаем ссылку на текущий элемент ListBoxItem). Затем в цикле While ищем корневой элемент ListBox и затем задаем его свойство SelectedIbdex.

    9) Теперь осталось создать сам элемент ListBox в главном окне приложения MainWindow.xaml:

    ...
    <Grid Background="#252525">
            <ListBox  Style="{DynamicResource ListBoxGallery}"  x:Name="lb"
                     ItemsSource="{Binding Mode=Default, Source={StaticResource DataDS}, XPath=/Gallery/Image}"
                     ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
                     />
    </Grid>

    Обратите внимание на выражение привязки при задании коллекции данных. Здесь используется расширение разметки XPath. (Если вы подумаете переписать данную галерею на Silverlight, то столкнетесь с проблемой поддержки XmlDataProvider, который попросту отсутствует в Silverlight. Как вариант можно использовать LINQ to XML и задавать коллекцию для ListBox в коде).

    Все, галерея готова!

    ----------------------------------------------------------------------------------------------------------------------------

    Данный пример был создан не для практического применения (конечно если вы не пишете что то типа Picasa). Все таки здесь я постарался наглядно продемонстрировать мощные стороны WPF (анимации, шаблоны, привязки, ресурсы и т.д.).

    Да, кто-то может сказать глядя на этот пример, что создание подобной галереи с помощью вручную написанного UserControl'a получился бы не намного тяжелее, но все таки здесь нам не приходится иметь дело с низкоуровневыми особенностями создания собственных элементов управления (такими, например, как регистрация свойств зависимости и т.п.).

или зарегистрируйтесь чтобы ответить