在WPF中,我如何将多种样式应用于 FrameworkElement?例如,我有一个已经有样式的控件。我还有一个单独的风格,我想在不破坏第一个风格的情况下添加它。这些样式具有不同的 TargetType,因此我不能只用其中一种来扩展另一种。

有帮助吗?

解决方案

我认为简单的答案是您无法(至少在这个版本的 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>


希望这可以帮助。

笔记:

有一点特别要注意。如果您更改 TargetType 在第二种样式中(在上面的第一组 xaml 中) 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>

其他提示

碧亚·斯托尔尼茨 一篇好博文 有关为此使用标记扩展的信息,请参阅标题“如何在 WPF 中设置多种样式?”

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


WPF 和 Silverlight 都提供了通过“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。

其他的 已经编写了使用标记扩展合并两种样式的出色解决方案。然而,我想要一个能够合并无限数量样式的解决方案,这有点棘手。

编写标记扩展很简单。第一步是创建一个派生自 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”的方法。从此方法返回的值将在标记扩展的目标中设置。

我首先为 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];
    }
}

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

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

style1.Merge(style2);

如果我在 ProvideValue 中有两种样式的实例,则此语法很有用。嗯,我不知道。我从构造函数中得到的只是这些样式的字符串键列表。如果构造函数参数中支持 params,我可以使用以下语法来获取实际的样式实例:

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

但这行不通。即使参数限制不存在,我们也可能会遇到标记扩展的另一个限制,我们必须使用属性元素语法而不是属性语法来指定静态资源,这是冗长和麻烦的(我解释了这一点)错误更好 上一篇博文)。即使这两个限制都不存在,我仍然宁愿仅使用样式名称来编写样式列表 - 它比每个样式的 StaticResource 更短且更易于阅读。

解决方案是使用代码创建 StaticResourceExtension。给定字符串类型的样式键和服务提供者,我可以使用 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" />

enter image description here

但你可以从另一个扩展..查看 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 本身并不提供此功能,但它确实提供了可扩展性,允许您执行您想要的操作。

我们遇到了同样的需求,最终创建了自己的 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/

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

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

希望有帮助。

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

结果:

enter image description here

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

如果通过使用 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;
        }
    }
}

然后你就这样应用它

 <TreeView>
     <TreeView.ItemContainerStyleSelector
         <myassembly:MyTreeStyleSelector DefaultStyle="{StaticResource DefaultItemStyle}"
                                         NewStyle="{StaticResource NewItemStyle}" />
     </TreeView.ItemContainerStyleSelector>
  </TreeView>

有时您可以通过嵌套面板来解决此问题。假设您有一个更改前景的样式,另一个更改字体大小的样式,您可以将后一个样式应用于 TextBlock,并将其放入其样式为第一个样式的网格中。这可能会有所帮助,并且在某些情况下可能是最简单的方法,尽管它不能解决所有问题。

当您重写 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;
    }

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top