воскресенье, 4 августа 2013 г.

Еще раз о шаблонах элементов

Я уже пару раз писал про шаблоны. Вот здесь, о том, как поменять внешний вид кнопки. А вот здесь, как в зависимости от свойств компонента можно подгружать разные шаблоны. Сегодня предлагаю посмотреть натягивание шаблонов на более сложный элемент управления, а именно на TreeView.

Основная задача, которая может возникнуть при определении шаблона в TreeView, это переопределить элементы дерева так, чтобы они показывались по другому. Например, чтобы вместо стандартного значка сворачивания/разворачивания была наша коартинка, или задача убрать отступ у вложенных элементов. Вот такой пример и посмотрим.
Пусть у нас стоит задача, реализовать дерево для показа прогресса закачки файлов по разнвм типам: видео, музыка, программы, разное. Это будут ноды корня. Ну а дочерние ноды, будут конкретными файлами. На динамике и красивостях особо заморачиваться не буду. Поэтому для хранения дерева элементов буду использовать вот такой класс:

public class DownloadItem
{
    public string Name { getset; }
 
    public double Progress { getset; }
 
    public List<DownloadItem> Children { getset; }
}

Для генерации тестовых данных добавлю вот такой метод:
public static IEnumerable<DownloadItem> GetTestData()
{
    List<DownloadItem> result = new List<DownloadItem>();
    result.Add(
        new DownloadItem()
        {
            Name = "Видео",
            Progress = 30,
            Children = new List<DownloadItem>(
                new DownloadItem[] { 
                    new DownloadItem() { Name = "Фильм.avi", Progress = 20 },
                    new DownloadItem() { Name = "Очень интересный фильм.mpeg", Progress = 60 },
                    new DownloadItem() { Name = "Ну ниче так фильм.mp4", Progress = 10 }
                })
        }
        );
    result.Add(
        new DownloadItem()
        {
            Name = "Музыка",
            Progress = 80,
            Children = new List<DownloadItem>(
                new DownloadItem[] { 
                    new DownloadItem() { Name = "Прикольная песня.mp3", Progress = 70 },
                    new DownloadItem() { Name = "Еще одна прикольная песня.mp3", Progress = 85 },
                    new DownloadItem() { Name = "Песня из мультика.mp3", Progress = 85 }
                })
        }
        );
    result.Add(
        new DownloadItem()
        {
            Name = "Программы",
            Progress = 30,
            Children = new List<DownloadItem>(
                new DownloadItem[] { 
                    new DownloadItem() { Name = "Visual Studio 2013 preview.iso", Progress = 30 }
                })
        }
        );
    return result;
}
Для удобаства, добавим DataTemplate, для показа имени и прогресса:
<HierarchicalDataTemplate x:Key="DownloadTemplate" ItemsSource="{Binding Children}">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Name}" Margin="5" />
        <ProgressBar Value="{Binding Progress}" Width="100" Margin="0,5,0,5" />
    </StackPanel>
</HierarchicalDataTemplate>
Ну и собственно TreeView. Кидаем его на форму и при загрузке прописываем инициализацию.
<TreeView ItemTemplate="{StaticResource DownloadTemplate}" x:Name="tvMain" />

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    tvMain.ItemsSource = DownloadItem.GetTestData();
}
После запуска, это все выглядит вот так:
Добавив в проект две картинки для отображения значка развернцтого и свернутого списка дочерних элементов, начинаем работать с шаблоном. Для этого у TreeView переопеределим ItemContainerStyle.
<TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TreeViewItem">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition MinWidth="19"
                                Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <ToggleButton IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" x:Name="ToogleButton"/>
                            <ToggleButton.Template>
                                <ControlTemplate TargetType="ToggleButton">
                                    <Grid Background="Transparent">
                                        <VisualStateManager.VisualStateGroups>
                                            <VisualStateGroup x:Name="CheckStates">
                                                <VisualState x:Name="Unchecked" />
                                                <VisualState x:Name="Checked">
                                                    <Storyboard>
                                                        <DoubleAnimation Storyboard.TargetName="UncheckedVisual" Storyboard.TargetProperty="Opacity" To="0" Duration="0" />
                                                        <DoubleAnimation Storyboard.TargetName="CheckedVisual" Storyboard.TargetProperty="Opacity" To="1" Duration="0" />
                                                    </Storyboard>
                                                </VisualState>
                                            </VisualStateGroup>
                                        </VisualStateManager.VisualStateGroups>
                                        <Image Source="expand.png" x:Name="UncheckedVisual" Height="30" Margin="3" />
                                        <Image Source="collapse.png" x:Name="CheckedVisual" Height="30" Opacity="0" Margin="3" />
                                    </Grid>
                                </ControlTemplate>
                            </ToggleButton.Template>
                        </ToggleButton>
                        <ContentControl Content="{TemplateBinding Header}" ContentTemplate="{StaticResource DownloadTemplate}" Grid.Column="1" />
 
                        <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Visibility="Collapsed" Grid.ColumnSpan="100" />
                    </Grid>    
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</TreeView.ItemContainerStyle>
На что следует обратить внимание. Во-первых, мы здесь используем состояния нашей ToggleButton. Все состояния можно посмотреть здесь. По умолчанию, показывается непрозрачной одна картинка, при изменении состояния - другая. Во-вторых, свойство IsChecked мы привязываем к родительскому шаблону, т.е. состояние нашей кнопки, это состояние свернуто-развернуто нашего элемента дерева. В-третьих, я задал для ItemsPresenter-а с дочерними элементами состояние Collapsed, т.е. по умолчанию их не видно. Изменения состояния пока не определено, поэтому если запустить приложение и потыкать в картинки, то меняться они будут, а вот дочерних элементов мы не увидим:
Для того, чтобы его сворачивать и разворачивать, необходимо в зависимости от состояния переключать его Visibility в Visible и и обратно в Collapsed. Правда в ToggleButton у нас это не получится, т.к. в визуальном дереве она лежит с ItemsPresenter на одном уровне. Но не беда, воспользуемся состояниями нашего TreeViewitem:
<Style TargetType="TreeViewItem">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="TreeViewItem">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ExpansionStates">
                            <VisualState x:Name="Collapsed" />
                            <VisualState x:Name="Expanded">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="(UIElement.Visibility)" Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"  />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

Теперь это выглядит вот так:
Практически то что надо, осталось только убрать иконку сворачивания-разворачивания, для файлов. Для этого добавим обработчик состояния NoItems нашего TreeView:
<VisualState x:Name="NoItems">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ToogleButton" Storyboard.TargetProperty="(UIElement.Visibility)" Duration="0">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"  />
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</VisualState>
Смотрим:
Вроде хорошо получилось.

Комментариев нет:

Отправить комментарий