Microsoft 將 XAML 定義為 "簡單"、"通用"、"聲明式" 的 "編程語言"。這意味著我們會在更多的地方看到它(比如 Silverlight),而且它顯然比其原始版本 XML (XAML 是一種基於 XML 且遵循 XML 結構規則的語言) 多了更多的邏輯處理手段。如果願意的話,我們完全可以拋開 XAML 來編寫 WPF 程序。只不過這有點類似用記事本開發 .NET 程序的意味,好玩不好用。XAML 的定義模式使得非編程人員可以用 "易懂" 的方式來刻畫 UI,並且這種方式我們早已熟悉,比如 WebForm,亦或者是我一直念念不忘的 Delphi Form (偶爾想起而已,其實早將 Object Pascal 忘得精光了)。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
</Grid>
</Window>
這是一個非常簡單的 XAML,它定義了一個空白 WPF 窗體(Window)。XAML 對應於 .NET 代碼,只不過這個過程由特定的 XAML 編譯器和運行時解釋器完成。當解釋器處理上面這段代碼時,相當於:
new Window1 { Title = "Window1" };
從這裡我們可以體會兩者的區別,用 XAML 的好處是可以在設計階段就能看到最終的展現效果,很顯然這是美工所需要的。你可以從 VS 命令行輸入 "xamlpad.exe",這樣你會看到直觀的效果。
作為一種應用於 .NET 平台的 "語言",XAML 同樣支持很多我們所熟悉也是必須的概念。
1. Namespace
XAML 默認將下列 .NET Namespace 映射到 "http://schemas.microsoft.com/winfx/2006/xaml/presentation":
System.Windows
System.Windows.Automation
System.Windows.Controls
System.Windows.Controls.Primitives
System.Windows.Data
System.Windows.Documents
System.Windows.Forms.Integration
System.Windows.Ink
System.Windows.Input
System.Windows.Media
System.Windows.Media.Animation
System.Windows.Media.Effects
System.Windows.Media.Imaging
System.Windows.Media.Media3D
System.Windows.Media.TextFormatting
System.Windows.Navigation
System.Windows.Shapes
除了這個包含絕大多數 WPF 所需類型的主要命名空間外,還有一個是 XAML 專用的命名空間 (System.Windows.Markup) —— "http://schemas.microsoft.com/winfx/2006/xaml"。使用非默認命名空間的語法有點類似於 C# Namespace Alias, 我們需要添加一個前綴,比如下面示例中的 "x"。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<TextBox x:Name="txtUsername" Background="{x:Null}"></TextBox>
</Grid>
</Window>
我們還可以引入 CLR Namespace。
<collections:Hashtable
xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<sys:Int32 x:Key="key1">1</sys:Int32>
</collections:Hashtable>
2. Property
我們可以用下面兩種方式來設置 XAML 元素的屬性。
方式1
<Label Name="label10" Foreground="Red">Label10</Label>
方式2
<Label Name="label11">
<Label.Content>
Label11
</Label.Content>
<Label.Foreground>
Blue
</Label.Foreground>
</Label>
WPF 會按下列順序將 XAML 中的屬性字符串轉換為實際屬性值。
(1) 屬性值以大括號開始,或者屬性是從 MarkupExtension 派生的元素,則使用標記擴展處理。
(2) 屬性用指定的 TypeConverter 聲明的,或者使用了轉換特性(TypeConverterAttribute),則提交到類型轉換器。
(3) 嘗試基元類型轉換,包括枚舉名稱檢查。
<Trigger Property="Visibility" Value="Collapsed,Hidden">
<Setter ... />
</Trigger>
3. TypeConverter
WPF 提供了大量的類型轉換器,以便將類似下面示例中的 Red 字符串轉換城 SystemWindows.Media.Brushes.Red。
<Label Name="label10" Foreground="Red"></Label>
等價於
this.label10.Foreground = System.Windows.Media.Brushes.Red;
不過下面的代碼更能反應運行期的實際轉換行為
var typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(Brush));
this.label10.Foreground = (Brush)typeConverter.ConvertFromInvariantString("Red");
有關轉換器列表,可參考:ms-help://MS.MSDNQTR.v90.chs/fxref_system/html/35bffd5f-b9aa-1ccd-99fe-b0833551e562.htm
4. MarkupExtension
對 XAML 的一種擴展,以便支持復雜的屬性值。這些標記擴展通常繼承自 MarkupExtension,並使用大括號包含。WPF 提供了一些常用的標記擴展,諸如 NullExtension、StaticExtension、DynamicResourceExtension、StaticResourceExtension、Binding 等。和 Attribute 規則類似,我們通常可以省略 Extension 這個後綴。需要注意的是某些標記擴展屬於 System.Windows.Markup,因此我們需要添加命名空間前綴。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<TextBox Background="{x:Null}"></TextBox>
</Grid>
</Window>
我們可以為標記擴展提供其所需的構造參數。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<Label Content="{x:Static SystemParameters.IconHeight}" />
</Grid>
</Window>
這個例子中,我們將 System.Windows.SystemParameters.IconHeight 值作為參數傳遞給 "public StaticExtension(string member)" 構造方法,這種參數通常被稱作定位參數。而另外一種參數是將特定的值傳給標記擴展對象屬性,語法上必須指定屬性名稱,故被稱之為命名參數。下面的例子表示將 textBox1.Text 參數綁定到 Label.Content 上,這樣當編輯框內容發生變化時,標簽內容自動保持同步。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<TextBox Name="textBox1" />
<Label Content="{Binding ElementName=textBox1, Path=Text}" />
</Grid>
</Window>
標記擴展允許嵌套,並可以引用自身。我們看另外一個例子。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<TextBox Name="textBox2" Width="128" Text="{Binding RelativeSource={RelativeSource Self}, Path=Width}" />
</Grid>
</Window>
這個例子的意思是將 TextBox.Text 內容綁定為其自身(Self)的高度值(Width)。
標記擴展帶來一個問題就是大括號的轉義,畢竟很多時候我們需要在內容顯示中使用它。解決方法是在前面添加一對額外的大括號。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<Label Content="{}{Hello, World!}" />
</Grid>
</Window>
如果覺得難看,也可以寫成下面這樣。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<Label>{Hello, World!}</Label>
</Grid>
</Window>
5. Content
XAML 這點和 HTML 非常類似,我們可以將任何內容添加到元素內容項中,這帶來更加豐富的 UI 表達能力,再也不像 WinForm 那樣 "能做什麼,不能做什麼"。
<Button>
<Hyperlink>Click</Hyperlink>
</Button>
有一點需要注意,內容項並不一定就是 Content。像 ComboBox、ListBox、TabControl 使用 Items 作為內容項。
6. XamlReader & XamlWriter
通常情況下,XAML 在項目編譯時會被壓縮成 BAML (Binary Application Markup Language) 保存到資源文件中。BAML 只是包含 XAML 的純格式聲明,並沒有任何事件之類的執行代碼,切記不要和 MSIL 相混淆。XAML 運行期解釋器解讀 BAML 並生成相應的元素對象。
System.Windows.Markup 命名空間中提供了 XamlReader、XamlWriter 兩個類型,允許我們手工操控 XAML 文件。
var window = (Window)XamlReader.Parse("<Window xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"></Window>");
window.ShowDialog();
當然,我們還可以從文件流中讀取。
using (var stream = new FileStream(@"test.xaml", FileMode.Open))
{
var window = (Window)XamlReader.Load(stream);
var button = (Button)window.FindName("btnOK");
button.Click += (s, ex) => MessageBox.Show("Hello, World!");
window.ShowDialog();
}
test.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid>
<Button x:Name="btnOK">OK</Button>
</Grid>
</Window>
需要注意的是 XamlReader 載入的 XAML 代碼不能包含任何類型(x:Class)以及事件代碼(x:Code)。
我們可以用 XamlWriter 將一個編譯的 BAML 還原成 XAML。
var xaml = XamlWriter.Save(new Window2());
MessageBox.Show(xaml);
輸出:
<Window2
Title="Window2" Width="300" Height="300"
xmlns="clr-namespace:Learn.WPF;assembly=Learn.WPF"
xmlns:av="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<av:Grid>
<av:Button Name="btnOK">OK</av:Button>
</av:Grid>
</Window2>
XAML 的動態載入在使用動態皮膚場景時非常有用,現在只要了解一下即可。