如何在WPF中应用多个样式

在WPF中,我将如何将多个样式应用于FrameworkElement ? 例如,我有一个已经有风格的控件。 我也有一个独立的风格,我想补充它,而不是吹走第一个。 样式有不同的TargetTypes,所以我不能只是扩展一个。

我认为简单的答案是,你不能做(至less在这个版本的WPF)你正在做什么。

也就是说,对于任何特定的元素,只能使用一种风格。

但是,正如其他人所说的,也许你可以使用BasedOn来帮助你。 看看下面一张松散的xaml。 在这里你会看到我有一个基础样式,它设置了我想要应用两个样式的元素的基类上存在的属性。 而且,在基于基本风格的第二种风格中,我设置了另一个属性。

所以,这里的想法是,如果你可以以某种方式分离你想要设置的属性…根据你想设置多个样式的元素的inheritance层次…你可能有一个解决方法。

 <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Page.Resources> <Style x:Key="baseStyle" TargetType="FrameworkElement"> <Setter Property="HorizontalAlignment" Value="Left"/> </Style> <Style TargetType="Button" BasedOn="{StaticResource baseStyle}"> <Setter Property="Content" Value="Hello World"/> </Style> </Page.Resources> <Grid> <Button Width="200" Height="50"/> </Grid> </Page> 

希望这可以帮助。

注意:

有一点特别要注意。 如果将第二种样式(在上面的第一组xaml中)中的TargetType更改为ButtonBase ,则不会应用这两个样式。 不过,请查看下面的xaml来解决这个限制。 基本上,这意味着你需要给风格一把钥匙,并用该钥匙来引用它。

 <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Page.Resources> <Style x:Key="baseStyle" TargetType="FrameworkElement"> <Setter Property="HorizontalAlignment" Value="Left"/> </Style> <Style x:Key="derivedStyle" TargetType="ButtonBase" BasedOn="{StaticResource baseStyle}"> <Setter Property="Content" Value="Hello World"/> </Style> </Page.Resources> <Grid> <Button Width="200" Height="50" Style="{StaticResource derivedStyle}"/> </Grid> </Page> 

Bea Stollnitz在标题为“如何在WPF中设置多个样式?”的标题下有一篇关于使用标记扩展的好博客文章 。

那个博客现在已经死了,所以我在这里复制这个post


WPF和Silverlight都提供了通过“BasedOn”属性从另一个样式派生样式的function。 此function使开发人员能够使用与类inheritance类似的层次来组织他们的样式。 考虑以下样式:

 <Style TargetType="Button" x:Key="BaseButtonStyle"> <Setter Property="Margin" Value="10" /> </Style> <Style TargetType="Button" x:Key="RedButtonStyle" BasedOn="{StaticResource BaseButtonStyle}"> <Setter Property="Foreground" Value="Red" /> </Style> 

使用此语法,使用RedButtonStyle的Button将其Foreground属性设置为Red,并将其Margin属性设置为10。

这个function在WPF中已经有很长时间了,在Silverlight 3中也是新function。

如果你想在一个元素上设置多个样式呢? WPF和Silverlight都没有为这个问题提供解决scheme。 幸运的是,有些方法可以在WPF中实现这个行为,我将在这篇博文中讨论这个问题。

WPF和Silverlight使用标记扩展来提供需要一些逻辑来获取的值的属性。 标记扩展很容易通过XAML中围绕它们的大括号来识别。 例如,{Binding}标记扩展包含从数据源获取值并在发生更改时进行更新的逻辑; {StaticResource}标记扩展包含从资源字典中获取基于密钥的值的逻辑。 对我们来说幸运的是,WPF允许用户编写自己的自定义标记扩展。 此function在Silverlight中尚不存在,因此此博客中的解决scheme仅适用于WPF。

其他人已经写出了很好的解决scheme来使用标记扩展来合并两种样式 但是,我想要一个能够合并无限数量样式的解决scheme,这有点棘手。

编写标记扩展很简单。 第一步是创build一个派生自MarkupExtension的类,并使用MarkupExtensionReturnType属性来指示您打算从标记扩展返回的值为Styletypes。

 [MarkupExtensionReturnType(typeof(Style))] public class MultiStyleExtension : MarkupExtension { } 

