WPF自訂控制項

by Vivid 12. 七月 2011 10:00

作者:許薰尹 精誠資訊恆逸教育訓練中心 資深講師

WPF提供許多內建的控制項來建立使用者介面,但有時你可能會有需求定義自己的自訂控制項。在WPF中設計控制項,必然會為其設計屬性。為了能夠與WPF的功能緊密整合,例如資料繫結等等,要將屬性設計成相依屬性(Dependency Property)。本文將介紹自訂控制項的分類以及如何設計自訂控制項的屬性、事件和佈景主題。

在設計自訂控制項之前,先談談控制項的類型,在WPF中控制項分為兩大類:

  • 使用者控制項(User Control):繼承自UserControl類別,由現有的控制項組成,開發時工具提供UI設計畫面。
  • 自訂控制項(Custom Control):繼承自Control或FrameworkElement類別,要自訂自己的UI與功能。

在WPF中提供三種方式建立自訂控制項(Custom Control),您可以按照需求選擇:

  • 繼承自UserControl類別,這是建立控制項最簡單方式,可以利用現有的WPF項目來設計,但無法使用樣版(Template)來客製化控制項。
  • 繼承自Control類別,WPF大部分的控制項都是繼承自Control。提供最大彈性,可以利用樣版(Template)來客製化控制項,也可以支援佈景主題Theme)。
  • 繼承自FrameworkElement類別,可以很精確地自行建立控制項的外觀。

設計自訂控制項

在本文的範例中希望設計一個自訂控制項,能夠顯示一張圖片和文字說明。Visual Studio 2010提供建立WPF自訂控制項的範本,您可以在Visual Studio 2010新增一個「WPF Custom Control Library」類型的專案,參考圖1所示,專案的名稱為CustControl。

clip_image002

圖 1:建立WPF自訂控制項專案。

定義類別與相依屬性

您可以為自訂控制項設計相依屬性與Routed事件。WPF的屬性是相依屬性,支援屬性異動通知、資料繫結以及觸發器等功能。只有繼承自DependencyObject類別的物件可以實作相依屬性,在WPF之中所有控制項都繼承DependencyObject類別。要自訂控制項通常會繼承自Control類別(Control類別繼承了DependencyObject類別),包含所有控制項預設功能。預設當你建立一個「WPF Custom Control Library」類型的專案,專案中便會包含一個CustomControl1類別,繼承Control類別。

定義相依屬性時必需在靜態建構函式中(Static Constructor) 向WPF執行時期註冊,然後使用DependencyObject類別的GetValue與SetValue方法來讀取以及設定它的值。以下宣告兩個相依屬性,分別命名為:CompanyNameProperty與SourceProperty,按照WPF命名慣例,相依屬性都是後置「Property」字串結尾。宣告時相依屬性標示了public、static且為readonly。

public class CustomControl1 : Control {
        public static readonly DependencyProperty CompanyNameProperty;
        public static readonly DependencyProperty SourceProperty;
        static CustomControl1 ( ) {
            DefaultStyleKeyProperty.OverrideMetadata ( typeof ( CustomControl1 ) ,
                        new FrameworkPropertyMetadata ( typeof ( CustomControl1 ) ) );
            FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata ( );
            SourceProperty = DependencyProperty.Register ( "Source" , typeof ( string ) ,
                        typeof ( CustomControl1 ) , metadata );
            metadata = new FrameworkPropertyMetadata( );
            CompanyNameProperty = DependencyProperty.Register ( "CompanyName" ,
                        typeof ( string ) , typeof ( CustomControl1 ) , metadata );
       }
        public CustomControl1 ( ) {
            this.DataContext = this;
        }
        public string CompanyName {
            get { return GetValue ( CompanyNameProperty ).ToString ( ); }
            set { SetValue ( CompanyNameProperty , value ); }
        }
        public string Source {
            get { return GetValue ( SourceProperty ).ToString ( ); }
            set {
                if ( Source != null ) {
                    SetValue ( SourceProperty , value );               
                }
            }
        }
}

在CustomControl1類別的靜態建構函式中利用DependencyProperty.Register方法進行註冊,你需要傳入.NET屬性包裝程式名稱、型別、屬性所屬類別以及一個FrameworkPropertyMetadata物件,它是用來提供額外的資訊用的。

上列程式最後宣告兩個.NET屬性包裝程式(CompanyName與Source),叫用GetValue與SetValue方法來存取或設定屬性。由於相依屬性是由Runtime設定的,除非是透過程式來設定屬性,否則最好不要在.NET屬性包裝程式中加入額外的程式碼。

WPF 佈景主題

