FluentTreeView Part.5

递归高亮

先来修正选择高亮的问题吧。我更愿意图片中的效果称为递归高亮。递归高亮可以看作是子节点都处于非递归高亮状态。这么想的话实现起来应该非常容易。但我希望FluentTreeView能够更灵活一些。最好通过属性来切换高亮模式。

说来就来,要往TreeViewItem上加新的属性,要由TreeViewItem导出子类。要使TreeView的直接ItemContainer使用我们自定义的子类,需要继承TreeView的子类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FluentTreeViewItem : TreeViewItem
{
    public bool RecursiveHighlightMode
    {
        get { return (bool) GetValue (RecursiveHighlightModeProperty); }
        set { SetValue (RecursiveHighlightModeProperty, value); }
    }

    // Using a DependencyProperty as the backing store for RecursiveHighlightMode.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RecursiveHighlightModeProperty =
        DependencyProperty.Register ("RecursiveHighlightMode", typeof (bool), typeof (FluentTreeViewItem), new PropertyMetadata (false));

    protected override DependencyObject GetContainerForItemOverride ()
    {
        return new FluentTreeViewItem ();
    }

    protected override bool IsItemItsOwnContainerOverride (object item)
    {
        return item is FluentTreeViewItem;
    }
}    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class FluentTreeView : TreeView
{
    protected override DependencyObject GetContainerForItemOverride ()
    {
        return new FluentTreeViewItem ();
    }

    protected override bool IsItemItsOwnContainerOverride (object item)
    {
        return item is FluentTreeViewItem;
    }
}

重写TreeView的GetContainerForItemOverride是常规操作,大家在自定义ItemContainer的时候肯定都做过。需要注意的是TreeViewItem也是ItemsControl,它的GetContainerForItemOverride也需要被重写。

接下来思考如何实现递归模式和非递归模式的切换。直接让子节点也处于高亮状态不错的办法,但高亮状态依赖的触发器,需要由IsSelected属性触发。为了界面而去修改数据,违背WPF数据驱动界面的原则。那么换一种思路,不再用分治的思路,而是直接在当前节点上全部处理掉。让父节点的高亮区域覆盖所有的子节点即可。宽度已经占满,只剩下高度了。对背景色,可以直接修改最外层面板的背景色。起指示作用的矩形,则可以修改其Grid.RowSpan让它充满整个高度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<ControlTemplate TargetType="cc:FluentTreeViewItem">
    <Grid x:Name="globalHighlight" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="24"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Rectangle Panel.ZIndex="20" Grid.RowSpan="1" x:Name="selector" Width="2" Visibility="Collapsed" Fill="Red" HorizontalAlignment="Left"></Rectangle>
        <Border x:Name="root">
            <Border.Style>
                <Style TargetType="Border">
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="Red"></Setter>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Border.Style>
            <Grid Background="Transparent" x:Name="selectorGrid">
                <Grid Margin="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem},Converter={StaticResource TreeLevelToIndentConverter}}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="24"></ColumnDefinition>
                        <ColumnDefinition></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <Expander IsExpanded="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Mode=TwoWay}" x:Name="expander"></Expander>
                    <ContentPresenter VerticalAlignment="Center" Grid.Column="1" ContentSource="Header"></ContentPresenter>
                </Grid>
            </Grid>
        </Border>
        <ItemsPresenter Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Converter={StaticResource BoolToVisibilityConverter}}" Grid.Row="1"></ItemsPresenter>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="HasItems" Value="False">
            <Setter TargetName="expander" Property="Visibility" Value="Collapsed"></Setter>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="True">
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
            <Setter TargetName="selector" Property="Visibility" Value="Visible"></Setter>
        </Trigger>
        <Trigger Property="RecursiveHighlightMode" Value="True">
            <Setter TargetName="selector" Property="Grid.RowSpan" Value="2"></Setter>
        </Trigger>
        <MultiTrigger >
            <MultiTrigger.Conditions>
                <Condition Property="RecursiveHighlightMode" Value="True"></Condition>
                <Condition Property="IsSelected" Value="True"></Condition>
            </MultiTrigger.Conditions>
            <Setter TargetName="globalHighlight" Property="Background" Value="Green"></Setter>
        </MultiTrigger>
        <MultiTrigger >
            <MultiTrigger.Conditions>
                <Condition Property="RecursiveHighlightMode" Value="False"></Condition>
                <Condition Property="IsSelected" Value="True"></Condition>
            </MultiTrigger.Conditions>
            <Setter TargetName="selectorGrid" Property="Background" Value="Green"></Setter>
        </MultiTrigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

