Як застосувати кілька стилів у WPF


153

Як я можу застосувати кілька стилів до WPF у WPF FrameworkElement? Наприклад, у мене є елемент управління, який вже має стиль. У мене також є окремий стиль, який я хотів би додати до нього, не здуваючи першого. Стилі мають різні цільові типи, тому я не можу просто розширити один з іншим.


ОП ніколи не уточнював, чи є його перший стиль унікальним лише для одного управління. Відповіді, надані на цій сторінці, передбачають необхідність ділитися обома стилями в декількох елементах управління. Якщо ви шукаєте спосіб , щоб використовувати базові стилі для елементів управління і перевизначити індивідуальні властивості безпосередньо окремих елементів управління: см ця відповідь: stackoverflow.com/a/54497665/1402498
JamesHoux

Відповіді:


154

Я думаю, що проста відповідь полягає в тому, що ви не можете зробити (принаймні, в цій версії WPF) те, що намагаєтесь зробити.

Тобто для будь-якого конкретного елемента можна застосувати лише один Стиль.

Однак, як уже говорили інші, можливо, ви можете скористатися, BasedOnщоб допомогти вам. Ознайомтеся з наступним фрагментом сипучої ксамлі. У ній ви побачите, що у мене є базовий стиль, який встановлює властивість, що існує в базовому класі елемента, до якого я хочу застосувати два стилі. І, у другому стилі, який базується на базовому стилі, я встановив іншу властивість.

Отже, ідея тут ... полягає в тому, якщо ви зможете якось розділити властивості, які ви хочете встановити ... відповідно до ієрархії спадкування елемента, на який ви хочете встановити кілька стилів, ... у вас може бути вирішення проблеми.

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

10
Пам'ятайте ... ** Замовлення важливе **. derivedStyleПовинен прийти післяbaseStyle
SliverNinja - MSFT

50

У Беа Столніц була хороша публікація в блозі про використання розширення розмітки для цього під заголовком "Як я можу встановити кілька стилів у 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, матиме властивість переднього плану встановлено на 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
{
}

Визначення входів до розширення розмітки

Ми хотіли б дати користувачам нашого розширення розмітки простий спосіб вказати стилі, які потрібно об’єднати. По суті є два способи, за допомогою яких користувач може вказати входи до розширення розмітки. Користувач може встановити властивості або передати параметри конструктору. Оскільки в цьому сценарії користувачеві потрібна можливість задавати необмежену кількість стилів, моїм першим підходом було створення конструктора, який приймає будь-яку кількість рядків за допомогою ключового слова «парами»:

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

Я почав зі створення методу розширення для 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 об'єднаного стилю я використовував той тип, який є більш похідним. Якщо другий стиль має стиль BasicOn, я зливаю його ієрархію стилів рекурсивно. Якщо у нього є ресурси, я копіюю їх у перший стиль. Якщо для цих ресурсів посилається {StaticResource}, вони статично вирішуються до виконання цього коду злиття, і тому переміщувати їх не потрібно. Я додав цей код, якщо ми використовуємо DynamicResources.

Метод розширення, показаний вище, включає наступний синтаксис:

style1.Merge(style2);

Цей синтаксис корисний за умови, що у мене є екземпляри обох стилів у ProvideValue. Ну, я не хочу. Все, що я отримую від конструктора, - це список рядкових клавіш для цих стилів. Якби підтримка парам в параметрах конструктора, я міг би використати наступний синтаксис, щоб отримати фактичні екземпляри стилю:

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

Але це не працює. І навіть якби обмеження для парами не існувало, ми, мабуть, потрапили б до іншого обмеження розширень розмітки, де нам би довелося використовувати синтаксис властивості-елемента замість синтаксису атрибутів для визначення статичних ресурсів, який є багатослівним і громіздким (я пояснюю це помилка краще в попередній публікації блогу ). І навіть якщо обох цих обмежень не існувало, я все-таки краще напишу список стилів, використовуючи лише їх імена - він коротший і простіший для читання, ніж 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
Дійсно гарне рішення, але я не розумію, чому немає простого рішення злити 3 або + стиль.
Містер Рубікс

31

Але ви можете поширитись на інше .. подивіться на властивість 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>

це було для мене достатньо. tnks!
Девід Лежав

Але це працює лише в тому випадку, якщо обидва стилі одного типу (помилка XAML: "Можна базуватись лише на стилі з цільовим типом, який є базовим типом" <type> ")
Кшиштоф Боцюрко

17

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

Це можливо, створивши хелперний клас для використання та загортання своїх стилів. Згаданий тут CompoundStyle показує, як це зробити. Існує кілька способів, але найпростішим є наступне:

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

Сподіваюся, що це допомагає.


2

Використовуйте 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>

Результат:

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


1

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


1

Можливо, ви можете отримати щось подібне, застосувавши це до колекції елементів за допомогою 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

Іноді до цього можна підійти, вклавши панелі. Скажімо, у вас є стиль, який змінює передній план, а інший змінює FontSize, ви можете застосувати останній на TextBlock і помістити його в сітку, в якій його стиль є першим. Це може допомогти і може бути найпростішим способом у деяких випадках, хоча це не вирішить усіх проблем.


1

Якщо ви перекриєте 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;
    }

0

Якщо ви намагаєтесь застосувати унікальний стиль лише до одного єдиного елемента як доповнення до базового стилю, існує зовсім інший спосіб зробити це, що є IMHO набагато кращим для читаного та збереженого коду.

Надзвичайно часто потрібно налаштовувати параметри для кожного елемента. Визначення стилів словника лише для використання на одному елементі є надзвичайно громіздким у підтримці чи сенсі. Щоб уникнути створення стилів лише для одноразових налаштувань елементів, прочитайте тут свою відповідь на моє власне запитання:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.