WPF 佈景主題(Theme)能夠讓你一次套用相同的外觀到多個控制項。預設當你建立一個WPF Custom Control Library類型的專案,專案中會有一個Themes目錄,其中便存放一個Generic.xaml檔案,此檔案中定義的樣版便是控制項通用的佈景主題。檢視Visual Studio 2010建立的Generic.xaml檔,其中定義了套用到自訂控制項的樣式(Style),以及預設的控制項樣版(Control Template)。本文的範例中希望設計一個自訂控制項,能夠顯示一張圖片和文字說明,因此在Border標籤中加入一個StackPanel,在其中再加入一個Image與TextBlock控制項。

<ResourceDictionary
    xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local = "clr-namespace:CustControl">
    <Style TargetType = "{x:Type local:CustomControl1}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType = "{x:Type local:CustomControl1}" >
                    <Border Background = "{TemplateBinding Background}"
                            BorderBrush = "{TemplateBinding BorderBrush}"
                            BorderThickness = "{TemplateBinding BorderThickness}">
                        <StackPanel>
                            <Image  Width = "16" Height = "16" Source = "{Binding Path=Source}" />
                            <TextBlock Foreground = "{TemplateBinding Foreground}"
           VerticalAlignment = "Center" HorizontalAlignment = "Center"
           Text = "{Binding Path=CompanyName}" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Image控制項利用資料繫結語法連結到控制項的Source屬性。而TextBlock的Text屬性則繫結到CompanyName屬性。到目前為止已經完成一個簡單的自訂控制項,你可以將自訂控制項編譯成dll類型的組件。

使用自訂控制項

要測試或使用WPF自訂控制項你需要在Visual Studio 2010方案中新增一個WPF應用程式。從Visual Studio 2010 「File」->「Add」->「New Project」選取「WPF Application」範本,加入一個WPF應用程式專案。當你在一個方案之中同時設計自訂控制項與WPF應用程式時,工具箱會自動顯示可拖曳到WPF視窗中的自訂控制項,參考圖2所示。

clip_image004

圖 2:使用自訂控制項。

從工具箱拖曳CustomControl1到WPF視窗設計畫面,Visual Studio 2010會自動在Window的XAML標籤中加入自訂控制項命名空間的宣告,以及所屬組件的名稱:「xmlns:my="clr-namespace:CustControl;assembly=CustControl"」。

接下來便可以利用屬性視窗來設定屬性。例如,本文範例設定Source屬性為「GreenBullet.png」圖檔,設定CompanyName屬性為公司名稱「uuu」。

<Window x:Class = "CustClient.MainWindow"
        xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
        Title = "MainWindow" Height = "350" Width = "525"
        xmlns:my = "clr-namespace:CustControl;assembly=CustControl">
    <Grid>
        <my:CustomControl1 HorizontalAlignment = "Left" Margin = "123,75,0,0" Name = "customControl11"
        VerticalAlignment = "Top" CompanyName = "uuu" Height = "47" Width = "133"
        Source = "GreenBullet.png" />
    </Grid>
</Window>

圖3是在設計階段使用自訂控制項的效果。

clip_image006

圖 3:使用自訂控制項。

再論WPF 佈景主題

前文提及WPF Custom Control Library類型的專案,專案中會有一個Themes目錄,其中便存放一個Generic.xaml檔案,此檔案中定義的樣版便是控制項通用的佈景主題。實際上,您可以為自訂控制項加入多個不同的佈景主題,若是要使用Windows Classic佈景主題,則XAML的檔名需為Classic.xaml;其它佈景主題的名稱命名原則為:

佈景主題名稱. 佈景主題顏色.xaml。

為了測試佈景主題,我們修改通用佈景主題Generic.xaml中的TextBlock,設定背景顏色為Green:

<TextBlock Background = "Green"…略 />

接下來複製一份Generic.xaml檔案,將Windows Classic佈景主題檔案命為Classic.xaml,然後設定TextBlock背景顏色為Red:

<TextBlock Background = "Red" … />

再複製一份Generic.xaml檔案,設計一個Windows Vista佈景主題,檔名為Aero.NormalColor.xaml,設定TextBlock背景顏色為Blue:

<TextBlock Background = "Blue" … />

最後修改AssemblyInfo.cs檔案,套用ThemeInfo attribute將第一個參數修改為ResourceDictionaryLocation. SourceAssembly,表示從來源組件中找尋佈景主題:

[assembly: ThemeInfo (
    ResourceDictionaryLocation.SourceAssembly , //where theme specific resource dictionaries are located
    //(used if a resource is not found in the page,
    // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
    //(used if a resource is not found in the page,
    // app, or any theme specific resource dictionaries)
)]

本文在Windows Server 2008的環境中測試,你需要確保作業系統中有啟動Themes服務,參考圖圖4所示。

clip_image008

