与之前发布的有修改!!
之前的版本,在某些情况下会失效(在鼠标悬停在某一行的空白处时,不会改变整行的背景色)。终其原因,是因为Border的背景为透明(Transparent),或者为null时,无法检测鼠标(我猜测是这样的)。因此,要解决这个问题,就必须让Border的内容,填充整个区域。
目标:做一个类似windows资源管理器的TreeView控件,用于展示层次化的数据结构。
功能要求:
鼠标悬停某一项时,改变整行的背景(而不是只改变内容部分的背景)
指定TreeView控件的数据源时,它默认会以TreeViewItem来展示每一个项。因此,TreeViewItem的样式,就非常重要。
一、TreeViewItem的布局
1.1 反编译TreeViewItem控件的Template属性,得到WPF默认的模板代码:
<!-- 为了方便各位学习,此处代码已经精简了布局无关的样式、触发器代码 -->
<ControlTemplate TargetType="TreeViewItem" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="19" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ToggleButton IsChecked="False" ClickMode="Press" Name="Expander"/>
<Border Name="Bd" Grid.Column="1">
<ContentPresenter ContentSource="Header" Name="PART_Header"
Content="{TemplateBinding HeaderedContentControl.Header}"/>
</Border>
<ItemsPresenter Name="ItemsHost" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2" />
</Grid>
</ControlTemplate>
此处布局的样式如下图所示:
从上图可知,每一个TreeViewItem的面板,都是一个两行、三列的Grid;
1:每个TreeViewItem都有一个ToggleButton,通过它的ControlTemplate,只是它的形状是一个三角形(每一项前面的三角形符号)
2:ContentPresenter控件表示内容控件,因为TreeViewItem是HeaderContentItemControl,所以设置ContentSource="Header",表示这个内容控件显示Header属性的内容。使用Border包裹它,用来设置背景色、边框等样式
3:ItemsPresenter表示该项的子项,它放在表格的第二行、第二列。
4:如此嵌套下去,则子项永远放在父项布局容器的第二行、第二列,因此产生类似缩进的效果。
5:代码中还有一个触发器(Trigger),用于当行为、属性值变成指定的值时,按指定的方式修改样式。
二、修改TreeViewItem模板
既然了解了TreeViewItem的默认模板构造,就可以动手自己修改模板了。要想整行选中,则必须有一个元素能填充整行,并且可以有IsMouseOver的触发器。很自然就想到了Border
如下代码所示:
<!-- 为了方便学习,精简了布局无关的代码 -->
<ControlTemplate TargetType="TreeViewItem">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- 不能绑定Background属性,否则IsMouseOver触发器会失效 -->
<Border Name="Bd" SnapsToDevicePixels="True"
BorderThickness="{TemplateBinding Border.BorderThickness}"
Padding="{TemplateBinding Control.Padding}"
BorderBrush="{TemplateBinding Border.BorderBrush}" >
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 绑定IsChecked属性到父级元素的IsExpanded属性:否则点击ToggleButton不起作用 -->
<ToggleButton Grid.Column="0" ClickMode="Press" Name="Expander"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"/ >
<!-- HorizontalAlignment必须为Stretch,才能在鼠标进入该行的空白区域时,Border的触发器也发挥作用:默认的水平对齐为左对齐,导致内容区域只有一点点,使得鼠标只有在进入内容区域时,Border的触发器才起作用-->
<ContentPresenter ContentSource="Header" Name="PART_Header" Grid.Column="1"
Content="{TemplateBinding HeaderedContentControl.Header}"
ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
ContentStringFormat="{TemplateBinding HeaderedItemsControl.HeaderStringFormat}"
HorizontalAlignment="Stretch"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Grid>
</Border>
<ItemsPresenter Name="ItemsHost" Grid.Row="1"/>
</Grid>
<ControlTemplate>
由上代码可知:TreeViewItem的布局是一个两行的表格,第一行放ToggleButton(三角形)和内容,第二行放内容的子项。
备注:
1. Border控件的Background属性默认绑定到了模板的Background属性(Background="{TemplateBinding Panel.Background}"). 当写了触发器后(IsMouseOver),不能更改Border的Background属性。可能的原因是原绑定是单项的,当鼠标进入Border后,修改了Background属性,但是Panel.Background属性又给改回去了。所以看起来无效。因此,在代码中需要删除这一绑定。
2. ToggleButton的IsChecked属性默认为False,且并没有指定当IsCheck为True时,显示子项。因此需要将IsChecked属性绑定到控件的IsExpanded属性。因为是双向绑定,无论哪一个属性改变,都会改变另一个属性的值。
3. ContentPresenter 的HorizontalAlignment属性默认值为{TemplateBinding HorizontalContentAlignment}。它会把内容区域的尺寸缩小到适合内容。如此一来,当鼠标进入项的空白区域(Border中,ContentPresenter之外的区域),将无法触发Border的IsMouseOver属性。因此,需要将ContentPresenter 的HorizontalAlignment属性值设置为Stretch。
三、修改数据模板
如果在TreeView中直接添加TreeViewItem,则上面的代码会正常工作。但如果是通过数据项来填充TreeView,则还需要设置ItemsTemplate(数据模板)。
<!-- TreeView中的项(TreeViewItem)的数据模板-->
<HierarchicalDataTemplate x:Key="TreeViewItemTemplate1" DataType="{x:Type repository:Folder}" ItemsSource="{Binding Items}">
<!-- 这个内容区域,也必须设置其HorizontalAlignment为Strech -->
<Grid>
<TextBlock HorizontalAlignment="Stretch" Margin="5,0,0,0" Text="{Binding Name}" VerticalAlignment="Center"/>
</Grid>
</HierarchicalDataTemplate>
虽然在TreeViewItem的模板中,ContentPresenter的HorizontalAlignment设置了Strech,但ContentPresenter实际上代码的就是整个HierarchicalDataTemplate下的Grid。如果这个Grid下的内容不是占据全行的话,TreeViewItem模板中的Border,依旧会有一些空白。
四、缩进1
经过上面的代码改造,当鼠标经过某一项时,这一项的整行的背景色都会发生改变。
但出现一个问题:所有项,包括子项,都没有缩进了。非常不方便观看层级结构。
要解决这个问题,可以通过项的Margin属性来控制项的位置。但是必须要知道当前元素的层级,因为不同级别的元素,缩进量是不同的。
因此,可以通过转换器来实现。写一个转换器如下:
public class IndentConverte : IValueConverter
{
// 1倍的缩进量
public int Indent { get; set; } = 8;
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var item = value as TreeViewItem;
if (item == null)
return new Thickness(0);
int level = this.GetLevels(item);
return new Thickness(Indent * level, 0, 0, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
// 获取 当前元素的在TreeView中的层级
public int GetLevels(TreeViewItem item)
{
int level = 0;
Type tree = typeof(TreeView);
FrameworkElement elem = item.Parent as FrameworkElement;
while(elem !=null && elem.GetType()!=tree)
{
level++;
elem = elem.Parent as FrameworkElement;
}
return level;
}
}
并在前台代码中,添加资源:
<local:IndentConverte x:Key="indentConverter"/>
添加资源还不够,还需要指定哪个元素的哪个绑定,使用这个资源管理器。
在TreeViewItem的模板中,第一行的第一个元素是ToggleButton,那就很自然应用到ToggleButton的Margin属性中,因为当ToggleButton缩进后,后面的内容,也就跟着缩进了。
因此在Template的ToggleButton中,需要添加如下代码:
<ToggleButton Grid.Column="0" ClickMode="Press" Name="Expander"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Margin="{Binding Converter={StaticResource indentConverter},RelativeSource={RelativeSource TemplatedParent}}">
备注:注意Margin属性
五、缩进2
缩进的基本思路是,找到该对象所以在层级,然后根据层级去计算缩进量。
在缩进1中,是遍历视觉上的父级元素去计算层级。但如果数通过数据绑定生成的TreeViewItem,则可以直接沿着对象的Parent属性向上查找,已确定其层级。
// 此元素在层次结构中的级别;最顶端的元素为0;
public int Level
{
get
{
int _level = 0;
Material mt = this;
while (mt.Parent != null)
{
if(_parent !=null)
{
_level += 1;
mt = mt.Parent;
}
}
return _level;
}
}
则可以简化转换器的写法:
public class MarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// value是数据的Level属性
int level = (int)value;
return new Thickness(level*12,0,0,0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
六、完整代码
由此,一个可以整行选中的TreeViewItem改造完毕。完整代码贴出如下:
1. 后台代码:
public class IndentConverte : IValueConverter
{
public int Indent { get; set; } = 8;
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var item = value as TreeViewItem;
if (item == null)
return new Thickness(0);
int level = this.GetLevels(item);
return new Thickness(Indent * level, 0, 0, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
// 获取 当前元素的在TreeView中的层级
public int GetLevels(TreeViewItem item)
{
int level = 0;
Type tree = typeof(TreeView);
FrameworkElement elem = item.Parent as FrameworkElement;
while(elem !=null && elem.GetType()!=tree)
{
level++;
elem = elem.Parent as FrameworkElement;
}
return level;
}
}
2. 前台代码:
<Window.Resources>
<local:IndentConverte x:Key="indentConverter"/>
<Style TargetType="TreeViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- 不能绑定Background属性:当鼠标进入区域时,设置了background属性,但Panel的属性又会覆盖设置的属性,导致触发器不起作用-->
<Border Name="Bd" SnapsToDevicePixels="True" Grid.Column="1"
BorderThickness="{TemplateBinding Border.BorderThickness}"
Padding="{TemplateBinding Control.Padding}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
>
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" ClickMode="Press" Name="Expander"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Margin="{Binding Converter={StaticResource indentConverter},RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.Resources>
<ResourceDictionary />
</Style.Resources>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Padding="5,5,5,5" Background="#00FFFFFF" Width="16" Height="16">
<Path Fill="#FFFFFFFF" Stroke="#FF818181" Name="ExpandPath">
<Path.Data>
<PathGeometry Figures="M0,0L0,6L6,0z" />
</Path.Data>
<Path.RenderTransform>
<RotateTransform Angle="135" CenterX="3" CenterY="3" />
</Path.RenderTransform>
</Path>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="RenderTransform" TargetName="ExpandPath">
<Setter.Value>
<RotateTransform Angle="180" CenterX="3" CenterY="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>
<!-- HorizontalAlignment必须为Stretch,才能在鼠标进入该行的空白区域时,Border的触发器也发挥作用:默认的水平对齐为左对齐,导致内容区域只有一点点,使得鼠标只有在进入内容区域时,Border的触发器才起作用-->
<ContentPresenter ContentSource="Header" Name="PART_Header" Grid.Column="1"
Content="{TemplateBinding HeaderedContentControl.Header}"
ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
ContentStringFormat="{TemplateBinding HeaderedItemsControl.HeaderStringFormat}"
HorizontalAlignment="Stretch"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Grid>
</Border>
<ItemsPresenter Name="ItemsHost" Grid.Row="1"/>
</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="Panel.Background" TargetName="Bd">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.HighlightBrushKey}" />
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.HighlightTextBrushKey}" />
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.GrayTextBrushKey}" />
</Setter.Value>
</Setter>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsSelectionActive" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" />
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" />
</Setter.Value>
</Setter>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
界面如下: