Вот визуальное представление о том, чего я пытаюсь достичь:

TreeView

Я хочу, чтобы все кнопки слева и справа были выровнены по вертикали, независимо от того, где находится TreeViewItem в дереве. У меня проблемы с достижением этого эффекта, когда заголовок также имеет отступ типичным вложенным способом.

Моя ближайшая попытка заключалась в изменении шаблона TreeViewItem; размещение кнопок в DockPanel (закрепление влево или вправо), которое охватывает все столбцы в основной сетке, размещение расширителя и заголовка в среднем столбце, а также размещение ItemsPresenter (ItemsHost) для всех столбцов в следующей строке. Это выравнивает все , включая заголовок.

Вот упрощенная версия того, что у меня сейчас есть для моего стиля TreeViewItem:

<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
    <Setter Property="Focusable" Value="False"/>
    <Setter Property="Width" Value="16"/>
    <Setter Property="Height" Value="16"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
                    <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}">
                        <Path.RenderTransform>
                            <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                        </Path.RenderTransform>
                    </Path>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="RenderTransform" TargetName="ExpandPath">
                            <Setter.Value>
                                <RotateTransform Angle="180" CenterY="3" CenterX="3"/>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/>
                        <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/>
                        <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsMouseOver" Value="True"/>
                            <Condition Property="IsChecked" Value="True"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/>
                        <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/>
                    </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<HierarchicalDataTemplate x:Key="HeaderTemplate" DataType="{x:Type models:Entity}" ItemsSource="{Binding Path=Entities}">
    <Label VerticalAlignment="Center">test</Label>
</HierarchicalDataTemplate>

<Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="HeaderTemplate" Value="{StaticResource HeaderTemplate}"></Setter>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
    <Setter Property="HorizontalAlignment" Value="Left"></Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TreeViewItem}">
                <Grid HorizontalAlignment="Left" Width="300">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30"></ColumnDefinition>
                        <ColumnDefinition Width="*"></ColumnDefinition>
                        <ColumnDefinition Width="30"></ColumnDefinition>
                    </Grid.ColumnDefinitions>

                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <DockPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="0" Margin="1,0,0,0">
                        <Button
                            DockPanel.Dock="Left"/>

                        <Button
                            DockPanel.Dock="Left"/>

                        <Button
                            DockPanel.Dock="Left"
                        Margin="0"/>

                        <Button
                            DockPanel.Dock="Right"
                            HorizontalAlignment="Right"/>

                        <Button
                            DockPanel.Dock="Right"
                            HorizontalAlignment="Right"/>

                        <Button
                            DockPanel.Dock="Right"
                            HorizontalAlignment="Right"/>
                    </DockPanel>

                    <StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="0" Margin="25,0,0,0">
                        <ToggleButton x:Name="Expander" ClickMode="Press" Margin="15,0,0,0" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/>

                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <ContentPresenter x:Name="PART_Header"  ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>

                    </StackPanel>

                    <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Width="305"/>
                </Grid>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsExpanded" Value="false">
                        <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
                    </Trigger>
                    <Trigger Property="HasItems" Value="false">
                        <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
                    </Trigger>
                    <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="IsSelectionActive" Value="false"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
                    </MultiTrigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Как сделать так, чтобы заголовок располагался как на картинке, сохраняя при этом выравнивание закрепленных кнопок?

1
Jake Steed 15 Апр 2020 в 00:27

1 ответ

Лучший ответ

Как вы, наверное, уже догадались, это не совсем тривиально. Проблема в том, что в отличие от ListBox и DataGrid и др., TreeView не имеет строковой структуры. Вместо этого он использует иерархическую структуру, которая выглядит примерно так:

enter image description here

Есть сетка, которая охватывает весь элемент управления, но для любой данной «строки» TreeViewItem не простирается полностью влево, где вы хотите разместить свои кнопки.

Итак, чтобы реализовать это, вам придется повторно шаблон TreeViewItem. В шаблоне по умолчанию для размещения своего содержимого используется сетка, при этом содержимое строки находится в строке 0, а дочерние элементы (если есть) в строке 1. Важно отметить, что дочерние элементы также помещаются в столбец 1, как TreeView выполняет свои отступы. Итак, первый шаг - разместить дополнительные 3 столбца слева и еще 3 справа, чтобы разместить шесть кнопок, которые вы хотите добавить в каждую строку. Затем вы захотите изменить Grid.Column и Grid.ColumnSpan для ItemsPresenter на каждом уровне, чтобы он занимал ширину всего элемента управления.

Теперь проблема, конечно же, в том, что вы потеряли отступ, поэтому вам придется добавить еще один столбец в сетку, чтобы добавить его обратно. Чтобы правильно выполнить отступ для любого заданного уровня, вам нужно знать отступ для его родительский элемент, который ранее был встроен в сам макет, но теперь удален. Есть несколько решений для этого, но самый простой IMO - использовать присоединенное свойство, которое я назову TreeViewItemHelper.Indent. Для каждого ItemsPresenter в вашем TreeView вы захотите вычислить отступ, который будет использоваться для всех дочерних элементов ниже текущего уровня:

<ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="10" Grid.Column="0" Grid.Row="1"
    local:TreeViewItemHelper.Indent="{Binding Path=(local:TreeViewItemHelper.Indent), Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}, Converter={StaticResource IndentConverter}}" />