我们根据RecursiveHighlightMode的值在globalHighlightselectorGrid里选出作为选择高亮的控件,修改selectorGrid.RowSpanGrid.RowSpan会使布局子系统的Measure失效。在依赖属性的元数据中声明属性变化影响Measure阶段,就实现了动态切换高亮模式。

1
2
public static readonly DependencyProperty RecursiveHighlightModeProperty =
    DependencyProperty.Register ("RecursiveHighlightMode", typeof (bool), typeof (FluentTreeViewItem), new FrameworkPropertyMetadata (false, FrameworkPropertyMetadataOptions.AffectsMeasure));

高亮优先级

回顾一下上一篇中模板的可视化树

1
2
3
4
5
6
Border(x:Name="root")
    Grid(x:Name="selectorGrid")
        Rectangle(x:Name="selector")
        Grid
            Expander
            ContentPreserenter

当选中且鼠标悬空时,rootselectorGrid的触发器都生效。因为rootselectorGrid的父级容器,渲染时就被置于靠下的一层,这就导致了选择高亮覆盖鼠标高亮的表象。要改其实很简单,把这他们两个容器的位置互换就行了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Grid Background="Transparent" x:Name="selectorGrid">
    <Border>
        <Border.Style>
            <Style TargetType="Border">
                <Style.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="Red"></Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Border.Style>

        <Grid>
            <Grid Margin="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem},Converter={StaticResource TreeLevelToIndentConverter}}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="24"></ColumnDefinition>
                    <ColumnDefinition></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <Expander IsExpanded="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Mode=TwoWay}" x:Name="expander"></Expander>
                <ContentPresenter VerticalAlignment="Center" Grid.Column="1" ContentSource="Header"></ContentPresenter>
            </Grid>
        </Grid>
    </Border>
</Grid>

配色太丑

剩下的就是一些小细节问题了。按照Fluent Design的标准,所有的图标都要使用Segoe MDL2 Assets实现,并且要根据Windows当前的主题模式和主题色动态更改颜色。

Fluent.WPF可以取得主题模式和主题色对应的画刷,但在使用它之前需要先在FluentTreeViewItem上新增属性,方便修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public Brush MouseOverBrush
{
    get { return (Brush) GetValue (MouseOverBrushProperty); }
    set { SetValue (MouseOverBrushProperty, value); }
}

// Using a DependencyProperty as the backing store for MouseOverBrush.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MouseOverBrushProperty =
    DependencyProperty.Register ("MouseOverBrush", typeof (Brush), typeof (FluentTreeViewItem), new PropertyMetadata (new SolidColorBrush (Colors.Red)));

public Brush SelectedBrush
{
    get { return (Brush) GetValue (SelectedBrushProperty); }
    set { SetValue (SelectedBrushProperty, value); }
}

// Using a DependencyProperty as the backing store for SelectedBrush.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedBrushProperty =
    DependencyProperty.Register ("SelectedBrush", typeof (Brush), typeof (FluentTreeViewItem), new PropertyMetadata (new SolidColorBrush (Colors.Green)));

选择高亮使用SystemAltMediumHighColorBrush,鼠标高亮使用SystemBaseMediumLowColorBrush,选择指示用的矩形使用SystemAccentColor

再把Expander用Segoe MDL2 Assets字体重新实现,我们的FluentTreeView就完成了。

递归高亮 fluent-treeview-recursive

非递归高亮 fluent-treeview-non-recursive


源代码参见 https://github.com/Verrickt/Melchior-Sample/tree/master/FluentTreeView_Part5

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计