WPF快速入门系列(7)——深入解析WPF模板

一、引言

  模板从字面意思理解是“具有一定规格的样板"。在现实生活中,砖块都是方方正正的,那是因为制作砖块的模板是方方正正的,如果我们使模板为圆形的话,则制作出来的砖块就是圆形的,此时我们并不能说圆形的”砖块“不是砖块吧。因为形状只是它们的外观,其制作材料还是一样的。所以,模板可以理解为表现形式。WPF中的模板同样是表现形式的意思。

  在WPF中包括三种模板:控件模板、数据模版和面板模板。它们都继承于FrameworkTemplate基类,其继承层次结果如下图所示:

  从上图可以发现,FrameworkTemplate确实有三个子类,它们正是WPF中支持的三种模板。对于控件模板,即控件外观外衣,可以通过修改控件模板来自定义控件的外观表现,例如,可以通过修改按钮的控件模板使按钮表现为圆形;数据模板,即数据的外衣。用于从一个对象中提取数据,并在内容控件或列表控件的各个项中显示数据。面板模板即面板的外衣,而面板又用于进行布局的,所以面板的外衣也就是布局的外衣,通过修改面板模板可以自定义控件的布局。例如,ListBox默认是自从向下地显示每一项的,此时可以通过修改面板模板使其自左向右地显示每一项。

  WPF模板其实都是外观的表现形式,不管是控件模板、数据模板还是面板模板,其都是改变控件的表现形式。只不过这三种控件的作用点不一样罢了。控件模板是针对于控件本身,修改它可以改变控件本身表现的样子;数据模板针对控件的数据,修改它可以改变控件绑定的数据表现样子。既然是决定数据的表现,从而决定其一般应用于数据绑定控件,如ListBox、ListView等控件。面板模板则针对于控件的布局,修改它可以影响控件的布局方式。

二、控件模板

  在分别介绍这三种控件模板之前,我觉得你有必要先了解WPF的逻辑树和可视化树的内容,因为你要修改控件模板,则首先需要了解控件的组成。

2.1 WPF的逻辑树和可视化树

  在许多技术中,元素和组件都是按树结构的形式进行组织的。使用这样的结构,开发人员可以直接操作树中的对象节点来程序对象,从而通过操作该对象来修改程序的表现和行为(这是了解逻辑树和可视化树的主要原因)。在WPF中,同样使用了树结构来组织元素之间的关系。WPF中支持逻辑树和可视化树的概念,并且WPF公开了两个提供树形视图帮助器类:LogicalTreeHelper 和 VisualTreeHelper。逻辑树指的是UI界面的组成元素的结构。先看下面的XAML代码的例子:

<Window x:Class="TemplateDemo.VisualTree"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
    <StackPanel Margin="5">
        <Button Padding="5" Margin="10">First Button</Button>
        <Button Padding="5" Margin="10">Second Button</Button>
    </StackPanel>
</Window>

  上面XAML的逻辑树如下图所示:

  可视化树是逻辑树的扩展版本,它将元素分成更小的部分。上面XAML代码对应的可视化树如下图所示:

  从上面可视化树可以看出,Button由多个可视化元素组成——使按钮具有阴影背景特征的边框(由ButtonChrome类表示)、内部的容器(一个ContentPresenter对象)以及存储按钮文本的文本块控件(由TextBlock表示)。上面的可视化树和逻辑树结构并不是我凭空想象出来的,而是有事实依据的,我们可以通过VisualTreeHelper类和LogicTreeHelper类提供的方法来查看窗口的可视化树和逻辑树,下面的例子实现了这个需求,具体的XAML实现如下所示:

<Window x:Class="TemplateDemo.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="380" Width="400">
    <StackPanel Margin="5">
        <Button Padding="5" Margin="5" Click="ShowLogicTree">Show Logic Tree Button</Button>
        <Button Padding="5" Margin="5" Click="ShowVisualTree">Show Visual Tree Button</Button>
        <!--TreeView控件用来显示窗口的逻辑树和可视化树-->
        <TreeView Name="treeElements" Margin="5"></TreeView>
    </StackPanel>
