WPF 类windows资源管理器(一)——TreeViewItem改造

与之前发布的有修改!!

之前的版本,在某些情况下会失效(在鼠标悬停在某一行的空白处时,不会改变整行的背景色)。终其原因,是因为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>

界面如下:

  • 2
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

raynadofan

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值