圖 4:啟動Themes服務

接著您就可以更改作業系統佈景主題,參考圖5,設定佈景主題為「Windows Classic」:

clip_image010

圖 5:設定佈景主題為「Windows Classic」。

當你套用之後,在Visual Studio 2010設計畫面中,便可以看到了套用佈景主題的效果,TextBlock的背景變成紅色的,參考圖6所示。

clip_image012

圖 6:套用「Windows Classic」佈景主題效果。

若更改作業系統佈景主題,參考圖7,設定佈景主題為「Windows Vista」。

clip_image014

圖 7:套用「Windows Vista」佈景主題。

當你套用之後,在Visual Studio 2010設計畫面中,便可以看到了套用佈景主題的效果,TextBlock的背景變成藍色的,參考圖8所示。
clip_image016

圖 8:套用「Windows Vista」佈景主題效果。

為自訂控制項設計事件

WPF的事件系統比較特殊,稱之為Routed事件,你可以在父項目攔截子項目的事件。我們些修改範例程式,定義一個名為CompanyNameChangedEvent的RoutedEvent,按照WPF命名慣例,事件的名稱需以Event字串結尾。並利用EventManager類別的RegisterRoutedEvent方法註冊事件。註冊時傳入的參數分別為:Routed事件的名稱、事件傳遞策略列舉常值、事件處理常式型別,事件擁有者型別。

public static readonly RoutedEvent CompanyNameChangedEvent = EventManager.RegisterRoutedEvent (
         "CompanyNameChanged" , RoutingStrategy.Bubble ,
         typeof ( RoutedPropertyChangedEventHandler<string> ) , typeof ( CustomControl1 ) );

註冊完成後,你可以為事件提供一個common language runtime (CLR)事件存取子來新增、移除事件:

public event RoutedPropertyChangedEventHandler<string> CompanyNameChanged {
            add { AddHandler ( CompanyNameChangedEvent , value ); }
            remove { RemoveHandler ( CompanyNameChangedEvent , value ); }
}

另外在自訂控制項的建構函式中註冊CompanyName屬性時利用FrameworkPropertyMetadata物件利用PropertyChangedCallback指明一個回呼函式,OnCompanyNameChanged,在CompanyName屬性值變動時將觸發CompanyNameChangedEvent事件。而OnCompanyNameChanged回呼函式中建立觸發CompanyNameChangedEvent事件所需的RoutedPropertyChangedEventArgs<string>參數,將屬性異動前、後的值封裝起來。最後叫用UIElement的RaiseEvent方法觸發事件:

static CustomControl1 ( ) {
            DefaultStyleKeyProperty.OverrideMetadata ( typeof ( CustomControl1 ) ,
                        new FrameworkPropertyMetadata ( typeof ( CustomControl1 ) ) );
            FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata ( );
            SourceProperty = DependencyProperty.Register ( "Source" , typeof ( string ) ,
                        typeof ( CustomControl1 ) , metadata );
            metadata = new FrameworkPropertyMetadata ( "" , new
                        PropertyChangedCallback ( OnCompanyNameChanged ) );
            CompanyNameProperty = DependencyProperty.Register ( "CompanyName" ,
                        typeof ( string ) , typeof ( CustomControl1 ) , metadata );
}
private static void OnCompanyNameChanged ( DependencyObject obj ,
DependencyPropertyChangedEventArgs args ) {
            CustomControl1 control = ( CustomControl1 ) obj;
            RoutedPropertyChangedEventArgs<string> e = new RoutedPropertyChangedEventArgs<string> (
                ( string ) args.OldValue , ( string  ) args.NewValue , CompanyNameChangedEvent );
            control.OnValueChanged ( e );
}

protected virtual void OnValueChanged ( RoutedPropertyChangedEventArgs<string> args ) {
            RaiseEvent ( args );
}

在WPF Window中使用自訂控制項時,您可以在控制項的標籤或是其父項目註冊事件,例如以下XAML在Grid項註冊CompanyNameChanged事件以及事件處理常式:

<Window x:Class = "CustClient.MainWindow"
        xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
        Title = "MainWindow" Height = "350" Width = "525"
        xmlns:my = "clr-namespace:CustControl;assembly=CustControl">
    <Grid my:CustomControl1.CompanyNameChanged = "customControl11_CompanyNameChanged">
        <my:CustomControl1 HorizontalAlignment = "Left"
                           Margin = "22,34,0,0"
                           Name = "customControl11"
                           VerticalAlignment = "Top"
                           CompanyName  ="uuu"
                           Height = "47" Width = "133"
                           Source = "GreenBullet.png"  />
        <Button Content = "Button" Height = "23" HorizontalAlignment = "Left" Margin = "275,30,0,0"
        Name = "button1" VerticalAlignment = "Top" Width = "75" Click = "button1_Click" />
    </Grid>
