简体   繁体   English

WPF TreeView:如何使控件相对于每个 TreeViewItem 对齐,但仍然获得 header 文本的缩进效果?

[英]WPF TreeView: How to make controls align relative to every TreeViewItem, yet still get the indentation effect for the header text?

Here's a visual idea of what I'm trying to accomplish:这是我要完成的工作的直观想法:

树视图

I want all the buttons on the left and right sides to align vertically, no matter where the TreeViewItem is located in the tree.我希望左右两侧的所有按钮都垂直对齐,无论 TreeViewItem 位于树中的哪个位置。 I'm having trouble achieving this effect while also getting the header to indent in a typical nested way.我在实现这种效果时遇到了麻烦,同时还让 header 以典型的嵌套方式缩进。

My closest attempt involved modifying the TreeViewItem template;我最接近的尝试涉及修改 TreeViewItem 模板; 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.将按钮放在跨越主网格中所有列的 DockPanel(向左或向右停靠)中,将扩展器 & header 放在中间列中,并让 ItemsPresenter (ItemsHost) 跨越下一行的所有列。 This makes everything align, including the header stuff.这使得一切都对齐,包括 header 的东西。

Here's a simplified version of what I currently have for my TreeViewItem style:这是我目前拥有的 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>

How can I get the header to nest like in the picture while maintaining the docked buttons' alignment?如何在保持停靠按钮 alignment 的同时让 header 像图片中那样嵌套?

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.问题在于,与 ListBox 和 DataGrid 等不同,TreeView 没有类似行的结构。 Instead, it uses a hierarchical structure that looks somewhat like this:相反,它使用看起来有点像这样的层次结构:

在此处输入图像描述

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.有一个包裹整个控件的网格,但对于任何给定的“行”,TreeViewItem 不会一直延伸到您想要放置按钮的左侧。

So to implement this, you're going to have to re-template TreeViewItem.因此,要实现这一点,您将不得不重新模板化 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.默认模板使用网格来放置其内容,行内容位于第 0 行,子项(如果有)位于第 1 行。重要的是,子项也放置在第 1 列,这就是 TreeView 缩进的方式。 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.所以第一步是在左边多放 3 列,在右边多放 3 列,以容纳要添加到每一行的六个按钮。 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.然后,您需要更改每个级别的 ItemsPresenter 的 Grid.Column 和 Grid.ColumnSpan,以便它占据整个控件的宽度。

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 .对此有多种解决方案,但最简单的 IMO 是使用附加属性,我将其称为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:对于 TreeView 中的每个 ItemsPresenter,您需要计算用于当前级别以下所有子项的缩进:

<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 (ie the width of the ToggleButtons used to expand the tree nodes):请注意,我实际上并没有在这里使用任何值,只是通过将每个TreeViewItemHelper.Indent绑定到上一级中的一个并通过一个转换器运行它来计算它应该是什么,该转换器只需添加一个固定量(即ToggleButtons 用于展开树节点):

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:我们已经在网格中为它创建了一个列,因此我们只需向该列添加一个虚拟控件并将其宽度绑定到它所在的任何 ItemsPresenter 的缩进级别:

<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:将所有这些加在一起,摇匀,这就是 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>

Here is the class for the attached property it uses:这是它使用的附加属性的 class:

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:结果如下:

在此处输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM