图形用户界面应用程序较之控制台界面应用程序最大的好处就是界面友好、数据显示直观。CUI程序中数据只能以文本的形式线性显示,GUI程序则允许数据以文本、列表、图形等多种形式立体显示。
用户体验在GUI程序设计中起着举足轻重的作用-----用户界面设计成什么样看上去才足够的漂亮?控件如何安排才简单易用并且少犯错误?这些都是设计师需要考虑的问题。WPF系统不但支持传统的Winfrom编程的用户界面和用户体验设计,更支持使用专门的设计工具Blend进行专业设计,同时还推出了以模板为核心的新一代设计理念。
1.1 模板的内涵
从字面上看,模板就是“具有一定规格的样板”,有了它,就可以依照它制造很多一样是实例。我们常把看起来一样的东西称为“一个模子里面刻出来的。”就是这个道理。然而,WPF中的模板的内涵远比这个深刻。
Binding和基于Binding数据驱动UI是WPF的核心部分,WPF最精彩的部分是什么呢?依我看,既不是美轮美奂的3D图形,也不是炫目多彩的动画,而是默默无闻的模板(Template)。实际上,就连2D/3D绘图也常常是为它锦上添花。
Templdate究竟有什么能力能够使得它在WPF体系中获此殊荣呢?这还要从哲学谈起,“形而上者谓之道,形而下者谓之器”,这句话出自《易经》,大意是我们能够观察到的世间万物形象之上的抽象的结果就是思维,而形象之下掩盖的就是其本质。显然,古人已经注意到“形”是连接本质和思维的枢纽,让我们把这句话引入计算机世界。
“形而上者谓之道”指的就是基于现实世界对万物进行抽象封装,理顺它们之间的关系,这个“道”不就是面向对象思想吗!如果再把面向对象进一步提升、总结出最优的对象组合关系,“道”就上升为设计模式思想。
“形而下者谓之气”指的是我们能够观察到的世间万物都是物质类容的本质表现形式。“本质与表现”或者说“类容与形式”是哲学范畴内的一对矛盾体。
软件之道并非本书研究的主要类容,本书研究的是WPF。WPF全称Windows
Presentation Foundation,Presentation一词的意思就是外观,呈现,表现,也就是说,在WIndows
GUI程序这个尺度上,WPF扮演的就是“形”的角色、是程序的外在形式,而程序的内容仍然是由数据和算法构成的业务逻辑。与WPF类似,Winform和Asp.net也都是内容的表现形式。
让我们把尺度缩小到WPF内部。这个系统与程序内容(业务逻辑)的边界是Binding,Binding把数据源源不断从程序内部送出来交由界面元素来显示,又把从界面元素搜集到的数据传回程序内部。界面元素间的沟通则依靠路由事件来完成。有时候路由事件和附加事件也会参与到数据的传输中。让我们思考一个问题:WPF作为Windows的表示方式,它究竟表示的是什么?换句话说,WPF作为一种“形式”,它表现的内容到底是什么?答案是程序的数据和算法----Binding传递的是数据,事件参数携带的也是数据;方法和委托的调用是算法,事件传递消息也是算法----数据在内存里就是一串串字符或字符。算法是一组组看不见摸不着的抽象逻辑,如何恰如其分的把它们展现给用户呢?
加入想表达一个bool类型,同时还想表达用户可以在这两个值之间自由切换这样一个算法,你会怎么做?你一定会想使用一个CheckBox控件来满足要求;再比如颜色值实际上是一串数字,用户基本上不可能只看数字就能想象出真正的颜色,而且用户也不希望只靠输入字符来表示颜色值,这时,颜色值这一“数据内容”的恰当表现形式就是一个填充着真实颜色的色块。,而用户即可以输入值又可以用取色吸管取色来设置值的“算法内容”恰当的表达方式是创建一个ColorPicker控件。相信你已经发现,控件(Control)是数据内容表现形式的双重载体。换句话说,控件即是数据的表现形式让用户可以直观的看到数据,又是算法的表现形式让用户方便的操作逻辑。
作为表现形式,每个控件都是为了实现某种用户操作算法和直观显示某种数据而生,一个控件看上去是什么样子由它的“算法内容”和“数据内容决定”,这就是内容决定形式,这里,我们引入两个概念:
控件的算法内容:值控件能展示哪些数据、具有哪些方法、能相应哪些操作、能激发什么事件,简而言之就是控件的功能,它们是一组相关的算法逻辑。
控件的数据内容:控件具体展示的数据是什么。
以往的GUI开发技术(ASP.NET+Winform)中,控件内部逻辑和数据是固定的,程序员不能改变;对于控件的外观,程序员能做的改变也非常的有限,一般也就是设置控件的属性,想改变控件的内部结构是不可能的。如果想扩展一个控件的功能或者更改器外观让其更适应业务逻辑,哪怕只是一丁点的改变,也需要创建控件的子类或者创建用户控件。造成这个局面的根本原因是数据和算法的“形式”和“内容”耦合的太紧了。
在WPF中,通过引入模板微软将数据和算法的内容与形式接耦合了。WPF中的Template分为两大类:
ControlTemplate:是算法和内容的表现形式,一个控件怎么组织其内部结构才能让它更符合业务逻辑、让用户操作起来更舒服就是由它来控制的。它决定了控件“长成什么样子”,并让程序员有机会在控件原有的内部逻辑基础上扩展自己的逻辑。
DataTemplate:是数据内容的展示方式,一条数据显示成什么样子,是简单的文本还是直观的图形就由它来决定了。
Template就是数据的外衣-----ControlTemplate是控件的外衣,DataTemplate是数据的外衣。
下面让我们欣赏两个例子:
WPF中控件不在具有固定的形象,仅仅是算法内容或数据内容的载体。你可以把控件理解为一组操作逻辑穿上了一套衣服,换套衣服就变成了另外一个模样。你看到的控件默认形象实际上就是出厂时微软为它穿上的默认衣服。看到下面图中的温度计,你是不是习惯性的猜到是由若干控件和图形拼凑起来的UserControl呢?实际上它是一个ProgressBar控件,只是我们的设计师为其设计了一套新衣服-----这套衣服改变了其一些颜色、添加了一些装饰品和刻度线并清除了脉搏动画,效果如下图:
WPF中数据显示成什么样子可以由自己来决定。比如下面这张图,只是为数据条目准备了一个DataTemplate,这个DataTemplate中用binding把一个TextBlock的Text属性值关联到数据对象的Year属性上、把一个Rectangle的Width属性和另外一个TextBlock的Text属性关联到数据对象的Price属性上,并使用StackPanel和Grid为这几个控件布局。一旦应用了这个DataTemplate,单调的数据就变成了直观的柱状图,如下图所示。以往这项工作不但需要先创建用于展示数据的UserControl,还要为UserControl添加显示/回写数据的代码。
如果别的项目中也需要用到这个柱状图,你要做的事情只是将这个XAML代码发给他们。其代码如下:
<DataTemplate> <Grid> <StackPanel Orientation="Horizontal"> <Grid> <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"></Rectangle> <TextBlock Text="{Binding Year}"></TextBlock> </Grid> <TextBlock Text="{Binding Price}" Margin="5,0"></TextBlock> </StackPanel> </Grid> </DataTemplate> |
我想,尽管你还没有学习什么DataTempldate,但借助前面学习的基础一样可以看个八九不离十了。
1.2 数据的外衣DataTemplate
“横看成岭侧成峰,远近高低各不同”庐山的美景如此,数据又何尝不是这样呢?同样一条数据,比如具有ID、Name、PhoneNumber、Address等Student的实例,放在GridView里面有时可能是简单的文本、每个单元格只显示一个属性;放在ListBox里面有时为了避免单调可以在最左端显示一个64*64的小图像,再将其它信息分两行显示在其后面;如果单独显示一个学生信息则可以用类似简历的复杂格式来展现学生的全部数据。一样的内容可以用不同的形式来展现,软件设计称之为“数据--视图”模式。以往的开发技术,如MFC、Winform、Asp.net等,视图要靠UserControl来实现。WPF不但支持UserControl还支持DataTemplate为数据形成视图。不要以为DataTempldate有多难!从Control升级到DataTemplate一般就是复制,粘贴一下再改几个字符的事儿。
DataTempldate常用的地方有三处,分别是:
ContentControl的ContentTempldate属性,相当于给ContentControl的内容穿衣服。
ItemControl的ItemTemplate,相当于给ItemControl的数据条目穿衣服。
GridViewColumn的CellTempldate属性,相当于给GridViewColumn的数据条目穿衣服。
让我们用一个例子对比UserControl和DataTemplate的使用。例子实现的需求是这样的:有一列汽车数据,这列数据显示在ListBox里面,要求ListBox的条目显示汽车的厂商图标和简要参数,单击某个条目后在窗体的详细内容区显示汽车的图片和详细参数。
无论是使用UserControl还是DataTemplate,厂商的Logo和汽车的照片都是要用到的,所以先在项目中建立资源管理目录并把图片添加进来。Logo文件名与厂商的名称一致,照片的名称则与车名一致。组织结构如图:
首先创建Car数据类型:
public class Car { public string AutoMark { get; set; } public string Name { get; set; } public string Year { get; set; } public string TopSpeed { get; set; } } |
为了在ListBox里面显示Car类型的数据,我们需要准备一个UserControl。命名为CarListItemView。
这个UserControl由一个Car类型实例在背后支持,当设置这个实例的时候,界面元素将实例的属性值显示在各个控件里。CarListItemView的XAML代码如下:
<UserControl x:Class="WpfApplication1.CarListViewItem" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid Margin="2"> <StackPanel Orientation="Horizontal"> <Image x:Name="igLogo" Grid.RowSpan="3" Width="64" Height="64"></Image> <StackPanel Margin="5,10"> <TextBlock x:Name="txtBlockName" FontSize="16" FontWeight="Bold"></TextBlock> <TextBlock x:Name="txtBlockYear" FontSize="14"></TextBlock> </StackPanel> </StackPanel> </Grid> </UserControl> |
CarlistItemView用于支持前台显示属性C#代码为:
/// <summary> /// CarListViewItem.xaml 的交互逻辑 /// </summary> public partial class CarListViewItem : UserControl { public CarListViewItem() { InitializeComponent(); } private Car car; public Car Car { get { return car; } set { car = value; this.txtBlockName.Text = car.Name; this.txtBlockYear.Text = car.Year; this.igLogo.Source = new BitmapImage(new Uri(@"Resource/Image/"+car.AutoMark+".png",UriKind.Relative)); } } } |
类似的原理,我们需要为Car类型准备一个详细信息视图。UserControl名称为CarDetailView,XAML部分代码如下:
<UserControl x:Class="WpfApplication1.CarDetailView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6"> <StackPanel> <Image x:Name="imgPhoto" Width="400" Height="250"></Image> <StackPanel Orientation="Horizontal" Margin="5,0"> <TextBlock Text="Name:" FontSize="20" FontWeight="Bold"></TextBlock> <TextBlock x:Name="txtBlockName" FontSize="20" Margin="5,0"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal" Margin="5,0"> <TextBlock Text="AutoMark:" FontWeight="Bold"></TextBlock> <TextBlock x:Name="txtBlockAutoMark" Margin="5,0"></TextBlock> <TextBlock Text="Year:" FontWeight="Bold"> </TextBlock> <TextBlock x:Name="txtBlockYear" Margin="5,0"> </TextBlock> <TextBlock Text="Top Speed:" FontWeight="Bold"> </TextBlock> <TextBlock x:Name="txtTopSpeed" Margin="5,0"> </TextBlock> </StackPanel> </StackPanel> </Border> </UserControl> |
后台支持数据大同小异:
/// <summary> /// CarDetailView.xaml 的交互逻辑 /// </summary> public partial class CarDetailView : UserControl { public CarDetailView() { InitializeComponent(); } private Car car; public Car Car { get { return car; } set { car = value; this.txtBlockName.Text = car.Name; this.txtBlockAutoMark.Text = car.AutoMark; this.txtBlockYear.Text = car.Year; this.txtTopSpeed.Text = car.TopSpeed; this.imgPhoto.Source = new BitmapImage(new Uri(@"Resource/Image/" + car.Name + ".jpg", UriKind.Relative)); } } } |
最后把它们组装到窗体上:
<Window x:Class="WpfApplication1.Window35" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window35" Height="350" Width="623" xmlns:my="clr-namespace:WpfApplication1"> <StackPanel Orientation="Horizontal" Margin="5"> <my:CarDetailView x:Name="carDetailView1" /> <ListBox x:Name="listBoxCars" Width="180" Margin="5,0" SelectionChanged="listBoxCars_SelectionChanged"> </ListBox> </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.Shapes; namespace WpfApplication1 { /// <summary> /// Window35.xaml 的交互逻辑 /// </summary> public partial class Window35 : Window { public Window35() { InitializeComponent(); InitialCarList(); } private void listBoxCars_SelectionChanged(object sender, SelectionChangedEventArgs e) { CarListViewItem viewItem = e.AddedItems[0] as CarListViewItem; if(viewItem!=null) { carDetailView1.Car = viewItem.Car; } } private void InitialCarList() { List<Car> infos = new List<Car>() { new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="200", Year="1990"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="250", Year="1998"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="300", Year="2002"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="350", Year="2011"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="500", Year="2020"} }; foreach (Car item in infos) { CarListViewItem viewItem = new CarListViewItem(); viewItem.Car = item; this.listBoxCars.Items.Add(viewItem); } } } public class Car { public string AutoMark { get; set; } public string Name { get; set; } public string Year { get; set; } public string TopSpeed { get; set; } } } |
运行并单击Item项,运行效果如下图:
很难说这样做是错的,但是WPF里面如此实现需求真的是浪费了数据驱动界面这一重要功能。我们常说把WPF当作Winform来用指的就是这种实现方法。这种做法对WPF最大的曲解就是没有借助Binding来实现数据驱动界面,并且认为ListBoxItem里面放置的控件---这种曲解迫使数据在界面元数据间交换并且程序员只能通过事件驱动方式来实现逻辑------程序员必须借助处理ListBox的SelecttionChanged事件来推动DetaIlView来显示数据,而数据又是由CarListItemView控件转交给CarDetailView的,之间还做了一次类型转换。下图用于说明事件驱动模式与期望中数据驱动界面模式的不同:
显然,事件驱动是控件和控件之间沟通或者说是形式和形式之间的沟通,数据驱动则是数据与控件之间的沟通,是内容决定形式。使用DataTemplate就可以方便的把事件驱动模式转换为数据驱动模式。
你是不是担心前面写的代码会被删掉呢?不会的!由UserControl升级为DataTemplate时90%的代码是Copy,10%的代码可以方向删除,再做一点点改动就可以了。让我们来试试看。
首先把连个UserControl的芯剪切出来,用DataTempldate进行包装,再放到主窗体的资源字典里。最重要的是为DataTemplate里面的每一个控件设置Binding,告诉各个控件应该关注的是数据的哪个属性。因为使用BInding在控件和数据间建立关联,免去了在C#代码中访问界面元素,所以XAML代码中的大部分x:Name都可以删掉。代码看上去也简介了不少。
有些属性不能直接拿来用,比如汽车厂商和名称不能直接拿来做为图片路径,这时就要使用Converter。有两种办法可以在XAML代码中使用Converter:
把Converter以资源字典的形式放进资源字典里(本例使用的方法)。
为Converter准备一个静态属性,形成单件模式,在XAML代码里面使用{x:Static}标记扩展来访问。
我们的两个Converter代码如下:
//厂商名称转换为Logo路径 public class AutoMarkToLogoPathConverter:IValueConverter { /// <summary> /// 正向转 /// </summary> /// <param name="value"></param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.png",(string)value),UriKind.Relative)); } /// <summary> /// 逆向转未用到 /// </summary> /// <param name="value"></param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } |
//汽车名称转换为照片路径 public class NameToPhotoPathConverter:IValueConverter { /// <summary> /// 正向转 /// </summary> /// <param name="value"></param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.jpg", (string)value), UriKind.Relative)); } /// <summary> /// 逆向转未用到 /// </summary> /// <param name="value"></param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } |
有了这两个Converter之后我们就可以设计DataTemplate了,完整的XAML代码如下:
<Window x:Class="WpfApplication1.Window36" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1.Model" Title="Window36" Height="350" Width="623"> <Window.Resources> <!--Converter--> <local:AutoMarkToLogoPathConverter x:Key="amp"/> <local:NameToPhotoPathConverter x:Key="npp"/> <!--DataTemplate For DatialView--> <DataTemplate x:Key="DatialViewTemplate"> <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6"> <StackPanel> <Image x:Name="imgPhoto" Width="400" Height="250"
Source="{Binding AutoMark,Converter={StaticResource npp}}"></Image> <StackPanel Orientation="Horizontal" Margin="5,0"> <TextBlock Text="Name:" FontSize="20" FontWeight="Bold"></TextBlock> <TextBlock FontSize="20" Margin="5,0" Text="{Binding Name}"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal" Margin="5,0"> <TextBlock Text="AutoMark:" FontWeight="Bold"></TextBlock> <TextBlock Margin="5,0" Text="{Binding AutoMark}"></TextBlock> <TextBlock Text="Year:" FontWeight="Bold"> </TextBlock> <TextBlock Text="{Binding Year}" Margin="5,0"> </TextBlock> <TextBlock Text="Top Speed:" FontWeight="Bold"> </TextBlock> <TextBlock Text="{Binding TopSpeed}" Margin="5,0"> </TextBlock> </StackPanel> </StackPanel> </Border> </DataTemplate> <!--Data Template For ItemView--> <DataTemplate x:Key="ItemView"> <Grid Margin="2"> <StackPanel Orientation="Horizontal"> <Image x:Name="igLogo" Grid.RowSpan="3" Width="64" Height="64"
Source="{Binding Name,Converter={StaticResource amp}}"></Image> <StackPanel Margin="5,10"> <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"></TextBlock> <TextBlock Text="{Binding Year}" FontSize="14"></TextBlock> </StackPanel> </StackPanel> </Grid> </DataTemplate> </Window.Resources> <!--窗体内容--> <StackPanel Orientation="Horizontal"> <UserControl ContentTemplate="{StaticResource DatialViewTemplate}"
Content="{Binding Path=SelectedItem,ElementName=lbInfos}"></UserControl> <ListBox x:Name="lbInfos" ItemTemplate="{StaticResource ItemView}"></ListBox> </StackPanel> </Window> |
代码对于初学者来说有点长但是结构非常简单。其中最重要的有两句:
ContentTemplate="{StaticResource
DatialViewTemplate}",相当于给一个普通的UserControl穿上了一件外衣、让Car数据以图文并茂的方式展现出来。这件外衣就是x:Key="DatialViewTemplate"标记的DataTemplate资源。
ItemTemplate="{StaticResource ItemView}",把每一件数据的外衣交给ListBox,当ListBox的ItemSource被赋值的时候,ListBox就会为每个条目穿上这件外衣。这件外衣是以x:Key="ItemView"标记的DataTemplate资源。
因为不再使用事件驱动,而且为数据穿衣服的事也已经自动完成,所以后台的C#代码就非常的简单。窗体的C#代码就只剩下这些:
/// <summary> /// Window36.xaml 的交互逻辑 /// </summary> public partial class Window36 : Window { public Window36() { InitializeComponent(); InitialCarList(); } private void InitialCarList() { List<Car> infos = new List<Car>() { new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="200", Year="1990"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="250", Year="1998"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="300", Year="2002"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="350", Year="2011"}, new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="500", Year="2020"} }; this.lbInfos.ItemsSource = infos; } } |
运行程序,效果如下图:
与之前用UserControl没有任何区别。用户永远不知道程序员在后台使用的是什么技术与模式,但是对于程序员,我们可以清楚的体会到使用DataTemplate可以让程序结构更加清晰、代码更加简洁、维护更方便。不夸张的说,是DataTemplate帮助彻底完成了“数据驱动界面”,让Bingding和数据驱动渗透到用户界面的每一个细胞中。
1.3 控件的外衣ControlTemplate
每每提到ControlTemplate我都会想到“披着羊皮的狼”这句话-----披上羊皮之后,虽然看上去像只羊,但其行为仍然是匹狼。狼的行为指的是它能吃别的动物、对着满月嚎叫等事情,控件也有自己的行为,比如显示数据、执行方法、激发事件等。控件的行为要靠编程逻辑来实现,所以也可以把控件的行为称为控件的算法内容。举个例子,WPF中的CheckBox与其基类ToggleButton的功能几乎完全一样,但外观差别上却非常的大,这就是更换ControlTemplate的结果。经过更换ControlTemplate,我们不但可以制作披着CheckBox外衣的ToggleButton,还能制作披着温度计外衣的ProgressBar控件。
注意:
实际项目中,ControlTemplate主要有两大用武之地:
通过更换ControlTemplate来更换控件的外观,使之具有更优的用户体验和外观。
借助ControlTemplate,程序员和设计师可以并行工作,程序员可以使用WPF标准控件进行编程,等设计师的工作完成之后,只需要把新的ControlTemplate应用的程序中即可。
如何为控件设计ControlTemplate呢?首先需要你了解每个控件的内部结构。你可能会问:在哪儿可以查看到控件的内部结构呢?没有文档可查,想知道一个控件的内部结构必须把控件“打碎”了看一下。用于打碎控件、查看控件内部结构的工具就是blend,目前最新版本是5.0。
1.3.1 庖丁解牛看控件
挑柿子应该找软的捏,剖析控件也得丛简单的入手。TextBox和Button最简单,我们就从这两个控件入手。运行Blend,新建一个项目或者打开一个已经存在的项目,先把窗体的颜色改为线性渐变,再在窗体的主区域画两个TextBox和一个Button。对于程序员来说,完全可以把Blend看做是一个功能更强大的窗体设计器,而对于设计师来说,可以把Blend理解为XAML代码的PhotoShop或者FireWorks。程序运行效果如下图:
现在的TextBox方方正正,有棱有角,与窗体和Button的圆角风格不太协调,怎么将它的边框变成圆角矩形呢?传统的方法可能是创建一个UserControl并在TextBox的外面套一个Border,然后还要声明一些属性和方法暴露封装在UserControl里的TextBox上。我们的办法是在TextBox上右击,在弹出的菜单项里面选择编辑模板----编辑副本,如下图所示:
之所以不选择创建空项是因为创建空项需要重头开始设计一个控件的Con't'rolTemplate,新做的衣服哪如改衣服来的快啊!单击菜单项后弹出资源对话框,尽管可以用C#来创建ControlTemplate,但是绝大多数情况下ControlTemplate是由XAML代码编写的并放在资源词典里,所以才会弹出对话框询问你资源的x:Key是什么、打算把资源放在哪里。作为资源,ControlTemplate可以放在三个地方:Application资源词典里、某个界面元素的资源词典里、或者放在外部XAML文件中。我们选择把它放在Application的资源词典里以方便t统一管理,并命名为RoundCornerTextBoxStyle,如下图所示:
单击确定按钮便进入了模板的编辑状态。在对象和时间线面板中观察已经解剖开的TextBox控件,发现它是由一个名为Bd的ListBoxChrome套着一个名为PART_ContentHost的ScrollViewer组成的。为了显示矩形的圆角边框,我们只需要把外层的ListBoxChrome换成Border,删掉Border不具备的属性值、设置它的圆角弧度即可。
更改后的核心代码如下:
<Style x:Key="RoundCornerTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/> <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Padding" Value="1"/> <Setter Property="AllowDrop" Value="true"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/> <Setter Property="Stylus.IsFlicksEnabled" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" SnapsToDevicePixels="true" CornerRadius="5"> <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> </Style> |
这段代码有以下几个看点:
看点一:作为资源的不是单纯的ControlTemplate而是Style,说是编辑ControlTemplate但是实际上是吧ControlTemplate包含在Style里,不知道微软会不会更正这个小麻烦。Style是什么呢?简单讲就是一组<Setter>,也就是一组属性设计器。回想一下Winfrom编程的时候,窗体设计器不是可以生成这样的代码吗:
// button1 // this.button1.Location = new System.Drawing.Point(1100, 199); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(75, 23); this.button1.TabIndex = 0; this.button1.Text = "报表"; this.button1.UseVisualStyleBackColor = true; this.button1.Click += new System.EventHandler(this.button1_Click); // // printPreviewDialog1 // this.printPreviewDialog1.AutoScrollMargin = new System.Drawing.Size(0, 0); this.printPreviewDialog1.AutoScrollMinSize = new System.Drawing.Size(0, 0); this.printPreviewDialog1.ClientSize = new System.Drawing.Size(400, 300); this.printPreviewDialog1.Enabled = true; this.printPreviewDialog1.Icon = ((System.Drawing.Icon)(resources.GetObject("printPreviewDialog1.Icon"))); this.printPreviewDialog1.Name = "printPreviewDialog1"; this.printPreviewDialog1.Visible = false; |
同样的逻辑如果在XAML代码里出就变成了这样:
<Style x:Key="a"> <Setter Property="pName1" Value="value"></Setter> <Setter Property="pName2" Value="value"></Setter> <Setter Property="pName3"> <Setter.Value> <!--ObjectValue--> </Setter.Value> </Setter> <Setter Property="pName4"> <Setter.Value> <!--ObjectValue--> </Setter.Value> </Setter> lt;/Style> |
使用Style是,如过Value值比较简单,那么就直接用Attribute值表示,如果Value值不能用一个简单的字符串来表示那么就需要用XAML的属性对象语法。例子中,TextBox的Template是一个ControlTemplate对象,如此复杂的值只能使用属性对象语法来描述。对于Style,后面会有专门的章节来描述。
看点二:直接将ListBoxChrome替换成Border标签,去掉Border不具备的属性并添加CornerRadius=“5”;
看点三:TemplateBinding。ControlTemplate最终会被用到一个控件上,我们称这个控件为模板目标控件或者模板化控件,ControlTemplate里面的控件可以使用TemplateBinding将自己的某个属性值关联到模板控件的某个属性值上,必要的时候还可以添加Converter。例如BackGround=“{TemplateBinding
Background}”这句,意思是让Border的Background和目标控件保持一致,产生的效果就是你为模板的目标控件设置的Background属性,Border被Background也会跟着变。回顾Binding一节,你会发现,TemplateBinding的功能和{Binding
RelativeSource={RelativeSource TemplatedParent}}一致;
好了,把我们设计好的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="WPFApplication.MainWindow" x:Name="Window" Title="MainWindow" Width="385" Height="275"> <Window.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF4D91C6" Offset="0"/> <GradientStop Color="#FFD9DBF1" Offset="1"/> </LinearGradientBrush> </Window.Background> <Grid x:Name="LayoutRoot"> <TextBox HorizontalAlignment="Left" TextWrapping="Wrap"
Text="TextBox" VerticalAlignment="Top" Margin="57,49,0,0"
Width="255.487" Style="{DynamicResource RoundCornerTextBoxStyle}"/> <TextBox TextWrapping="Wrap" Text="TextBox"
Margin="57,106,56.513,109.163" d:LayoutOverrides="Height"
Style="{DynamicResource RoundCornerTextBoxStyle}"/> <Button Content="Button" VerticalAlignment="Bottom" Margin="149,0,145,42.163"/> </Grid> </Window> |
程序运行效果如下图:
以同样的方法打碎Button,你会发现Button的内部结构和TextBox差不多。但是如果打碎一个ProgressBar,你会发现它的内部结构就复杂多了,如下图:
在Blend里面你可以通过控制控件后面的眼睛来控制控件的显示与隐藏,这样就可以区分每个子控件的用处,这也是学习控件的最好方法。如果想把这个ProgressBar改变成一个温度计,只需要再此基础上添加一个背景、更改控件指示器的前景色、再在合适的控件外面套一个画出刻度Grid(刻度可以根据要求计算出来也可以是固定的)。
不知道大家意识到了没有,其实每一个控件本身就是一颗UI元素树。WPF中的UI元素树可以看做是两个树-----LogicTree和VisualTree,这两棵树的交点就是ControlTemplate。如果把界面上的控件元素看作一个节点,那元素构成的就是LogicTree,如果把控件内部由ControTemplate生成的控件也算上,那就构成了VisualTree。换句话说,LogicTree上导航不会进入控件内部,而在VisualTree上导航则可以检索到控件内部由ControlTemplate生成的子级控件。
1.3.2 ItemsControl的PanelTemplate
ItemsControl具有一个名为ItemsPanel的属性,它的数据类型是ItemsPanelTemplate。ItemsPanelTemplate也是一种控件Template,它的做用是可以让程序员可以控制ItemControl的条目容器。
举例而言,在我们的印象中ListBox中的条目都是至上而下排列的,如果客户要求我们做一个水平排列的ListBox怎么办呢?WPF之前,我们只能重写控件比较底层的方法和属性,而现在我们只需要调整ListBox的ItemPanel属性。请看下面的代码。
这是一个没有经过调整的ListBox,条目纵向排列
<Grid x:Name="LayoutRoot" Margin="5"> <ListBox> <TextBlock>Darren</TextBlock> <TextBlock>Andy</TextBlock> <TextBlock>Jacky</TextBlock> <TextBlock>T-Soft</TextBlock> </ListBox> </Grid> |
如果我们把代码改成这样:
<Grid x:Name="LayoutRoot" Margin="5"> <ListBox> <!--ItemsPanel--> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"></StackPanel> </ItemsPanelTemplate> </ListBox.ItemsPanel> <!--条目--> <TextBlock>Darren</TextBlock> <TextBlock>Andy</TextBlock> <TextBlock>Jacky</TextBlock> <TextBlock>T-Soft</TextBlock> </ListBox> </Grid> |
条目就会包装在一个水平排列的StackPanel中,从而横向排列,如下图所示:
1.4 DataTemplate和ControlTemplate的关系和应用
1.4.1 DataTemplate和ControlTemplate的关系
学习过DataTemplate和ControlTemplate,你应该已经体会到,控件只是数据的行为和载体,是个抽象的概念,至于它本身长成什么样子(控件内部结构),它的数据会长成什么样子(数据显示结构)都是靠Template生成的。决定控件外观的ControlTemplate,决定数据外观的DataTemplate,它们正式Contro类的Template和ContentTemplate两个属性值。它们的作用范围如下图:
凡是Template,最终都要作用在控件上,这个控件就是Template的目标控件,也叫模板化控件。你可能会问:DataTemplate的目标应该是数据呀,怎么会是控件呢。DataTemplate给人的感觉的确是施加在数据对象上,但施加在数据对象上生成的一组控件总得有个载体吧?这个载体一般落实在一个叫做ContentPresenter对象上。ContentPresenter类只有ContentTemplate属性、没有Template属性,这就证明了承载由DataTemplate生成的一组控件是他的专门用途。
至此我们可以看出,由ControlTemplate生成的控件树其树根就是ControleTemplate的目标控件,此模板化控件的Template属性值就是一个ControlTemplate实例。与之相仿,由DataTemplate生成的控件树其根是一个ContentPresenter控件,此模板化控件的ContentTemplate属性值就是这个DataTemplate实例。因为ContentPresenter控件是ControlTemplate控件树上的一个节点,所以DataTemplate控件树是ControlTemplate里面的一个子树。它们的关系如下图:
既然Template生成的控件树都有根,那么如何去找到根呢?办法很简单,每个控件都有个名为TemplateParent属性,如果它的值不为null,说明这个控件是由Template自动生成的,而属性值就是应用了模板的控件。如果由Template生成的控件使用了TemplateBinding获取属性值,则TemplateBinding的数据源就是应用这个模板的目标控件。
回顾一下本章开头的DataTemplate源码:
<DataTemplate> <Grid> <StackPanel Orientation="Horizontal"> <Grid> <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"></Rectangle> <TextBlock Text="{Binding Year}"></TextBlock> </Grid> <TextBlock Text="{Binding Price}" Margin="5,0"></TextBlock> </StackPanel> </Grid> </DataTemplate> |
这里用的普通的Binding而不是TemplateBinding,那数据源又是谁呢?不知道大家是否还记得,当为一个Binding只指定Path而不指定Source的时候,Binding会沿逻辑树一直向上找、查看没一个节点的DataContext属性,如果DataContext引用的对象具有Path指定的属性名,Binding就会把这个对象当作自己的数据源。显然,如果把数据对象赋值给ContentPresenter的DataContext属性,由DataTeplate生成的控件自然会找到这个数据对象并把它当作自己的数据源。
1.4.2 DataTemplate和Control的应用
为Template设置其应用目标有两种方法,一个是逐个设置控件的Template/ContentTemplate/ItemTemlate/CellTemplate等属性,不想应用Template的控件不设置;另一种是整体应用,即把Template应用到某个类型的控件或者数据上。
把ControlTemplate应用到所有控件上需要借助Style来实现,但Style不能标记X:KEY,例如下面的代码:
<Style BasedOn="{x:Null}" TargetType="{x:Type TextBox}"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/> <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Padding" Value="1"/> <Setter Property="AllowDrop" Value="true"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/> <Setter Property="Stylus.IsFlicksEnabled" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" SnapsToDevicePixels="true" CornerRadius="5"> <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> lt;/Style> |
Style没有X:key标记,默认为引用到所有的x:type指定的控件上,如果不想应用则将style标记为{x:null}。运行效果如下图:
把DataTemplate应用到某个数据类型上是设置DataTemplate的DataType属性,并且DataTemplate作为资源时也不能带x:key标记,
例如下面的代码:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WPFApplication" xmlns:c="clr-namespace:System.Collections;assembly=mscorlib" x:Class="WPFApplication.Window3" x:Name="Window" Title="Window3" Width="288" Height="181"> <Window.Resources> <!--DataTemplate--> <DataTemplate DataType="{x:Type local:Unit}"> <Grid> <StackPanel Orientation="Horizontal"> <Grid> <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"></Rectangle> <TextBlock Text="{Binding Year}"></TextBlock> </Grid> <TextBlock Text="{Binding Year}" Margin="5,0"></TextBlock> </StackPanel> </Grid> </DataTemplate> <!--数据源--> <c:ArrayList x:Key="ds"> <local:Unit Year="2001年" Price="100"></local:Unit> <local:Unit Year="2002年" Price="120"></local:Unit> <local:Unit Year="2003年" Price="140"></local:Unit> <local:Unit Year="2004年" Price="160"></local:Unit> <local:Unit Year="2005年" Price="180"></local:Unit> <local:Unit Year="2006年" Price="200"></local:Unit> </c:ArrayList> </Window.Resources> <StackPanel> <ListBox ItemsSource="{StaticResource ds}"></ListBox> <ComboBox ItemsSource="{StaticResource ds}" Margin="5"></ComboBox> </StackPanel> </Window> |
代码中的DataTemplate的目标数据类型和ListBox的条目类型都是Unit:
public class Unit { public string Year{get;set;} public int Price{get;set;} } |
此时DataTemplate会自动加载到所有的Unit类型对象上,尽管我没有为ListBox和CompBox指定ItemTemplate,一样会得到下图的效果:
很多时候数据是以XML形式存取的,如果把XML节点先转换为CLR数据类型再应用DataTemplate就麻烦了。DataTemplate很智能,具有直接把XML数据节点当作目标对象的功能-----XML数据中的元素名(标签名)可以作为DataType,元素的子节点和Attribute可以使用XPath来访问。下面的代码使用XmlDataProvider作为数据源(其XPath指出的必须是一组节点),请注意细节之处的变化:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPFApplication.Window4" x:Name="Window" Title="Window4" Width="314" Height="210"> <Window.Resources> <!--DataTemplate--> <DataTemplate DataType="Unit"> <Grid> <StackPanel Orientation="Horizontal"> <Grid> <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding XPath=@Price}"></Rectangle> <TextBlock Text="{Binding XPath=@Year}"></TextBlock> </Grid> <TextBlock Text="{Binding XPath=@Year}" Margin="5,0"></TextBlock> </StackPanel> </Grid> </DataTemplate> <!--数据源--> <XmlDataProvider x:Key="ds" XPath="Units/Unit"> <x:XData> <Units xmlns=""> <Unit Price="100" Year="2001"></Unit> <Unit Price="120" Year="2002"></Unit> <Unit Price="140" Year="2003"></Unit> <Unit Price="160" Year="2004"></Unit> <Unit Price="180" Year="2005"></Unit> <Unit Price="200" Year="2006"></Unit> </Units> </x:XData> </XmlDataProvider> </Window.Resources> <StackPanel> <ListBox ItemsSource="{Binding Source={StaticResource ds}}"></ListBox> <ComboBox ItemsSource="{Binding Source={StaticResource ds}}" Margin="5"></ComboBox> </StackPanel> </Window> |
XML的优势就是可以方便的表示带有层级的数据,比如:年级----班级----小组 或 主菜单---次菜单----三级菜单。同时WPF准备了TreeView和MenuItem控件来显示层级数据。能够帮助层级控件显示层级数据的模板是HierachicalDataTemplate。下面两个实际工作中常见的例子:
<?xml version="1.0" encoding="utf-8" ?> <Data xmlns=""> <Grade Name="一年级"> <Class Name="甲班"> <Group Name="A组"> </Group> <Group Name="B组"> </Group> <Group Name="C组"> </Group> </Class> <Class Name="乙班"> <Group Name="A组"> </Group> <Group Name="B组"> </Group> <Group Name="C组"> </Group> </Class> </Grade> <Grade Name="二年级"> <Class Name="甲班"> <Group Name="A组"> </Group> <Group Name="B组"> </Group> <Group Name="C组"> </Group> </Class> <Class Name="乙班"> <Group Name="A组"> </Group> <Group Name="B组"> </Group> <Group Name="C组"> </Group> </Class> </Grade> </Data> |
程序XAML代码如下:
<Window x:Class="WPFApplication.Window6" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window6" Height="268" Width="362"> <Window.Resources> <!--数据源--> <XmlDataProvider x:Key="ds" Source="XMLStudent.xml" XPath="Data/Grade"></XmlDataProvider> <!--年级模板--> <HierarchicalDataTemplate DataType="Grade" ItemsSource="{Binding XPath=Class}"> <TextBlock Text="{Binding XPath=@Name}"></TextBlock> </HierarchicalDataTemplate> <!--班级模板--> <HierarchicalDataTemplate DataType="Class" ItemsSource="{Binding XPath=Group}"> <RadioButton Content="{Binding XPath=@Name}" GroupName="gn"></RadioButton> </HierarchicalDataTemplate> <!--小组模板--> <HierarchicalDataTemplate DataType="Group" ItemsSource="{Binding XPath=Student}"> <CheckBox Content="{Binding XPath=@Name}"></CheckBox> </HierarchicalDataTemplate> </Window.Resources> <Grid> <TreeView Margin="5" ItemsSource="{Binding Source={StaticResource ds}}"> </TreeView> </Grid> </Window> |
程序运行效果如下图:
第二个例子是同一种数据类型的嵌套结构,这种情况下只设计一个HierarchicalDataTemplate就可以了,它会产生自动迭代应用效果。
数据依然存放在XML文件中。数据全部是OPeration'类型:
<?xml version="1.0" encoding="utf-8" ?> <Data xmlns=""> <Operation Name="文件" Gesture="F"> <Operation Name="新建" Gesture="N"> <Operation Name="项目" Gesture="Ctr+P"/> <Operation Name="网站" Gesture="Ctr+W"/> <Operation Name="文档" Gesture="Ctr+D"/> </Operation> <Operation Name="保存" Gesture="S"/> <Operation Name="打印" Gesture="P"/> <Operation Name="退出" Gesture="X"/> </Operation> <Operation Name="编辑" Gesture="E"> <Operation Name="剪切" Gesture="Ctr+X"/> <Operation Name="复制" Gesture="Ctr+C"/> <Operation Name="粘贴" Gesture="Ctr+V"/> </Operation> </Data> |
程序XAML代码如下:
<Window x:Class="WPFApplication.Window7" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window7" Height="300" Width="300"> <Window.Resources> <!--数据源--> <XmlDataProvider x:Key="ds" Source="MenuXML.xml" XPath="Data/Operation"></XmlDataProvider> <!--Operation模板--> <HierarchicalDataTemplate DataType="Operation" ItemsSource="{Binding XPath=Operation}" > <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding XPath=@Name}" Margin="10,0"></TextBlock> <TextBlock Text="{Binding XPath=@Gesture}"></TextBlock> </StackPanel> </HierarchicalDataTemplate> </Window.Resources> <StackPanel> <Menu ItemsSource="{Binding Source={StaticResource ds}}"></Menu> </StackPanel> </Window> |
运行效果如下图:
值得一提的是,HierarchicalDataTemplate的作用不是MenuItem的内容而是它的Header。如果对MenuItem的单击事件进行侦听处理,我们就可以从被单击的MenuItem的Header中取出XML数据。
XAML代码如下:
<StackPanel MenuItem.Click="StackPanel_Click"> <Menu ItemsSource="{Binding Source={StaticResource ds}}"></Menu> </StackPanel> |
事件处理代码如下:
private void StackPanel_Click(object sender, RoutedEventArgs e) { MenuItem item = e.OriginalSource as MenuItem; XmlElement xe = item.Header as XmlElement; MessageBox.Show(xe.Attributes["Name"].Value); } |
一旦拿到了数据,使用数据去驱动什么样的逻辑完全由你来决定了。比如可以维护一个CommandHelper类,根据拿到的数据来决定执行什么RoutedCommand。
1.4.3 寻找失落的控件
“井水不犯河水”常用来形容两个组织之间界限分明、互不相干,LogicTree与控件内部这颗小树之间就保持着这种关系。换句话说,如果UI元素树上有个X:Name=“TextBox1”的控件,某个控件内部也是由Template生成的x:Name="TextBox1"的控件,它们并不冲突,LogicTree不会看到控件内部的细节,控件内部元素也不会去理会控件外面是什么值。你可能会想:“这样一来,万一我想从控件外部访问内部的控件,获取它的属性值,岂不是做不到了。”放心,WPF为我们准备了访问控件内部小世界的入口,现在我们就开始出发寻找那些失落的控件。
由ControlTemplate和DataTemplate生成的控件都是“由Template生成的控件”。ControlTemplate和DataTemplate两个类均派生自FrameWorkTemplate类,这个类有个名为FindName的方法供我们检索其内部控件。也就是说,只要我们能拿到Template,找到其内部控件就不成问题。对于ControlTemplate对象,访问其目标控件的Datatemplate属性就可以拿到,但想拿到DataTemplate就要费一番周折了。千万不要以为ListBoxItem或者CompBoxItem容器就是DataTemplate的目标控件哦!因为控件的Template和ContentTemplate完全是两码事。
我们先来寻找由ControlTemplate生成的控件。首先设计一个ControlTemplate并把它应用在一个UserControl控件上。界面上还有一个Button,在它的Click事件处理器中我们检索ControlTemplate生成的代码。
程序的XAML代码如下:
<Window x:Class="WPFApplication.Window8" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window8" Height="300" Width="300"> <Window.Resources> <ControlTemplate x:Key="xtTemp"> <StackPanel Background="Orange"> <TextBox Margin="6" x:Name="textbox1"></TextBox> <TextBox Margin="6,0" x:Name="textbox2"></TextBox> <TextBox Margin="6" x:Name="textbox3"></TextBox> </StackPanel> </ControlTemplate> </Window.Resources> <StackPanel Background="Yellow"> <UserControl x:Name="uc" Template="{StaticResource xtTemp}" Margin="5"></UserControl> <Button Content="Find By Name" Width="120" Height="30" Click="Button_Click"></Button> </StackPanel> </Window> |
Button的事件处理器代码如下:
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逻辑,获取业务逻辑数据在底层就能做到,一般不会跑到表层来找。
先来看一个简单的例子。作为业务逻辑的类如下:
public class Student38 { public int Id { get; set; } public string Name { get; set; } public string Skill { get; set; } public bool HasJob { get; set; } } |
界面XAML代码如下:
<Window x:Class="WpfApplication1.Window38" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" Title="Window38" Height="227" Width="269"> <Window.Resources> <!--数据对象--> <local:Student38 x:Key="stu" Id="1" Skill="WPF" Name="Timothy" HasJob="True"></local:Student38> <!--DataTemplate--> <DataTemplate x:Key="dtStu"> <Border BorderBrush="Orange" BorderThickness="2" CornerRadius="5"> <StackPanel> <TextBlock Text="{Binding Id}" Margin="5"></TextBlock> <TextBlock x:Name="txtBlockName" Text="{Binding Name}" Margin="5"></TextBlock> <TextBlock Text="{Binding Skill}" Margin="5"></TextBlock> </StackPanel> </Border> </DataTemplate> </Window.Resources> <!--主窗体布局--> <StackPanel> <ContentPresenter x:Name="cp" Content="{StaticResource stu}"
ContentTemplate="{StaticResource dtStu}" Margin="5"> </ContentPresenter> <Button Content="Find" Margin="5,0" Click="Button_Click"> </Button> </StackPanel> </Window> |
Button的事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { TextBlock tb = this.cp.ContentTemplate.FindName("txtBlockName", this.cp) as TextBlock; MessageBox.Show(tb.Text); //Student38 stu = this.cp.Content as Student38; //MessageBox.Show(stu.Name); } |
未被注释的代码是使用DataTemplate的FindName方法获取由DataTemplate生成的控件并访问其属性,被注释的代码是直接使用其底层数据。显然,为了获取Student的某个属性,应该使用被注释的代码而不必要绕到控件上来,除非你想得到的是控件的长度,高度。与业务逻辑无关的纯UI属性。
下面再看一个复杂的例子。DataTemplate最常用的地方就是GridViewColumn的CellTemplate属性。把GridViewColumn放置在一个GridView控件里就可以生成表格了。GridViewColumn的默认CellTemplate是使用TextBlock只读属性显示数据,如果我们想让用户能修改数据或者使用CheckBox显示bool类型的数据的话就需要自定义DataTemplate了。
还是先定义这个Student的类:
public class Student39 { public int Id { get; set; } public string Name { get; set; } public string Skill { get; set; } public bool HasJob { get; set; } } |
准备数据集合,呈现数据的工作全部由XAML代码来完成:
<Window x:Class="WpfApplication1.Window39" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:System.Collections;assembly=mscorlib" xmlns:local="clr-namespace:WpfApplication1" Title="Window39" Height="338" Width="446"> <Window.Resources> <!--数据集合--> <c:ArrayList x:Key="stuList"> <local:Student39 Id="1" Name="Timoty Liu" Skill="WPF" HasJob="True"></local:Student39> <local:Student39 Id="2" Name="Tom Chang" Skill="BI/SQL" HasJob="True"></local:Student39> <local:Student39 Id="3" Name="Guan Chong" Skill="Writing" HasJob="False"></local:Student39> <local:Student39 Id="4" Name="Shanshan" Skill="C#/Java" HasJob="False"></local:Student39> <local:Student39 Id="5" Name="Pingping Zhang" Skill="Writing" HasJob="False"></local:Student39> <local:Student39 Id="6" Name="kenny Tian" Skill="Asp.net" HasJob="False"></local:Student39> </c:ArrayList> <!--DataTemplate--> <DataTemplate x:Key="nameDT"> <TextBox x:Name="txtBoxName" Text="{Binding Name}"></TextBox> </DataTemplate> <DataTemplate x:Key="skillDT"> <TextBox x:Name="txtSkill" Text="{Binding Skill}"></TextBox> </DataTemplate> <DataTemplate x:Key="hasJobDT"> <CheckBox IsChecked="{Binding HasJob}"></CheckBox> </DataTemplate> </Window.Resources> <Grid Margin="5"> <ListView x:Name="lvStudent" ItemsSource="{StaticResource stuList}"> <ListView.View> <GridView> <GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}"></GridViewColumn> <GridViewColumn Header="姓名" CellTemplate="{StaticResource nameDT}"></GridViewColumn> <GridViewColumn Header="技术" CellTemplate="{StaticResource skillDT}"></GridViewColumn> <GridViewColumn Header="已工作" CellTemplate="{StaticResource hasJobDT}"></GridViewColumn> </GridView> </ListView.View> </ListView> </Grid> </Window> |
程序运行效果如下图:
然后我们为显示姓名的TextBox添加GetFocus事件处理器:
<DataTemplate x:Key="nameDT"> <TextBox x:Name="txtBoxName" Text="{Binding Name}" GotFocus="txtBoxName_GotFocus"></TextBox> </DataTemplate> |
因为我们是在DataTemplate里面添加了事件处理器,所以界面上任何一个由此DataTemplate生成的TextBox都会在获得焦点的时候调用txtBoxName_GotFocus这个事件处理器。txtBoxName_GotFocus的代码如下:
private void txtBoxName_GotFocus(object sender, RoutedEventArgs e) { TextBox tb = e.OriginalSource as TextBox; //获取事件发起的源头 ContentPresenter cp = tb.TemplatedParent as ContentPresenter;//获取模板目标 Student39 stu = cp.Content as Student39;//获取业务逻辑数据 this.lvStudent.SelectedItem = stu;//设置ListView选中项 //访问界面元素 ListViewItem lvi = this.lvStudent.ItemContainerGenerator.ContainerFromItem(stu) as ListViewItem; CheckBox cb = this.FindVisualChild<CheckBox>(lvi); MessageBox.Show(cb.Name); } private ChildType FindVisualChild<ChildType>(DependencyObject obj) where ChildType : DependencyObject { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { DependencyObject child = VisualTreeHelper.GetChild(obj,i); if (child != null && child is ChildType) { return child as ChildType; } else { ChildType childOfChild = FindVisualChild<ChildType>(child); if(childOfChild!=null) { return childOfChild; } } } return null; } |
当使用GridView作为ListView的View属性时,如果某一列使用TextBox作为CellTemplate,那么即使这列中的TextBox被鼠标单击并获得了焦点ListView也不会把此项做为自己的SelectedItem。所以,txtBoxName_GotFocus的前半部分是获得数据的源头(TextBox),然后沿UI元素树上朔到DataTemplate目标控件(ContentPresenter)并获取它的内容,它的内容一定是一个Student实例。
txtBoxName_GotFocus的后半部分则借助VisualTreeHelper类检索由DataTemplate生成的控件。前面说过,每个ItemControl的派生类(如ListBox,Combox,ListView)都具有自己独特的条目容器,本例中是一个包装着Student对象的ListViewItem(注意,此ListViewItem对象的Content也是Student对象)。可以把这个ListViewItem控件视为一颗树的根,使用VisualTreeHelper类就可以遍历它的各个节点。本例中是吧遍历算法分装在了FindVisualChild泛型方法里。
运行程序,并单击某个显示姓名的TextBox,效果如下图所示:
由本例可以看出,无论是从事件源头“自下而上”的找,还是使用ItemContainerGenerator.ContainerFromItem方法找到条目容器再“自上而下”的找,总之,找到业务逻辑数据(Student实例)并不难,而工作中大多是操作业务逻辑数据。如果真的想找由DataTemplate生成的控件,对于结构简单的控件,可以使用DataTemplate对象的FindName方法;对于结构复杂的控件,则需要借助VisualTreeHelper来实现。
1.5 深入浅出话Style
Style直译过来就是“风格”,“样式”。拿人来举例,人的风格指静态的外观和行为举止。同样一个人,如果留平头、穿上足球队的队服、脚蹬战靴,看上去就是一名叱咤球场的运动员;如果让他换一身笔挺的西装、穿上皮靴、再拎上一个公文包,看上去就是一个上午人士;如果让他梳起爆炸头、戴上墨镜、打上耳孔再穿一身肥大的休闲装,活脱脱一个非主流形象。这些就是静态的外观风格,是通过改变一些属性值的搭配起来实现的。除了从静态外观来判断一个人的风格,我们还会观察他的行为特点。比如遇到困难时,有些人很乐观,照样谈笑风生。有些人很谨慎、仔细分析问题;有些人很悲观、成天哀声叹气,这就是行为风格,行为风格是由外界刺激的响应体现出来的。说到这儿,大家一定能想到一种职业---演员。演员就是靠调整自己的静态行为和风格来饰演各种角色的。
如果把WPF窗体看作一个舞台,那么窗体上的控件就是一个演员,它们的职责就是在用户界面上按照业务逻辑的需要扮演自己的角色。为了让同一种控件能担起不同的角色,程序员就要为它们设计多种多样的外观样式和行为动作,这就是Style。构成Style最重要的两种元素是Setter和Trigger,Setter类帮助我们设置控件的静态外观风格,Trigger则帮助我们设置控件的行为风格。
1.5.1 Style中的Setter
Setter,设置器。什么的设置器呢?属性值的。我们给属性赋值的时候一般都采用“属性名=属性值”的形式。Setter类的Property属性用来指明你想为属性的哪个属性赋值;Setter类的Value属性则是你提供的属性值。
下面的例子中在WIndows的资源词典中放置一个针对TextBlock的Style,Style中使用若干Setter来设定TextBlock的一些属性,这样程序中的TextBlock就会具有统一的风格,除非你使用{x:null}显示的清空Style。
XAML代码如下:
<Window x:Class="WpfApplication1.Window40" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window40" Height="310" Width="426"> <Window.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="24"></Setter> <Setter Property="TextDecorations" Value="Underline"></Setter> <Setter Property="FontStyle" Value="Italic"></Setter> </Style> </Window.Resources> <StackPanel Margin="5"> <TextBlock Text="Hello WPF!"></TextBlock> <TextBlock Text="This is a sample for style!"></TextBlock> <TextBlock Text="by Time 2012-11-12!" Style="{x:Null}"></TextBlock> </StackPanel> </Window> |
因为Style的内容属性是Setters,所以我们可以直接在<Style>标签的内容区域使用Setter。
运行效果如下图:
根据上面这个例子我们可以推知,如果想设置控件的ControlTemplate,只需要把Setter的Property设为Template并为Value提供一个ControlTemplate对象即可。
1.5.2 Style的Trigger
Trigger,触发器,即当某些条件满足的时候会触发一个行为(比如某些值的变化或动画的发生等)。触发器比较像事件。事件一般由用户操作触发的,而触发器除了有事件触发型的EventTrigger外还有数据变化触发行的Trigger/DataTrigger及多条件触发型MultiTrigger/MultiDataTrigger等。
1.基本的Trigger
Trigger是最基本的触发器。类似于Setter,Trigger也有Property和Value两个属性,Property是Trigger关注的属性名称,Value是触发条件。Trigger还有一个Setters属性,此属性是一组Setter,一旦触发条件满足,这组Setter的“属性---值”就会被应用,触发条件不在满足后,各属性值会被还原。
下面这个例子针对的是CheckBox的Style,当CheckBox的IsCheck属性为True时前景色和字体会改变。XAML代码如下:
<Window x:Class="WpfApplication1.Window41" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window41" Height="258" Width="352"> <Window.Resources> <Style TargetType="CheckBox"> <Style.Triggers> <Trigger Property="IsChecked" Value="True"> <Trigger.Setters> <Setter Property="FontSize" Value="20"></Setter> <Setter Property="Foreground" Value="Orange"></Setter> </Trigger.Setters> </Trigger> </Style.Triggers> </Style> </Window.Resources> <Window.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF4589D8" Offset="0" /> <GradientStop Color="White" Offset="1" /> </LinearGradientBrush> </Window.Background> <StackPanel> <CheckBox Content="锄禾日当午" Margin="5"></CheckBox> <CheckBox Content="汗滴禾下土" Margin="5,0"></CheckBox> <CheckBox Content="谁知盘中餐" Margin="5"></CheckBox> <CheckBox Content="粒粒皆辛苦" Margin="5,0"></CheckBox> </StackPanel> </Window> |
因为Triggers不是Style的内容属性,所以<Style.Trigger>...</Style.Trigger>这层标签不能省略,但Trigger的Setters属性是Trigger的内容属性,所以<Trigger.Setters>...</Trigger.Setters>这层标签是可以省略的。
运行效果如下图:
2. MultiTrigger
MultiTrigger是一个很容易让人误解的名字,会让人以为是多个Trigger集成在一起,实际上叫MultiConditionTrigger更合适,因为必须多个条件同时成立才会被触发。MultiTrigger比Trigger多了一个Conditions属性,需要同时成立的条件就放在这个集合当中。
让我们稍微改动一下上面的例子,要求同时满足CheckBox被选中且Content必须为“粒粒皆辛苦”时才会被触发,XAML代码如下(仅Style部分):
<Style TargetType="CheckBox"> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsChecked" Value="True"></Condition> <Condition Property="Content" Value="粒粒皆辛苦"></Condition> </MultiTrigger.Conditions> <MultiTrigger.Setters> <Setter Property="FontSize" Value="20"></Setter> <Setter Property="Foreground" Value="Orange"></Setter> </MultiTrigger.Setters> </MultiTrigger> </Style.Triggers> </Style> |
运行效果如下图:
3. 由数据触发DataTrigger
程序中经常会遇到基于数据执行某些判断情况,遇到这种情况我们就可以考虑使用DataTrigger。DataTrigger对象的Binding属性会把数据源源不断的送出来,一旦送出来的值与Value属性一致,DataTrigger即被触发。
下面的例子中,当TextBox的Text长度小于7个字符其Border会保持红色。XAML代码如下:
<Window x:Class="WpfApplication1.Window42" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" Title="Window42" Height="184" Width="324"> <Window.Resources> <local:L2BConverter x:Key="cbtr"></local:L2BConverter> <Style TargetType="TextBox"> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=Text.Length,Converter={StaticResource cbtr}}" Value="false"> <Setter Property="BorderBrush" Value="Red"></Setter> <Setter Property="BorderThickness" Value="1"></Setter> </DataTrigger> </Style.Triggers> </Style> </Window.Resources> <StackPanel> <TextBox Margin="5"></TextBox> <TextBox Margin="5,0"></TextBox> <TextBox Margin="5"></TextBox> </StackPanel> </Window> |
这个例子中唯一需要解释的就是DataTrigger的Binding。为了将控件自身做为数据源,我们使用了RelativeSource,初学者经常认为“不明确指出Source的值Binding就会将自己作为数据的来源”,这是错误的,因为不明确指出Source的值Binding就会把控件的DataContext做为自己的数据来源。Binding的Path设置为Text.Length,即我们关注的是字符串的长度。长度是一个具体的数字,如何基于这个长度值来做判断呢?这就用到了Converter。我们创建如下Converter:
public class L2BConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { int textLength = (int)value; return textLength > 6 ? true : false; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } |
经过Converter转换以后,长度值就会变为bool类型值。DataTrigger的value设置为false,也就是说当TextBox的文本长度小于7时DataTrigger会使用自己一组Setter把TextBox的边框设置为红色。运行效果如下图:
4. 多数据条件触发的MultiDataTrigger
有时候我们会遇到要求多个数据条件同时满足才能触发变化的需求,此时可以考虑使用MultiDataTrigger。比如有这样一个需求:用户界面上使用ListBox显示一列Student数据,当Student对象满足ID为2、Name为Darren的时候,条目就高亮显示。
事例XAML代码如下:
<Window x:Class="WpfApplication1.Window43" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window43" Height="262" Width="425"> <Window.Resources> <Style TargetType="ListBoxItem"> <!--使用Style设置Datatemplate--> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Id}" Width="60"></TextBlock> <TextBlock Text="{Binding Name}" Width="120"></TextBlock> <TextBlock Text="{Binding Skill}" Width="60"></TextBlock> </StackPanel> </DataTemplate> </Setter.Value> </Setter> <!--MultiDataTrigger--> <Style.Triggers> <MultiDataTrigger> <MultiDataTrigger.Conditions> <Condition Binding="{Binding Path=Id}" Value="2"></Condition> <Condition Binding="{Binding Path=Name}" Value="Darren"></Condition> </MultiDataTrigger.Conditions> <MultiDataTrigger.Setters> <Setter Property="Background" Value="Orange"></Setter> </MultiDataTrigger.Setters> </MultiDataTrigger> </Style.Triggers> </Style> </Window.Resources> <StackPanel> <ListBox x:Name="lbInfos" Margin="5"></ListBox> </StackPanel> </Window> |
后台代码如下:
public Window43() { InitializeComponent(); InitialInfo(); } private void InitialInfo() { List<Student38> infos = new List<Student38>() { new Student38(){ Id=2, Name="Darren", Skill="WPF"}, new Student38(){ Id=1, Name="Tom", Skill="Java"}, new Student38(){ Id=3, Name="Jacky", Skill="Asp.net"}, new Student38(){ Id=2, Name="Andy", Skill="C#"}, }; this.lbInfos.ItemsSource = infos; } |
Student38类已经在上面的文章中提到,再此就不再多讲。运行效果如下图:
5. 由事件触发的EventTrigger
EventTrigger是触发器中最特殊的一个。首先,它不是由属性值或者数据的变化来触发而是由事件来触发;其次,被触发以后它并非应用一组Setter,而是执行一段动画。因此,UI的动画效果往往和EventTrigger相关联。
在下面这个例子中创建一个针对Button的Style,这个Style包含两个EventTrigger,一个由MouseEnter触发,另一个由MouseLeave触发。XAML代码如下:
<Window x:Class="WpfApplication1.Window44" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window44" Height="258" Width="377"> <Window.Resources> <Style TargetType="Button"> <Style.Triggers> <!--鼠标进入--> <EventTrigger RoutedEvent="MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Width"></DoubleAnimation> <DoubleAnimation To="150" Duration="0:0:0.2"Storyboard.TargetProperty="Height"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger> <!--鼠标离开--> <EventTrigger RoutedEvent="MouseLeave"> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Width"></DoubleAnimation> <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <Button Width="40" Height="40" Content="OK"></Button> </Grid> </Window> |
无需任何c#代码,我们就获得了如下图所示的结果:
自此,各种触发器就介绍完了,提醒大家一点:虽然在Style里面大量使用触发器,但触发器并非只能应用在Style中-----各种Template也可以拥有自己的触发器,请大家根据需要决定触发器放在Style里面还是Template里面。
|