1. 程式人生 > >WPF模板(一)詳細介紹

WPF模板(一)詳細介紹

原文: 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)是資料內容表現形式的雙過載體。換句話說,控制元件即是資料的表現形式讓使用者可以直觀的看到資料,又是演算法的表現形式讓使用者方便的操作邏輯。

作為表現形式,每個控制元件都是為了實現某種使用者操作演算法和直觀顯示某種資料而生,一個控制元件看上去是什麼樣子由它的“演算法內容”和“資料內容決定”,這就是內容決定形式,這裡,我們引入兩個概念:

 

  • 控制元件的演算法內容:值控制元件能展示哪些資料、具有哪些方法、能相應哪些操作、能激發什麼事件,簡而言之就是控制元件的功能,它們是一組相關的演算法邏輯。
  • 控制元件的資料內容:控制元件具體展示的資料是什麼。
以往的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程式碼發給他們。其程式碼如下:
  1. <DataTemplate>  
  2.      <Grid>  
  3.          <StackPanel Orientation="Horizontal">  
  4.              <Grid>  
  5.                  <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"></Rectangle>  
  6.                  <TextBlock Text="{Binding Year}"></TextBlock>  
  7.              </Grid>  
  8.              <TextBlock Text="{Binding Price}" Margin="5,0"></TextBlock>  
  9.          </StackPanel>  
  10.      </Grid>  
  11.  </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資料型別:  
  1. public class Car  
  2.     {  
  3.         public string AutoMark { get; set; }  
  4.         public string Name { get; set; }  
  5.         public string Year { get; set; }  
  6.         public string TopSpeed { get; set; }  
  7.     }  

為了在ListBox裡面顯示Car型別的資料,我們需要準備一個UserControl。命名為CarListItemView。 這個UserControl由一個Car型別例項在背後支援,當設定這個例項的時候,介面元素將例項的屬性值顯示在各個控制元件裡。CarListItemView的XAML程式碼如下:  
  1. <UserControl x:Class="WpfApplication1.CarListViewItem"  
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">  
  4.     <Grid Margin="2">  
  5.         <StackPanel Orientation="Horizontal">  
  6.             <Image x:Name="igLogo" Grid.RowSpan="3" Width="64" Height="64"></Image>  
  7.             <StackPanel Margin="5,10">  
  8.                 <TextBlock x:Name="txtBlockName" FontSize="16" FontWeight="Bold"></TextBlock>  
  9.                 <TextBlock x:Name="txtBlockYear" FontSize="14"></TextBlock>  
  10.             </StackPanel>  
  11.         </StackPanel>  
  12.     </Grid>  
  13. </UserControl>  


CarlistItemView用於支援前臺顯示屬性C#程式碼為:
  1. /// <summary>  
  2. /// CarListViewItem.xaml 的互動邏輯  
  3. /// </summary>  
  4. public partial class CarListViewItem : UserControl  
  5. {  
  6.     public CarListViewItem()  
  7.     {  
  8.         InitializeComponent();  
  9.     }  
  10.   
  11.     private Car car;  
  12.   
  13.     public Car Car  
  14.     {  
  15.         get { return car; }  
  16.         set  
  17.         {  
  18.             car = value;  
  19.             this.txtBlockName.Text = car.Name;  
  20.             this.txtBlockYear.Text = car.Year;  
  21.             this.igLogo.Source = new BitmapImage(new Uri(@"Resource/Image/"+car.AutoMark+".png",UriKind.Relative));  
  22.         }  
  23.     }  
  24. }  
