Как применить несколько стилей в WPF - PullRequest
140 голосов
/ 19 августа 2008

Как в WPF применить несколько стилей к FrameworkElement? Например, у меня есть элемент управления, который уже имеет стиль. У меня также есть отдельный стиль, который я хотел бы добавить к нему, не выбрасывая первый. Стили имеют разные TargetTypes, поэтому я не могу просто расширить один с другим.

Ответы [ 11 ]

144 голосов
/ 03 октября 2008

Я думаю, что простой ответ заключается в том, что вы не можете (по крайней мере, в этой версии 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>
42 голосов
/ 08 декабря 2009

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

При таком синтаксисе для свойства Button, использующего RedButtonStyle, будет установлено свойство Redground 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». Значение, возвращаемое этим методом, будет установлено в качестве цели расширения разметки.

Я начал с создания метода расширения для стиля, который знает, как объединить два стиля. Код для этого метода довольно прост:

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. Ну, я не Все, что я получаю от конструктора, это список строковых ключей для этих стилей. Если бы была поддержка параметров в параметрах конструктора, я мог бы использовать следующий синтаксис для получения реальных экземпляров стиля:

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

Но это не работает. И даже если бы ограничение params не существовало, мы, вероятно, столкнулись бы с другим ограничением расширений разметки, где нам пришлось бы использовать синтаксис элемента свойства вместо синтаксиса атрибута для указания статических ресурсов, что является многословным и громоздким (я объясняю ошибка лучше в предыдущем сообщении в блоге ). И даже если бы этих ограничений не было, я бы все же предпочел написать список стилей, используя только их имена - он короче и проще для чтения, чем StaticResource для каждого из них.

Решением является создание StaticResourceExtension с использованием кода. Учитывая ключ стиля типа string и поставщика услуг, я могу использовать 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

31 голосов
/ 19 августа 2008

Но вы можете перейти от другого .. взгляните на свойство 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>
17 голосов
/ 04 января 2009

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/

3 голосов
/ 04 октября 2012

Это возможно, создав вспомогательный класс для использования и переноса ваших стилей. Упомянутый CompoundStyle здесь показывает, как это сделать. Есть несколько способов, но проще всего сделать следующее:

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

Надеюсь, это поможет.

2 голосов
/ 11 сентября 2017

Используйте 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

1 голос
/ 05 февраля 2014

Когда вы переопределяете 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;
    }
1 голос
/ 24 ноября 2013

Иногда вы можете подойти к этому, вложив панели. Допустим, у вас есть стиль, который меняет Foreground, а другой - FontSize, вы можете применить последний к TextBlock и поместить его в Grid, стиль которого является первым. Это может помочь и может быть самым простым способом в некоторых случаях, хотя и не решит всех проблем.

1 голос
/ 15 октября 2008

Возможно, вы получите что-то похожее, применив это к коллекции элементов с помощью 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>
1 голос
/ 16 сентября 2008

если вы не касаетесь каких-либо определенных свойств, вы можете получить все базовые и общие свойства для стиля, целевым типом которого будет FrameworkElement. затем вы можете создавать специфические варианты для каждого целевого типа, без необходимости повторного копирования всех этих общих свойств.

...