WPF模板(一)詳細介紹
本次隨筆來源於電子書,人家的講解很好,我就不畫蛇添足了。
圖形使用者介面應用程式較之控制檯介面應用程式最大的好處就是介面友好、資料顯示直觀。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)是資料內容表現形式的雙過載體。換句話說,控制元件即是資料的表現形式讓使用者可以直觀的看到資料,又是演算法的表現形式讓使用者方便的操作邏輯。
作為表現形式,每個控制元件都是為了實現某種使用者操作演算法和直觀顯示某種資料而生,一個控制元件看上去是什麼樣子由它的“演算法內容”和“資料內容決定”,這就是內容決定形式,這裡,我們引入兩個概念:
- 控制元件的演算法內容:值控制元件能展示哪些資料、具有哪些方法、能相應哪些操作、能激發什麼事件,簡而言之就是控制元件的功能,它們是一組相關的演算法邏輯。
- 控制元件的資料內容:控制元件具體展示的資料是什麼。
- ControlTemplate:是演算法和內容的表現形式,一個控制元件怎麼組織其內部結構才能讓它更符合業務邏輯、讓使用者操作起來更舒服就是由它來控制的。它決定了控制元件“長成什麼樣子”,並讓程式設計師有機會在控制元件原有的內部邏輯基礎上擴充套件自己的邏輯。
- 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>
- ContentControl的ContentTempldate屬性,相當於給ContentControl的內容穿衣服。
- ItemControl的ItemTemplate,相當於給ItemControl的資料條目穿衣服。
- GridViewColumn的CellTempldate屬性,相當於給GridViewColumn的資料條目穿衣服。
- 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));
- }
- }
- }
- <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}標記擴充套件來訪問。
- //廠商名稱轉換為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>