天天看點

WPF 類windows資料總管(一)——TreeViewItem改造一、TreeViewItem的布局二、修改TreeViewItem模闆 三、修改資料模闆四、縮進1五、縮進2六、完整代碼

與之前釋出的有修改!!

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

此處布局的樣式如下圖所示:

WPF 類windows資料總管(一)——TreeViewItem改造一、TreeViewItem的布局二、修改TreeViewItem模闆 三、修改資料模闆四、縮進1五、縮進2六、完整代碼

從上圖可知,每一個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>
           

界面如下:

WPF 類windows資料總管(一)——TreeViewItem改造一、TreeViewItem的布局二、修改TreeViewItem模闆 三、修改資料模闆四、縮進1五、縮進2六、完整代碼

繼續閱讀