深入WPF--Style
Style 用來在類型的不同實例之間共享屬性、資源和事件處理程序,您可以將 Style 看作是將一組屬性值應用到多個元素的捷徑。
這是MSDN上對Style的描述,翻譯的還算中規中矩。Style(樣式),簡單來說,就是一種對屬性值的批處理,類似于Html的CSS,可以快速的設置一系列屬性值到UI元素。
示例
一個最簡單的Style的例子:
關于Resources的知識,請參見MSDN,這里創建了一個目標類型為Button的ButtonStyle,兩個Button使用靜態資源(StaticResource)的查找方式來找到這個Style。Style中定義了Button的高度(Height)和寬度(Width),當使用了這個Style后,兩個Button無需手動設置,即可自動設置它們的高度和寬度為ButtonStyle的預設值22和60。
Style作為屬性,資源,事件的批處理,它提供了一種捷徑來對控件進行快速設置,使用Style的好處有二:
- 把一些控件的通用設置抽出來變成Style,使這些控件具有統一的風格,修改Style中的屬性值可以方便的作用在所有應用該Style的控件上。
- 可以對同一類型控件定義多個Style,通過替換Style來方便的更改控件的樣式。
Style的元素
上面Style的例子中,Style內部使用了Setter來定義控件屬性的預設值,Style不僅支持對屬性的批處理,也可以共享資源和事件處理,如:
Style中定義了資源SolidColorBrush,定義了屬性Height和Width,以及使用了EventSetter來定義了Loaded事件的處理。
Trigger
Style使用了Setter和EventSetter來分別設置控件的屬性和事件處理,Setter這個單詞的含義是設置。Style在設計好了這兩種設置后,又引入了更先進的思路:條件設置。
對于單純的Setter:<Setter Property=”Height” Value=”22”>來說,含義淺顯易懂:設置高度為22。條件設置的含義是,在某種條件下,去設置某個對象的某個值。
WPF引入了Trigger(觸發器)來觸發這個條件,它的寫法是:
這里Trigger的含義是,在Button的IsMouseOver屬性被設置為True的條件下,設置Button的寬度(Width)為80。
在Style中,不需要指定Setter作用的對象(TargetName),默認作用的對象就是使用該Style的控件。Trigger,作為觸發器,當觸發時設置寬度為80,當IsMouseOver屬性為False,也就是觸發條件失效時,寬度回到默認Setter的設置值60。
WPF定義了五種Trigger來作為觸發條件,分別是:Trigger,DataTrigger,MultiTrigger,MultiDataTrigger,EventTrigger,他們的觸發條件分別是:
- Trigger:以控件的屬性作為觸發條件,如前面的IsMouseOver為True的時候觸發。
- DataTrigger:以控件DataContext的屬性作為觸發條件。
- MultiTrigger:以控件的多個屬性作為觸發條件。
- MultiDataTrigger:以控件DataContext的多個屬性作為觸發條件。
- EventTrigger:以RoutedEvent作為觸發條件,當指定的路由事件Raise時觸發。
關于這5種Trigger的具體使用,請參見MSDN,這里就不詳細介紹了。
Implicit Style
上面的例子中,都是使用StaticResource來設置Style的,當然,你也可以使用DynamicResource來設置Style。這兩種方式都需要你在XAML或者后臺代碼中手動注明,為了使用方便,WPF提出了隱式(Implicit) Style的方式允許自動設置Style到控件,如:
在Gird的Resource中定義Style時,沒有給Style起名字(Key),這個Style會自動應用在Grid的所有子Button中,如果像button1一樣在Button中顯式定義了Style(這里設置了一個空值Null),那么這種隱式(Implicit)的Style會不起作用。
深入Style
Style是一個不錯的概念,作為一個Presentation的框架,把UI對象的結構,樣式和行為分離這是一種很好的設計。Style也比較容易上手,像它的隱式(Implicit)Style的設計也是水到渠成的想法,但實際使用中也會出現一些問題。這些問題在WPF中也會經常遇見:概念不錯,描述簡單,前景美好,Bug稀奇古怪,要把這些問題說清楚,就要從根本來看,Style是個什么東西?
按照通常的想法,Style應該類似于一個Dictionary<string, object> setters,預存了屬性的名字和預設值,然后作用到UI對象上。WPF在Style處的想法很多,圍繞著幾個關鍵技術也加入了很多功能,詳細的介紹一下:
Style & Dependency Property
Dependency Property(簡稱DP)是WPF的核心,Style就是基于Dependency Property的,關于DP的內幕,請參見深入WPF--依賴屬性。Style中的Setter就是作用在DP上的,如果你在控件中定義了一個CLR屬性,Style是不能設置的。Dependency Property設計的精髓在于把字段的存取和對象(Dependency Object)剝離開,一個屬性值內部用多個字段來存儲,根據取值條件的優先級來決定當前屬性應該取哪個字段。
Dependency Property取值條件的優先級是(從上到下優先級從低到高):
對于一個具體例子來說:
第4行用Style的Setter設置Width=60,這個優先級是Style;第6行當IsMouseOver為True時設置Width=80,這個優先級是StyleTrigger;第13行使用Style的Button定義Width=20,這個優先級是Local。Local具有最高的優先級,所以即使鼠標移到Button上,第6行的Trigger也會因為優先級不夠高而不起作用。如果去掉了第13行中的Width=20,那么鼠標移到Button上時Width會變為80,鼠標移開后會回到第4行的設置的60來。
Style & FrameworkElement
Style作為一個屬性定義在FrameworkElement上,所有繼承自FrameworkElement的控件都可以使用Style。FrameworkElement定義了多個Style:Style,ThemeStyle,FocusVisualStyle:
- FocusVisualStyle:是當控件獲得鍵盤焦點時,顯示在外面的一個虛線框,這個Style并沒有直接作用在對應的FrameworkElement上,而是當控件獲得鍵盤焦點時使用AdornLayer創建了一個新的Control,然后再這個Control上使用FocusVisualStyle,再把它遮蓋在對應的FrameworkElement上形成一個虛線框的效果。
- Style:就是我們前面一直設置的Style。
- ThemeStyle:這里引入了一個Theme的概念,具體來談一下它。
Windows定了很多Theme(主題),你可以在控制面板中切換Theme,如圖:
最上面的兩排都屬于Aero主題,當從Aero主題切換到Windows Classic主題后,任務欄,窗口以及窗口內的控件外觀都會發生變化。為了更好的切換主題,WPF引入了ThemeStyle這個概念。當我們使用VS2010的模板生成一個自定義控件(Custom Control)后,會自動添加一個Themes的文件夾以及一個Generic.xaml的文件,如圖:
這里的Aero.NormalColor.xaml是手動添加的,先略去不談,來談談控件(Control)的默認樣式。
WPF默認提供了很多控件,Button,ListBox,TabControl等等,我們使用這些控件時,是沒有指定它的樣式(Style)的,WPF為我們提供了默認Style,這個默認Style是與Windows主題相關的。比如我們切換Windows的主題從Aero到Classic,WPF窗口里的控件外觀也會發生變化。這些默認的Style是以ResourceDictionary的形式保存在PresentationFramework.Aero.dll,PresentationFramework.Classic.dll等dll中的,這里的命名規則是:程序集名稱+Theme名稱+.dll。
那么WPF又是如何根據Windows的Theme找到對應的ThemeStyle呢?WPF提出了ThemeInfo這個Attribute來指定Theme信息。ThemeInfo一般定義在Properties/AssemblyInfo.cs中,如:
ThemeInfo有兩個參數,第一個參數指的是ThemeResource,第二個參數指的是GenericResource,它們的類型是ResourceDictionaryLocation:
ResourceDictionaryLocation的None指不存在對應的Resource,SourceAssembly指該程序集(Assembly)中存在對應的Resource,ExternalAssembly指對應的Resource保存在外部的程序集(Assembly)中,這個外部程序集的查找規則就是我們前面看到的:程序集名稱+Theme名稱+.dll。
對于一個控件,無論是系統自帶的控件還是我們自定義的控件,WPF啟動時都會通過當前Windows系統的Theme查找它對應的ThemeStyle。這個查找規則是:
- 先通過控件的類型(Type)找到它對應的程序集(Assembly),然后獲取程序集中的ThemeInfo,看看它的ThemeResource和GenericResource在哪里。如果ThemeResource的值不是None,系統會讀取到ThemeResource對應的ResourceDictionary,在這個ResourceDictionary中查找是否定義了TargetType={x:Type 控件類型},如果有,把控件的ThemeStyle指定為這個Style。
- 如果第一步的查找失敗,那么GenericResource派上用場,Generic這個詞表示一般。WPF會查看ThemeInfo的第二個參數GenericResource來查找它的ThemeStyle,查找規則同第一步,如果查找成功,把這個Style指定為控件的ThemeStyle。
任意一個控件,如果不顯式指定它的Style,并且查不到默認的ThemeStyle,這個控件是沒有外觀的。為了編程方便,當我們使用VS添加自定義控件時,VS默認幫我們生成了Generic.xaml,如果我們希望自定義的控件也要支持系統的Theme變化,可以在Themes這個文件夾下加入對應的ResourceDictionary,比如上面的Aero.NormalColor.xaml,并且指定程序集ThemeInfo的第一個參數為SourceAssembly,表明該程序集支持系統Theme變化并且對應的資源文件在該程序集中。當然,ResourceDictionary一定要放在Themes文件夾下,因為WPF查找ResourceDictionary時使用的是類似:
這樣的方法。
Style & ResourceDictionary
前面提到了很多次ResourceDictionary,關于WPF的Resource系統,以后再來細談。WPF的Resource系統使用ResourceDictionary來儲存Resource,ResourceDictionary,顧名思義,也是一個Dictionary,既然是Dictionary,就是按鍵/值對來存儲的。我們最前面在Window的Resource中創建Style時,指定了Style對應的鍵值(x:Key),后面又用StaticResource來引用這個鍵值。
如果在ResourceDictionary中添加一個對象Button,不指定它的鍵值(x:Key),是不能通過編譯的。我們前面介紹的隱式(Implicit)Style,只指定了一個TargetType={x:Type 類型},并沒有指定鍵值,為什么它可以通過編譯呢?
對于在ResourceDictionary中添加Style,如果我們沒有指定鍵值(x:Key),WPF會默認幫我們生成鍵值,這個鍵值不是一個String,而是一個類型object(具體來說是Type實例),也就是說相當于:
后面的x:Key可以省略掉。
Appliation以及FrameworkElement類都定義了Resources屬性,內部都持有一個ResourceDictionary,Resource查找遵循的最基本原則是就近原則,如:
Window和StackPanel的Resources中都分別定義了toggleBtnStyle以及隱式Style(Button),根據就近原則,StackPanel內部的ToggleButton和Button會應用StackPanel的Resource而不會使用Window的。
Style Merge
這里要提到本篇的重點也是不被人注意卻經常出錯的地方,Style的合并(Merge)。
前面提到了很多Style,ThemeStyle,Style,隱式Style。我們提過,Style相當于一個屬性值的批處理,那么對于一個屬性,只能有一個預設值而不能多個,這些Style在運行時要進行合并,然后作用在FrameworkElement上。
Style的合并,要分兩步進行:
- 找到所有Style。
- 確定Style的優先級,根據優先級來合并Style。
以Button來說:
- 如果當前Windows的Theme是Aero,啟動后會從PresentationFramework.Aero.dll中找到對應的ThemeStyle。
- 如果在Button上使用StaticResource或者DynamicResource指定了Style,會通過鍵值在Resource系統中找到對應的Style。
- 如果沒有在Button上顯式指定Style,會通過Resource系統查找隱式Style(x:Type Button)。
- 第二步和第三步是排他的,這兩步只能確定一個Style,然后把這個Style和ThemeStyle進行合并(Merge)得到Button最終的效果。
先從合并來說,顯式或者隱式Style的優先級是高于ThemeStyle的,如果Style和ThemeStyle的Setter中都對同一屬性進行了預設,那么會取Style里面的Setter而忽略ThemeStyle。這里比較特殊的是EventSetter,EventSetter使用的是RoutedEvent,如果兩個Style的EventSetter對同一個RoutedEvent進行了設置,兩個都會注冊到RoutedEvent上。
前面看到,顯式和隱式Style是排他的,兩者只能取一,在實際項目中,在全局定義好Button的基本樣式,然后具體使用上再根據基本樣式做一些特殊處理,這種需求是很常見的。為了解決這種需求,Style提出了BasedOn屬性,來表示繼承關系,如:
為了更清晰的解釋,給出了一個不太常見的例子。第16行創建了一個隱式Style(Button),它的BasedOn屬性仍然是隱式Style(Button),Resource系統會向上查找找到Window的Resorces中的隱式Style(Button),然后把兩者合并。對于同一個ResourceDictionary,是不允許有重復鍵值的,StackPanel和Window各有各自的ResourceDictionary,他們的鍵值不受干擾,查找時會通過就近原則來找到優先級最高的Resource。第20行ToggleButton的例子和Button是一樣的,只是它查找到的第8行toggleBtnStyle的TargetStyle是ButtonBase,ButtonBase是ToggleButton的基類,BasedOn屬性也可以作用。
WPF的Style機制是一個密封(Seal)機制,它的書寫方式很靈活,可以支持合并等,當最后合并后,Style就被密封(Seal),內部的Setter等不允許再被修改。這種密封的設計有它的道理,但在Style的動態性上就稍顯不足。
以自定義控件為例,自定義一個Button,名字叫MyButton,它繼承自Button,在自定義控件中,經常可以看到這樣的代碼:
這里出現了DefaultStyle,這個是WPF對ThemeStyle的另一個說法,ThemeStyle就是用來確定默認的Style的,后來包括BaseValueSource中也使用了DefaultStyle來表示ThemeStyle。在MyButton的靜態函數中重載DefaultStyleKeyProperty內部Metadata的含義是告訴WPF系統,查找MyButton的ThemeStyle使用的鍵值從{x:Type Button}被改成了{x:Type MyButton}。
如果像上述代碼一樣修改了DefaultStyleKeyProperty,那么需要我們在Themes/Generic.xaml中定義好MyButton的默認(Theme)Style,否則MyButton是沒有外觀的,因為查找ThemeStyle的鍵值已經被修改,PresentationFramework.Aero.dll等dll中是沒有定義{x:Type MyButton}的。
前面是關于ThemeStyle的用法,那么回到隱式Style上來,如果我們在Application的Resources中定義了Button的隱式Style(TargetType={x:Type Button}),即使沒有顯式設置MyButton的Style,所有的MyButton控件也不會使用這個隱式Style的。需要你在Application的Resources中,在定義Button隱式Style的下面定義
這里就回到Style的合并(Merge)上來了,Style的Merge是很基本(很傻)的合并(Merge),它不具備Auto性。具體來說,就是:
- 基類控件的隱式Style不會作用到派生類控件上。
- 像前面在Window和StackPanel中分別定義了隱式Style(Button),這兩個隱式Style不會智能合并后再作用到Button上,而是通過就近原則只選其一。
- Style的BasedOn屬性只支持StaticResource方式引用,因為Style繼承自DispatcherObject而不是DependencyObject,DynamicResource只支持DP。
這些問題都需要通過Style的BasedOn來解決,因為BasedOn用的是靜態引用(StaticResource),當隱式Style發生變化時就有麻煩了。
換膚
UI程序的換膚是很炫的玩意,換膚分兩種:1,更換整個控件的Style;2,更換Style中的顏色畫刷(Brush)。后者的實現很簡單,定義好顏色畫刷的資源文件(ResourceDictionary),使用畫刷的時候使用DynamicResource綁定,換膚的時候替換畫刷的資源文件就可以了。
很多公司都有自己皮膚庫,這些皮膚庫一般都是隱式的Style,定義了所有控件的隱式Style,使用時把這個皮膚資源Merge到Application的Resources中。換膚時把舊的皮膚資源從Application的Resources中刪除,替換成新的皮膚資源ResourceDictionary。
這種做法很好理解,但是碰到Style的BasedOn屬性就不起作用了,BasedOn屬性使用是StaticResource,是靜態的一次性的。新的皮膚庫被添加到Application資源文件后,如果在Application的資源文件中已經定義過<Style TargetType=“{x:Type Button}” BasedOn=“{StaticResource {x:Type Button}}”/>這樣隱式的Style,控件是不會更新皮膚的。如果有這方面的需求,需要手動合并(Merge)Style來解決問題,類似:
這里還需要加上一些條件判斷,以及決定是否要遞歸合并otherStyle的BasedOn,回到前面,程序需要使用DynamicResource來監聽Application資源中隱式Style的變化,用一個附加屬性來解決:
SetResourceReference是XAML中DynamicResource的代碼表示,相當于Behavior.BaseOnStyle={DynamicResource type}。對控件使用SetResourceReference,監聽的鍵值是type,監聽的屬性是一個我們自定義的附加屬性BaseOnStyleProperty。當換膚替換Application的資源文件時,BaseOnStyle屬性被更新,在BaseOnStyleProperty的Changed事件中可以讀取控件的Style屬性和新的ThemeStyle,調用Merge方法Merge兩者然后再設置到控件的Style屬性上。
總結
WPF中Style的設計中規中矩,把UI對象樣式和結構分離是它的最初想法,其中也加入了Trigger等一些好的設計,但在使用中還是會出現一些問題,它本身也不是那么智能完美。希望朋友們都能從內到外的看待Style,更好的玩轉它。
閑話
這個深入WPF系列也寫了好幾篇了,比起用嘴上白話一通,寫文章需要更多的耐心和細致。講解有很多境界:把簡單的東西講復雜;把復雜的東西講復雜;把復雜的東西講簡單;把復雜的東西講簡單,而且還有詩情哲理。我達不到那么高的境界,希望能做到直接不回避的把技術主線講清楚,也希望能更多的聽到朋友們的反饋,我會繼續補充,爭取把這個系列寫好。
謝謝支持,謝謝您頂一下。 ^_^
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。



浙公網安備 33010602011771號