</Window>

  对应的后台代码实现入下所示:

public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        // 把公共代码抽象出一个方法,从而使代码重用
        public void ProcessElement(object obj, TreeViewItem item, TreeViewItem previousItem)
        {
            item.Header = obj.GetType().Name;
            item.IsExpanded = true;

            // 如果当前元素是第一个元素就添加到树集合上
            // 如果是内嵌元素,则添加到它的父节点上
            if (previousItem == null)
            {
                treeElements.Items.Add(item);
            }
            else
            {
                previousItem.Items.Add(item);
            }
        }

        private void PrintLogicTree(object obj, TreeViewItem previousItem)
        {
            TreeViewItem item = new TreeViewItem();
            ProcessElement(obj, item, previousItem);

            // 如果不是DependencyObject,则返回
            if (!(obj is DependencyObject))
                return;

            // 递归打印逻辑树
            foreach(object child in LogicalTreeHelper.GetChildren(obj as DependencyObject))
            {
                // 这里为了避免死循环,因为TreeView的子元素包含Window1、StackPanel等控件
                // 如果不加这个条件,控件会一直反复循环
                if (child is TreeView)
                    return;
                PrintLogicTree(child, item);
            }
        }

        private void PrintVisualTree(DependencyObject obj, TreeViewItem previousItem)
        {
            TreeViewItem item = new TreeViewItem();
            ProcessElement(obj, item, previousItem);

            //  递归输出视觉树
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                if (obj is TreeView)
                    return;

                PrintVisualTree(VisualTreeHelper.GetChild(obj, i), item);
            }
        }

        private void ShowLogicTree(object sender, RoutedEventArgs e)
        {
            treeElements.Items.Clear();
            PrintLogicTree(this, null);
        }

        private void ShowVisualTree(object sender, RoutedEventArgs e)
        {
            treeElements.Items.Clear();
            PrintVisualTree(this, null);
        }

    }

  程序的运行效果如下图所示:

2.2 通过控件模板自定义控件外观

  控件模板既然是控件的外衣,自然我们可以创建的新的控件模板,然后把新的控件模板应用到需要应用的控件中,这时候应用了新控件模板的控件,将会使用新的控件模板来渲染自身,从而改变控件的外观。这也是自定义控件外观的要旨。在WPF中按钮的默认控件是长方形的,我们可以通过创建一个新的控件模板来改变按钮的外观,下面的例子就实现了通过控件模板的方式自定义了一个圆形的按钮。具体的XAML代码如下所示:

<Window x:Class="TemplateDemo.ControlTemplate"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ControlTemplate" Height="300" Width="300">
    <Window.Resources>
        <!--定义控件模板,并使用key标记-->
        <ControlTemplate x:Key="roundButtonTemplate" TargetType="Button">
            <Grid>
                <Ellipse Name="ell" Fill="Orange" Width="100" Height="100"></Ellipse>
                <!--使用模板绑定来绑定按钮的内容-->
                <ContentPresenter Content="{TemplateBinding Button.Content}" VerticalAlignment="Center" HorizontalAlignment="Center"></ContentPresenter>
            </Grid>
            <!--定义模板触发器-->
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="ell"  Property="Fill" Value="Yellow"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>
    <StackPanel Margin="10">
        <Button Content="Round Button" Template="{StaticResource roundButtonTemplate}"></Button>
    </StackPanel>
</Window>

  此时,你就可以看到按钮是一个圆形的了,并且当鼠标移动到按钮上时,会触发模板触发器来改变Ellipse的填充色,具体的运行效果如下图所示:

  从上面的控件模板的使用可知,它和创建自定义控件不同,在很多情况下,你不需要编写自己的控件,你只是希望更改控件的外观。使用控件面板非常简单:

  • 首先在资源集合中创建一个ControlTemplate,并指定key标记
  • 然后赋值到控件的Template属性中。  

