如何在 WPF 中应用多种样式

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

109237 次浏览

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

如果不涉及任何特定属性,则可以将所有基本属性和常见属性转换为样式,目标类型为 FrameworkElement。然后,您可以为您需要的每个目标类型创建特定的风味,而无需再次复制所有这些公共属性。

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

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

然而,正如其他人上面所说的,也许你可以使用 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>

如果你使用 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>

WPF/XAML 本身并不提供这种功能,但是它确实提供了可扩展性,允许您做您想做的事情。

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

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

Bea Stollnitz 在“如何在 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 将具有其 前景属性设置为 Red,其 Margin 属性设置为10。

这个特性在 WPF 中已经存在很长时间了,而且是新的 银光3。

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

WPF 和 Silverlight 使用标记扩展来提供属性 值,这些值需要一些逻辑才能获得。标记扩展很容易 中的花括号可以识别 例如,{ Binding }标记扩展包含 从数据源获取值,并在发生更改时更新该值; { StaticResource }标记扩展包含从中获取值的逻辑 基于键的资源字典。幸运的是,WPF 允许 用户编写自己的自定义标记扩展 但是仍然存在于 Silverlight 中,所以这个博客中的解决方案只有 适用于 WPF。

其他人 已经写了很好的解决方案来合并两种风格使用标记 然而,我想要一个解决方案,它能够提供 合并无限数量的样式,这有点棘手。

编写标记扩展很简单 创建一个从 MarkupExtension 派生的类,并使用 属性来指示您打算使用 从标记扩展返回的值为 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)
{
}

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

< 按钮样式 = “{ local: MultiStyle BigButtonStyle GreenButtonStyle }” .../>

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

计算标记扩展的输出

要计算标记扩展的输出,我们需要重写 方法,该方法名为“ ProviValue”。返回的值为 将在标记扩展的目标中设置。

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

如果我有两种样式的实例,那么这种语法是有用的 我不知道,我从构造函数得到的只是 这些样式的字符串键列表 在构造函数参数中,我可以使用以下内容 语法来获取实际的样式实例:

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

但这不管用,即使不存在参数限制, 我们可能会遇到标记扩展的另一个限制 我们必须使用 property-element 语法而不是 tribute 语法来指定静态资源,这是详细的,而且 麻烦(我在之前的博客中更好地解释了这个 bug 职位空缺)。 即使这两个限制都不存在,我还是宁愿 只用他们的名字写出风格列表-这样更短 比每一个静态资源更容易阅读。

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

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

作为风格;

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

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

enter image description here

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

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

希望能帮上忙。

有时您可以通过嵌套面板来解决这个问题。假设有一个样式可以改变前景,另一个样式可以改变 FontSize,你可以将后一个样式应用到 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;
}

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

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

用法: (将 Local = “ clr-nampace: style _ a _ class _ like _ css”指向正确的名称空间)

<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

如果您试图仅对一个元素 应用唯一的样式作为基本样式的补充,那么有一种完全不同的方法可以做到这一点,恕我直言,这种方法对于可读性和可维护性代码来说更好。

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

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