WPF TreeView: How to make controls align relative to every TreeViewItem, yet still get the indentation effect for the header text?
Asked Answered
S

1

2

Here's a visual idea of what I'm trying to accomplish:

TreeView

I want all the buttons on the left and right sides to align vertically, no matter where the TreeViewItem is located in the tree. I'm having trouble achieving this effect while also getting the header to indent in a typical nested way.

My closest attempt involved modifying the TreeViewItem template; putting the buttons in a DockPanel (docking left or right) that spans all columns in the main grid, putting the expander & header in the middle column, and having the ItemsPresenter (ItemsHost) span all columns on the following row. This makes everything align, including the header stuff.

Here's a simplified version of what I currently have for my TreeViewItem style:

<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>

How can I get the header to nest like in the picture while maintaining the docked buttons' alignment?

Stem answered 14/4, 2020 at 21:27 Comment(0)
T
7

As you've probably figured out by now, this isn't exactly trivial. The problem is that unlike ListBox and DataGrid et al, TreeView doesn't have a row-like structure. Instead, it uses a hierarchical structure that looks somewhat like this:

enter image description here

There's a grid that wraps the entire control, but for any given "row" the TreeViewItem doesn't extend all the way over to the left where you want to place your buttons.

So to implement this, you're going to have to re-template TreeViewItem. The default template uses a grid to place its content, with the line content on row 0 and the children (if any) on row 1. Importantly, the children are also placed in column 1, which is how TreeView does its indenting. So the first step is to put an extra 3 columns on the left, plus another 3 on the right, to accommodate the six buttons you want to add to each row. Then you'll want to change the Grid.Column and Grid.ColumnSpan for the ItemsPresenter at each level so that it takes up the width of the entire control.

The problem now, of course, is that you've lost your indenting, so you'll have to add another column to the grid to add that back in. To do indenting properly for any given level, you need to know the indent for its parent, which was previously inherent in the layout itself but has now been stripped out. There are several solutions to this, but the easiest IMO is to use an attached property, which I'll call TreeViewItemHelper.Indent. For each ItemsPresenter in your TreeView you'll want to calculate the indentation to use for all the children below the current level:

<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}}" />

Note that I'm not actually using the value for anything here, just calculating what it should be by binding each TreeViewItemHelper.Indent to one in the previous level and running it through a converter which simply adds a fixed amount (i.e. the width of the ToggleButtons used to expand the tree nodes):

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

Finally, you need to actually apply the indentation somewhere. We've already created a column in our grid for it, so we can just add a dummy control to that column and bind its width to the indentation level of whatever ItemsPresenter it's sitting inside:

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

Add all that together, shake well, and here's what the XAML looks like to re-template TreeViewItem and assign it with a style:

<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>

Here is the class for the attached property it uses:

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)));
}

And finally the indentation converter:

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;
    }
}

And here's the result:

enter image description here

Tirewoman answered 16/4, 2020 at 1:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.