指定标记扩展的input

我们希望给用户标记扩展一个简单的方法来指定要合并的样式。 基本上有两种用户可以指定标记扩展input的方法。 用户可以设置属性或传递参数给构造函数。 由于在这种情况下用户需要指定无限数量的样式的能力,我的第一种方法是创build一个构造函数,使用“params”关键字来获取任意数量的string:

 public MultiStyleExtension(params string[] inputResourceKeys) { } 

我的目标是能够写入input如下:

 <Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … /> 

注意分隔不同的样式键的逗号。 不幸的是,自定义标记扩展不支持无限数量的构造函数参数,所以这种方法会导致编译错误。 如果我事先知道要合并多less种样式,我可以使用相同的XAML语法和一个构造函数来获取所需数量的string:

 public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2) { } 

作为一种解决方法,我决定让构造函数参数采用一个单独的string来指定由空格分隔的样式名称。 语法不是太糟糕:

 private string[] resourceKeys; public MultiStyleExtension(string inputResourceKeys) { if (inputResourceKeys == null) { throw new ArgumentNullException("inputResourceKeys"); } this.resourceKeys = inputResourceKeys.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (this.resourceKeys.Length == 0) { throw new ArgumentException("No input resource keys specified."); } } 

计算标记扩展的输出

要计算标记扩展的输出,我们需要重写一个名为“ProvideValue”的MarkupExtension方法。 从这个方法返回的值将被设置在标记扩展的目标中。

我开始创build一个样式知道如何合并两个样式的扩展方法。 这个方法的代码很简单:

 public static void Merge(this Style style1, Style style2) { if (style1 == null) { throw new ArgumentNullException("style1"); } if (style2 == null) { throw new ArgumentNullException("style2"); } if (style1.TargetType.IsAssignableFrom(style2.TargetType)) { style1.TargetType = style2.TargetType; } if (style2.BasedOn != null) { Merge(style1, style2.BasedOn); } foreach (SetterBase currentSetter in style2.Setters) { style1.Setters.Add(currentSetter); } foreach (TriggerBase currentTrigger in style2.Triggers) { style1.Triggers.Add(currentTrigger); } // This code is only needed when using DynamicResources. foreach (object key in style2.Resources.Keys) { style1.Resources[key] = style2.Resources[key]; } } 

通过上面的逻辑,第一个样式被修改为包含第二个样式的所有信息。 如果存在冲突(例如,两个样式都具有相同属性的setter),则第二个样式将胜出。 注意,除了复制样式和触发器之外,我还考虑了TargetType和BasedOn值以及第二种样式可能具有的任何资源。 对于合并样式的TargetType,我使用了更多派生的types。 如果第二个样式具有BasedOn样式,我将recursion地合并它的样式层次结构。 如果它有资源,我将它们复制到第一个样式。 如果使用{StaticResource}引用这些资源,则在合并代码执行之前,它们将被静态parsing,因此不必移动它们。 我添加了这个代码,以防我们使用DynamicResources。

上面显示的扩展方法启用以下语法:

 style1.Merge(style2); 

这个语法很有用,前提是我在ProvideValue中有两个样式的实例。 那么,我不知道。 我从构造函数中得到的是这些样式的string键列表。 如果在构造函数参数中支持参数,我可以使用下面的语法来获得实际的样式实例:

 <Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … /> 
 public MultiStyleExtension(params Style[] styles) { } 

但是这不起作用。 即使params的限制不存在,我们可能会遇到另一个标记扩展的限制,在这里我们必须使用property-element语法而不是属性语法来指定静态资源,这是冗长和繁琐的(我解释了这一点在以前的博客文章中错误更好)。 即使这两个限制都不存在,我仍然宁愿使用他们的名字来编写样式列表 – 它比每个StaticResource的读取都要简短得多。

解决scheme是使用代码创build一个StaticResourceExtension。 给定一个stringtypes和服务提供者的样式键,我可以使用StaticResourceExtension来检索实际的样式实例。 这里是语法:

 Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style; 

现在我们有了编写ProvideValue方法所需的所有代码:

 public override object ProvideValue(IServiceProvider serviceProvider) { Style resultStyle = new Style(); foreach (string currentResourceKey in resourceKeys) { Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style; if (currentStyle == null) { throw new InvalidOperationException("Could not find style with resource key " + currentResourceKey + "."); } resultStyle.Merge(currentStyle); } return resultStyle; } 

以下是MultiStyle标记扩展的使用的完整示例:

 <Window.Resources> <Style TargetType="Button" x:Key="SmallButtonStyle"> <Setter Property="Width" Value="120" /> <Setter Property="Height" Value="25" /> <Setter Property="FontSize" Value="12" /> </Style> <Style TargetType="Button" x:Key="GreenButtonStyle"> <Setter Property="Foreground" Value="Green" /> </Style> <Style TargetType="Button" x:Key="BoldButtonStyle"> <Setter Property="FontWeight" Value="Bold" /> </Style> </Window.Resources> <Button Style="{local:MultiStyle SmallButtonStyle GreenButtonStyle BoldButtonStyle}" Content="Small, green, bold" /> 

在这里输入图像说明

但是,你可以从另一个扩展..看看BasedOn属性

 <Style TargetType="TextBlock"> <Setter Property="Margin" Value="3" /> </Style> <Style x:Key="AlwaysVerticalStyle" TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}"> <Setter Property="VerticalAlignment" Value="Top" /> </Style> 

