在 WPF 中,如何将多种样式应用于FrameworkElement
? 例如,我有一个已经有样式的控件。我也有一个单独的风格,我想添加到它而不吹走第一个风格。这些样式有不同的 TargetTypes,所以我不能只用另一个扩展。
11 回答
我认为简单的答案是你不能做你想做的事情(至少在这个版本的 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 以绕过该限制。基本上,这意味着您需要给 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>
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 将其 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) { }
作为一种解决方法,我决定让构造函数参数采用单个字符串,该字符串指定由空格分隔的样式名称。语法还不错:
<Button Style="{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."); } }
计算标记扩展的输出
要计算标记扩展的输出,我们需要从 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]; } }
使用上面的逻辑,第一种样式被修改为包含第二种样式的所有信息。如果存在冲突(例如,两种样式都有相同属性的设置器),则第二种样式获胜。请注意,除了复制样式和触发器之外,我还考虑了 TargetType 和 BasedOn 值以及第二种样式可能具有的任何资源。对于合并样式的 TargetType,我使用了更衍生的类型。如果第二种样式具有 BasedOn 样式,我会递归地合并其样式层次结构。如果它有资源,我将它们复制到第一个样式。如果这些资源是使用 {StaticResource} 引用的,则它们会在此合并代码执行之前静态解析,因此无需移动它们。我添加了这段代码以防我们使用 DynamicResources。
上面显示的扩展方法启用以下语法:
style1.Merge(style2);
如果我在 ProvideValue 中有两种样式的实例,则此语法很有用。好吧,我没有。我从构造函数中得到的只是这些样式的字符串键列表。如果在构造函数参数中支持 params,我可以使用以下语法来获取实际的样式实例:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles) { }
但这不起作用。即使不存在 params 限制,我们也可能会遇到标记扩展的另一个限制,我们必须使用属性元素语法而不是属性语法来指定静态资源,这既冗长又繁琐(我解释了这个以前的博客文章中的错误更好)。即使这两个限制都不存在,我仍然宁愿只使用它们的名称来编写样式列表——它比每个样式的 StaticResource 更短、更易于阅读。
解决方案是使用代码创建一个 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" />
但是你可以从另一个扩展..看看 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/
这可以通过创建一个辅助类来使用和包装您的样式来实现。这里提到的 CompoundStyle展示了如何做到这一点。有多种方法,但最简单的方法是执行以下操作:
<TextBlock Text="Test"
local:CompoundStyle.StyleKeys="headerStyle,textForMessageStyle,centeredStyle"/>
希望有帮助。
用于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;
}
}
用法:(将xmlns:local="clr-namespace: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>
结果:
如果您没有触及任何特定属性,则可以获取目标类型为 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.ItemContainerStyleSelector <myassembly:MyTreeStyleSelector DefaultStyle="{StaticResource DefaultItemStyle}" NewStyle="{StaticResource NewItemStyle}" /> </TreeView.ItemContainerStyleSelector> </树视图>
有时您可以通过嵌套面板来解决此问题。假设您有一个更改 Foreground 的 Style,另一个更改 FontSize,您可以将后者应用于 TextBlock,并将其放在其 Style 为第一个的 Grid 中。这可能会有所帮助,并且在某些情况下可能是最简单的方法,尽管它不能解决所有问题。
当您覆盖 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;
}
如果您尝试将独特的样式应用于仅一个元素作为基本样式的补充,那么有一种完全不同的方法可以做到这一点,恕我直言,对于可读和可维护的代码来说更好。
需要调整每个元素的参数是非常常见的。定义仅用于一个元素的字典样式维护或理解起来非常麻烦。为避免仅为一次性元素调整创建样式,请在此处阅读我对自己问题的回答: