【问题标题】:How to apply multiple styles in WPF如何在 WPF 中应用多种样式
【发布时间】:2010-09-06 04:14:57
【问题描述】:

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

【问题讨论】:

  • OP 从未指定他的第一个样式是否仅对单个控件是唯一的。本页给出的答案假定需要在多个控件之间共享这两种样式。如果您正在寻找一种在控件上使用基本样式并直接在单个控件上覆盖单个属性的方法:请参阅此答案:stackoverflow.com/a/54497665/1402498

标签: .net wpf styles


【解决方案1】:

Bea Stollnitz 在“如何在 WPF 中设置多种样式?”标题下向 a good blog post 提出了关于为此使用标记扩展的意见。

那个博客现在已经死了,所以我在这里复制这篇文章:

WPF 和 Silverlight 都提供了从 另一个 Style 通过“BasedOn”属性。此功能使 开发人员使用类似于类的层次结构来组织他们的样式 遗产。考虑以下样式:

<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。

这个功能在 WPF 中已经存在很长时间了,它是新的 Silverlight 3.

如果你想在一个元素上设置多个样式怎么办? WPF都不是 Silverlight 也没有为这个问题提供开箱即用的解决方案。 幸运的是,有一些方法可以在 WPF 中实现这种行为,我 将在这篇博文中讨论。

WPF 和 Silverlight 使用标记扩展来提供属性 需要一些逻辑才能获得的值。标记扩展很容易 可以通过在它们周围的大括号来识别 XAML。例如,{Binding} 标记扩展包含逻辑 从数据源中获取值并在发生更改时更新它;这 {StaticResource} 标记扩展包含从中获取值的逻辑 基于键的资源字典。幸运的是,WPF 允许 用户编写自己的自定义标记扩展。此功能不 还存在于 Silverlight 中,因此本博客中的解决方案仅 适用于 WPF。

Others 编写了使用标记合并两种样式的出色解决方案 扩展名。但是,我想要一个能够提供以下功能的解决方案 合并无限数量的样式,这有点棘手。

编写标记扩展很简单。第一步是 创建一个派生自 MarkupExtension 的类,并使用 MarkupExtensionReturnType 属性表明您打算 从您的标记扩展返回的值为 Style 类型。

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

指定标记扩展的输入

我们希望为使用我们的标记扩展程序的用户提供一种简单的方法 指定要合并的样式。基本上有两种方式 用户可以指定标记扩展的输入。用户可以 设置属性或将参数传递给构造函数。由于在这 场景用户需要能够指定无限数量的 样式,我的第一种方法是创建一个构造函数,它接受任何 使用“params”关键字的字符串数:

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

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

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

请注意分隔不同样式键的逗号。很遗憾, 自定义标记扩展不支持无限数量的 构造函数参数,所以这种方法会导致编译错误。 如果我提前知道我想要合并多少种风格,我可以拥有 使用相同的 XAML 语法,构造函数采用所需的数字 字符串:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

作为一种解决方法,我决定让构造函数参数采用 单个字符串,指定由空格分隔的样式名称。这 语法还不错:

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.");
    }
}

计算标记扩展的输出

要计算标记扩展的输出,我们需要覆盖 来自 MarkupExtension 的方法称为“ProvideValue”。返回的值 from 这个方法将被设置在标记扩展的目标中。

我首先为 Style 创建了一个扩展方法,它知道如何 合并两种风格。这个方法的代码很简单:

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];
    }
}

通过上面的逻辑,第一个样式被修改为包含所有 来自第二个的信息。如果存在冲突(例如两种样式 具有相同属性的设置器),第二种样式获胜。注意 除了复制样式和触发器之外,我还考虑了 TargetType 和 BasedOn 值以及第二个的任何资源 风格可能有。对于合并样式的 TargetType,我使用了 无论哪种类型更派生。如果第二种样式有一个 BasedOn 样式,我递归地合并其样式层次结构。如果有 资源,我将它们复制到第一种样式。如果这些资源 提到使用 {StaticResource},它们之前是静态解析的 此合并代码执行,因此无需移动 他们。我添加了这段代码以防我们使用 DynamicResources。

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

style1.Merge(style2);

如果我有两种样式的实例,此语法很有用 在提供值内。好吧,我没有。我从构造函数中得到的只是 这些样式的字符串键列表。如果有支持 params 在构造函数参数中,我可以使用以下 获取实际样式实例的语法:

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

但这不起作用。即使不存在参数限制, 我们可能会遇到标记扩展的另一个限制,其中 我们将不得不使用属性元素语法而不是属性 指定静态资源的语法,它是冗长且 麻烦(我在previous blog post 中更好地解释了这个错误)。 即使这两个限制都不存在,我仍然宁愿 仅使用名称编写样式列表 - 它更短且 比一个静态资源更容易阅读。

解决方案是使用代码创建一个 StaticResourceExtension。给定 字符串类型的样式键和服务提供者,我可以使用 StaticResourceExtension 来检索实际的样式实例。这是 语法:

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

作为样式;

现在我们有了编写 ProvideValue 方法所需的所有部分:

public override object ProvideValue(IServiceProvider serviceProvider)
{
    Style resultStyle = new Style();

    foreach (string currentResourceKey in resourceKeys)
    {
        Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider)

作为样式;

        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" />

【讨论】:

  • 确实很好的解决方案,但是我不明白为什么没有简单的解决方案来合并3或+样式。
【解决方案2】:

如果您尝试将独特的样式应用于单个元素作为对基本样式的补充,那么有一种完全不同的方法可以做到这一点,恕我直言,它的可读性和可维护性要好得多代码。

需要调整每个元素的参数是非常常见的。定义仅用于一个元素的字典样式维护或理解起来非常麻烦。为避免仅为一次性元素调整创建样式,请在此处阅读我对自己问题的回答:

https://stackoverflow.com/a/54497665/1402498

【讨论】:

    【解决方案3】:

    使用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;
        }
    }
    

    用途:

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

    结果:

    【讨论】:

      【解决方案4】:

      当您覆盖 SelectStyle 时,您可以通过反射获得 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;
          }
      

      【讨论】:

        【解决方案5】:

        有时您可以通过嵌套面板来解决此问题。假设您有一个更改 Foreground 的 Style,另一个更改 FontSize,您可以将后者应用于 TextBlock,并将其放在其 Style 为第一个的 Grid 中。这可能会有所帮助,并且在某些情况下可能是最简单的方法,尽管它不能解决所有问题。

        【讨论】:

          【解决方案6】:

          但是你可以从另一个扩展......看看 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>
          

          【讨论】:

          • 这对我来说已经足够了。坦克!
          • 但这仅在两种样式属于同一类型时才有效(XAML 错误:“只能基于目标类型为基类型''的样式)
          【解决方案7】:

          我认为简单的答案是(至少在这个版本的 WPF 中)你不能做你想做的事情。

          也就是说,对于任何特定元素,只能应用一种样式。

          但是,正如上面其他人所说,也许您可​​以使用BasedOn 来帮助您。查看以下松散的 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 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 以绕过该限制。基本上,这意味着您需要为 Style 提供一个键并使用该键引用它。

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

          【讨论】:

          • 记住...**顺序很重要**。 derivedStyle 必须在 baseStyle 之后
          【解决方案8】:

          这可以通过创建一个帮助类来使用和包装你的样式来实现。提到的 CompoundStyle here 展示了如何做到这一点。有多种方法,但最简单的方法是执行以下操作:

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

          希望对您有所帮助。

          【讨论】:

            【解决方案9】:

            WPF/XAML 本身不提供此功能,但它确实提供了可扩展性,让您可以随心所欲。

            我们遇到了同样的需求,最终创建了自己的 XAML 标记扩展(我们称之为“MergedStylesExtension”),以允许我们从其他两种样式创建新样式(如果需要,可以使用多个连续多次继承更多样式)。

            由于 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/

            【讨论】:

              【解决方案10】:

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

              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;
                      }
                  }
              }
              

              然后你就这样应用它

              树视图>

              【讨论】:

                【解决方案11】:

                如果您不涉及任何特定属性,则可以获取目标类型为 FrameworkElement 的样式的所有基本属性和通用属性。然后,您可以为所需的每种目标类型创建特定的风格,而无需再次复制所有这些通用属性。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2012-07-28
                  • 2014-08-06
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多