Spiga

深入WPF--Style

2011-08-01 12:07 by 周永恒, 3027 visits, 收藏, 编辑

Style 用来在类型的不同实例之间共享属性、资源和事件处理程序,您可以将 Style 看作是将一组属性值应用到多个元素的捷径。

  这是MSDN上对Style的描述,翻译的还算中规中矩。Style(样式),简单来说,就是一种对属性值的批处理,类似于Html的CSS,可以快速的设置一系列属性值到UI元素。

示例

  一个最简单的Style的例子:

   1: <Window>
   2:     <Grid>
   3:         <Grid.Resources>
   4:             <Style TargetType="{x:Type Button}" x:Key="ButtonStyle">
   5:                 <Setter Property="Height" Value="22"/>
   6:                 <Setter Property="Width" Value="60"/>
   7:             </Style>
   8:         </Grid.Resources>
   9:         <Button Content="Button" Style="{StaticResource ButtonStyle}"/>
  10:         <Button Content="Button" Style="{StaticResource ButtonStyle}" Margin="156,144,286,145" />
  11:     </Grid>
  12: </Window>

  关于Resources的知识,请参见MSDN,这里创建了一个目标类型为Button的ButtonStyle,两个Button使用静态资源(StaticResource)的查找方式来找到这个Style。Style中定义了Button的高度(Height)和宽度(Width),当使用了这个Style后,两个Button无需手动设置,即可自动设置它们的高度和宽度为ButtonStyle的预设值22和60。

  Style作为属性,资源,事件的批处理,它提供了一种捷径来对控件进行快速设置,使用Style的好处有二:

  1. 把一些控件的通用设置抽出来变成Style,使这些控件具有统一的风格,修改Style中的属性值可以方便的作用在所有应用该Style的控件上。
  2. 可以对同一类型控件定义多个Style,通过替换Style来方便的更改控件的样式。

Style的元素

  上面Style的例子中,Style内部使用了Setter来定义控件属性的预设值,Style不仅支持对属性的批处理,也可以共享资源和事件处理,如:

   1: <Window>
   2:     <Window.Resources>
   3:         <Style TargetType="{x:Type Button}" x:Key="ButtonStyle">
   4:             <Style.Resources>
   5:                 <SolidColorBrush x:Key="brush" Color="Yellow"/>
   6:             </Style.Resources>
   7:             <Setter Property="Height" Value="22"/>
   8:             <Setter Property="Width" Value="60"/>
   9:             <EventSetter Event="Loaded" Handler="Button_Loaded"/>
  10:         </Style>
  11:     </Window.Resources>
  12:     <x:Code>
  13:         <![CDATA[
  14:             void Button_Loaded(object sender, RoutedEventArgs e)
  15:             {
  16:                 MessageBox.Show((sender as Button).Name + " Loaded");
  17:             }
  18:         ]]>
  19:     </x:Code>
  20:     <Grid>
  21:         <Button x:Name="button1" Style="{StaticResource ButtonStyle}" Background="{DynamicResource brush}"/>
  22:         <Button x:Name="button2" Style="{StaticResource ButtonStyle}" Background="{DynamicResource brush}" Margin="156,144,286,145" />
  23:     </Grid>
  24: </Window>

  Style中定义了资源SolidColorBrush,定义了属性Height和Width,以及使用了EventSetter来定义了Loaded事件的处理。