三、数据模板

  数据模板是数据的外衣,数据模板是一段定义如何绑定数据对象的XAML标记,有两种类型的控件支持数据模板:

  • 内容控件通过ContentTemplate属性支持数据模板。内容模板用于显示任何放在Content属性中的内容。
  • 列表控件,即继承自ItemsControl类的控件,通过ItemPlate属性支持数据模板。该模板用于显示由ItemSource提供集合中的每一项。

  基于列表的模板实际上是以内容控件模板为基础的,因为列表中的每一项由一个内容控件包装的。如ListBox控件的ListBoxItem元素。下面让我们具体看看如何去创建一个数据模板吧。

3.1 如何定义数据模板

  具体的XAML代码如下所示:

<Window x:Class="TemplateDemo.DataTemplate"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local ="clr-namespace:TemplateDemo;assembly=TemplateDemo"
        Title="DataTemplate" Height="300" Width="300">
    <Window.Resources>
        <!--创建数据模板-->
        <DataTemplate x:Key="personDataTem">
            <Border Name="blueBorder" Margin="3" BorderThickness="3" BorderBrush="Blue"
              CornerRadius="5">
                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <TextBlock Name="nametxt" FontWeight="Bold" Text="{Binding Path=Name}"></TextBlock>
                    <TextBlock Grid.Row="1" Text="{Binding Path=Age}"></TextBlock>
                </Grid>
            </Border>
            <!--定义数据模板触发器-->
            <DataTemplate.Triggers>
                <Trigger SourceName="blueBorder" Property="IsMouseOver" Value="True">
                    <Setter TargetName="blueBorder" Property="Background" Value="LightGray"/>
                    <Setter TargetName="nametxt" Property="FontSize" Value="20"/>
                </Trigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </Window.Resources>
    <StackPanel Margin="5">
        <ListBox Name="lstPerson" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource personDataTem}"></ListBox>
    </StackPanel>
</Window>

  其对应的后台代码如下所示:

 public partial class DataTemplate : Window
    {
        ObservableCollection<Student> persons = new ObservableCollection<Student>()
        {
            new Student() { Name ="LearningHard", Age=25},
            new Student() { Name ="HelloWorld", Age=22}
        };
        public DataTemplate()
        {
            InitializeComponent();

            lstPerson.ItemsSource = persons;
        }
    }

public class Student : INotifyPropertyChanged
    {
        public string ID { get { return Guid.NewGuid().ToString(); } }

        public string Name { get; set; }

        public int Age { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }
    }

  其运行效果如下图所示:

  从上面数据模板的创建可知,使用DataTemplate很简单:

  • 首先在资源集合中创建一个数据模板,并设置key标签。
  • 然后将key赋值到控件的CellTemplate或ContentTemplate或ItemTemplate属性上即可。

3. 2 数据模板与控件模板的关系

  从上面的介绍可知,控件只是数据和行为的载体,至于它本身长什么样子和数据长什么样子都是靠Template决定的。决定控件外观的是ControlTemplate,决定数据外观的是DataTemplate,它们正是Control类的Template和ContentTemplate两个属性的值。

  一般来说,ControlTemplate内都有一个ContentPresenter,这个ContentPresenter的ContentTemplate就是DataTemplate类型。所以数据模板和控件模板的关系如下图所示:

四、创建面板模板

  ItemsPanelTemplate用于指定项的布局。 ItemsControl 类型具有一个类型为ItemsPanelTemplate 的 ItemsPanel 属性。

  每种ItemsControl都有其默认的ItemsPanelTemplate。对于 ListBox,默认值使用 VirtualizingStackPanel。 对于 MenuItem,默认值使用 WrapPanel。 对于 StatusBar,默认值使用 DockPanel

  自定义面板模板与自定义数据面板和数据面板一样简单,一样只需要首先定义一个面板模板在资源集合中,然后将其Key指定给ItemsPanel属性即可。具体的XAML实现如下所示:

