與之前釋出的有修改!!
之前的版本,在某些情況下會失效(在滑鼠懸停在某一行的空白處時,不會改變整行的背景色)。終其原因,是因為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>
界面如下: