WPF中的模板

本文深入探讨了WPF中的模板,包括ControlTemplate和DataTemplate的使用。ControlTemplate定义了控件的外观,允许自定义控件的内部逻辑,而DataTemplate则是数据内容的表现形式,用于展示数据的不同视图。文章通过实例展示了如何使用DataTemplate为不同场景的数据创建视图,以及如何使用ControlTemplate改变控件的外观。此外,还提到了ItemsControl的PanelTemplate和模板之间的关系,以及如何查找和访问模板内部的控件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

模板的内涵

从字面上看,模板就是“具有一定规格的样板”,有了模板,我们就可以依照它制造很多一样的实例。

Binding和基于Binding的数据驱动界面是WPF的核心部分,WPF最精彩的部分是模板(Template)。

WPF系统与程序内容(业务逻辑)的边界是Binding,Binding把数据源源不断地从程序内部送出来,交由界面元素来显示,又把从界面元素收集来的数据传送回程序内部。界面元素间的沟通则依靠路由事件来完成,有时候路由事件和附加事件也会参与到数据的传输中。WPF作为一种“形式”,它要表现的“内容”是程序的数据和算法——Binding传递的是数据,事件参数携带的也是数据;方法和委托的调用是算法,事件传递消息也是算法。

控件的“算法内容”:指控件能展示哪些数据、具有哪种方法、能响应哪些操作、能激发什么事件,简而言之就是控件的功能,它们是一组相关的算法逻辑。

控件的“数据内容”:控件所展示的具体数据是什么。

在WPF中,通过引入模板(Template)将数据和算法的“内容”与“形式”解耦了。WPF中的Template分为两大类:

1) ControlTemplate:是算法内容的表现形式。它决定了控件“长成什么样子”,并让程序员有机会再控件原有的内部逻辑基础上扩展自己的逻辑。

2) DataTemplate: 是数据内容的表现形式,一条数据显示成什么样子,是简单的文本还是直观的图形动画就由它决定。

简单来说Template就是“外衣”; ControlTemplate是控件的外衣;DataTemplate是数据的外衣。

例如:

<Window x:Class="Chapter11.Page205.DataTemplateDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <ListView x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <StackPanel Orientation="Horizontal">
                            <Grid>
                                <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"/>
                                <TextBlock Text="{Binding Year}"/>
                            </Grid>
                            <TextBlock Text="{Binding Price}" Margin="5"/>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter11.Page205.DataTemplateDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            List<TempData> listData = new List<TempData>()
            {
                new TempData(){Year="1990年",Price=100D},
                new TempData(){Year="1991年",Price=130D},
                new TempData(){Year="1992年",Price=150D},
                new TempData(){Year="1993年",Price=160D},
            };
            this.listView.ItemsSource = listData;
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Chapter11.Page205.DataTemplateDemo
{
    public class TempData
    {
        public string Year { set; get; }
        public double Price { set; get; }
    }
}

运行结果:



数据的外衣DataTemplate

同样一条数据,比如具有Id,Name,PhoneNumber、Address等属性的Student实例,放在GridView里有时候可能就要简单的文本,每个单元格只显示一个属性;放在一个ListBox里有时为了避免单调可以在最末端显示头像,再将其他属性分两行排列在后面;如果是单独显示一个学生的信息则可以用类似简历的复杂格式来展现学生的全部数据。一样的内容可以用不同的形式来展现,软件设计称之为“数据——视图”(Data——View)模式。以往的开发技术,视图要靠UserControl来实现,WPF不但支持UserControl还支持用DataTemplate为数据形成视图。

DataTemplate常用的3个地方:

1) ContentControl的ContentTemplate属性,相当于给ContentControl的内容穿衣服。

2) ItemsControl的ItemTemplate属性,相当于给ItemsControl的数据条目穿衣服。

3) GridViewColumn的CellTemplate属性,相当于给GridViewColumn单元格里的数据穿衣服。


让我们用一个例子来对比UserControl与DataTemplate的使用。需求是这样的:一列汽车数据,这列数据显示在一个ListBox里,要求ListBox的条目显示汽车的厂商图标和简要参数,单击某个条目后在窗体的详细内容区域显示汽车的照片和详细参数。