<Window x:Class="TemplateDemo.ItemsPanelTemplate"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ItemsPanelTemplate" Height="300" Width="300">
    <Window.Resources>
        <!--定义DataTemplate-->
        <DataTemplate x:Key="personDataTem">
            <Border Name="blueBorder" Margin="3" BorderThickness="3" BorderBrush="Blue"
              CornerRadius="5">
                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <TextBlock Name="nametxt" FontWeight="Bold" Text="{Binding Path=Name}"></TextBlock>
                    <TextBlock Grid.Row="1" Text="{Binding Path=Age}"></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
        <!--定义ItemsPanelTemplate-->
        <ItemsPanelTemplate x:Key="listItemsPanelTem">
            <StackPanel Orientation="Horizontal"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Left"/>
        </ItemsPanelTemplate>
    </Window.Resources>

    <!--使用ItemsPanelTemplate只需要赋值给ItemsPanel属性即可-->
    <ListBox Name="lstPerson" ItemsPanel="{StaticResource listItemsPanelTem}" ItemTemplate="{StaticResource personDataTem}" />
</Window>

  其后台代码和数据模板的后台代码一样,其实现代码为:

public partial class ItemsPanelTemplate : Window
    {
        ObservableCollection<Student> persons = new ObservableCollection<Student>()
        {
            new Student() { Name ="LearningHard", Age=25},
            new Student() { Name ="HelloWorld", Age=22}
        };
        public ItemsPanelTemplate()
        {
            InitializeComponent();
            lstPerson.ItemsSource = persons;
        }
    }

  此时程序运行的效果如下图所示,从下图结果可以看出,此时ListBox中的项不再是自上而下排列了,而是从左向右排列的。

五、总结

  到这里,WPF模板的内容就介绍结束了,本文主要介绍了WPF中支持的三种模板:控件模板、数据模板和面板模板,然后各自定义并使用了自定义的模板,最后介绍了这三个模板之间的联系。使用这三个模板的方式都非常简单,都是先定义一个模板,然后在把对应的key应用到控件对应的属性中,对于控件模板,应用的是控件的Template,对于数据模板,应用的是控件的ItemTemplate属性,对于面板模板,应用的是控件的ItemsPanel属性。在下面的一篇博文将介绍如何实现一个MVVM的实例程序。

  本文所有源码下载:TemplateDemo.zip

时间: 2025-01-04 18:30:00

WPF快速入门系列(7)——深入解析WPF模板的相关文章

WPF快速入门系列(4)——深入解析WPF绑定

一.引言 WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能.WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信息来设置目标对象的属性.目标属性总是依赖属性.然而,源对象可以是任何内容,可以是一个WPF元素.或ADO.NET数据对象或自定义的数据对象等.下面详细介绍了WPF绑定中的相关知识点. 二.绑定元素对象 2.1 如何实现绑定元素对象 这里首先介绍绑定最简单的情况——绑定元素对象,即数据源是一个WPF元

WPF快速入门系列(5)——深入解析WPF命令

一.引言 WPF命令相对来说是一个崭新的概念,因为命令对于之前的WinForm根本没有实现这个概念,但是这并不影响我们学习WPF命令,因为设计模式中有命令模式,关于命令模式可以参考我设计模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html.命令模式的要旨在于把命令的发送者与命令的执行者之间的依赖关系分割开了.对此,WPF中的命令也是一样的,WPF命令使得命令源(即命令发送者,也称调用程序)和命令目标(即命令执行者,也称处理程序)分离.现

WPF快速入门系列(3)——深入解析WPF事件机制

一.引言 WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件. 路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理.与依赖属性一样,可以使用传统的事件方式使用路由事件.尽管路由事件的使用方式与传统的事件一样,但是理解其工作原理还是相当重要的. 二.路由事件的详细介绍 对于.NET中的事件,大家应该在熟悉不过了.事件指的在某个事情发生时,由对象发送用于通知代码的消息.WPF中的路由事件允许事件可以被

