WPF之模板
WPF系统不但支持传统的Winfrom编程的用户界面和用户体验设计,更支持使用专门的设计工具Blend进行专业设计,同时还推出了以模板为核心的新一代设计理念。
模板的内涵#
“形而上者谓之道,形而下者谓之器”出自《易经》,在计算机世界中:
- “形而上者谓之道”指的就是基于现实世界对万物进行抽象封装、理顺它们之间的关系,这个“道”就是面向对象思想,进一步提升、总结出对象之间的最优组合关系就上升为设计模式思想了。
- “形而下者谓之器”指的是能观察到的世间万物都是物质本质内容的表现形式,“本质与表现”或者说“内容与形式”是哲学范畴内的一对矛盾统一体。
WPF的全称是Windows Presentation Foundation,Presentation 一词的意思就是外观、呈现、表现,在Windows GUI程序这个尺度上WPF扮演的就是“形”的角色、是程序的外在“形式”,而程序的“内容”仍然是由数据和算法构成的业务逻辑。
WPF作为一种“形式”,它要表现的“内容”是程序的数据和算法——Binding传递的是数据,事件参数携带的也是数据;方法和委托的调用是算法,事件传递消息也是算法。
控件(Control)是数据内容表现形式和算法内容表现形式的双重载体,控件既是数据的表现形式让用户可以直观地看到数据,又是算法的表现形式让用户方便地操作逻辑。
作为“表现形式”,每个控件都是为了实现某种用户操作算法和直观显示某种数据而生,一个控件看上去是什么样子由它的“算法内容”和“数据内容”决定,这就是内容决定形式。这里引入两个概念:
- 控件的“算法内容”:指控件能展示哪些数据、具有哪些方法、能响应哪些操作、能激发什么事件,简而言之就是控件的功能,它们是一组相关的算法逻辑。
- 控件的“数据内容”:控件所展示的具体数据是什么。
以往的GUI开发技术(如Windows Forms和ASP.NET)中,控件内部的逻辑和数据是固定的,程序员不能改变。
在WPF中,通过引入模板(Template)微软将数据和算法的“内容”与“形式”解耦了。WPF中的Template分为两大类:
- ControlTemplate是算法内容的表现形式,一个控件怎样组织其内部结构才能让它更符合业务逻辑、让用户操作起来更舒服就是由它来控制的。它决定了控件“长成什么样子”,并让程序员有机会在控件原有的内部逻辑基础上扩展自己的逻辑。
- DataTemplate是数据内容的表现形式,一条数据显示成什么样子,是简单的文本还是直观的图形动画就由它来决定。
WPF中的控件不再具有固定的形象,仅仅是算法内容或数据内容的载体,数据显示成什么样子也可以自由设定。
数据的外衣DataTemplate#
一样的内容可以用不同的形式来展现,软件设计称之为“数据-视图”(Data-View)模式。以往的开发技术,如MFC、Windows Forms等,视图要靠UserControl 来实现,WPF不但支持UserControl 还支持用DataTemplate为数据形成视图(从UserControl升级到DataTemplate也很简单)。
DataTemplate常用的地方有3处,分别是:
- ContentControl的ContentTemplate属性,相当于给ContentControl的内容穿衣服。
- ItemsControl的ItemTemplate属性,相当于给ItemsControl的数据条目穿衣服。
- GridViewColumn的CellTemplate属性,相当于给GridViewColumn单元格里的数据穿衣服。
用一个例子对比UserControl与DataTemplate的使用,ListBox的条目显示汽车的厂商图标和简要参数,单击某个条目后在窗体的详细内容区域显示汽车的照片和详细参数。
先在项目中建立资源管理目录(/Resources/Logos和/Resources/Images)并把图片添加进来,Logo的文件名与厂商名称一致,照片的文件名则与车名一致。
UserControl例子#
创建Car数据类型:
public class Car
{
public string AutoMark { get; set; }
public string Name { get; set; }
public string Year { get; set; }
public string TopSpeed { get; set; }
}
准备一个名为CarListItemView的UserControl用于在ListBox里面显示Car类型的数据,XAML代码如下:
<UserControl x:Class="WpfApp.CarListItemView"
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="imageLogo" Grid.RowSpan="3" Width="64" Height="64"/>
<StackPanel Margin="5,10">
<TextBlock x:Name="textBlockName" FontSize="16" FontWeight="Bold"/>
<TextBlock x:Name="textBlockYear" FontSize="14"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
CarlistItemView用于支持前台显示的属性C#代码为:
public partial class CarListItemView : UserControl
{
public CarListItemView()
{
InitializeComponent();
}
private Car car;
public Car Car
{
get { return car; }
set
{
car = value;
this.textBlockName.Text = car.Name;
this.textBlockYear.Text = car.Year;
string uirStr = string.Format(@"Resource/Logos/{0}.png", car.AutoMark);
this.imageLogo.Source = new BitmapImage(new Uri(uirStr, UriKind.Relative));
}
}
}
准备一个名为CarDetailView的UserControl作为Car类型的详细信息视图,XAML代码如下:
<UserControl x:Class="WpfApp.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="imagePhoto" Width="400" Height="250"/>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Name:" FontSize="20" FontWeight="Bold"/>
<TextBlock x:Name="textBlockName" FontSize="20" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="AutoMark:" FontWeight="Bold"/>
<TextBlock x:Name="textBlockAutoMark" Margin="5,0"/>
<TextBlock Text="Year:" FontWeight="Bold"/>
<TextBlock x:Name="textBlockYear" Margin="5,0"/>
<TextBlock Text="Top Speed:" FontWeight="Bold"/>
<TextBlock x:Name="textTopSpeed" Margin="5,0"/>
</StackPanel>
</StackPanel>
</Border>
</UserControl>
后台支持数据C#代码:
public partial class CarDetailView : UserControl
{
public CarDetailView()
{
InitializeComponent();
}
private Car car;
public Car Car
{
get { return car; }
set
{
car = value;
this.textBlockName.Text = car.Name;
this.textBlockAutoMark.Text = car.AutoMark;
this.textBlockYear.Text = car.Year;
this.textTopSpeed.Text = car.TopSpeed;
string uirStr = string.Format(@"Resource/Images/{0}.jpg", car.Name);
this.imagePhoto.Source = new BitmapImage(new Uri(uirStr, UriKind.Relative));
}
}
}
把它们组装到主窗体上,XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="350" Width="650">
<StackPanel Orientation="Horizontal" Margin="5">
<local:CarDetailView x:Name="detailView" />
<ListBox x:Name="listBoxCars" Width="200" Margin="5,0" SelectionChanged="listBoxCars_SelectionChanged"/>
</StackPanel>
</Window>
窗体的后台代码如下:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitialCarList();
}
//初始化ListBox
private void InitialCarList()
{
List<Car> carList = new List<Car>() {
new Car(){ AutoMark="Lamborghini", Name="Diablo", TopSpeed="340", Year="1990"},
new Car(){ AutoMark="Lamborghini", Name="Murcielago", TopSpeed="353", Year="2001"},
new Car(){ AutoMark="Lamborghini", Name="Gallardo", TopSpeed="325", Year="2003"},
new Car(){ AutoMark="Lamborghini", Name="Reventon", TopSpeed="356", Year="2008"},
};
foreach (Car car in carList)
{
CarListItemView view = new CarListItemView();
view.Car = car;
this.listBoxCars.Items.Add(view);
}
}
//选项变化事件的处理器
private void listBoxCars_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CarListItemView view = e.AddedItems[0] as CarListItemView;
if (view != null)
{
this.detailView.Car = view.Car;
}
}
}
运行程序并单击ListBox里的条目,效果如下(未准备相关图片):
这种实现方法是“把WPF当作WindowsForms来用”,浪费了数据驱动界面这一重要功能,没有借助Binding实现数据驱动界面,并且认为ListBox.Items属性里放置的是控件—一这种曲解迫使数据在界面元素间交换并且程序员只能使用事件驱动方式来实现逻辑。
目前的事件驱动模式与期望中数据驱动界面模式的不同:事件驱动是控件和控件之间沟通或者说是形式与形式之间的沟通,数据驱动则是数据与控件之间的沟通、是内容决定形式,使用DataTemplate就可以很方便地把事件驱动模式升级为数据驱动模式。
DataTemplate例子#
由UserControl升级为DataTemplate时90%的代码可以原样拷贝,另10%可以放心删除,再做一点点改动就可以了:
- 把两个UserControl的“芯”剪切出来,用
标签包装,再放进主窗体的资源词典里。 - 为DataTemplate里的每个控件设置Binding,告诉各个控件应该关注数据的哪个属性,免去在C#代码中访问界面元素(去掉大多数x:Name)。
汽车的厂商和名称不能直接拿来作为图片的路径,需要使用Converter,有两种办法可以在XAML代码中使用Converter:
- 把 Converter以资源的形式放在资源词典里(本例使用的方法)。
- 为Converter准备一个静态属性,形成单件模式,在XAML代码里使用(x:Static)标签扩展来访问。
两个Converter代码如下:
//厂商名称转换为Logo路径
public class AutoMarkToLogoPathConverter : IValueConverter
{
// 正向转换
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string uirStr = string.Format(@"Resource/Logos/{0}.png", (string)value);
return new BitmapImage(new Uri(uirStr, UriKind.Relative));
}
// 逆向转未用到
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
//汽车名称转换为照片路径
public class NameToPhotoPathConverter : IValueConverter
{
// 正向转换
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string uirStr = string.Format(@"Resource/Images/{0}.jpg", (string)value);
return new BitmapImage(new Uri(uirStr, UriKind.Relative));
}
// 逆向转未用到
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
有了Converter之后就可以设计DataTemplate了,完整的XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="350" Width="630">
<Window.Resources>
<!--Converter-->
<local:AutoMarkToLogoPathConverter x:Key="a2l"/>
<local:NameToPhotoPathConverter x:Key="n2p"/>
<!--DataTemplate For Datial View-->
<DataTemplate x:Key="carDetailViewTemplate">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">
<StackPanel Margin="5">
<Image x:Name="imagePhoto" Width="400" Height="250" Source="{Binding Name,Converter={StaticResource n2p}}"/>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Name:" FontSize="20" FontWeight="Bold"/>
<TextBlock FontSize="20" Margin="5,0" Text="{Binding Name}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="AutoMark:" FontWeight="Bold"/>
<TextBlock Margin="5,0" Text="{Binding AutoMark}"/>
<TextBlock Text="Year:" FontWeight="Bold"/>
<TextBlock Text="{Binding Year}" Margin="5,0"/>
<TextBlock Text="Top Speed:" FontWeight="Bold"/>
<TextBlock Text="{Binding TopSpeed}" Margin="5,0"/>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
<!--Data Template For Item View-->
<DataTemplate x:Key="carListItemViewTemplate">
<Grid Margin="2">
<StackPanel Orientation="Horizontal">
<Image x:Name="imageLogo" Grid.RowSpan="3" Width="64" Height="64" Source="{Binding AutoMark,Converter={StaticResource a2l}}"/>
<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 Path=SelectedItem,ElementName=listBoxCars}"/>
<ListBox x:Name="listBoxCars" ItemTemplate="{StaticResource carListItemViewTemplate}" Margin="5,0"/>
</StackPanel>
</Window>
代码结构非常简单,其中最重要的两句是:
- ContentTemplate="{StaticResource carDetailViewTemplate}",相当于给一个普通UserControl的数据内容穿上一件外衣、让Car类型数据以图文并茂的形式展现出来——以x:Key="carDetailViewTemplate"标记的DataTemplate资源。
- ItemTemplate="{StaticResource carListItemViewTemplate}",是把一件数据的外衣交给ListBox,当ListBox.ItemsSource被赋值时,ListBox会为每个条目穿上这件外衣——以xKey="carListltemViewTemplate"标记的DataTemplate资源。
窗体的C#代码只剩下这些:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitialCarList();
}
//初始化ListBox
private void InitialCarList()
{
List<Car> carList = new List<Car>() {
new Car(){ AutoMark="Lamborghini", Name="Diablo", TopSpeed="340", Year="1990"},
new Car(){ AutoMark="Lamborghini", Name="Murcielago", TopSpeed="353", Year="2001"},
new Car(){ AutoMark="Lamborghini", Name="Gallardo", TopSpeed="325", Year="2003"},
new Car(){ AutoMark="Lamborghini", Name="Reventon", TopSpeed="356", Year="2008"},
};
//填充数据
this.listBoxCars.ItemsSource = carList;
}
}
运行程序,效果如下(与使用UserControl实现的没有任何区别):
使用DataTemplate可以让程序结构更清晰、代码更简洁、维护更方便,是DataTemplate帮助彻底完成了“数据驱动界面”,让Binding和数据关联渗透到用户界面的每一个细胞中。
控件的外衣 ControlTemplate#
实际项目中,ControlTemplate主要有两大用武之地:
- 通过更换ControTemplate改变控件外观,使之具有更优的用户使用体验及外观。
- 借助ControlTemplate,程序员与设计师可以并行工作,程序员可以先用WPF标准控件进行编程,等设计师的工作完成后,只需把新的ControfTemplate应用到程序中就可以了。
第一点让程序更换皮肤变得非常容易,第二点则解决了团队分工与合作的问题。
为控件设计ControlTemplate需要了解每个控件的内部结构,用于打碎控件、查看内部结构的工具就是Microsoft Expression套装中的Blend。
解剖控件#
先从结构简单的TextBox和Button入手,运行Blend,新建一个WPF项目(或者打开一个由VS2008创建的WPF项目),先把窗体的背景色改为线性渐变,再在窗体的主容器Grid 里画上两个TextBox和一个Buton。
让TextBox的边框变为圆角矩形,操作如下:
- 在TextBox上右击,在弹出菜单中选择“编辑模板”一>“编辑副本”,不选择“创建空白项”是因为“创建空白项”是从头开始设计一个控件的ControlrTemplate(没有改模板快)。
- 单击菜单项后弹出资源对话框,弹出对话框设置资源的x:Key是什么、打算把资源放在哪里(大多数情况下ControlTemplate是由XAML代码编写的并放在资源词典里)。
- 选择把模板放在Application的资源词典里以便统一管理,并命名为RoundCornerTextBoxStyle。
- 单击“确定”按钮便进入了控件的模板的编辑状态。
注:ControlTemplate可以放在三个地方:Application的资源词典里、某个界面元素的资源词典里,或者放在外部XAML文件中。
在“对象和时间线”面板中观察已经解剖开的TextBox控件,发现它是由一个名为border的Border套着一个名为PART_ContentHost的ScrollViewer组成的,如下所示:
为了显示圆角矩形边框,只需要设置最外层Border的圆角弧度即可,更改后的核心代码如下:
<Style x:Key="RoundCornerTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="AllowDrop" Value="true"/>
<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="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True"
CornerRadius="5">
<ScrollViewer x:Name="PART_ContentHost" Focusable="false"
HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" TargetName="border" Value="0.56"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
</MultiTrigger>
</Style.Triggers>
</Style>
这段代码有如下几个看点:
- 作为资源的不是单纯的ControlTemplate而是Style,实际上是把ControlTemplate包含在Style里。使用Style时,如果Value的值比较简单,那就直接用Attribute值来表示,如果Value值不能用一个简单的字符串描述就需要使用XAML的属性对象语法(如TextBox的Template属性)。
- ControlTemplate最终将被应用到一个控件上——称为模板目标控件或模板化控件(Templated Control),ControlTemplate里的控件可以使用TemplateBinding将自己的属性值关联在目标控件的某个属性值上,必要的时候还可以添加Converter(如Background"{TemplateBinding Background)"),TemplateBinding的功能与{Binding RelativeSource={RelativeSource TemplatedParent}}一致。
把设计的圆角Style应用到两个TextBox上,代码如下:
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="248.98" Width="377.551">
<Grid>
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<Button Content="Button" HorizontalAlignment="Left" Margin="139.98,160.753,0,0" VerticalAlignment="Top" Width="75"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="118.449,55.653,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120" Style="{DynamicResource RoundCornerTextBoxStyle}"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="118.449,104.632,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120" Style="{DynamicResource RoundCornerTextBoxStyle}"/>
</Grid>
</Window>
每个控件本身就是一棵UI元素树,WPF的UI元素可以看作两棵树——LogicalTree和VisualTree,这两棵树的交点就是ControlTemplate。
如果把界面上的控件元素看作是一个结点,那元素们构成的就是LogicalTree,如果把控件内部由ControlTemplate生成的控件也算上,那构成的就是VisualrTree。
ItemsControl的PanelTemplate#
ItemsControl具有一个名为ltemsPanel的属性,它的数据类型为ItemsPanelTemplate。
ItemsPanerTemplate 也是一种控件Template,它的作用就是让程序员有机会控制 ltemsControl的条目容器。
ListBox中的条目都是自上而下排列的,代码如下:
<Grid Margin="6">
<ListBox>
<TextBlock Text="Allan"/>
<TextBlock Text="Kevin"/>
<TextBlock Text="Drew"/>
<TextBlock Text="Timothy"/>
</ListBox>
</Grid>
制作一个条目水平排列的ListBox,只需要调整ListBox的ItemsPanel属性,条目就会包装在一个水平排列的StackPanel中从而横向排列,代码如下:
<Grid Margin="6">
<ListBox>
<!--ItemsPanel-->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<!--条目-->
<TextBlock Text="Allan"/>
<TextBlock Text="Kevin"/>
<TextBlock Text="Drew"/>
<TextBlock Text="Timothy"/>
</ListBox>
</Grid>
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控件树的一棵子树。
每个控件都有个名为TemplatedParent的属性,如果它的值不为null,说明这个控件是由Template自动生成的,而属性值就是应用了模板的控件(模板的目标,模板化控件)。如果由Template生成的控件使用了TemplateBinding获取属性值,则TemplateBinding的数据源就是应用了这个模板的目标控件。
回顾一下开头的DataTemplate实例代码:
<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,0"/>
</StackPanel>
</Grid>
</DataTemplate>
这里用到的是普通Binding而不是TemplateBinding,如果把数据对象赋值给ContentPresenter的DataContext属性,由DataTemplate生成的控件自然会找到这个数据对象并把它当作自己的数据源。
DataTemplate与ControlTemplate的应用#
为Template设置其应用目标有两种方法,一种是逐个设置控件的Template、ContentTemplate、ItemsTemplate、CellTemplate等属性,不想应用Template的控件不设置;另一种是整体应用,即把Template应用在某个类型的控件或数据上。
把ControlTemplate应用在所有目标上#
把ControlTemplate应用在所有目标上需要借助Style来实现,但Style不能标记x:Key,如下面的代码:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="133.333" Width="241.458">
<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<!--与前面例子相同-->
</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="fx:Null]"Margin="5"/>
</StackPanel>
</Window>
Style没有x:Key标记,默认为应用到所有由x:Type指定的控件上,如果不想应用则需把控件的Style标记为{x:Null},效果如下所示:
把DataTemplate应用在某个数据类型上#
把DataTemplate应用在某个数据类型上的方法是设置DataTemplate的DataType属性,并且DataTemplate作为资源时也不能带有x:Key标记,如下面的代码:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
Title="MainWindow" Height="500" Width="378.571">
<Window.Resources>
<!--Data Template-->
<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.0"/>
</StackPanel>
</Grid>
</DataTemplate>
<!--数据源-->
<c:ArrayList x:Key="ds">
<local:Unit Year="2001年" Price="100"/>
<local:Unit Year="2002年" Price="120"/>
<local:Unit Year="2003年" Price="140"/>
<local:Unit Year="2004年" Price="160"/>
<local:Unit Year="2005年" Price="180"/>
<local:Unit Year="2006年" Price="200"/>
</c:ArrayList>
</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{StaticResource ds}"/>
<ComboBox ItemsSource="{StaticResource ds}" Margin="5"/>
</StackPanel>
</Window>
代码中DataTemplate的目标数据类型和ListBox的条目类型都是Unit:
public class Unit
{
public int Price { get; set; }
public string Year { get; set; }
}
DataTemplate会自动加载到所有Unit类型对象上,效果如下:
很多时候数据是以XML形式存储的,DataTemplate具有直接把XML数据结点当作目标对象的功能——XML数据中的元素名(标签名)可以作为DataType,元素的子结点和Attribute可以使用XPath来访问。
下面的代码使用XmlDataProvider作为数据源,代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
Title="MainWindow" Height="500" Width="333.035">
<Window.Resources>
<!--Data Template-->
<DataTemplate DataType="Unit">
<Grid>
<StackPanel Orientation="Horizontal">
<Grid>
<Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding XPath=@Price}"/>
<TextBlock Text="{Binding XPath=@Year}"/>
</Grid>
<TextBlock Text="{Binding XPath=@Price}" Margin="5.0"/>
</StackPanel>
</Grid>
</DataTemplate>
<!--数据源-->
<XmlDataProvider x:Key="ds" XPath="Units/Unit">
<x:XData>
<Units xmlns="">
<Unit Year="2001" Price="100"/>
<Unit Year="2001" Price="120"/>
<Unit Year="2001" Price="140"/>
<Unit Year="2001" Price="160"/>
<Unit Year="2001" Price="180"/>
<Unit Year="2001" Price="200"/>
</Units>
</x:XData>
</XmlDataProvider>
</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{Binding Source={StaticResource ds}}"/>
<ComboBox ItemsSource="{Binding Source={StaticResource ds}}" Margin="5"/>
</StackPanel>
</Window>
显示层级数据的模板HierarchicalDataTemplate
XML最大的优势是可以方便地表示带有层级的数据,WPF准备了TreeView和Menultem控件用来显示层级数据,能够帮助层级控件显示层级数据的模板是HierarchicalDataTemplate。
第一个例子是使用TreeView显示多层级、不同类型数据,需要为每种数据设计一个模板,有机会使每种数据类型有自己独特的外观。
数据保存在项目根目录的Data.xml文件中,内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<Data xmlns="">
<Grade Name="一年级">
<Class Name="甲班">
<Group Name="A组"/>
<Group Name="B组"/>
<Group Name="C组"/>
</Class>
<Class Name="乙班">
<Group Name="A组"/>
<Group Name="B组"/>
<Group Name="C组"/>
</Class>
</Grade>
<Grade Name="二年级">
<Class Name="甲班">
<Group Name="A组"/>
<Group Name="B组"/>
<Group Name="C组"/>
</Class>
<Class Name="乙班">
<Group Name="A组"/>
<Group Name="B组"/>
<Group Name="C组"/>
</Class>
</Grade>
</Data>
程序的XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="400" Width="333.035">
<Window.Resources>
<!--数据源-->
<XmlDataProvider x:Key="ds" Source="Data.xml" XPath="Data/Grade"/>
<!--年级模板-->
<HierarchicalDataTemplate DataType="Grade" ItemsSource="{Binding XPath=Class}">
<TextBlock Text="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>
<!--班级模板-->
<HierarchicalDataTemplate DataType="Class" ItemsSource="{Binding XPath=Group}">
<RadioButton Content="{Binding XPath=@Name}" GroupName="gn"/>
</HierarchicalDataTemplate>
<!--小组模板-->
<HierarchicalDataTemplate DataType="Group" ItemsSource="{Binding XPath=Student}">
<CheckBox Content="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Margin="5" ItemsSource="{Binding Source={StaticResource ds}}"/>
</Grid>
</Window>
第二个例子是同一种数据类型的嵌套结构,这种情况下只需设计一个HierarchicalDataTemplate,它会产生自动迭代应用的效果。
数据仍然存放在Data.xml文件中,数据全都是Operation类型:
<?xml version="1.0" encoding="utf-8" ?>
<Data xmlns="">
<Operation Name="文件" Gesture="F">
<Operation Name="新建" Gesture="N">
<Operation Name="项目" Gesture="Control+P"/>
<Operation Name="网站" Gesture="Control+W"/>
<Operation Name="文档" Gesture="Control+D"/>
</Operation>
<Operation Name="保存" Gesture="S"/>
<Operation Name="打印" Gesture="P"/>
<Operation Name="退出" Gesture="X"/>
</Operation>
<Operation Name="编辑" Gesture="E">
<Operation Name="拷贝" Gesture="Control+C"/>
<Operation Name="剪切" Gesture="Control+X"/>
<Operation Name="粘贴" Gesture="Control+V"/>
</Operation>
</Data>
程序的XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="300" Width="300">
<Window.Resources>
<!--数据源-->
<XmlDataProvider x:Key="ds" Source="Data.xml" XPath="Data/Operation"/>
<!--Operation 模板-->
<HierarchicalDataTemplate DataType="Operation" ItemsSource="{Binding XPath=Operation}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding XPath=@Name}" Margin="10,0"/>
<TextBlock Text="{Binding XPath=@Gesture}"/>
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<StackPanel MenuItem.Click="StackPanel_Click">
<Menu ItemsSource="{Binding Source={StaticResource ds}}"/>
</StackPanel>
</Window>
HierarchicalDataTemplate的作用目标是Menultem的Header,可以从被单击Menultem的Header中取出XML数据。事件处理器代码如下:
private void StackPanel_Click(object sender, RoutedEventArgs e)
{
MenuItem mi = e.OriginalSource as MenuItem;
XmlElement xe = mi.Header as XmlElement;
MessageBox.Show(xe.Attributes["Name"].Value);
}
注:可以维护一个CommandHelper类,根据拿到的数据来决定执行什么RoutedCommand。
从外界访问Template内部的控件及其属性值#
由ControlTemplate或DataTemplate生成的控件都是“由Template生成的控件”,ControlTemplate和DataTemplate两个类均派生自FrameworkTemplate类,有个名为FindName的方法可以检索其内部控件。
检索ControlTemplate生成的控件#
设计一个ControlTemplate并把它应用在一个UserControl上,界面上还有一个Button,在它的Click事件处理器中检索由ControlTemplate生成的代码。
程序的XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="160" Width="300">
<Window.Resources>
<ControlTemplate x:Key="cTmp">
<StackPanel Background="Orange">
<TextBox x:Name="textBox1" Margin="6"/>
<TextBox x:Name="textBox2" Margin="6,0"/>
<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>
Button的Click事件处理器代码如下:
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 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="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="175" Width="220">
<Window.Resources>
<!--数据对象-->
<local:Student x:Key="stu" Id="1" Name="Timothy" Skill="WPF" HasJob="True"/>
<!--Data Template-->
<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 Background="Yellow">
<ContentPresenter x:Name="cp" Content="{StaticResource stu}" ContentTemplate="{StaticResource stuDT}" Margin="5"/>
<Button Content="Find" Margin="5,0" Click="Button_Click"/>
</StackPanel>
</Window>
Button的Click事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e)
{
//使用DataTemplate的FindName方法获取由DataTemplate生成的控件并访问其属性
TextBlock tb = this.cp.ContentTemplate.FindName("textBlockName", this.cp) as TextBlock;
MessageBox.Show(tb.Text);
//如果为了获取Student的某个属性,应该使用被注释的代码,直接使用底层数据
//Student stu=this. cp. Content as Student;
// MessageBox.Show(stu.Name);
}
复杂例子
DataTemplate的一个常用之处是GridViewColumn的CellTemplate属性,把GridViewColumn放置在GridView控件里就可以生成表格了。
GridViewColumn的默认CellTemplate是使用TextBlock只读性地显示数据,想让用户能修改数据或者使用CheckBox显示bool类型数据就需要自定义DataTemplate。
准备数据集合、呈现数据的工作全部由XAML代码来完成:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
Title="MainWindow" Height="212.72" Width="280">
<Window.Resources>
<!--数据集合-->
<c:ArrayList x:Key="stuList">
<local:Student Id="1" Name="Timoty Liu" Skill="WPF" HasJob="True"/>
<local:Student Id="2" Name="Tom Chang" Skill="BI/SQL" HasJob="True"/>
<local:Student Id="3" Name="Guan Chong" Skill="Writing" HasJob="False"/>
<local:Student Id="4" Name="Shanshan" Skill="C#/Java" HasJob="False"/>
<local:Student Id="5" Name="Pingping Zhang" Skill="Writing" HasJob="False"/>
<local:Student Id="6" Name="Kenny Tian" Skill="ASP.NET" HasJob="False"/>
</c:ArrayList>
<!--Data Templates-->
<DataTemplate x:Key="nameDT">
<TextBox x:Name="textBoxName" Text="{Binding Name}" GotFocus="TextBoxName_GotFocus"/>
</DataTemplate>
<DataTemplate x:Key="skillDT">
<TextBox x:Name="textBoxSkill" Text="{Binding Skill}"/>
</DataTemplate>
<DataTemplate x:Key="hjDT">
<CheckBox x:Name="checkBoxJob" IsChecked="{Binding HasJob}"/>
</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 hjDT}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
为显示姓名的TextBox添加GotFocus事件的处理器:
private void TextBoxName_GotFocus(object sender, RoutedEventArgs e)
{
//访问业务逻辑数据
TextBox tb = e.OriginalSource as TextBox; //获取事件发起的源头
ContentPresenter cp = tb.TemplatedParent as ContentPresenter; //获取模板目标
Student stu = cp.Content as Student; //获取业务逻辑数据
this.listViewStudent.SelectedItem = stu; //设置ListView的选中项
// 访问界面元素
//获得包装着指定条目数据的容器,本例中是一个包装着Student对象的ListViewItem(Content是Student对象)
ListViewItem lvi = this.listViewStudent.ItemContainerGenerator.ContainerFromItem(stu) as ListViewItem;
//遍历各个结点
CheckBox chb = this.FindVisualChild<CheckBox>(lvi);
MessageBox.Show(chb.Name);
}
private ChildType FindVisualChild<ChildType>(DependencyObject obj) where ChildType: DependencyObject
{
//借助VisualTreeHelper类检索由DataTemplate生成的控件
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;
}
如果真的要寻找由DataTemplate生成的控件,对于结构简单的控件,可以使用DataTemplate对象的FindName方法;对于结构复杂的控件,则需要借助VisualTreeHelper来实现。
Style#
构成Style最重要的两种元素是Setter和Trigger,Setter类帮助我们设置控件的静态外观风格,Trigger类则帮助我们设置控件的行为风格。
Style中的Setter#
Setter,属性值的设置器,给属性赋值的时候一般都采用“属性名=属性值”的形式。Setter类的Property属性用来指明想为目标的哪个属性赋值;Setter类的Value属性则是提供的属性值。
在Window的资源词典中放置一个针对TextBlock的Style,Style中使用若干Setter来设定TextBlock的一些属性,程序中的TextBlock就会具有统一的风格(除非使用{x:Null}显示地清空Style)。
XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="214.26" Width="347.742">
<Window.Resources>
<Style TargetType="TextBlock">
<!--Style的内容属性是Setters,可以直接在<Style>标签的内容区域写Setter-->
<Setter Property="FontSize" Value="24"/>
<Setter Property="TextDecorations" Value="Underline"/>
<Setter Property="FontStyle" Value="Italic"/>
</Style>
</Window.Resources>
<StackPanel Margin="5">
<TextBlock Text="Hello WPF!"/>
<TextBlock Text="This is a sample for Style!"/>
<TextBlock Text="by Tim 2009.12.23" Style="{x:Null}"/>
</StackPanel>
</Window>
注:根如果想设置控件的ControlTemplate,只需要把Setter的Property设为Template并为Value提供一个ControlTemplate对象即可。
Style中的Trigger#
Trigger,触发器,即当某些条件满足时会触发一个行为(比如某些值的变化或动画的发生等),主要类型有:
- 事件触发型的EventTrigger
- 数据变化触发型的Trigger/DataTrigger
- 多条件触发型的MultiTrigger/MultiDataTrigger
基本Trigger#
Trigger类是最基本的触发器。类似于Setter,Trigger也有Property和Value这两个属性,Property是Trigger 关注的属性名称,Value是触发条件。
Trigger类还有一个Setters属性,此属性值是一组Setter,一旦触发条件被满足,这组Setter的“属性一值”就会被应用,触发条件不再满足后,各属性值会被还原。
一个针对CheckBox的Style,当CheckBox的IsChecked属性为true的时候前景色和字体会改变。XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="185.425" Width="300">
<Window.Resources>
<Style TargetType="CheckBox">
<!--Triggers不是Style的内容属性,<Style.Triggers>...</Style.Triggers>这层标签不能省略-->
<Style.Triggers>
<Trigger Property="IsChecked" Value="true">
<!--Trigger的Setters属性是Trigger的内容属性,<Trigger.Setters.…</Trigger.Seters>这层标签可以省略-->
<!--<Trigger.Setters>-->
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="Orange"/>
<!--</Trigger.Setters>-->
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<CheckBox Content="悄悄的我走了" Margin="5"/>
<CheckBox Content="正如我悄悄的来" Margin="5,0"/>
<CheckBox Content="我挥一挥衣袖" Margin="5"/>
<CheckBox Content="不带走一片云彩" Margin="5,0"/>
</StackPanel>
</Window>
MultiTrigger#
MultiTrigger必须多个条件同时成立时才会被触发,比Trigger多了一个Conditions属性,需要同时成立的条件就存储在这个集合中。
改动上面的例子,要求同时满足CheckBox被选中且Content为“正如我悄悄的来”时才会被触发,XAML代码如下(仅Style部分):
<Style TargetType="CheckBox">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="true"/>
<Condition Property="Content" Value="正如我悄悄的来"/>
</MultiTrigger.Conditions>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="Orange"/>
</MultiTrigger>
</Style.Triggers>
</Style>
由数据触发的DataTrigger#
基于数据执行某些判断可以考虑使用DataTrigger,DataTrigger对象的Binding属性会把数据源源不断送过来,一旦送来的值与Value属性一致DataTrigger即被触发。
下面例子中,当TextBox的Text长度小于7个字符时其Border会保持红色,XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="123.435" Width="204.297">
<Window.Resources>
<local:L2BConverter x:Key="cvtr"/>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{ Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text.Length, Converter={ StaticResource cvtr}}" Value="false">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="1"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Margin="5"/>
<TextBox Margin="5,0"/>
<TextBox Margin="5"/>
</StackPanel>
</Window>
为了将控件自己作为数据源需要使用RelativeSource,如果不明确指出Source时Binding会把控件的DataContext属性当作数据源而非把控件自身当作数据源。
Binding的Path被设置为Text.Length,字符串的长度是一个具体的数字,基于这个长度值做判断时需要用到Converter,创建如下的Converter:
public class L2BConverter : IValueConverter
{
public object Convert(object value,Type targetype,object parameter, CultureInfo culture)
{
int textLength = (int)value;
return textLength > 6 ? true : false;
}
public object ConvertBack(object value, Type targetype, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
经Converter转换后,长度值会转换成bool类型值,DataTrigger的Value被设置为false。
多数据条件触发的MultiDataTrigger#
遇到要求多个数据条件同时满足时才能触发变化的需求,此时可以考虑使用MultiDataTrigger。
用户界面上使用ListBox显示了一列Student数据,当Student对象同时满足ID为2、Name为Tom的时候条目就高亮显示,示例的XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="123.435" Width="300">
<Window.Resources>
<Style TargetType="ListBoxItem">
<!--使用Style设置DataTemplate-->
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ID}" Width="60"/>
<TextBlock Text="{Binding Name}" Width="120"/>
<TextBlock Text="{Binding Age}" Width="60"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
<!--MultiDataTrigger-->
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=ID}" Value="2"/>
<Condition Binding="{Binding Path=Name}" Value="Tom"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Background" Value="Orange"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<ListBox x:Name="listBoxStudent" Margin="5"/>
</StackPanel>
</Window>
C#后台代码如下:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<Student> studentList = new List<Student>()
{new Student(){ ID=1, Name="Tim", Age=21},
new Student(){ ID=2, Name="Tom", Age=22 }};
this.listBoxStudent.ItemsSource = studentList;
}
}
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
由事件触发的EventTrigger#
EventTrigger是触发器中最特殊的一个,它是由事件来触发,被触发后执行一段动画,U1层的动画效果往往与EventTrigger相关联。
创建了一个针对Button的Style,这个Style包含两个EventTrigger,一个由MouseEnter事件触发,另一个由MouseLeave事件触发。XAML代码如下:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="300">
<Window.Resources>
<Style TargetType="Button">
<Style.Triggers>
<!--鼠标进入-->
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Width"/>
<DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Height"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<!--鼠标离开-->
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Width"/>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Canvas>
<Button Width="40" Height="40" Content="OK"/>
</Canvas>
</Window>
注:触发器并非只能应用在Style中一—各种Template也可以拥有自己的触发器。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
2020-03-12 简单实用算法— 冒泡排序