<Window x:Class="WpfApplication19.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication19"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:StringToBitmapImageConverter x:Key="stbCon"/>
    </Window.Resources>
    <StackPanel>
        <ListBox x:Name="listBox" SelectionChanged="listBox_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding carIcon,Converter={StaticResource ResourceKey=stbCon}}" Height="60" Width="60"/>
                        <TextBlock Text="{Binding simplePara}" Height="60"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Image x:Name="carImg" Height="100" Width="100"/>
        <TextBlock x:Name="carDetailPara" Height="100"/>
    </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Imaging;

namespace WpfApplication19
{
    public class Car
    {
        public string carIcon { set; get; }
        public string simplePara { set; get; }
        public string carImg { set; get; }
        public string detailPara { set; get; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication19
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            List<Car> listCar = new List<Car>()
            {
                new Car()
                {                     
                    //carIcon = new BitmapImage(new Uri(@"/Icon/Bomber.jpg",UriKind.Relative)),
                    carIcon = @"/Icon/Bomber.jpg",
                    carImg = @"/Icon/Bomber.jpg",
                    detailPara = "detailPara1",
                    simplePara = "simplePara1"
                },
                new Car()
                { 
                    carIcon = @"/Icon/Plane.jpg",
                    carImg = @"/Icon/Plane.jpg",
                    detailPara = "detailPara2",
                    simplePara = "simplePara2"
                },
            };

            this.listBox.ItemsSource = listCar;
        }

        private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Car car = e.AddedItems[0] as Car;            
            this.carImg.Source = new BitmapImage(new Uri(car.carImg,UriKind.Relative));
            this.carDetailPara.Text = car.detailPara;
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Media.Imaging;

namespace WpfApplication19
{
    public class StringToBitmapImageConverter:IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string str = value as string;
            Uri uri = new Uri(str, UriKind.Relative);
            BitmapImage image = new BitmapImage(uri);
            return image;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}


有两种办法在XAML代码中使用Converter:

1) 把Converter以资源的形式放在资源词典里(本例使用的方法)。

2) 为Converter准备一个静态属性,形成单件模式,在XAML代码中使用{x:Static}标签扩展来访问。


另外一种写法:

两个Converter代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Media.Imaging;

namespace Chapter11.Page211.Demo
{
    public class AutomakerToLogoPathConverter:IValueConverter
    {
        //正向转换
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string uriStr = string.Format(@"/Resources/Logos/{0}.jpg",(string)value);
            return new BitmapImage(new Uri(uriStr,UriKind.Relative));
        }

        //未被用到
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Media.Imaging;

namespace Chapter11.Page211.Demo
{
    //汽车名称转换为照片路径
    public class NameToPhotoPathConverter:IValueConverter
    {
        //正想转换
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string uriStr = string.Format(@"/Resources/Images/{0}.jpg", (string)value);
            return new BitmapImage(new Uri(uriStr,UriKind.Relative));
        }