WPF快速入门系列(2)——深入解析依赖属性

一.引言 感觉最近都颓废了,好久没有学习写博文了,出于负罪感,今天强烈逼迫自己开始更新WPF系列.尽管最近看到一篇WPF技术是否老矣的文章,但是还是不能阻止我系统学习WPF.今天继续分享WPF中一个最重要的知识点——依赖属性. 二.依赖属性的全面解析 听到依赖属性,自然联想到C#中属性的概念.C#中属性是抽象模型的核心部分,而依赖属性是专门基于WPF创建的.在WPF库实现中,依赖属性使用普通的C#属性进行了包装,使得我们可以通过和以前一样的方式来使用依赖属性,但我们必须明确,在WPF中我们大多数

WPF快速入门系列(1)——WPF布局概览

一.引言 关于WPF早在一年前就已经看过<深入浅出WPF>这本书,当时看完之后由于没有做笔记,以至于我现在又重新捡起来并记录下学习的过程,本系列将是一个WPF快速入门系列,主要介绍WPF中主要的几个不同的特性,如依赖属性.命令.路由事件等. 在正式介绍之前,我还想分享下为什么我又要重新捡起来WPF呢?之前没有记录下来的原来主要是打算走互联网方向的,后面发现互联网方向经常加班,又累,有时候忙的连自己写了什么都不知道的,所以后面机缘巧合地进了一家外企,在外企不像互联网行业那样,比较清楚,有更多的时

WPF快速入门系列(8)——MVVM快速入门

一.引言 在前面介绍了WPF一些核心的内容,其中包括WPF布局.依赖属性.路由事件.绑定.命令.资源样式和模板.然而,在WPF还衍生出了一种很好的编程框架,即WVVM,在Web端开发有MVC,在WPF客户端开发中有MVVM,其中VM就相当于MVC中C(Control).在Web端,微软开发了Asp.net MVC这样的MVC框架,同样在WPF领域,微软也开发了Prism这样的MVVM框架.Prism项目地址是:http://compositewpf.codeplex.com/SourceCont

WPF快速入门系列(9)——WPF任务管理工具实现

转载自:http://www.cnblogs.com/shanlin/p/3954531.html WPF系列自然需要以一个实际项目为结束.这里分享一个博客园博客实现的一个项目,我觉得作为一个练手的项目非常合适.担心博主后期会删除什么,这里先备份在自己的博客里面分享给大家. 本文所有源码下载:TaskScheduler.zip 时光如梭,距离第一次写的 WPF学习开发客户端软件-任务助手(已上传源码)  已有三个多月,期间我断断续续地对该项目做了优化.完善等等工作,现在重新向大家介绍一下,希望各

WPF快速入门系列(6)——WPF资源和样式

一.引言 WPF资源系统可以用来保存一些公有对象和样式,从而实现重用这些对象和样式的作用.而WPF样式是重用元素的格式的重要手段,可以理解样式就如CSS一样,尽管我们可以在每个控件中定义格式,但是如果多个控件都应用了多个格式的时候,我们就可以把这些格式封装成格式,然后在资源中定义这个格式,之前如果用到这个格式就可以直接使用这个样式,从而达到重用格式的手段.从中可以发现,WPF资源和WPF样式是相关的,我们经常把样式定义在资源中. 二.WPF资源详解 2.1 资源基础介绍 尽管可以在代码中创建和操

快速入门系列--MVC--01概述

虽然使用MVC已经不少年,相关技术的学习进行了多次,但是很多技术思路的理解其实都不够深入.其实就在MVC框架中有很多设计模式和设计思路的体现,例如DependencyResolver类就包含我们常见的DI依赖注入概念和注册表模式(GetService)等内容,ExceptionFilter等过滤器就体现AOP的概念,整个MVC内置了一个IOC容器,基本上所有的框架类的对象都是通过这种方式来创建的.此外,一直觉得很j2ee的spring很棒,其实如果大家很熟悉EHAB(微软企业开发库)的话,就会发