WPF / XAML本身并不提供这个function,但它提供了可扩展性,可以让你做你想做的事情。

我们遇到了同样的需求,最终创build了自己的XAML标记扩展(我们称之为“MergedStylesExtension”),允许我们从另外两种样式创build一个新样式(如果需要的话,可能会多次使用行以inheritance更多的样式)。

由于WPF / XAML的错误,我们需要使用属性元素语法来使用它,但除此之外,它似乎工作正常。 例如,

 <Button Content="This is an example of a button using two merged styles"> <Button.Style> <ext:MergedStyles BasedOn="{StaticResource FirstStyle}" MergeStyle="{StaticResource SecondStyle}"/> </Button.Style> </Button> 

我最近在这里写了: http : //swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/

这可以通过创build一个帮助类来使用和包装你的样式。 这里提到的CompoundStyle显示了如何去做。 有多种方式,但最简单的是做到以下几点:

 <TextBlock Text="Test" local:CompoundStyle.StyleKeys="headerStyle,textForMessageStyle,centeredStyle"/> 

希望有所帮助。

如果你没有触及任何特定的属性,你可以得到所有的基础和共同的属性的风格,它的目标types将是FrameworkElement。 那么您可以为每个需要的目标types创build特定的风格,而不需要再次复制所有这些常用属性。