        //未被用到
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Car类代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Chapter11.Page211.Demo
{
    public class Car
    {
        public string Automaker { get; set; }
        public string Name { get; set; }
        public string Year { get; set; }
        public string TopSpeed { get; set; }
    }
}

主窗体代码:

<Window x:Class="Chapter11.Page211.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Chapter11.Page211.Demo"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!--Converter-->
        <local:AutomakerToLogoPathConverter x:Key="a2l"/>
        <local:NameToPhotoPathConverter x:Key="n2p"/>
        <!--DataTemplate for Detail View-->
        <DataTemplate x:Key="carDetailViewTemplate">
            <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">
                <StackPanel Margin="5">
                    <Image Width="400" Height="250" Source="{Binding Name,Converter={StaticResource n2p}}"/>
                    <StackPanel Orientation="Horizontal" Margin="5,0">
                        <TextBlock Text="Name:" FontWeight="Bold" FontSize="20"/>
                        <TextBlock Text="{Binding Name}" FontSize="20" Margin="5"/>                        
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Margin="5">
                        <TextBlock Text="Automaker:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Automaker}" Margin="5,0"/>
                        <TextBlock Text="Year:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Year}" Margin="5"/>
                        <TextBlock Text="Top Speed:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding TopSpeed}" Margin="5"/>
                    </StackPanel>                    
                </StackPanel>
            </Border>
        </DataTemplate>
        <!--DataTemplate for Item View-->
        <DataTemplate x:Key="carListItemViewTemplate">
            <Grid Margin="2">
                <StackPanel Orientation="Horizontal">
                    <Image Source="{Binding Automaker,Converter={StaticResource a2l}}" Grid.RowSpan="3" Width="64" Height="64"/>
                    <StackPanel Margin="5,10">
                        <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Year}" FontSize="14"/>
                    </StackPanel>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <!--窗体内容-->
    <StackPanel Orientation="Horizontal" Margin="5">
        <UserControl ContentTemplate="{StaticResource carDetailViewTemplate}" Content="{Binding SelectedItem,ElementName=listBoxCars}"/>
        <ListBox x:Name="listBoxCars" Width="180" Margin="5" ItemTemplate="{StaticResource carListItemViewTemplate}"/>
    </StackPanel>
</Window>

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter11.Page211.Demo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            List<Car> listCar = new List<Car>()
            {
                new Car(){ Automaker = "Bomber", Name = "Bomber", TopSpeed="340", Year="2009"},
                new Car(){ Automaker = "Plane", Name = "Plane", TopSpeed="240", Year="2010"},
            };

            this.listBoxCars.ItemsSource = listCar;
        }
    }
}


源代码:  http://download.youkuaiyun.com/detail/eric_k1m/6661913


其中最重要的两句是:

1) ContentTemplate=“{StaticResource carDetailViewTemplate}”    相当于给一个普通UserControl的数据内容穿上了一件外衣,让Car类型数据以图文并茂的形式表现出来。这件外衣就是以 x:Key = "carDetailViewTemlate"标记的DataTemplate资源。

2) ItemTemplate=“{StaticResource carListItemViewTemplate}”   是把一件数据的外衣交给ListBox,当ListBox.ItemSource被赋值时,ListBox会给每个条目穿上这件外衣。这件外衣是以   x:Key = “carListViewTemplate” 标记的DataTemplate资源。

不夸张的说是DataTemplate帮助彻底完成了“数据驱动界面”。


控件的外衣ControlTemplate

每次提到ControlTemplate,就会想起“披着羊皮的狼”,披上羊皮后,虽然看上去是只羊,但其实是只匹狼。WPF中的CheckBox与其基类ToggleButton在功能上几乎完全一样,但外观区别却很大,这就是更换了ControlTemplate的结果。经过更换ControlTemplate,我们不但可以制作披着CheckBox外衣的ToggleButton,还能制作出披着温度计外衣的PrograssBar控件。

实际项目中ControlTemplate主要有使用地方有:

1) 通过更换ControlTemplate改变控件外观,使之具有更优的用户使用体验及外观。

2) 借助ControlTemplate,程序员可以与设计师并行工作,程序员可以先用WPF标准控件进行编程,等设计师的工作完成后,只需要把新的ControlTemplate应用到程序中即可。

首先需要了解每一个控件的内部结构,需要使用Expression Blend。

1) 运行Blend,新建一个WPF项目,然后再主窗体里添加两个TextBox和一个Button,对于程序员来说,完全可以把Blend理解为一个功能更强大的窗体设计器,而对于设计师来说,可以把Blend理解为会写XAML代码的Photoshop或Fireworks。

2) 现在运行的话,可以看到TextBox是方方正正的,与窗体和Button不太协调,如果我们需要一个圆角的TextBox该如何做呢?传统的方法可能是创建一个UserControl并在TextBox上套一个Border。

<UserControl x:Class="WpfApplication23.TextBoxUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel>
        <Border BorderBrush="Black" CornerRadius="10" Margin="5" Background="Red">
            <TextBox Margin="5"/>
        </Border>
    </StackPanel>
</UserControl>

在主页面上调用该UserControl:

<Window x:Class="WpfApplication23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication23"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <local:TextBoxUserControl Margin="10"/>
    </Grid>
</Window>

运行结果:


