代码之家  ›  专栏  ›  技术社区  ›  MojoFilter

如何在WPF中应用多种样式

  •  136
  • MojoFilter  · 技术社区  · 16 年前

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

    11 回复  |  直到 6 年前
        1
  •  141
  •   akjoshi HCP    12 年前

    我认为简单的答案是你不能做(至少在这个版本的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>
    
        2
  •  40
  •   Wilka    8 年前

    比亚斯托尔尼茨 a good blog post 关于为此使用标记扩展名,请在标题“如何在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的按钮将其Foreground属性设置为红色,其Margin属性设置为10。

    这个功能在WPF中已经存在很长一段时间了,它在Silverlight3中是新的。

    如果你想在一个元素上设置多个样式怎么办?WPF和Silverlight都没有现成的解决方案来解决这个问题。幸运的是,有一些方法可以在WPF中实现这种行为,我将在本文中讨论。

    WPF和Silverlight使用标记扩展为属性提供需要某些逻辑才能获取的值。标记扩展可以通过在XAML中围绕它们的大括号很容易识别。例如,绑定标记扩展包含从数据源获取值并在发生更改时更新该值的逻辑;静态资源标记扩展包含从基于键的资源字典获取值的逻辑。幸运的是,WPF允许用户编写自己的自定义标记扩展。Silverlight中还没有此功能,因此此日志中的解决方案仅适用于WPF。

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

    编写标记扩展是很简单的。第一步是创建一个从markup extension派生的类,并使用markupextensionReturnType属性指示您希望从标记扩展返回的值具有类型样式。

    [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的方法。此方法返回的值将在标记扩展的目标中设置。

    我首先为知道如何合并两个样式的样式创建一个扩展方法。此方法的代码非常简单:

    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引用这些资源,则在执行此合并代码之前静态解析这些资源,因此无需移动它们。我添加了这段代码以防使用动态资源。

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

    style1.Merge(style2);
    

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

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

    以下是使用多样式标记扩展的完整示例:

    <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

        3
  •  30
  •   akjoshi HCP    12 年前

    但你可以从另一个扩展……看看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>
    
        4
  •  17
  •   Jeff    16 年前

    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/

        5
  •  3
  •   Shahar Prish    12 年前

    这可以通过创建一个助手类来使用和包装样式。提到的复合样式 here 演示如何执行。有多种方法,但最简单的方法是:

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

    希望有帮助。

        6
  •  2
  •   google dev    7 年前

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

    结果:

    enter image description here

        7
  •  1
  •   Greg    16 年前

    如果不接触任何特定属性,则可以将所有基本属性和公共属性获取到目标类型为FrameworkElement的样式。然后,您可以为所需的每个目标类型创建特定的口味,而无需再次复制所有这些公共属性。

        8
  •  1
  •   Dave    16 年前

    如果通过使用样式选择器将此应用于项集合,您可能会得到类似的结果,我使用此方法处理了根据树中绑定的对象类型在TreeView项上使用不同样式时的类似问题。您可能需要稍微修改下面的类以适应您的特定方法,但希望这将使您开始学习。

    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>
    
        9
  •  1
  •   hillin    11 年前

    有时您可以通过嵌套面板来实现这一点。假设你有一个改变前景的样式和另一个改变字体大小的样式,你可以将后一个应用到一个文本块上,并把它放在一个网格中,它的样式是第一个。这可能有帮助,在某些情况下可能是最简单的方法,尽管它不能解决所有问题。

        10
  •  1
  •   Sérgio Henrique    11 年前

    当重写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;
        }
    
        11
  •  0
  •   JamesHoux    6 年前

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

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

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