Trigger

  Style使用了Setter和EventSetter来分别设置控件的属性和事件处理,Setter这个单词的含义是设置。Style在设计好了这两种设置后,又引入了更先进的思路:条件设置。

  对于单纯的Setter:<Setter Property=”Height” Value=”22”>来说,含义浅显易懂:设置高度为22。条件设置的含义是,在某种条件下,去设置某个对象的某个值。

  WPF引入了Trigger(触发器)来触发这个条件,它的写法是:

   1: <Style TargetType="{x:Type Button}" x:Key="ButtonStyle">
   2:     <Setter Property="Width" Value="60"/>
   3:     <Style.Triggers>
   4:         <Trigger Property="IsMouseOver" Value="True">
   5:             <Setter Property="Width" Value="80"/>
   6:         </Trigger>
   7:     </Style.Triggers>
   8: </Style>

  这里Trigger的含义是,在Button的IsMouseOver属性被设置为True的条件下,设置Button的宽度(Width)为80。

  在Style中,不需要指定Setter作用的对象(TargetName),默认作用的对象就是使用该Style的控件。Trigger,作为触发器,当触发时设置宽度为80,当IsMouseOver属性为False,也就是触发条件失效时,宽度回到默认Setter的设置值60。

  WPF定义了五种Trigger来作为触发条件,分别是:Trigger,DataTrigger,MultiTrigger,MultiDataTrigger,EventTrigger,他们的触发条件分别是:

  1. Trigger:以控件的属性作为触发条件,如前面的IsMouseOver为True的时候触发。
  2. DataTrigger:以控件DataContext的属性作为触发条件。
  3. MultiTrigger:以控件的多个属性作为触发条件。
  4. MultiDataTrigger:以控件DataContext的多个属性作为触发条件。
  5. EventTrigger:以RoutedEvent作为触发条件,当指定的路由事件Raise时触发。

  关于这5种Trigger的具体使用,请参见MSDN,这里就不详细介绍了。

Implicit Style

  上面的例子中,都是使用StaticResource来设置Style的,当然,你也可以使用DynamicResource来设置Style。这两种方式都需要你在XAML或者后台代码中手动注明,为了使用方便,WPF提出了隐式(Implicit) Style的方式允许自动设置Style到控件,如:

   1: <Window>
   2:     <Grid>
   3:         <Grid.Resources>
   4:             <Style TargetType="{x:Type Button}">
   5:                 <Setter Property="Height" Value="22"/>
   6:                 <Setter Property="Width" Value="60"/>
   7:             </Style>
   8:         </Grid.Resources>
   9:         <Button x:Name="button1" Style="{x:Null}"/>
  10:         <Button x:Name="button2" Margin="156,144,286,145" />
  11:         <Button x:Name="button3" Margin="196,144,0,145" />
  12:     </Grid>
  13: </Window>

  在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取值条件的优先级是(从上到下优先级从低到高):

   1: public enum BaseValueSource
   2: {
   3:     Unknown,
   4:     Default,
   5:     Inherited,
   6:     DefaultStyle,
   7:     DefaultStyleTrigger,
   8:     Style,
   9:     TemplateTrigger,
  10:     StyleTrigger,
  11:     ImplicitStyleReference,
  12:     ParentTemplate,
  13:     ParentTemplateTrigger,
  14:     Local
  15: }

  对于一个具体例子来说:

   1: <Window>
   2:     <Window.Resources>
   3:         <Style TargetType="{x:Type Button}" x:Key="ButtonStyle">
   4:             <Setter Property="Width" Value="60"/>
   5:             <Style.Triggers>
   6:                 <Trigger Property="IsMouseOver" Value="True">
   7:                     <Setter Property="Width" Value="80"/>
   8:                 </Trigger>
   9:             </Style.Triggers>
  10:         </Style>
  11:     </Window.Resources>
  12:     <Grid>
  13:         <Button x:Name="button1" Style="{StaticResource ButtonStyle}" Background="{DynamicResource brush}" Width="20"/>
  14:     </Grid>
  15: </Window>

  第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:

  1. FocusVisualStyle:是当控件获得键盘焦点时,显示在外面的一个虚线框,这个Style并没有直接作用在对应的FrameworkElement上,而是当控件获得键盘焦点时使用AdornLayer创建了一个新的Control,然后再这个Control上使用FocusVisualStyle,再把它遮盖在对应的FrameworkElement上形成一个虚线框的效果。
  2. Style:就是我们前面一直设置的Style。
  3. ThemeStyle:这里引入了一个Theme的概念,具体来谈一下它。

  Windows定了很多Theme(主题),你可以在控制面板中切换Theme,如图:

 

Theme

  最上面的两排都属于Aero主题,当从Aero主题切换到Windows Classic主题后,任务栏,窗口以及窗口内的控件外观都会发生变化。为了更好的切换主题,WPF引入了ThemeStyle这个概念。当我们使用VS2010的模板生成一个自定义控件(Custom Control)后,会自动添加一个Themes的文件夹以及一个Generic.xaml的文件,如图:

generic

  这里的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中,如:

   1: [assembly: ThemeInfo(
   2:     ResourceDictionaryLocation.SourceAssembly,     
   3:     ResourceDictionaryLocation.SourceAssembly)
   4: )]

  ThemeInfo有两个参数,第一个参数指的是ThemeResource,第二个参数指的是GenericResource,它们的类型是ResourceDictionaryLocation:

   1: public enum ResourceDictionaryLocation
   2: {
   3:     None = 0,
   4:     SourceAssembly = 1,
   5:     ExternalAssembly = 2,
   6: }

  ResourceDictionaryLocation的None指不存在对应的Resource,SourceAssembly指该程序集(Assembly)中存在对应的Resource,ExternalAssembly指对应的Resource保存在外部的程序集(Assembly)中,这个外部程序集的查找规则就是我们前面看到的:程序集名称+Theme名称+.dll。

  对于一个控件,无论是系统自带的控件还是我们自定义的控件,WPF启动时都会通过当前Windows系统的Theme查找它对应的ThemeStyle。这个查找规则是:

  1. 先通过控件的类型(Type)找到它对应的程序集(Assembly),然后获取程序集中的ThemeInfo,看看它的ThemeResource和GenericResource在哪里。如果ThemeResource的值不是None,系统会读取到ThemeResource对应的ResourceDictionary,在这个ResourceDictionary中查找是否定义了TargetType={x:Type 控件类型},如果有,把控件的ThemeStyle指定为这个Style。
  2. 如果第一步的查找失败,那么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时使用的是类似:

   1: string relativePackUriForResources = "/" +
   2:         themeAssemblyName.FullName +
   3:         ";component/themes/" +
   4:         themeName + "." +
   5:         colorScheme + ".xaml";

这样的方法。

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实例),也就是说相当于:

   1: <Style TargetType="{x:Type Button}" x:Key="{x:Type Button}">

后面的x:Key可以省略掉。

  Appliation以及FrameworkElement类都定义了Resources属性,内部都持有一个ResourceDictionary,Resource查找遵循的最基本原则是就近原则,如:

   1: <Window>
   2:     <Window.Resources>
   3:         <Style TargetType="{x:Type Button}">
   4:             <Setter Property="Background" Value="Yellow"/>
   5:         </Style>
   6:         <Style TargetType="{x:Type ToggleButton}" x:Key="toggleBtnStyle">
   7:             <Setter Property="Background" Value="Red"/>
   8:         </Style>
   9:     </Window.Resources>
  10:     <StackPanel>
  11:         <StackPanel.Resources>
  12:             <Style TargetType="{x:Type Button}">
  13:                 <Setter Property="Background" Value="Blue"/>
  14:             </Style>
  15:             <Style TargetType="{x:Type ToggleButton}" x:Key="toggleBtnStyle">
  16:                 <Setter Property="Background" Value="Green"/>
  17:             </Style>
  18:         </StackPanel.Resources>
  19:         <ToggleButton Width="80" Height="20" Style="{DynamicResource toggleBtnStyle}"/>
  20:         <Button Width="80" Height="20"  Content="button2" Click="Button_Click"/>
  21:     </StackPanel>
  22: </Window>

  Window和StackPanel的Resources中都分别定义了toggleBtnStyle以及隐式Style(Button),根据就近原则,StackPanel内部的ToggleButton和Button会应用StackPanel的Resource而不会使用Window的。

Style Merge

  这里要提到本篇的重点也是不被人注意却经常出错的地方,Style的合并(Merge)。

  前面提到了很多Style,ThemeStyle,Style,隐式Style。我们提过,Style相当于一个属性值的批处理,那么对于一个属性,只能有一个预设值而不能多个,这些Style在运行时要进行合并,然后作用在FrameworkElement上。

  Style的合并,要分两步进行:

  1. 找到所有Style。
  2. 确定Style的优先级,根据优先级来合并Style。

  以Button来说:

  1. 如果当前Windows的Theme是Aero,启动后会从PresentationFramework.Aero.dll中找到对应的ThemeStyle。
  2. 如果在Button上使用StaticResource或者DynamicResource指定了Style,会通过键值在Resource系统中找到对应的Style。
  3. 如果没有在Button上显式指定Style,会通过Resource系统查找隐式Style(x:Type Button)。
  4. 第二步和第三步是排他的,这两步只能确定一个Style,然后把这个Style和ThemeStyle进行合并(Merge)得到Button最终的效果。

  先从合并来说,显式或者隐式Style的优先级是高于ThemeStyle的,如果Style和ThemeStyle的Setter中都对同一属性进行了预设,那么会取Style里面的Setter而忽略ThemeStyle。这里比较特殊的是EventSetter,EventSetter使用的是RoutedEvent,如果两个Style的EventSetter对同一个RoutedEvent进行了设置,两个都会注册到RoutedEvent上。

  前面看到,显式和隐式Style是排他的,两者只能取一,在实际项目中,在全局定义好Button的基本样式,然后具体使用上再根据基本样式做一些特殊处理,这种需求是很常见的。为了解决这种需求,Style提出了BasedOn属性,来表示继承关系,如:

   1: <Window>
   2:     <Window.Resources>
   3:         <Style TargetType="{x:Type Button}">
   4:             <Setter Property="Width" Value="80"/>
   5:             <Setter Property="Height" Value="20"/>
   6:             <EventSetter Event="Click" Handler="btnBase_Click"/>
   7:         </Style>
   8:         <Style TargetType="{x:Type ButtonBase}" x:Key="toggleBtnStyle">
   9:             <Setter Property="Width" Value="80"/>
  10:             <Setter Property="Height" Value="20"/>
  11:             <Setter Property="Background" Value="Red"/>
  12:         </Style>
  13:     </Window.Resources>
  14:     <StackPanel>
  15:         <StackPanel.Resources>
  16:             <Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
  17:                 <Setter Property="Background" Value="Blue"/>
  18:                 <EventSetter Event="Click" Handler="btn_Click"/>
  19:             </Style>
  20:             <Style TargetType="{x:Type ToggleButton}" x:Key="toggleBtnStyle" BasedOn="{StaticResource toggleBtnStyle}">
  21:                 <Setter Property="Background" Value="Green"/>
  22:             </Style>
  23:         </StackPanel.Resources>
  24:         <ToggleButton Style="{DynamicResource toggleBtnStyle}"/>
  25:         <Button Content="button2"/>
  26:     </StackPanel>
  27: </Window>

  为了更清晰的解释,给出了一个不太常见的例子。第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,在自定义控件中,经常可以看到这样的代码:

   1: static MyButton()
   2: {
   3:     DefaultStyleKeyProperty.OverrideMetadata(typeof(MyButton), new FrameworkPropertyMetadata(typeof(MyButton)));
   4: }

  这里出现了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的下面定义

   1: <Style TargetType="{x:Type local:MyButton}" BasedOn="{StaticResource {x:Type Button}}"/>

  这里就回到Style的合并(Merge)上来了,Style的Merge是很基本(很傻)的合并(Merge),它不具备Auto性。具体来说,就是:

  1. 基类控件的隐式Style不会作用到派生类控件上。
  2. 像前面在Window和StackPanel中分别定义了隐式Style(Button),这两个隐式Style不会智能合并后再作用到Button上,而是通过就近原则只选其一。
  3. 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来解决问题,类似:

   1: public static void Merge(this Style style, Style otherStyle)
   2: {
   3:     foreach (SetterBase currentSetter in otherStyle.Setters)
   4:     {
   5:         style.Setters.Add(currentSetter);
   6:     }
   7:  
   8:     foreach (TriggerBase currentTrigger in otherStyle.Triggers)
   9:     {
  10:         style.Triggers.Add(currentTrigger);
  11:     }
  12:  
  13:     foreach (object key in otherStyle.Resources.Keys)
  14:     {
  15:         style.Resources[key] = otherStyle.Resources[key];
  16:     }
  17: }

  这里还需要加上一些条件判断,以及决定是否要递归合并otherStyle的BasedOn,回到前面,程序需要使用DynamicResource来监听Application资源中隐式Style的变化,用一个附加属性来解决:

   1: public static readonly DependencyProperty AutoMergeStyleProperty =
   2:     DependencyProperty.RegisterAttached("AutoMergeStyle", typeof(Type), typeof(Behavior),
   3:         new FrameworkPropertyMetadata((Type)null,
   4:             new PropertyChangedCallback(OnAutoMergeStyleChanged)));
   5:  
   6: private static void OnAutoMergeStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   7: {
   8:     if (e.OldValue == e.NewValue)
   9:     {
  10:         return;
  11:     }
  12:  
  13:     FrameworkElement control = d as FrameworkElement;
  14:     if (control == null)
  15:     {
  16:         throw new NotSupportedException("AutoMergeStyle can only used in FrameworkElement");
  17:     }
  18:  
  19:     Type type = e.NewValue as Type;
  20:     if (type != null)
  21:     {
  22:         control.SetResourceReference(Behavior.BaseOnStyleProperty, type);
  23:     }
  24:     else
  25:     {
  26:         control.ClearValue(Behavior.BaseOnStyleProperty);
  27:     }
  28: }

  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系列也写了好几篇了,比起用嘴上白话一通,写文章需要更多的耐心和细致。讲解有很多境界:把简单的东西讲复杂;把复杂的东西讲复杂;把复杂的东西讲简单;把复杂的东西讲简单,而且还有诗情哲理。我达不到那么高的境界,希望能做到直接不回避的把技术主线讲清楚,也希望能更多的听到朋友们的反馈,我会继续补充,争取把这个系列写好。

  谢谢支持,谢谢您顶一下。 ^_^

 

 

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Add your comment

22 条回复

  1. #1楼 吾爱孟夫子      2011-08-01 13:30
    看了半天,到文章结尾才看到是周的文章,难怪这么好。
     回复 引用 查看   
  2. #2楼[楼主] 周永恒      2011-08-01 14:20
    @吾爱孟夫子
    谢谢捧场,过奖了。
     回复 引用 查看   
  3. #3楼 saleman      2011-08-01 14:37
    支持,学习了
     回复 引用 查看   
  4. #4楼 Work Hard Live Up      2011-08-01 15:37
    好文!有个问题请教下:利用ef4.1 获取有自关联表的 treeview 的当前选定项?
     回复 引用 查看   
  5. #5楼 Curry      2011-08-01 16:00
    鼓掌,不过换肤这节还是不太清楚,能不能弄个DEMO,或者弄个更具体的例子,先谢谢了。
     回复 引用 查看   
  6. #6楼[楼主] 周永恒      2011-08-01 16:04
    @Curry
    你可以看看我以前Illusion那个例子,换肤部分的代码要调整一下,不过思路是一样的。
     回复 引用 查看   
  7. #7楼 Learn more      2011-08-01 20:27
    好文啊,真详细,谢谢博主
     回复 引用 查看   
  8. #8楼 Lee's Blog      2011-08-01 22:23
    写的很详细。
     回复 引用 查看   
  9. #9楼 Curry      2011-08-02 13:30
    @周永恒
    谢谢,不过我们现在碰到个问题,如下:
        <Window.Resources>
            <ResourceDictionary Source="/PresentationFramework.Luna, Version=3.0.0.0, Culture=neutral,
                                PublicKeyToken=31bf3856ad364e35,
                                ProcessorArchitecture=MSIL;component/themes/luna.normalcolor.xaml" />
        </Window.Resources>
        <StackPanel>
            <Button>
                <Button.Style>
                    <Style TargetType="{x:Type Button}" >
                        <Setter  Property="Margin" Value="10"/>
                    </Style>
                </Button.Style>
                ok
            </Button>
        </StackPanel>
    

    如果你的主题是Areo里面的button样式就是Areo,但我想要的是Luna就只能写个BaseOn = {StaticResource {x:Type Button}}
     回复 引用 查看   
  10. #10楼[楼主] 周永恒      2011-08-02 13:52
    @Curry
    对啊,你需要写BaseOn属性,我说过Style的Merge是很傻的。你把Luna里面的Resource Merge到Windows中了,其中就包括{x:Type Button},但实际使用时应用了就近原则,根本不管你Windows中的Resource。

    有三个办法:
    1,在你自己定义的Style处写BasedOn={StaticResource {x:Type Button}}。
    2,如果你嫌麻烦,你可以用RoutedEvent注册全局的Loaded事件,然后拿到Application中的{x:Type Button}以及控件的style,这个Style已经Sealed了,你需要克隆一份style,然后设置style.BasedOn=application.对应Style,再赋给控件。
    3.使用我提到的Merge方法,这样支持DynamicResource替换,代码我给你粘贴到下面:
    我把代码给你粘贴过来,其中style.Merge(otherStyle)就是我文中的Merge Style。
    		#region AutoMergeStyle
    
    		public static readonly DependencyProperty AutoMergeStyleProperty =
    			DependencyProperty.RegisterAttached("AutoMergeStyle", typeof(Type), typeof(Behavior),
    				new FrameworkPropertyMetadata((Type)null,
    					new PropertyChangedCallback(OnAutoMergeStyleChanged)));
    
    		public static Type GetAutoMergeStyle(DependencyObject d)
    		{
    			return (Type)d.GetValue(AutoMergeStyleProperty);
    		}
    
    		public static void SetAutoMergeStyle(DependencyObject d, Type value)
    		{
    			d.SetValue(AutoMergeStyleProperty, value);
    		}
    
    		private static void OnAutoMergeStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    		{
    			if (e.OldValue == e.NewValue)
    			{
    				return;
    			}
    
    			FrameworkElement control = d as FrameworkElement;
    			if (control == null)
    			{
    				throw new NotSupportedException("AutoMergeStyle can only used in FrameworkElement");
    			}
    
    			Type type = e.NewValue as Type;
    
    			if (type != null)
    			{
    				control.SetResourceReference(Behavior.BaseOnStyleProperty, type);
    			}
    			else
    			{
    				control.ClearValue(Behavior.BaseOnStyleProperty);
    			}
    		}
    
    		#endregion
    
    		#region BaseOnStyle
    
    		public static readonly DependencyProperty BaseOnStyleProperty =
    			DependencyProperty.RegisterAttached("BaseOnStyle", typeof(Style), typeof(Behavior),
    				new FrameworkPropertyMetadata((Style)null,
    					new PropertyChangedCallback(OnBaseOnStyleChanged)));
    
    		public static Style GetBaseOnStyle(DependencyObject d)
    		{
    			return (Style)d.GetValue(BaseOnStyleProperty);
    		}
    
    		public static void SetBaseOnStyle(DependencyObject d, Style value)
    		{
    			d.SetValue(BaseOnStyleProperty, value);
    		}
    
    		private static void OnBaseOnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    		{
    			if (e.OldValue == e.NewValue)
    			{
    				return;
    			}
    
    			FrameworkElement control = d as FrameworkElement;
    			if (control == null)
    			{
    				throw new NotSupportedException("BaseOnStyle can only used in FrameworkElement");
    			}
    
    			Style baseOnStyle = e.NewValue as Style;
    			Style originalStyle = GetOriginalStyle(control);
    			if (originalStyle == null)
    			{
    				originalStyle = control.Style;
    				SetOriginalStyle(control, originalStyle);
    			}
    			Style newStyle = originalStyle;
    
    			if (originalStyle.IsSealed)
    			{
    				newStyle = new Style();
    				newStyle.TargetType = originalStyle.TargetType;
    				
    				//1. Merge
    				newStyle.Merge(originalStyle);
    
    				//2. Set BaseOn Style
    				newStyle.BasedOn = baseOnStyle;
    			}
    			else
    			{
    				originalStyle.BasedOn = baseOnStyle;
    			}
    
    			control.Style = newStyle;
    		}
    
    		#endregion
    
    		#region OriginalStyle
    
    		public static readonly DependencyProperty OriginalStyleProperty =
    			DependencyProperty.RegisterAttached("OriginalStyle", typeof(Style), typeof(Behavior),
    				new FrameworkPropertyMetadata((Style)null));
    
    		public static Style GetOriginalStyle(DependencyObject d)
    		{
    			return (Style)d.GetValue(OriginalStyleProperty);
    		}
    
    		public static void SetOriginalStyle(DependencyObject d, Style value)
    		{
    			d.SetValue(OriginalStyleProperty, value);
    		}
    
    		#endregion
    


    在XAML中使用
    <Button.Style>
        <Style TargetType="{x:Type Button}" > 
            <Setter Property="Margin" Value="10"/>
            <Setter Property="Behavior.AutoMergeStyle" Value="{x:Type Button}"/>
        </Style>
    </Button.Style>
    
    
     回复 引用 查看   
  11. #11楼 Curry      2011-08-02 16:15
    @周永恒
    谢谢,那个代码我昨天已经看了,不错的想法,可我感觉奇怪的是你变更操作系统主题的时候那个按钮样式是改变的,我其实比较想知道它内部是怎么做的,然后是否能够利用它内部的方法。

    可能表达的不是很清楚,再说下整个原因:
    1.我们在开发环境的时候用的是win7很显然主题是Areo,但是生产环境的机器是XP主题是经典。
    2.这样的有个问题,有些修改过样式的地方会是Areo,因为我们拷贝了Areo的样式,以致与整个样式不统一:有些是经典的有些是Areo的。
    3.我们想把样式都统一到Areo上来,所以在APP中Merge了Areo的样式。
    4.可我们发现凡是自己定义过样式的地方都会显现经典样式,也就是操作系统主题样式。
    5.我们不想每个自定义过Style的地方都写BaseOn。
    6.我们想知道为什么自定义过的地方为什么会默认的使用操作系统的主题,比如我上个问题中代码,如果在经典模式下,按钮的样式就是经典的,在Areo下就是Areo样式,按照操作系统来的。
    7.我们能不能利用这种方式来协调,默认的是操作系统风格,能不能指定到一个指定的主题上来。
     回复 引用 查看   
  12. #12楼 JerryT      2011-08-03 09:25
    你终于出下篇了
     回复 引用 查看   
  13. #13楼 攀攀      2011-08-03 10:42
    相当不错,最后换肤不太详细,没太看懂
     回复 引用 查看   
  14. #14楼[楼主] 周永恒      2011-08-03 11:11
    @攀攀
    @Curry

    有时间我写个Style-补再详细介绍一下吧
     回复 引用 查看   
  15. #15楼 潇潇兮      2011-08-05 15:38
    留爪~
     回复 引用 查看   
  16. #16楼 PTF      2011-08-09 15:40
    lz的博客让我学习的很多wpf知识,一直跟着,呵呵,希望lz坚持下去。
     回复 引用 查看   
  17. #17楼 Smlant.      2011-09-01 11:21
    不错 楼主写的很好 继续学习楼主的其他文章
     回复 引用 查看   
  18. #18楼 work hard work smart      2011-09-08 15:15
    后半部分不是很懂,最好有个例子吧,哈哈...
     回复 引用 查看   
  19. #19楼[楼主] 周永恒      2011-09-08 15:16
    @work hard work smart
    请参见前面Illusion换肤的例子
     回复 引用 查看   
  20. #20楼 要有好的心情      2011-11-07 17:43
    谢谢分享
     回复 引用 查看   
  21. #21楼 gaochuan.joey      2011-12-13 16:30
    楼主辛苦了!赞赞赞
     回复 引用 查看   
  22. #22楼 xiaokang088      2012-02-16 16:14
    最好,再加上 Template,就全了。
     回复 引用 查看   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 2123610 /ZRZ7ZS1QDM=