3) 另一种办法是使用ControlTemplate来设计,首先在Blend的Objects and Timeline上右键选择TextBox,选择Edit Template->Edit a Copy:


之所以不选择Create Empty是因为Create Empty是从头开始设计一个控件的ControlTemplate,新作衣服哪如改衣服快。单击菜单项后弹出资源对话框,尽管可以使用C#代码来创建ControlTemplate,但绝大多数情况下ControlTemplate是由XAML代码编写的并放在资源词典里,所以才会弹出对话框询问你资源的x:Key是什么、打算把资源放在哪里。


作为资源,ControlTemplate可以放在三个地方:Application的资源词典里(App.xaml文件里);某个界面元素的资源词典里;放在外部XAML文件中。我们选择把它放在Application的资源词典里以便统一管理,并命名为RoundCornerTextBoxStyle。

4) 单击OK按钮便可以进入空间的模板编辑状态。在Objects and Timeline面板中观察已经解剖开的TextBox控件,发现它是由一个名为Bd的ListBoxChrome套着的一个名为PART_ContentHost的ScrollViewer组成的。为了显示圆角矩形边框,我们需要把最外层的ListBoxChrome换成Border,并删掉Border不具备的属性值,设置它的圆角弧度即可。


5) 更改后的核心代码如下:

			<Setter Property="Template">
				<Setter.Value>
					<ControlTemplate TargetType="{x:Type TextBox}">
						<!--
						<Microsoft_Windows_Themes:ListBoxChrome x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}" SnapsToDevicePixels="true">
							<ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
						</Microsoft_Windows_Themes:ListBoxChrome>
						-->
						<Border x:Name="Bd" SnapsToDevicePixels="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10">
							<ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
						</Border>
						<ControlTemplate.Triggers>
							<Trigger Property="IsEnabled" Value="false">
								<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
								<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
							</Trigger>
						</ControlTemplate.Triggers>
					</ControlTemplate>
				</Setter.Value>
			</Setter>

6) 最后把我们设计的圆角Style应用到第一个TextBox上,代码如下:

<Window
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
	x:Class="WpfApplication24.MainWindow"
	x:Name="Window"
	Title="MainWindow"
	Width="640" Height="480">

	<Grid x:Name="LayoutRoot">
		<TextBox Margin="81,47,311.513,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Height="53.837" Style="{DynamicResource RoundCornerTextBoxStyle}"/>
		<TextBox Margin="81,116,311.513,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Height="53.837"/>
		<Button Content="Button" HorizontalAlignment="Left" Margin="112,217,0,183.163" Width="143.691"/>
	</Grid>
</Window>

这里使用的是Style=“{ DynamicResources RoundCornerTextBoxStyle}” 来使用。运行的话就可以发现第一个TextBox已经变成了圆角的边框。


我们可以用Edit Template的这种方法来查看其他的控件。

其实每个控件本身就是一颗UI元素树。WPF的UI元素可以看做两棵树——Logical Tree和Visual Tree,这两棵树的交点就是ControlTemplate。如果把界面上的控件元素当做是一个结点,那元素们构成的就是LogicalTree,如果把控件内部由ControlTemplate生成的控件也算上,那构成的就是VisualTree。换句话说,LogicalTree上导航不会进入到控件内部,而在VisualTree上导航则可检索到控件内部由ControlTemplate生成的子级控件。


ItemsControl的PanelTemplate

ItemsControl有一个名为ItemsPanel的属性,它的数据类型为ItemsPanelTemplate。ItemsPanelTemplate也是一种控件Template,它的作用就是让程序员有机会控制ItemsControl的条目容器。

举例来说,我们印象中ListBox中的条目都是自上而下排列的,如果需要一个条目水平排列的ListBox该怎么做,这里是一个没有经过调整的ListBox,条目自上而下:

        <ListBox>
            <TextBlock Text="Allen"/>
            <TextBlock Text="Kevin"/>
            <TextBlock Text="Dior"/>
        </ListBox>

条目水平排列的ListBox:

        <ListBox>
            <!--ItemsPanel-->
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <!--条目-->
            <TextBlock Text="Allen"/>
            <TextBlock Text="Kevin"/>
            <TextBlock Text="Dior"/>
        </ListBox>