Обратите внимание, что я на самом деле не использую здесь значение для чего-либо, просто вычисляю, каким оно должно быть, привязывая каждый TreeViewItemHelper.Indent к одному на предыдущем уровне и пропуская его через преобразователь, который просто добавляет фиксированное значение (т.е. кнопок ToggleButtons, используемых для раскрытия узлов дерева):

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    return new GridLength(((GridLength)value).Value + IndentSize);
}

Наконец, вам нужно где-нибудь нанести отступ. Мы уже создали для него столбец в нашей сетке, поэтому мы можем просто добавить фиктивный элемент управления к этому столбцу и привязать его ширину к уровню отступа любого ItemsPresenter, внутри которого он находится:

<Rectangle Grid.Column="3" Width="{Binding Path=(local:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent"/>

Сложите все это вместе, хорошо встряхните, и вот как выглядит XAML для повторного шаблона TreeViewItem и присвоения ему стиля:

<local:IndentConverter x:Key="IndentConverter" />

<ControlTemplate x:Key="TreeViewItemControlTemplate1" TargetType="{x:Type TreeViewItem}">
    <Grid x:Name="tvGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <!--<ColumnDefinition Width="0"/>-->
            <ColumnDefinition MinWidth="19" Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!-- Left buttons -->
        <Button Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />
        <Button Grid.Column="1" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />
        <Button Grid.Column="2" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />

        <Rectangle Grid.Column="3" Width="{Binding Path=(local:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent"/>

        <ToggleButton x:Name="Expander" Grid.Column="4" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}">
            <ToggleButton.Style>
                <Style TargetType="{x:Type ToggleButton}">
                    <Setter Property="Focusable" Value="False"/>
                    <Setter Property="Width" Value="16"/>
                    <Setter Property="Height" Value="16"/>
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type ToggleButton}">
                                <Border Background="Transparent" Height="16" Padding="5" Width="16">
                                    <Path x:Name="ExpandPath" Data="M0,0 L0,6 L6,0 z" Fill="White" Stroke="#FF818181">
                                        <Path.RenderTransform>
                                            <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                                        </Path.RenderTransform>
                                    </Path>
                                </Border>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsChecked" Value="True">
                                        <Setter Property="RenderTransform" TargetName="ExpandPath">
                                            <Setter.Value>
                                                <RotateTransform Angle="180" CenterY="3" CenterX="3"/>
                                            </Setter.Value>
                                        </Setter>
                                        <Setter Property="Fill" TargetName="ExpandPath" Value="#FF595959"/>
                                        <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF262626"/>
                                    </Trigger>
                                    <Trigger Property="IsMouseOver" Value="True">
                                        <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF27C7F7"/>
                                        <Setter Property="Fill" TargetName="ExpandPath" Value="#FFCCEEFB"/>
                                    </Trigger>
                                    <MultiTrigger>
                                        <MultiTrigger.Conditions>
                                            <Condition Property="IsMouseOver" Value="True"/>
                                            <Condition Property="IsChecked" Value="True"/>
                                        </MultiTrigger.Conditions>
                                        <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF1CC4F7"/>
                                        <Setter Property="Fill" TargetName="ExpandPath" Value="#FF82DFFB"/>
                                    </MultiTrigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ToggleButton.Style>
        </ToggleButton>
        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="5" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
            <ContentPresenter x:Name="PART_Header" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" ContentStringFormat="{TemplateBinding HeaderStringFormat}" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
        </Border>

        <!-- Right buttons -->
        <Button Grid.Column="7" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />
        <Button Grid.Column="8" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />
        <Button Grid.Column="9" Width="12" Height="12" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" />

        <ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="10" Grid.Column="0" Grid.Row="1"
            local:TreeViewItemHelper.Indent="{Binding Path=(local:TreeViewItemHelper.Indent), Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}, Converter={StaticResource IndentConverter}}" />
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded" Value="False">
            <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasItems" Value="False">
            <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
        </Trigger>
        <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="IsSelectionActive" Value="False"/>
            </MultiTrigger.Conditions>
            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
        </MultiTrigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

    <Style TargetType="TreeViewItem">
        <Setter Property="Template" Value="{DynamicResource TreeViewItemControlTemplate1}" />
        <Setter Property="IsExpanded" Value="True" />
    </Style>

Вот класс для прикрепленного свойства, которое он использует:

public static class TreeViewItemHelper
{
    public static GridLength GetIndent(DependencyObject obj)
    {
        return (GridLength)obj.GetValue(IndentProperty);
    }

    public static void SetIndent(DependencyObject obj, GridLength value)
    {
        obj.SetValue(IndentProperty, value);
    }

    // Using a DependencyProperty as the backing store for Indent.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IndentProperty =
        DependencyProperty.RegisterAttached("Indent", typeof(GridLength), typeof(TreeViewItemHelper), new PropertyMetadata(new GridLength(0)));
}

И, наконец, конвертер отступов:

public class IndentConverter : IValueConverter
{
    private const int IndentSize = 16;  // hard-coded into the XAML template

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return new GridLength(((GridLength)value).Value + IndentSize);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Binding.DoNothing;
    }
}

И вот результат:

enter image description here

4
Mark Feldman 16 Апр 2020 в 01:59