雖然 Windows Presentation Foundation 中的控件模型非常多,但仍不可能提供需要的每一種控件。這時候,控件編寫就派上用場了。在本文中,我將向您講述如何使用 Windows® Presentation Foundation 自定義現有控件,以及如何為您的項目創建全新的控件(或元素)。
在開發一個自定義控件之前,應該先問問自己是否真的需要它。在 Windows Presentation Foundation 中,組合、樣式和模板化功能使您可以自定義現有控件,這是以前的技術所無法比擬的。在決定創建新控件之前,我們先快速講述一下上述三種自定義控件的方法。
使用組合
創建組合控件是常見要求。所謂組合控件是指由一個以上控件組成的控件。假定您有一個用於啟動視頻播放的 Play 按鈕。XAML 和控件如圖 1 所示。
Figure 1 簡單的 Play 控件
<StackPanel> <Button Height=”50” Width=”50” Content=”Play” /> <Polygon HorizontalAlignment=”Center” Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel>
您需要能夠得到 play 圖標並將它放置在該按鈕上。您可以使用組合將 XAML 元素實際嵌入其他 XAML 元素內。例如,您可以通過更改 XAML 來創建標簽和圖標(作為該按鈕的內容)。將這些元素放置在該按鈕內的容器(此處為 StackPanel)中,這樣可將它們分配給 Button 類的 Content 屬性,如圖 2 中所示。這樣得到的按鈕會象任何其他按鈕一樣正常工作,但它裡面卻有您自己的內容。
Figure 2 按鈕中的所有內容
<StackPanel> <Button Height=”50” Width=”50”> <StackPanel> <TextBlock>Play</TextBlock> <Polygon Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel> </Button> </StackPanel>
使用組合來創建此類控件非常簡單。與 Windows Forms、Visual Basic® 6.0 和 MFC 等演示技術中的控件不同,大多數組合控件都是其他控件的容器。當您真正需要的只是一個組合控件時,就不必編寫自定義控件。
使用樣式
如果您需要的只是改變控件的外觀,怎麼辦?使用樣式就可以解決問題。通過創建一個類似以下樣式的樣式,您可以指定一個帶有紅色邊框的按鈕樣式。
<StackPanel> <StackPanel.Resources> <Style TargetType=”Button” x:Key=”RedButton”> <Setter Property=”BorderBrush” Value=”Red” /> </Style> </StackPanel.Resources> ... </StackPanel>
現在,您可以通過為特定的按鈕分配樣式來更改它們的邊框,如圖 3 所示。第一個按鈕是標准外觀,而第二個按鈕則將自己和共享樣式綁定在一起。
Figure3將樣式應用到按鈕
圖 3
<Button Height=”50” Width=”50”> <StackPanel> <TextBlock>Play</TextBlock> <Polygon Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel> </Button> <Button Height=”50” Width=”50” Style=”{StaticResource RedButton}”> <StackPanel> <TextBlock>Play</TextBlock> <Polygon Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel> </Button>
您甚至可以使用樣式來更改某容器內特定類型 XAML 元素的所有實例的外觀。例如,您可以創建一個指定所有按鈕外觀的樣式,而不用創建一個可重用樣式來更改按鈕,如圖 4 中所示。此示例將所有按鈕的背景設置為灰色/綠色/漸進灰色。此示例中的樣式忽略了樣式的 Key 值。因此,這會影響 TargetType 屬性中指定的所有元素。
Figure 4 將樣式應用於整個容器
圖 4
<StackPanel> <StackPanel.Resources> <Style TargetType=”Button”> <Setter Property=”Background”> <Setter.Value> <LinearGradientBrush> <GradientStop Color=”#DDDDDD” Offset=”0” /> <GradientStop Color=”#88FF88” Offset=”.6” /> <GradientStop Color=”#EEEEEE” Offset=”1” /> </LinearGradientBrush> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <Button Height=”50” Width=”50”> <StackPanel> <TextBlock>Play</TextBlock> <Polygon Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel> </Button> <Button Height=”50” Width=”50”> <StackPanel> <TextBlock>Play</TextBlock> <Polygon Points=”0,0 0,26 17,13” Fill=”Black” /> </StackPanel> </Button> </StackPanel>
使用模板
樣式僅限於設置 XAML 元素上的默認屬性。例如,當我在前面的示例中設置 BorderBrush 時,我可以指定畫筆,而不能指定邊框的寬度。若要完全自由地定義控件的外觀,您需要使用模板。為此,可以創建樣式並指定 Template 屬性(參見圖 5)。Template 屬性的 Value 成為指定如何編寫控件本身的 ControlTemplate 元素。在本示例中,我指定了一個圓中心有 play 圖標的按鈕。為此,我將 play 圖標覆在了 Ellipse 元素上。新的模板按鈕顯示在普通按鈕旁邊。
Figure 5 使用模板
圖 5
<StackPanel> <StackPanel.Resources> <Style TargetType=”{x:Type Button}” x:Key=”PlayButton” > <Setter Property=”Template”> <Setter.Value> <ControlTemplate TargetType=”{x:Type Button}”> <Grid> <Ellipse Width=”{TemplateBinding Width}” Height=”{TemplateBinding Height}” Stroke=”DarkGray” VerticalAlignment=”Top” HorizontalAlignment=”Left” Fill=”LightGray” /> <Polygon Points=”18,12 18,38 35,25” Fill=”Black” /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <Button Height=”50” Width=”50”>Normal Button</Button> <Button Height=”50” Width=”50” Style=”{StaticResource PlayButton}” /> </StackPanel>
總之,樣式和模板仍只允許您更改控件的外觀。若要為按鈕添加行為和其他功能,則需要創建自定義控件。
編寫控件
在編寫自己的控件之前,應該采取的第一步是決定創建控件所使用的方法。在 Windows Presentation Foundation 中創建控件主要有兩種方法:用戶控件和自定義控件。兩種方法各有各的好處。
使用用戶控件,您可以得到類似於 Windows Presentation Foundation 應用程序開發的簡單開發模型。如果您需要根據現有組件編寫控件,而不必進行復雜的自定義(像使用模板和樣式那樣),則應使用用戶控件。如果希望完全控制外觀、需要特殊的顯示支持,或希望您的控件成為其他控件的容器,那麼最好應使用自定義控件。
如果無法決定選擇哪類控件,請使用用戶控件。如果您使用用戶控件時遇到功能障礙,那麼隨後也可以比較輕松地切換到自定義控件。
創建用戶控件時,要做的第一件事情是將新的項添加到項目中。右鍵單擊您的項目並單擊“添加”,您可能希望從上下文菜單中選擇“用戶控件”選項。可惜,此操作只能創建新的 Windows Forms 用戶控件。因此,請選擇“添加新項”選項。在“添加新項”對話框中,選擇“用戶控件 (WPF)”項。
創建新的用戶控件會創建新的 XAML 文件和備份代碼文件。該 XAML 文件類似於隨新的 Windows Presentation Foundation 項目創建的主文件,不同之處在於新的 XAML 文件的根元素是 UserControl 元素。在 UserControl 元素內,您要創建組成您的控件的內容。
對此示例而言,繼續使用前面用過的同一 XAML 來為 PlayButton 控件創建模板。此新控件會將自己綁定到 MediaElement,以便對某些數字媒體的播放或暫停進行控制。圖 6 顯示 PlayButton 的 XAML。
Figure 6 PlayButton XAML
圖 7 WPF 窗口中的 PlayButton
<!-- PlayButton.xaml --> <UserControl x:Class=”CustomWPF.PlayButton” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”> <Grid> <Ellipse Width=”50” Height=”50” Stroke=”DarkGray” VerticalAlignment=”Top” HorizontalAlignment=”Left” Name=”ButtonBack” Fill=”LightGray” /> <Path Name=”PlayIcon” Fill=”Black” Data=”M18,12 18,38 35,25”/> <Path Name=”PauseIcon” Fill=”Black” Opacity=”0” Data=”M15,12 15,38 23,38 23,12z M27,12 27,38 35,38 35,12” /> </Grid> </UserControl>
從模板示例中,我已經添加了一個新的 Path (PauseIcon)。由於用於暫停媒體的圖標不能表示為 Polygon,而將 PlayIcon 更改為 Path 會更容易一些,這樣您就可以將每個圖標作為代碼隱藏中的 Path 對象進行處理。我希望能夠通過以下操作來控制 MediaElement 元素:在單擊該按鈕時暫停或播放媒體,以及在單擊按鈕時圖標發生相應變化從而正確表示相應的操作(暫停或播放)。
在添加該邏輯之前,先確保我的按鈕顯示正確而且可以顯示在窗口中。在此情況下,我希望將此新控件顯示在窗口上。在 XAML 文檔中使用任何自定義元素(用戶控件或自定義控件)之前,必須先創建對它的引用。如果該自定義元素與 XAML 的其余自定義元素位於同一項目中,那麼只需添加一個 XML 命名空間聲明即可引用它。在下列幾行代碼中,我創建了命名空間聲明 (xmlns:cust),該聲明用於指定控件所在的公共語言運行庫 (CLR) 命名空間。
<!-- MainWindow.xaml --> <Window x:Class=”Tester.MainWindow” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:cust=”clr-namespace:CustomWPF“ Title=”Control Viewer” Height=”100” Width=”200”> <!-- ... --> </Window>
XML 命名空間聲明中指定的 clr-namespace (CustomWPF) 與該控件的實際 CLR 命名空間 (CustomWPF) 相匹配。如果您要使用的控件位於另一個程序集中,則必須在該命名空間聲明中注明該程序集的名稱。XML 命名空間聲明不會自動將該程序集導入您的項目中,您還必須手動將對該程序集的引用添加到您的項目中。
<!-- MainWindow.xaml --> <Window x:Class=”Tester.MainWindow” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF” Title=”Control Viewer” Height=”100” Width=”200”> <!-- ... --> </Window>
一旦建立了對該 XML 命名空間的引用,您就可以使用它來創建新用戶控件的實例。為此,您需要使用命名空間聲明的名稱。這裡指的是 XML 命名空間別名,不是 CLR 命名空間。您應在 XML 命名空間別名之後指定控件的實際名稱。這樣可確保 XAML 文件中使用的名稱與類名稱相匹配。若要將 PlayButton 類的實例添加到 XAML,您需要將 cust:PlayButton 指定為元素名稱。
<StackPanel> <TextBlock HorizontalAlignment=”Center”>User Control:</TextBlock> <cust:PlayButton /> </StackPanel>
現在,您可以看到 PlayButton 控件已駐留在典型的 Windows Presentation Foundation 窗口中,如圖 7 所示。
自定義屬性
在編寫控件時,您會發現需要通過完善某些屬性來控制控件的外觀及其運行時行為。例如,PlayButton 控件需要獲得和設置圖標顏色的能力。為此,您可以創建一個簡單的 CLR 屬性,如圖 8 所示(請注意,您可在本文的下載內容中下載 Visual Basic 示例代碼)。
Figure 8 獲取 Icon 的顏色
// PlayButton.xaml.cs public partial class PlayButton : System.Windows.Controls.UserControl { // ... Brush _iconColor = Brushes.Black; public Brush IconColor { get {return _iconColor; } set { _iconColor = value; PlayIcon.Fill = _iconColor; PauseIcon.Fill = _iconColor; } } }
簡單的 CLR 屬性(如 IconColor 屬性)足夠用了。只需使用屬性名稱即可在 XAML 中對其進行設置。
<cust:PlayButton IconColor=”Black” />
不過,簡單的屬性對大多數控件而言都不夠,因為它們不支持高級功能,如數據綁定或動畫支持。例如,如果您希望通過數據綁定將控件的 IconColor 指定為 XAML 中矩形的填充顏色,那麼簡單的屬性無法做到這一點,如圖 9 所示。
Figure 9 此數據綁定不起作用
<!-- MainWindow.xaml --> <Window x:Class=”Tester.MainWindow” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml” xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF” Title=”Control Viewer” Height=”100” Width=”200”> <StackPanel> <Rectangle Name=”theRect” Fill=”Red” /> <TextBlock>User Control:</TextBlock> <!-- Simple Assignment Works --> <cust:PlayButton IconColor=”Blue” /> <!—Data Binding Does Not --> <cust:PlayButton IconColor=”{Binding ElementName=theRect, Path=Fill}” /> </StackPanel> </Window>
若要利用所有可用的功能,您必須使用 Dependency 屬性而不是簡單的 CLR 屬性。Dependency 屬性允許以各種方式設置元素的值,包括動畫和數據綁定。若要通過一種方法設置元素的值,可以通過調用 DependencyProperty.Register 方法在控件上創建一個靜態(在 Visual Basic 中為共享)DependencyProperty 字段。此方法會注冊您的屬性,並返回所創建的 DependencyProperty 的實例。
創建 DependencyProperty 字段後,您可以使用它通過調用控件的 GetValue/SetValue 方法來設置和獲取該屬性。例如,可以將 PlayButton 的 IconColor 屬性更改為 DependencyProperty,如圖 10 所示。
請注意,我從該類中刪除了 Brush 字段。DependencyProperty 會存儲每個實例的值以及該屬性的元數據。這意味著 PlayButton 的每個實例不需要有其自己的字段來存儲有關屬性的數據。
現在,您已有了 DependencyProperty,可以將它用於數據綁定和動畫。目前,還沒有一種好的方法來判斷屬性是否已更改,或者是否有默認值。由於屬性值的設置沒有經過公共屬性(CLR 屬性是 DependencyProperty 的包裝器,而不是相反),所以當該值發生更改時,您不能只使用 set 訪問器來更改圖標顏色。而需要添加一個在屬性更改時調用的事件。為此,在注冊 DependencyProperty 時要指定屬性更改時回調的靜態或共享方法。您可以修正注冊,以便使其包括用於指定默認值和更改回調的 FrameworkPropertyMetadata 對象,如下所示:
public static readonly DependencyProperty IconColorProperty = DependencyProperty.Register( “IconColor”, typeof(Brush), typeof(PlayButton), new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnIconColorChanged )));
最後,您需要實現該回調。該回調是控件的靜態(或共享)方法,控件可以接受發生更改的對象以及用於指定舊值和新值的參數。通常,要在發生更改的對象上調用一個方法來更新對象。例如,如果 IconColor 已更改,則需要設置兩個圖標的 Fill。回調方法和更新方法如下所示:
private static void OnIconColorChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
// When the color changes, set the icon color
PlayButton control = (PlayButton)obj;
control.PlayIcon.Fill = control.IconColor;
control.PauseIcon.Fill = control.IconColor;
}
若要完成控件,還需要 DependencyProperty,從而允許將 MediaElement 分配給控件(參見圖 11)。
有了該屬性後,您可以將 MediaElement 添加到 XAML 中,也可以將該元素數據綁定到新的 MediaPlayer 屬性。在將 MediaElement 添加到 XAML 中時,您需要將 LoadedBehavior 設置為 Manual,這樣您可以手動控制播放。新的 XAML 如圖 12 所示。
Figure 12 修訂後的 MediaElement XAML
<!-- MainWindow.xaml -->
<Window x:Class=”Tester.MainWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF”
Title=”Control Viewer”
Height=”100” Width=”200”>
<StackPanel>
<MediaElement Width=”150” Height=”100”
Name=”theMedia”
Source=”http://download.microsoft.com/.../ctorrec9billg.wmv”
LoadedBehavior=”Manual” />
<TextBlock>User Control:</TextBlock>
<cust:PlayButton MediaPlayer=”{Binding ElementName=theMedia}” />
</StackPanel>
</Window>
接下來,在 PlayButton 用戶控件上實現單擊事件。首先,在控件的主 Grid 中添加 MouseLeftButtonUp 事件。
<!-- PlayButton.xaml -->
<UserControl x:Class=”CustomWPF.PlayButton”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Grid MouseLeftButtonUp=”PlayButton_Clicked”>
<!-- ... -->
</Grid>
</UserControl>
這樣您就可以實現控件的行為以及更改圖標。此事件處理程序的實現如圖 13 所示。完成的控件和視頻如圖 14 所示。
圖 14 完成的用戶控件 自定義控件
您剛創建的 PlayButton 控件工作得很好,但缺乏完全的模板和主題支持。如果您的控件需要支持此功能,則需要將它構建為自定義控件。自定義控件由控件層次結構中的其他類派生而來。例如,可以將 PlayButton 控件重構為可重用程度更高的 MediaButton 控件。
若要創建 MediaButton 控件,首先要在 Visual Studio 的“添加新項”對話框中選擇“自定義控件 (WPF)”。這會在項目中添加一個新的自定義控件類 (MediaButton.cs) 文件,還會新增一個包含 generic.xaml 文件的主題文件夾。該 generic.xaml 文件中包含用於新控件類的模板。它使用此 XAML 文件來允許該控件有不同主題。generic.xaml 文件用作備用主題;對大多數控件而言,這是您要創建的唯一主題文件。如果您想編寫的控件要根據當前主題來更改其外觀,您可以在此目錄中創建主題文件。圖 15 顯示標准主題文件以及它們的使用時間。
通過指定多個主題,您可以根據用戶的主題選擇來更改控件的外觀。為了給我的簡單控件開發一個通用主題,我可以使用用戶控件的 XAML 文件,並將其移入 ControlTemplate 標記中。將 XAML 放入模板後,您需要使用模板綁定來根據該控件的屬性設置 XAML 的屬性。到目前為止,PlayButton 的寬度和高度一直被設置為五十個邏輯單位。對於簡單的控件,這完全可以,但如果您真的希望可重用該控件,那麼應該使模板的大小可以調整。為此,通過在模板中將高度和寬度分別標記為 {TemplateBinding Width} 和 {TemplateBinding Height},即可用控件的高度和寬度來替換模版的高度和寬度。
在 PlayButton 中,只需根據圖標需要顯示播放還是暫停來更改兩個圖標的 Opacity。雖然可以進入模板去更改 Opacity,但這麼做太不聰明了。更好的方法是利用單個圖標 Path 對象,並更改繪制數據來繪制合適的圖標。這樣,可視樹的大小就會直接影響 XAML 文檔的性能。為此,需要引入一個新的 DependencyProperty 來存儲當前要使用的圖標。創建一個枚舉指定要使用哪個圖標,然後將它作為 DependencyProperty 公開。使用新的 Icon 屬性,您可以對模板進行修改,使之包括用於觸發 Icon 屬性更改的觸發器。
該圖標使用 Path 元素來定義該按鈕使用的每個圖標的外觀。如果該按鈕的大小始終固定,則一切正常,但由於情況並非如此,所以您需要找到一種方法來使圖標的大小隨該按鈕而調整。方法之一是在背景圓上創建一個覆蓋橢圓,並使用 VisualBrush 給該圖標繪色。VisualBrush 允許背景的大小隨控件而調整。圖 16 中顯示了完成的 generic.xaml 模板。
此 MediaButton 應該控制 MediaElement,就像 PlayButton 控制其元素一樣。將 MediaPlayer DependencyProperty 從 PlayButton 復制到 MediaButton。請確保將所復制的代碼中的任何引用都從 PlayButton 更改為 MediaButton(尤其是在 DependencyProperty 注冊中)。
與 PlayButton 不同,您不需要在模板中處理鼠標單擊事件。而是可以重寫 OnMouseLeftButtonUp 事件來響應單擊。在此方法中,您可以更改圖標以及播放或暫停媒體。最終的自定義控件代碼如圖 17 所示。現在新控件可以調整圖標的大小和設置圖標的屬性,您可以賦予用戶更大的靈活性,讓他們能夠更改控件的大小和圖標。圖 18 顯示了該控件在大小和圖標不同時的情況。
圖 18 所有形狀和大小
在此控件示例中,我是直接從 System.Windows.Controls.Control 類中進行派生的,但自定義控件的特性允許從類層次結構中的任何位置進行派生。您可以使用自定義控件來重寫和更改內置控件的行為,也可以完全從頭開始構建自己的控件。例如,從 FrameworkElement 派生讓您能夠以非常少的內置布局結構來創建控件。從 Panel 派生讓您能夠為其他對象創建自己專用的容器。決定要從中派生的正確類並不容易,具體取決於您的控件的要求。
我們說到哪兒了?
當您需要專門的控件功能時,您有多個選項,其中包括組合、樣式和模板。通過使用組合,您通常可以創建組合控件,不必編寫新控件。當您需要更改的只是控件的外觀時,可以使用樣式。最後,使用模板可以完全控制現有控件的編寫。有關使用模板來自定義控件的詳細信息,請參閱 Charles Petzold 的 Foundations 專欄。
當您確實決定編寫新的控件時,仍可以使用簡化的編程模型,就如同編寫您自己的窗口或頁面一樣。能夠為外觀、甚至行為隨不同操作系統主題而變化的控件創建模板是自定義控件的獨特優勢。
將現有的用戶控件遷移到自定義控件並不是特別困難,但由於您在使用模板,而不是直接訪問 XAML 對象,所以需要改變自定義控件的構造方法。
使用 Windows Presentation Foundation 時,只是偶爾需要編寫自定義控件,不是必須編寫自定義控件。僅當您真的要創建自定義行為時,才需要鑽研控件編寫。有關 Windows Presentation Foundation 中控件的詳細信息,請參閱 msdn2.microsoft.com/ms754130.aspx 上的 SDK。