DataTemplate与ControlTemplate的关系与应用

DataTemplate与ControlTemplate的关系

控件只是数据和行为的载体,是个抽象的概念,至于它本身会长成什么样子(控件内部构造),它的数据会长成什么样子(数据显示结构)都是靠Template生成的。

决定控件外观的是ControlTemplate,决定数据外观的是DataTemplate,它们是Control类的Template和ContentTemplate两个属性的值。它们的作用范围:


凡是Template,最终都要作用在控件上,这个控件就是Template的目标控件,也叫模板控件(Templated Control)。DataTemplate给人的感觉的确是施加在了数据对象上,但施加在数据对象上生成的一组控件总得有个载体,这个载体一般是落实在一个ContentPresenter对象上。ContentPresenter类只有ContentTemplate属性,没有Template属性,这就证明了承载由DataTemplate生成的一组控件是它的专门用途。

至此我们可以看出,由ControlTemplate生成的控件树其树根就是ControlTemplate的目标控件,此模板化控件的Template属性值就是这个ControlTemplate实例;与之相仿,由DataTemplate生成的控件树其根是一个ContentPresenter控件,此模板化控件的ContentTemplate属性值就是这个DataTemplate实例。因为ContentPresenter控件是ControlTemplate控件树上的一个结点,所以DataTemplate控件树是ControlTemplate控件树的一棵子数。

既然Template生成的控件树都有跟,那么如何找到树根呢?办法很简单,每个控件都有一个名为TemplatedParent的属性,如果它的值不为null,说明这个控件是由Template自动生成的,而属性值就是应用了模板的控件(模板的目标,模板化控件)。如果由Template生成的控件使用了TemplateBinding获取属性值,则TemplateBinding的数据源就是应用了这个模板的目标控件。

当为一个Binding指定Path不指定Source时,Binding会沿着逻辑树一直向上找,查看每个结点的DataContext属性,如果DataContext引用的对象具有Path指定的属性名,Binding就会把这个对象当做自己的数据源。显然,如果把数据对象赋值给ContentPresenter的DataContext属性,由DataTemplate生成的控件自然会找到这个数据对象并把它当做自己的数据源。


DataTemplate与ControlTemplate的应用

为Template设置其应用目标有两种方法,一种是逐个设置控件的Template / ContentTemplate / ItemsTemplate / CellTemplate等属性,不想应用Template的控件不设置;

另一种是整体应用,即把Template应用在某个类型的控件或数据上。

把ControlTemplate应用在所有目标上需要借助Style来实现,但Style不能标记x:Key,例如:

<Window x:Class="Chapter11.Page223.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <!--与前面例子相同-->
                        <Border x:Name="Bd" SnapsToDevicePixels="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10">
                            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Margin" Value="5"/>
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="Height" Value="25"/>
        </Style>
    </Window.Resources>
    <StackPanel>
        <TextBox/>
        <TextBox/>
        <TextBox Style="{x:Null}" Margin="5"/>
    </StackPanel>
</Window>

Style没有x:Key标记,默认为应用到所有由x:Type指定的控件上,如果不想应用则需要把控件的Style标记为{x:Null}。



把DataTemplate应用在某个数据类型上的方法是设置DataTemplate的DataType属性,并且DataTemplate作为资源时也不能带有x:Key标记

如果不是用数据类型是这样的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WpfApplication26
{
    public class Unit
    {
        public string Year { set; get; }
        public string Price { set; get; }
    }
}

<Window x:Class="WpfApplication26.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication26"
        xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!--数据源-->
        <c:ArrayList x:Key="unitList">
            <local:Unit Price="100" Year="2009"/>
            <local:Unit Price="200" Year="2010"/>
        </c:ArrayList>
        <!--DataTemplate-->
        <DataTemplate x:Key="listBoxDT">
            <StackPanel Orientation="Horizontal">
                <Rectangle Width="{Binding Price}" Fill="Yellow"/>
                <TextBlock Text="{Binding Year}"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <ListBox ItemsSource="{StaticResource unitList}" ItemTemplate="{StaticResource listBoxDT}"/>
        <ComboBox ItemsSource="{StaticResource unitList}" Margin="10" ItemTemplate="{StaticResource listBoxDT}"/>
    </StackPanel>
