FluentTreeView Part.4

这篇起我们正式开始实现FluentTreeView。先看看图片
FluentTreeView

TreeView左侧有一选择高亮,Item有一鼠标高亮。这两个高亮与整个TreeView一样宽。
再回想一下我们上一篇用Expander实现的TreeView,Item的缩进是如何实现的?

- Column1 Column2
Row1 Expander ContentPreserenter
Row2 / ItemsPreserenter

ItemsPreserenter将会被展开为

1
2
3
4
ItemsPanel
TreeViewItem
TreeViewItem
TreeViewItem

这就意味着,下一级的TreeViewItem永远处于上一级TreeViewItem的第二列里。将第一列的列宽设置为定值,就实现了各个层级的缩进。

进一步思考,如果我们在这样的结构里去修改TreeViewItem的面板的背景色当作高亮的话,那这个面板自身也是被缩进的。
我们试一试,把Style改成这样

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
<Style TargetType="TreeViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Grid x:Name="root">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Expander IsExpanded="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Mode=TwoWay}" x:Name="expander"></Expander>
<ContentPresenter VerticalAlignment="Center" Grid.Column="1" ContentSource="Header"></ContentPresenter>
<ItemsPresenter Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Converter={StaticResource BoolToVisibilityConverter}}" Grid.Row="1" Grid.Column="1"></ItemsPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="expander" Property="Visibility" Value="Collapsed"></Setter>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="root" Property="Background" Value="Red"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

在选中非第一级Item时,高亮色就出现了我们不需要的左边距。这不符合我们的要求,所以不能用递归的方式来实现不同层级的缩进。
recursive-indent
我们所想要的是,每一层TreeViewItem的宽度都与TreeView本身保持一致,所以TreeViewItem自己必须是打平的。需要特殊处理的是ContentPreserenter。可以根据TreeViewItem的层级计算出所需要的左边距。

再次考虑整个TreeViewItem的布局,鼠标和选择高亮应该撑满可用宽度,倒三角符号(▽)和ContentPreserenter则应该随着层级缩进。有了这些做参照,很容易能写出这样的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid x:Name="root">
<Rectangle x:Name="selector" Width="2" Visibility="Collapsed" Fill="Green" HorizontalAlignment="Left"></Rectangle>
<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>
<ItemsPresenter Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},Path=IsExpanded,Converter={StaticResource BoolToVisibilityConverter}}" Grid.Row="1"></ItemsPresenter>
</Grid>

TreeLevelToIndentConverter是根据等级计算缩进的Converter。

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
public class TreeLevelToIndentConverter : IValueConverter
{
public Thickness Margin { get; set; }
public object Convert (object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TreeViewItem ti)
{
int level = 0;
FrameworkElement current = ti;
do
{
if (VisualTreeHelper.GetParent (current) is FrameworkElement fe)
{
if (fe is TreeViewItem)
{
level++;
}
if (fe is TreeView)
{
break;
}
current = fe;
}
} while (current != null);
return new Thickness (Margin.Left * level, 0, Margin.Right * level, 0);
}
return value;
}
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

选择高亮已经差不多了,但鼠标高亮还有点问题
fluent-tree-view-with-selector

一番搜索发现,当前鼠标悬空的TreeViewItem以及它所有的祖先的IsMouseOver触发器都会起作用

当然,你也可以像我一样偷懒:给Border设置MouseOver的触发器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>
<Rectangle x:Name="selector" Width="2" Visibility="Collapsed" Fill="Green" HorizontalAlignment="Left"></Rectangle>
<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>

fluent-tree-view-mouseover-fix

看起来已经像模像样了,但还有瑕疵:

  • 选择高亮并不对所有的子节点生效。
  • 鼠标高亮没有覆盖选择高亮
  • 配色不对,丑

我们会在下一篇中改正这些问题


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