你也许可以得到类似的东西,如果通过使用StyleSelector将其应用于项目集合,我已经使用它来解决在TreeViewItems上使用不同样式的类似问题,具体取决于树中绑定的对象types。 您可能需要稍微修改下面的类以适应您的特定方法,但希望这会使您开始

 public class MyTreeStyleSelector : StyleSelector { public Style DefaultStyle { get; set; } public Style NewStyle { get; set; } public override Style SelectStyle(object item, DependencyObject container) { ItemsControl ctrl = ItemsControl.ItemsControlFromItemContainer(container); //apply to only the first element in the container (new node) if (item == ctrl.Items[0]) { return NewStyle; } else { //otherwise use the default style return DefaultStyle; } } } 

然后你这样应用

  <树视图>
      <TreeView.ItemContainerStyleSelector
          <myassembly:MyTreeStyleSelector DefaultStyle =“{StaticResource DefaultItemStyle}”
                                          NewStyle =“{StaticResource NewItemStyle}”/>
      </TreeView.ItemContainerStyleSelector>
   </树视图>

有时你可以通过嵌套面板来解决这个问题。 假设你有一个Style改变了Foreground,另一个改变了FontSize,你可以在TextBlock上应用后者,并把它放在Grid中,其样式是第一个。 这可能有所帮助,可能是在某些情况下最简单的方法,但它不能解决所有问题。

当你重写SelectStyle时,你可以像下面这样通过reflection来获得GroupBy属性:

  public override Style SelectStyle(object item, DependencyObject container) { PropertyInfo p = item.GetType().GetProperty("GroupBy", BindingFlags.NonPublic | BindingFlags.Instance); PropertyGroupDescription propertyGroupDescription = (PropertyGroupDescription)p.GetValue(item); if (propertyGroupDescription != null && propertyGroupDescription.PropertyName == "Title" ) { return this.TitleStyle; } if (propertyGroupDescription != null && propertyGroupDescription.PropertyName == "Date") { return this.DateStyle; } return null; } 

使用AttachedProperty设置多个样式,如下面的代码:

 public class Css { public static string GetClass(DependencyObject element) { if (element == null) throw new ArgumentNullException("element"); return (string)element.GetValue(ClassProperty); } public static void SetClass(DependencyObject element, string value) { if (element == null) throw new ArgumentNullException("element"); element.SetValue(ClassProperty, value); } public static readonly DependencyProperty ClassProperty = DependencyProperty.RegisterAttached("Class", typeof(string), typeof(Css), new PropertyMetadata(null, OnClassChanged)); private static void OnClassChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ui = d as FrameworkElement; Style newStyle = new Style(); if (e.NewValue != null) { var names = e.NewValue as string; var arr = names.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (var name in arr) { Style style = ui.FindResource(name) as Style; foreach (var setter in style.Setters) { newStyle.Setters.Add(setter); } foreach (var trigger in style.Triggers) { newStyle.Triggers.Add(trigger); } } } ui.Style = newStyle; } } 

Usege:

 <Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:style_a_class_like_css" mc:Ignorable="d" Title="MainWindow" Height="150" Width="325"> <Window.Resources> <Style TargetType="TextBlock" x:Key="Red" > <Setter Property="Foreground" Value="Red"/> </Style> <Style TargetType="TextBlock" x:Key="Green" > <Setter Property="Foreground" Value="Green"/> </Style> <Style TargetType="TextBlock" x:Key="Size18" > <Setter Property="FontSize" Value="18"/> <Setter Property="Margin" Value="6"/> </Style> <Style TargetType="TextBlock" x:Key="Bold" > <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <StackPanel> <Button Content="Button" local:Css.Class="Red Bold" Width="75"/> <Button Content="Button" local:Css.Class="Red Size18" Width="75"/> <Button Content="Button" local:Css.Class="Green Size18 Bold" Width="75"/> </StackPanel> </Window> 

结果:

在这里输入图像说明

你可以像这样定义一个样式:

  <Style x:Key=”buttonStyle”> <Setter Property=”Button.FontSize” Value=”22”/> <Setter Property=”Button.Background” Value=”Purple”/> <Setter Property=”Button.Foreground” Value=”White”/> <Setter Property=”Button.Height” Value=”50”/> <Setter Property=”Button.Width” Value=”50”/> <Setter Property=”Button.RenderTransformOrigin” Value=”.5,.5”/> <Setter Property=”Button.RenderTransform”> <Setter.Value> <RotateTransform Angle=”10”/> </Setter.Value> </Setter> </Style> 

并像这样申请:

 <Button Style=”{StaticResource buttonStyle}”>1</Button> <Button Style=”{StaticResource buttonStyle}”>2</Button> <Button Style=”{StaticResource buttonStyle}”>3</Button> 

如果你想定义另一种风格,就给它另一个名字,如:

 <Style x:Key=”buttonStyleBlue”> <Setter Property=”Button.FontSize” Value=”22”/> <Setter Property=”Button.Background” Value=”Blue”/> <Setter Property=”Button.Foreground” Value=”Black”/> <Setter Property=”Button.Height” Value=”50”/> <Setter Property=”Button.Width” Value=”50”/> <Setter Property=”Button.RenderTransformOrigin” Value=”.5,.5”/> <Setter Property=”Button.RenderTransform”> <Setter.Value> <RotateTransform Angle=”10”/> </Setter.Value> </Setter> 

然后将其应用于您定义的x:Key