</Window>

使用数据类型后:

<Window x:Class="Chapter11.Page224.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        
        xmlns:local="clr-namespace:Chapter11.Page224.Demo"
        xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
        
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!--DataTemplate-->
        <DataTemplate DataType="{x:Type local:Unit}">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <Grid>
                        <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"/>
                        <TextBlock Text="{Binding Year}"/>
                    </Grid>
                    <TextBlock Text="{Binding Price}" Margin="5"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
        <!--数据源-->
        <c:ArrayList x:Key="ds">
            <local:Unit Price="100" Year="2001"/>
            <local:Unit Price="200" Year="2003"/>
        </c:ArrayList>
    </Window.Resources>
    <StackPanel>
        <ListBox ItemsSource="{StaticResource ds}"/>
        <ComboBox ItemsSource="{StaticResource ds}" Margin="5"/>
    </StackPanel>
</Window>

也就是说使用了DataTemplate的DataType属性赋值后,在该XAML页面里使用到的ItemsSource为Unit类型的数据都会自动把自己的DataTemplate转变为我们所指定DataType的DataTemplate。

寻找失落的控件

LogicalTree与控件内部之间保持着界面分明、互不相干的关系。换句话说,如果UI元素上有个x:Name=“TextBox1”的控件,某个控件内部也有一个由Template生成的x:Name=“TextBox1”的控件,它们并不冲突,LogicalTree不会看到控件内部的细节,控件内部元素也不会去理会控件外部有什么。如果我们想从外界访问Template内部的控件,获取它的属性值,该怎么办?

由ControlTemplate或DataTemplate生成的控件都是“由Template生成的控件”。ControlTemplate和DataTemplate两个类均派生自FrameworkTemplate类,这个类有个名为FindName的方法供我们检索其内部控件。也就是说,只要我们能拿到Template,找到其内部控件就不成问题。对于ControlTemplate对象,访问其目标控件的Template属性就能拿到,但想要拿到DataTemplate对象就比较麻烦。千万不要以为ListBoxItem或者ComboBoxItem容器就是DataTemplate的目标控件。因为控件的Template属性和ContentTemplate属性可是两码事。

我们先找由ControlTemplate生成的控件。首先设计一个ControlTemplate并把它应用在一个UserControl上。界面上有一个Button,在它的Click事件处理中我们检索由ControlTemplate生成的代码。

<Window x:Class="Chapter11.Page230.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ControlTemplate x:Key="cTmp">
            <StackPanel Background="Orange">
                <TextBox x:Name="textBox1" Margin="6"/>
                <TextBox x:Name="textBox2" Margin="6"/>
                <TextBox x:Name="textBox3" Margin="6"/>
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>
    <StackPanel Background="Yellow">
        <UserControl x:Name="uc" Template="{StaticResource cTmp}" Margin="5"/>
        <Button Content="Find By Name" Width="120" Height="30" Click="Button_Click"/>
    </StackPanel>
</Window>

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter11.Page230.Demo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            TextBox tb = this.uc.Template.FindName("textBox1", this.uc) as TextBox;
            tb.Text = "Hello WPF";
            StackPanel sp = tb.Parent as StackPanel;
            (sp.Children[1] as TextBox).Text = "Hello ControlTemplate";
            (sp.Children[2] as TextBox).Text = "I can find you";
        }
    }
}

接下来我们来寻找DataTemplate生成的控件。请先考虑一个问题:寻找一个由DataTemplate生成的控件后,我们想从中获取哪些数据,如果想获取单纯与用户界面相关的数据(比如控件的宽度、高度等),这么做是对的;但是如果是获取业务逻辑相关的数据,那就要考虑程序的设计是不是出了问题——因为WPF采用的是数据驱动UI逻辑,获取业务逻辑数据的事情在底层就能做到,一般是不会跑到表层上来找。

业务逻辑数据的类如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Chapter11.Page231.Demo
{
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Skill { get; set; }
        public bool HasJob { get; set; }
    }
}

