簡介
這是一篇記錄筆者閱讀學習劉鐵猛老師的《深入淺出WPF》的讀書筆記,如果文中內容閱讀不暢,推薦購買正版書籍詳細閱讀。
Template 模板的內涵
WPF系統不但支持傳統的Windows Forms編程的用戶界面和用戶體驗設計,更支持使用專門設計工具Microsoft Expreession Blend進行專業設計,更推出了以模板為核心的新一代設計理念。
程序的本質是算法和數據結構,WPF中作為一種“形式”,它要表現的“內容”就是算法和數據結構,Binding傳遞的是數據,事件參數攜帶的也是數據;方法和委托調用的是算法,事件傳遞消息也是算法······,作為“表現形式”,每個控件都是為了實現某種用戶操作算法和直觀顯示某種數據而生,一個控件看上去是什么樣子由它的“算法內容”和“數據內容”決定,這就是內容決定形式。
- 控件的“算法內容”:指控件能展示哪些數據、具有哪些方法、能響應那些操作、能激發什么事件、簡而言之就是控件的功能,它們是一組相關的算法邏輯。
- 控件的“數據內容”:控件所展示的具體數據是什么。
以往的GUI開發技術(Windows Forms)耦合度過高,控件內部的邏輯和數據是固定的,程序員無法改變,外觀可以操作的空間也較少,造成這個局面的根本原因就是數據和算法的”形式“和”內容“耦合度太緊了。
在WPF中,通過引入Template(模板)將數據和算法的”內容“與“形式”解耦了。WPF中的Template分為兩大類:
- ControlTemplate 是算法內容的表現形式,一個控件怎樣組織其內部結構才能讓它更符合業務邏輯,讓用戶操作起來更舒服就是由它來控制的。它決定了控件”長什么樣子“,並讓程序員有機會在控件原有的內部邏輯基礎上擴展自己的邏輯。
- DataTemplate 是數據內容的表現形式,一條數據顯示成什么樣子,是簡單的文本還是直觀的圖形動畫就是由它來決定。
一言蔽之,Template就是”外衣“——ControlTemplate是控件的外衣,DataTemplate是數據的外衣。
DataTemplate 數據外衣-數據內容的表現形式
DataTemplate常用的地方有3處,分別是:
- ContentControl 的ContentTemplate 屬性,相當於給ContentControl的內容穿衣服。
- ItemsControl 的ItemTemplate屬性,相當於給ItemsControl 的數據條目傳衣服。
- GridViewColumn的CellTemplate屬性,相當於給GridViewColumn單元格里的數據穿衣服。
示例:
需求:有一列汽車數據,這里數據顯示在一個ListBox里,要求ListBox的條目顯示汽車的廠商標志和簡要參數,單擊某個條目后在窗口的詳細內容區域顯示汽車的照片和詳細參數。
- 添加資源文件夾引入對應汽車圖標
- 創建詳細內容窗口的DataTemplate
- 創建ListBox的DataTemplate
- 使用對應模板
- Binding對應數據
<Window x:Class="Template.MainWindow"
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"
xmlns:local="clr-namespace:Template"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<!--Converters-->
<!--創建詳細視圖的數據模板-->
<DataTemplate x:Key="carDetailViewTemplate">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">
<StackPanel Margin="5">
<Image Width="400" Height="250"
Source="G:\VsProject\WPF練習\Template\Resources\Aodi.jpg"/>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="Name:" FontWeight="Bold" FontSize="20"/>
<TextBlock Text="{Binding Name}" FontSize="20" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Automaker:" FontWeight="Bold"/>
<TextBlock Text="{Binding Automaker}" Margin="5,0"/>
<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>
<!--創建ListBox條目的數據模板-->
<DataTemplate x:Key="carListItemViewTemplate">
<Grid Margin="2">
<StackPanel Orientation="Horizontal">
<Image Grid.RowSpan="3" Width="64" Height="64"
Source="G:\VsProject\WPF練習\Template\Resources\Aodi.png"/>
<StackPanel Margin="5,0">
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
<TextBlock Text="{Binding Year}" FontSize="14"/>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<!--窗體內容 使用對應模板-->
<Grid>
<UserControl ContentTemplate="{StaticResource carDetailViewTemplate}"
Content="{Binding SelectedItem,ElementName=listBoxCars}"/>
<ListBox x:Name="listBoxCars" Width="180" Margin="607,0,13,0"
ItemTemplate="{StaticResource carListItemViewTemplate}"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitialCarList();
}
private void InitialCarList()
{
List<Car> carList = new List<Car>()
{
new Car(){Automaker = "Lamborghini",Name = "Diablo", Year = "1990",TopSpeed = "340"},
new Car(){Automaker = "Lamborghini",Name = "Murcielago",Year = "2001",TopSpeed="353"},
new Car(){Automaker = "Lamborghini",Name="Callardo",Year="2003",TopSpeed="325"},
new Car(){Automaker="Lamborghini",Name="Reventon",Year="2008",TopSpeed="356"},
};
this.listBoxCars.ItemsSource = carList;
}
}
ControlTemplate 控件的外衣-算法內容的表現形式
ControlTemplate的兩個作用:
- 通過更換ControlTemplate改變控件外觀,使之具有更優的用戶使用體驗及外觀。
- 借助ControlTemplate,程序員與設計師可以並行工作,程序員可以先用WPF標准控件進行編程,等設計師的工作完成后,只需把新的ControlTemplate應用到程序中就可以了。
示例:
- 文檔大綱-》選中需要設計的控件-》右鍵編輯模板-編輯副本-》設置名稱和位置
- 修改設計需要的模板ControTemplate,TemplateBinding將控件模板中的屬性值關聯到目標控件上,產生的效果就是你為目標控件設置的值以后,控件模板的值也會隨之改變。
- 目標控件使用對應的模板,Style="{DynamicResource RoundCornerTexBoxStyle}
TemplateBinding是為了某個特定場景優化出來的數據綁定版本--需要把ControlTemplate里面的某個Property綁定到應用該ControlTemplate的控件的對應Property上。
中文表達比較拗口,MSDN的原文“Links the value of a property in a control template to be the value of a property on the templated control.”翻譯:將控件模板中屬性的值鏈接為模板化控件上的屬性的值
ItemsControl的PanelTemplate
ItemsControl具有一個名為ItemsPanel的屬性,它的數據類型為ItemsPanelTemplate,也是一種控件Template,可以控制ItemsControl的條目容器。
示例:制作一個橫向排列的ListBox
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<TextBlock Text="菜單"/>
<TextBlock Text="幫助"/>
<TextBlock Text="請求"/>
<TextBlock Text="張三"/>
</ListBox>
DataTemplate與ControlTemplate的關系與應用
DataTemplate與ControlTemplate的關系
控件只是數據和行為的載體、是個抽象的概念,至於它本身長什么樣子(控件的內部結構)、它的數據會長成什么樣子(數據顯示結構)都是靠Template生成的。
- ControlTemplate決定控件的外觀,生成的控件樹的樹根是ControlTemplate的目標控件,此模塊化控件的Template屬性值就是這個ControlTemplate實例。
- DataTemplate決定數據外觀,生成的控件樹的樹根是一個ContentPresenter控件,此模塊化控件的ContentTemplate屬性值就是這個DataTemplate示例。
因為ContentPresenter控件是ControlTemplate控件樹上的一個節點,所以DataTemplate控件樹是ControlTemplate控件樹的一棵子樹。
DataTemplate與ControlTemplate的應用
為Template設置其應用目標有兩種方法:
- 逐個設置控件的Template、ContentTemplate、ItemsTemplate、CellTemplate等屬性,不想應用Template的控件不設置。
- 把Template應用在某個類型的控件或數據上。
使用方法
-
把ControlTemplate應用在所有目標上需要借助Style來實現,但Style不能標記x:Key。Style沒有x:Key標記,默認為應用到所有由x:Type指定的控件上,如果不想應用則需把控件的Style標記為{x:Null}.
-
把DataTemplate應用在某個數據類型上的方法是設置DataTemplate的DataType屬性,並且DataTemplate作為資源時也不能帶有x:Key標記。DataTemplate具有直接把XML數據節點當作目標對象的功能——XML數據中的元素名(標簽名)可以作為DataType,元素的子節點可以使用XPath來訪問
- HierarchicalDataTemplate層級數據模板能夠幫助層級控件顯示數據,例如:TreeView,MenuItem控件。
DataTemplate示例:
<Window.Resources>
<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 Year="2001年" Price="100"/>
<local:Unit Year="2002年" Price="120"/>
<local:Unit Year="2001年" Price="100"/>
<local:Unit Year="2002年" Price="120"/>
<local:Unit Year="2001年" Price="100"/>
</c:ArrayList>
</Window.Resources>
<Grid>
<StackPanel>
<ListBox ItemsSource="{StaticResource ds}"/>
<ComboBox ItemsSource="{StaticResource ds}"/>
</StackPanel>
</Grid>
//C#代碼
public class Unit
{
public int Price { get; set; }
public string Year { get; set; }
}
<!--使用XPath訪問元素的子節點-->
<Window.Resources>
<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"/>
</StackPanel>
</Grid>
</DataTemplate>
<!--數據源-->
<XmlDataProvider x:Key="ds" XPath="Units/Unit">
<x:XData>
<Units xmlns="">
<Unit Year="2001年" Price="100"/>
<Unit Year="2002年" Price="120"/>
<Unit Year="2001年" Price="100"/>
<Unit Year="2002年" Price="120"/>
<Unit Year="2001年" Price="100"/>
</Units>
</x:XData>
</XmlDataProvider>
</Window.Resources>
<Grid>
<StackPanel>
<ListBox ItemsSource="{Binding Source={StaticResource ds}}"/>
<ComboBox ItemsSource="{Binding Source={StaticResource ds}}"/>
</StackPanel>
</Grid>
HierarchicalDataTemplate示例:
<!--TreeView示例-->
<Window.Resources>
<!--數據源-->
<XmlDataProvider x:Key="ds" Source="G:\VsProject\WPF練習\TreeView\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>
<!--TreeView示例-->
<Window.Resources>
<!--數據源-->
<XmlDataProvider x:Key="ds" Source="G:\VsProject\WPF練習\Menu\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>
<Grid>
<StackPanel>
<Menu ItemsSource="{Binding Source={StaticResource ds}}"/>
</StackPanel>
</Grid>
Style樣式
Style簡單來說,就是一種對屬性值的批處理,類似於Html的CSS,可以快速的設置一系列屬性值到UI元素。
Style最重要的兩個元素是Setter和Trigger,Setter類設置控件的靜態外觀風格,Trigger類設置控件的行為風格。
Style和Template就如同化妝和整容,Style可以為某類控件設置統一的樣式,如果不想使用該樣式使用{x:Null}就可清空Style.
Setter設置器
Setter設置器的兩個重要元素是Property和Value,Property屬性用來指明你想為那個目標的那個屬性賦值;Value屬性則是你提供的屬性值。
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="24"/>
<Setter Property="TextDecorations" Value="Underline"/>
<Setter Property="FontStyle" Value="Italic"/>
</Style>
</Window.Resources>
<StackPanel Margin="5">
<TextBlock Text="你好"/>
<TextBlock Text="這是設置好的樣式"/>
<TextBlock Text="沒有風格" Style="{x:Null}"/>
</StackPanel>
Trigger觸發器
Trigger,觸發器,即當某些條件滿足時會觸發一個行為(比如某些值的變化或動畫的發生等)。觸發器比較像事件。事件一般是由用戶操作觸發的,而觸發器除了由事件觸發的EventTrigger外還有數據變化觸發型的Trigger、DataTrigger及多條件觸發型的MultiTrigger、MultiDataTrigger等。
基本Trigger
Trigger類是最基本的觸發器,類似於Setter,Trigger也有Property和Value這兩個屬性,Property是Trigger關注的屬性名稱,Value是觸發條件。Trigger還有一個Setters屬性,此屬性值是一組Setter,一旦觸發條件被滿足,這組Seteer的“屬性-值”就會被應用,觸發條件不再滿足后,各屬性值會被還原。
示例:
CheckBox的Style,當IsChecked屬性為true時,前景色和字體變化。
<Window.Resources>
<Style TargetType="CheckBox">
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Trigger.Setters>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="Orange"/>
</Trigger.Setters>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<CheckBox Content="悄悄的我走了" Margin="5"/>
<CheckBox Content="悄悄的我走了" Margin="5"/>
<CheckBox Content="悄悄的我走了" Margin="5"/>
<CheckBox Content="悄悄的我走了" Margin="5"/>
<CheckBox Content="悄悄的我走了" Margin="5"/>
</StackPanel>
</Grid>
MultiTrigger
MultiTrigger必須多個條件同時成立才會被觸發,MultiTrigger比Trigger多了一個Conditions屬性,需要同時成立的條件就存儲在這個集合中。
示例:同時滿足CheckBox被選中且選中為“吃飯”時才會被觸發。
<Window.Resources>
<Style TargetType="CheckBox">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="true" />
<Condition Property="Content" Value="吃飯"/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="Orange"/>
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<CheckBox Content="吃飯"/>
<CheckBox Content="睡覺"/>
<CheckBox Content="打豆豆"/>
<CheckBox Content="用四川話說"/>
</StackPanel>
</Grid>
由數據觸發DataTrigger
DataTrigger,基於數據執行某些判斷,DataTrigger對象的Binding屬性會源源不斷送過來,一旦送過來的值與Value屬性一致,DataTrigger即被觸發。
示例:當TextBox的Text長度小於7個字符時其Border會保持紅色
<Window.Resources>
<local:L2BConverter x:Key="cvtr"/>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Value="false"
Binding="{Binding RelativeSource={x:Static RelativeSource.Self},Path=Text.Length,Converter={StaticResource cvtr}}">
<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>
這個例子中唯一需要解釋的就是DataTrigger的Binding,為了將自己作為數據源,使用了RelativeSource,初學者經常認為“不明確指出Source的值Binding就會將控件自己作為數據的來源”,這是錯誤的,因為不明確指出Source時Binding會把控件的DataContext屬性當作數據源而非把控件自身當作數據源。Binding的Path被設置為Text.Lenght,即我們關注的是字符串的長度,長度是一個具體的數字,如何基於這個長度值做判斷呢?這就用到了Converter。我們創建如下的Converter:
class L2BConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int textLenght = (int)value;
return textLenght > 6 ? true : false;
}
public object ConvertBack(object value,Type targetType,object parmeter,CultureInfo culture)
{
throw new NotImplementedException();
}
}
多條數據條件觸發的MultiDataTrigger
示例:用戶界面上使用ListBox顯示了一列Student數據,當Student對象同時滿足ID為2、Name為張三的時候,條目就會高亮顯示。
<Window.Resources>
<Style TargetType="ListBoxItem">
<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>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=ID}" Value="2"/>
<Condition Binding="{Binding Path=Name}" Value="張三"/>
</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>
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public List<Student> Students { get; set; }
}
public MainWindow()
{
InitializeComponent();
Student st = new Student();
List<Student> students = new List<Student>();
students.Add(new Student() { ID = 1, Name = "張三", Age = 20 });
students.Add(new Student() { ID = 2, Name = "張三", Age = 20 });
students.Add(new Student() { ID = 3, Name = "張三", Age = 20 });
listBoxStudent.Items.Clear();
listBoxStudent.ItemsSource = students;
}
由事件觸發的EventTrigger
EventTrigger是觸發器中最特殊的一個。首先,它不是由屬性值或數據的變化來觸發而是由事件來觸發;其次被觸發后它並非應用一組Setter,而是執行一段動畫。因此UI層的動畫效果往往與EventTrigger相關聯。
示例:創建一個針對Button的Style,這個Style包含兩個EventTrigger,一個由MouseEnter事件觸發,另外一個由MouseLeave事件觸發。
<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>