Как применить несколько стилей в WPF

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

Ответ 1

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

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, будет иметь свойство 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". Значение, возвращаемое этим методом, будет установлено в целевом расширении разметки.

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

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

введите описание изображения здесь

Ответ 3

Но вы можете перейти от другого. Посмотрите на свойство 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

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

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

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

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

Ответ 6

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

Результат:

введите описание изображения здесь

Ответ 7

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

Ответ 8

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

Ответ 9

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

Ответ 10

При переопределении 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

Если вы пытаетесь применить уникальный стиль только к одному элементу в качестве дополнения к базовому стилю, есть совершенно другой способ сделать это, который ИМХО намного лучше для удобочитаемого и поддерживаемого кода.

Очень часто необходимо настроить параметры для каждого элемента. Определение стилей словаря, предназначенных только для использования с одним элементом, чрезвычайно сложно поддерживать или иметь смысл. Чтобы избежать создания стилей только для настройки одного элемента, прочитайте мой ответ на мой собственный вопрос здесь:

fooobar.com/questions/17021045/...

Ответ 12

вы можете определить такой стиль:

    <Style x:Key="buttonStyle">
    <Setter Property="Button.FontSize" Value="22"/>
    <Setter Property="Button.Background" Value="Purple"/>
    <Setter Property="Button.Foreground" Value="White"/>
    <Setter Property="Button.Height" Value="50"/>
    <Setter Property="Button.Width" Value="50"/>
    <Setter Property="Button.RenderTransformOrigin" Value=".5,.5"/>
    <Setter Property="Button.RenderTransform">
    <Setter.Value>
    <RotateTransform Angle="10"/>
    </Setter.Value>
    </Setter>
</Style>

и выполните следующие действия:

<Button Style="{StaticResource buttonStyle}">1</Button>
<Button Style="{StaticResource buttonStyle}">2</Button>
<Button Style="{StaticResource buttonStyle}">3</Button>

если вы хотите определить другой стиль, просто укажите ему другое имя:

<Style x:Key="buttonStyleBlue">
<Setter Property="Button.FontSize" Value="22"/>
<Setter Property="Button.Background" Value="Blue"/>
<Setter Property="Button.Foreground" Value="Black"/>
<Setter Property="Button.Height" Value="50"/>
<Setter Property="Button.Width" Value="50"/>
<Setter Property="Button.RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="Button.RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>

а затем примените его с помощью x: Key, который вы определили