界面XAML代码如下:

<Window x:Class="Chapter11.Page231.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Chapter11.Page231.Demo"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!--数据对象-->
        <local:Student x:Key="stu" Id="1" Name="Kim" Skill="WPF" HasJob="True"/>
        <!--DataTemplate-->
        <DataTemplate x:Key="stuDT">
            <Border BorderBrush="Orange" BorderThickness="2" CornerRadius="5">
                <StackPanel>
                    <TextBlock Text="{Binding Id}" Margin="5"/>
                    <TextBlock x:Name="textBlockName" Text="{Binding Name}" Margin="5"/>
                    <TextBlock Text="{Binding Skill}" Margin="5"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <ContentPresenter x:Name="cp" Content="{StaticResource stu}" ContentTemplate="{StaticResource stuDT}" Margin="5"/>
        <Button Content="Find" Margin="5" Click="Button_Click"/>
    </StackPanel>
</Window>

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter11.Page231.Demo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            TextBlock tb = this.cp.ContentTemplate.FindName("textBlockName", this.cp) as TextBlock;
            MessageBox.Show(tb.Text);

            //Student stu = this.cp.Content as Student;
            //MessageBox.Show(stu.Name);
        }
    }
}

未被注释的代码是使用DataTemplate的FindName方法获取由DataTemplate生成的控件并访问其属性,被注释的代码是直接使用底层数据。显然,如果为了获取Student的某个属性,应该使用被注释的代码而不必绕道表层控件上来,除非想得到的是控件的长度、宽度等业务逻辑无关的纯UI属性。


下面来看一个复杂的例子。DataTemplate的一个常用之处是GridViewColumn的CellTemplate属性。把GridViewTemplate放置在GridView控件里就可以生成表格了。GridViewColumn的默认CellTemplate是使用TextBlock只读性地显示数据,如果我们想让用户修改数据或者使用CheckBox显示bool类型的话就需要自定义DataTemplate。

还是先定义这个类名Student的类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Chapter11.Page232.Demo
{
    public class Student
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string Skill { set; get; }
        public bool HasJob { set; get; }
    }
}

准备数据集合、呈现数据的工作全部由XAML代码来完成:

<Window x:Class="Chapter11.Page232.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Chapter11.Page232.Demo"
        xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!--数据集合-->
        <c:ArrayList x:Key="stuList">
            <local:Student Id="1" Name="Name1" Skill="Skill1" HasJob="True"/>
            <local:Student Id="2" Name="Name2" Skill="Skill2" HasJob="False"/>
            <local:Student Id="3" Name="Name3" Skill="Skill3" HasJob="True"/>
        </c:ArrayList>
        <!--DataTemlate-->
        <DataTemplate x:Key="nameDT">
            <TextBlock x:Name="textBoxName" Text="{Binding Name}"/>
        </DataTemplate>
        <DataTemplate x:Key="skillDT">
            <TextBox x:Name="textBoxSkill" Text="{Binding Skill}"/>
        </DataTemplate>
        <DataTemplate x:Key="hasjobDT">
            <CheckBox x:Name="checkBoxJob" IsChecked="{Binding HasJob}" IsThreeState="True"/>
        </DataTemplate>
    </Window.Resources>

    <!--主要布局-->
    <Grid Margin="5">
        <ListView x:Name="listViewStudent" ItemsSource="{StaticResource stuList}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}"/>
                    <GridViewColumn Header="姓名" CellTemplate="{StaticResource nameDT}"/>
                    <GridViewColumn Header="技术" CellTemplate="{StaticResource skillDT}"/>
                    <GridViewColumn Header="已工作" CellTemplate="{StaticResource hasjobDT}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

然后我们在显示姓名的TextBox添加GotFocus事件的处理器:

        <DataTemplate x:Key="skillDT">
            <TextBox x:Name="textBoxSkill" Text="{Binding Skill}" GotFocus="textBoxSkill_GotFocus"/>
        </DataTemplate>

因为我们是在DataTemplate里添加事件处理器,所以界面上任何一个由此DataTemplate生成的TextBox都会在获得焦点时调用textBoxSkill_GotFocus这个事件处理器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值