</Window>

在事件處理常式中,將屬性的新值與原始值顯示在視窗標題上:

private void customControl11_CompanyNameChanged ( object sender , RoutedPropertyChangedEventArgs<string> e ) {
            this.Title = e.NewValue + "," + e.OldValue;
}

後續只要CompanyName名稱變動時,就會觸發事件。例如在Window中加入一個Button在Click事件中修改了自訂控制項的CompanyName屬性,以觸發CompanyNameChanged事件:

private void button1_Click ( object sender , RoutedEventArgs e ) {
            customControl11.CompanyName = "New";
}

設計Visual States

WPF的控制項可以有許多的狀態,如TextBox取得焦點時,狀態為Focused。WPF 中的Visual State Manager能夠增強控制項的功能,讓控制項的功能與狀態整合在一起,以便於提供更豐富視覺效果的控制項外觀。你可以利用TemplateVisualState Attribute來指定控制項的視覺化合約(Visual Contract),讓其它開發工具,如Microsoft Expression Blend 4能夠客製化控制項的外觀,並在不同狀態轉換時,可以定義一些過場動作。’

你可以利用VisualStateManager.VisualStateGroups附加屬性為控制項新增Visual States,VisualStateManager.VisualStateGroups附加屬性是一個集合,可以包含多個VisualState項目,每個VisualState項目代表一個狀態,包含一個Storyboard物件用來描述狀態變更時要做的過場效果。

以下程式碼為CustomControl1自訂控制項的StackPanel項目定義兩個狀態:Normal與Change。當CustomControl1自訂控制項的CompanyNameProperty屬性值變更時,透過Storyboard執行ColorAnimation做顏色變更特效,將StackPanel的背景顏色變成黃色的。

<ResourceDictionary
    xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local = "clr-namespace:CustControl">
    <Style TargetType = "{x:Type local:CustomControl1}" >
        <Setter Property = "Template" >
            <Setter.Value>
                <ControlTemplate TargetType = "{x:Type local:CustomControl1}" >
                    <Border Background = "{TemplateBinding Background}"
                            BorderBrush = "{TemplateBinding BorderBrush}"
                            BorderThickness = "{TemplateBinding BorderThickness}" >
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup Name = "CommonStates" >
                                <VisualState Name = "Normal" />
                                <VisualState Name = "Change" >
                                    <Storyboard>
                                        <ColorAnimation To = "Yellow" 
                          Storyboard.TargetName = "rectBrush" 
                          Storyboard.TargetProperty = "Color" />
                                    </Storyboard>
                                </VisualState>
                                <VisualStateGroup.Transitions>
                                    <VisualTransition To = "Normal" GeneratedDuration = "00:00:00"/>
                                    <VisualTransition To = "Change" GeneratedDuration = "00:00:00.5">
                                        <VisualTransition.GeneratedEasingFunction>
                                            <ExponentialEase EasingMode = "EaseOut" Exponent = "10"/>
                                        </VisualTransition.GeneratedEasingFunction>
                                    </VisualTransition>
                                </VisualStateGroup.Transitions>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <StackPanel>
                            <StackPanel.Background>
                                <SolidColorBrush x:Name = "rectBrush"
Color = "{TemplateBinding Background}" />
                            </StackPanel.Background>
                            <Image  Width = "16" Height = "16" Source = "{Binding Path=Source}" />
                            <TextBlock Foreground = "{TemplateBinding Foreground}"
           VerticalAlignment = "Center" HorizontalAlignment = "Center"
           Text = "{Binding Path=CompanyName}" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

在定義VisualStateManager.VisualStateGroups時要注意,它必需是控制項樣版(Control Template)根項目(Root Element)下的第一個子項目。VisualStateGroup.Transitions項目則是用來定義狀態變動時要執行的過場特效。

在自訂控制項的CompanyNameChanged事件觸發時,我們便可以利用VisualStateManager類別的GoToState方法變更狀態:

protected virtual void OnValueChanged ( RoutedPropertyChangedEventArgs<string> args ) {
            RaiseEvent ( args );
            VisualStateManager.GoToState ( this , "Change" , true );
        }

總結

使用者控制項、自訂控制項都可以讓你自行訂定控制項。使用者控制項無法讓你利用樣版(Template)來更改控制項的外觀;而自訂控制項可以讓你使用樣版(Template)定義控制項的外觀以及功能。若為自訂控制項實作相依屬性與Routed事件,那麼自訂控項就可以整合WPF的樣式(Style)、資料繫結、觸發器等功能。

 
   

Tags:

WPF | 許薰尹Vivid Hsu | .NET Magazine國際中文電子雜誌

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List