類似的原理,我們需要為Car型別準備一個詳細資訊檢視。UserControl名稱為CarDetailView,XAML部分程式碼如下:  
  1. <UserControl x:Class="WpfApplication1.CarDetailView"  
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">  
  4.     <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">  
  5.         <StackPanel>  
  6.             <Image x:Name="imgPhoto" Width="400" Height="250"></Image>  
  7.             <StackPanel Orientation="Horizontal" Margin="5,0">  
  8.                 <TextBlock Text="Name:" FontSize="20" FontWeight="Bold"></TextBlock>  
  9.                 <TextBlock x:Name="txtBlockName" FontSize="20" Margin="5,0"></TextBlock>  
  10.             </StackPanel>  
  11.             <StackPanel Orientation="Horizontal" Margin="5,0">  
  12.                 <TextBlock Text="AutoMark:" FontWeight="Bold"></TextBlock>  
  13.                 <TextBlock x:Name="txtBlockAutoMark" Margin="5,0"></TextBlock>  
  14.                 <TextBlock Text="Year:" FontWeight="Bold">  
  15.                       
  16.                 </TextBlock>  
  17.                     <TextBlock x:Name="txtBlockYear" Margin="5,0">  
  18.   
  19.                 </TextBlock>  
  20.                 <TextBlock Text="Top Speed:" FontWeight="Bold">  
  21.                       
  22.                 </TextBlock>  
  23.                 <TextBlock x:Name="txtTopSpeed" Margin="5,0">  
  24.                       
  25.                 </TextBlock>  
  26.             </StackPanel>  
  27.         </StackPanel>  
  28.     </Border>  
  29. </UserControl>  
後臺支援資料大同小異:  
  1. /// <summary>  
  2. /// CarDetailView.xaml 的互動邏輯  
  3. /// </summary>  
  4. public partial class CarDetailView : UserControl  
  5. {  
  6.     public CarDetailView()  
  7.     {  
  8.         InitializeComponent();  
  9.     }  
  10.   
  11.     private Car car;  
  12.   
  13.     public Car Car  
  14.     {  
  15.         get { return car; }  
  16.         set  
  17.         {  
  18.             car = value;  
  19.             this.txtBlockName.Text = car.Name;  
  20.             this.txtBlockAutoMark.Text = car.AutoMark;  
  21.             this.txtBlockYear.Text = car.Year;  
  22.             this.txtTopSpeed.Text = car.TopSpeed;  
  23.             this.imgPhoto.Source = new BitmapImage(new Uri(@"Resource/Image/" + car.Name + ".jpg", UriKind.Relative));  
  24.         }  
  25.     }  
  26. }  
最後把它們組裝到窗體上:  
  1. <Window x:Class="WpfApplication1.Window35"  
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.         Title="Window35" Height="350" Width="623" xmlns:my="clr-namespace:WpfApplication1">  
  5.     <StackPanel Orientation="Horizontal" Margin="5">  
  6.         <my:CarDetailView x:Name="carDetailView1" />  
  7.         <ListBox x:Name="listBoxCars" Width="180" Margin="5,0" SelectionChanged="listBoxCars_SelectionChanged">  
  8.               
  9.         </ListBox>  
  10.     </StackPanel>  
  11. </Window>  
窗體的後臺程式碼如下:  
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Windows;  
  6. using System.Windows.Controls;  
  7. using System.Windows.Data;  
  8. using System.Windows.Documents;  
  9. using System.Windows.Input;  
  10. using System.Windows.Media;  
  11. using System.Windows.Media.Imaging;  
  12. using System.Windows.Shapes;  
  13.   
  14. namespace WpfApplication1  
  15. {  
  16.     /// <summary>  
  17.     /// Window35.xaml 的互動邏輯  
  18.     /// </summary>  
  19.     public partial class Window35 : Window  
  20.     {  
  21.         public Window35()  
  22.         {  
  23.             InitializeComponent();  
  24.             InitialCarList();  
  25.         }  
  26.   
  27.         private void listBoxCars_SelectionChanged(object sender, SelectionChangedEventArgs e)  
  28.         {  
  29.             CarListViewItem viewItem = e.AddedItems[0] as CarListViewItem;  
  30.             if(viewItem!=null)  
  31.             {  
  32.                 carDetailView1.Car = viewItem.Car;  
  33.             }  
  34.         }  
  35.   
  36.         private void InitialCarList()  
  37.         {  
  38.             List<Car> infos = new List<Car>() {   
  39.             new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="200", Year="1990"},  
  40.             new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="250", Year="1998"},  
  41.             new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="300", Year="2002"},  
  42.             new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="350", Year="2011"},  
  43.             new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="500", Year="2020"}  
  44.             };  
  45.             foreach (Car item in infos)  
  46.             {  
  47.                 CarListViewItem viewItem = new CarListViewItem();  
  48.                 viewItem.Car = item;  
  49.                 this.listBoxCars.Items.Add(viewItem);  
  50.             }  
  51.         }  
  52.     }  
  53.   
  54.     public class Car  
  55.     {  
  56.         public string AutoMark { get; set; }  
  57.         public string Name { get; set; }  
  58.         public string Year { get; set; }  
  59.         public string TopSpeed { get; set; }  
  60.     }  
  61. }  

執行並單擊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程式碼如下:  
  1. //廠商名稱轉換為Logo路徑  
  2.    public class AutoMarkToLogoPathConverter:IValueConverter  
  3.    {  
  4.        /// <summary>  
  5.        /// 正向轉  
  6.        /// </summary>  
  7.        /// <param name="value"></param>  
  8.        /// <param name="targetType"></param>  
  9.        /// <param name="parameter"></param>  
  10.        /// <param name="culture"></param>  
  11.        /// <returns></returns>  
  12.        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
  13.        {  
  14.            return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.png",(string)value),UriKind.Relative));  
  15.        }  
  16.        /// <summary>  
  17.        /// 逆向轉未用到  
  18.        /// </summary>  
  19.        /// <param name="value"></param>  
  20.        /// <param name="targetType"></param>  
  21.        /// <param name="parameter"></param>  
  22.        /// <param name="culture"></param>  
  23.        /// <returns></returns>  
  24.        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
  25.        {  
  26.            throw new NotImplementedException();  
  27.        }  
  28.    }  
 
  1. //汽車名稱轉換為照片路徑  
  2.     public class NameToPhotoPathConverter:IValueConverter  
  3.     {  
  4.         /// <summary>  
  5.         /// 正向轉  
  6.         /// </summary>  
  7.         /// <param name="value"></param>  
  8.         /// <param name="targetType"></param>  
  9.         /// <param name="parameter"></param>  
  10.         /// <param name="culture"></param>  
  11.         /// <returns></returns>  
  12.         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
  13.         {  
  14.             return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.jpg", (string)value), UriKind.Relative));  
  15.         }  
  16.         /// <summary>  
  17.         /// 逆向轉未用到  
  18.         /// </summary>  
  19.         /// <param name="value"></param>  
  20.         /// <param name="targetType"></param>  
  21.         /// <param name="parameter"></param>  
  22.         /// <param name="culture"></param>  
  23.         /// <returns></returns>  
  24.         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)  
  25.         {  
  26.             throw new NotImplementedException();  
  27.         }  
  28.     }  

有了這兩個Converter之後我們就可以設計DataTemplate了,完整的XAML程式碼如下:  
  1. <Window x:Class="WpfApplication1.Window36"  
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.         xmlns:local="clr-namespace:WpfApplication1.Model"  
  5.         Title="Window36" Height="350" Width="623">  
  6.     <Window.Resources>  
  7.         <!--Converter-->  
  8.         <local:AutoMarkToLogoPathConverter x:Key="amp"/>  
  9.         <local:NameToPhotoPathConverter x:Key="npp"/>  
  10.         <!--DataTemplate For DatialView-->  
  11.         <DataTemplate x:Key="DatialViewTemplate">  
  12.             <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">  
  13.                 <StackPanel>  
  14.                     <Image x:Name="imgPhoto" Width="400" Height="250" Source="{Binding AutoMark,Converter={StaticResource npp}}"></Image>  
  15.                     <StackPanel Orientation="Horizontal" Margin="5,0">  
  16.                         <TextBlock Text="Name:" FontSize="20" FontWeight="Bold"></TextBlock>  
  17.                         <TextBlock FontSize="20" Margin="5,0" Text="{Binding Name}"></TextBlock>  
  18.                     </StackPanel>  
  19.                     <StackPanel Orientation="Horizontal" Margin="5,0">  
  20.                         <TextBlock Text="AutoMark:" FontWeight="Bold"></TextBlock>  
  21.