Jak zastosować wiele stylów w WPF


153

W WPF, jak zastosować wiele stylów do pliku FrameworkElement? Na przykład mam kontrolkę, która ma już styl. Mam też osobny styl, który chciałbym do niego dodać bez zdmuchiwania pierwszego. Style mają różne typy TargetTypes, więc nie mogę po prostu rozszerzyć jednego z drugim.


OP nigdy nie określił, czy jego pierwszy styl jest unikalny dla jednej pojedynczej kontrolki. Odpowiedzi podane na tej stronie zakładają potrzebę współdzielenia obu stylów w wielu kontrolkach. Jeśli szukasz sposobu na używanie stylów podstawowych w kontrolkach i zastępowanie poszczególnych właściwości bezpośrednio w poszczególnych kontrolkach: zobacz tę odpowiedź: stackoverflow.com/a/54497665/1402498
JamesHoux

Odpowiedzi:


154

Myślę, że prosta odpowiedź jest taka, że ​​nie możesz zrobić (przynajmniej w tej wersji WPF) tego, co próbujesz zrobić.

Oznacza to, że do każdego elementu można zastosować tylko jeden styl.

Jednak, jak powiedzieli inni powyżej, może możesz użyć, BasedOnaby ci pomóc. Sprawdź poniższy fragment luźnego XAML. Zobaczysz w nim, że mam styl bazowy, który ustawia właściwość istniejącą w klasie bazowej elementu, do którego chcę zastosować dwa style. W drugim stylu, który jest oparty na stylu podstawowym, ustawiłem inną właściwość.

Więc pomysł tutaj ... jest taki, że jeśli możesz w jakiś sposób oddzielić właściwości, które chcesz ustawić ... zgodnie z hierarchią dziedziczenia elementu, dla którego chcesz ustawić wiele stylów ... możesz mieć obejście tego problemu.

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


Mam nadzieję że to pomoże.

Uwaga:

Na szczególną uwagę zasługuje jedna rzecz. Jeśli zmienisz TargetTypew drugim stylu (w pierwszym zestawie xaml powyżej) na ButtonBase, te dwa style nie zostaną zastosowane. Jednak zapoznaj się z poniższym xaml, aby obejść to ograniczenie. Zasadniczo oznacza to, że musisz nadać stylowi klucz i odwołać się do niego za pomocą tego klucza.

<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
Pamiętaj ... ** Kolejność jest ważna **. derivedStyleMusi pochodzić pobaseStyle
SliverNinja - MSFT

50

Bea Stollnitz zamieściła dobry post na blogu o używaniu do tego rozszerzenia znaczników, pod nagłówkiem „Jak ustawić wiele stylów w WPF?”

Ten blog jest teraz martwy, więc odtwarzam ten wpis tutaj


WPF i Silverlight oferują możliwość wyprowadzenia stylu z innego stylu za pomocą właściwości „BasedOn”. Ta funkcja umożliwia programistom organizowanie stylów przy użyciu hierarchii podobnej do dziedziczenia klas. Rozważ następujące style:

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

W tej składni Button, który używa RedButtonStyle, będzie miał właściwość Foreground ustawioną na Red, a właściwość Margin ustawioną na 10.

Ta funkcja jest dostępna w WPF od dłuższego czasu i jest nowa w Silverlight 3.

A co jeśli chcesz ustawić więcej niż jeden styl na elemencie? Ani WPF, ani Silverlight nie zapewniają rozwiązania tego problemu po wyjęciu z pudełka. Na szczęście istnieją sposoby na zaimplementowanie tego zachowania w WPF, które omówię w tym wpisie na blogu.

WPF i Silverlight używają rozszerzeń znaczników, aby zapewnić właściwości z wartościami, które wymagają pewnej logiki do uzyskania. Rozszerzenia znaczników są łatwo rozpoznawalne dzięki obecności nawiasów klamrowych otaczających je w języku XAML. Na przykład rozszerzenie znaczników {Binding} zawiera logikę do pobierania wartości ze źródła danych i aktualizowania jej w przypadku wystąpienia zmian; rozszerzenie znaczników {StaticResource} zawiera logikę do pobierania wartości ze słownika zasobów na podstawie klucza. Na szczęście dla nas WPF umożliwia użytkownikom pisanie własnych niestandardowych rozszerzeń znaczników. Ta funkcja nie jest jeszcze obecna w Silverlight, więc rozwiązanie w tym blogu ma zastosowanie tylko do WPF.

Inni napisali świetne rozwiązania do scalania dwóch stylów za pomocą rozszerzeń znaczników. Zależało mi jednak na rozwiązaniu dającym możliwość łączenia nieograniczonej liczby stylów, co jest nieco trudniejsze.

Pisanie rozszerzenia znaczników jest proste. Pierwszym krokiem jest utworzenie klasy, która pochodzi od MarkupExtension i użycie atrybutu MarkupExtensionReturnType, aby wskazać, że zamierzasz, aby wartość zwracana z rozszerzenia znaczników była typu Style.

[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}

Określanie danych wejściowych do rozszerzenia znaczników

Chcielibyśmy dać użytkownikom naszego rozszerzenia znaczników prosty sposób określania stylów do scalenia. Zasadniczo istnieją dwa sposoby określania danych wejściowych rozszerzenia znaczników przez użytkownika. Użytkownik może ustawić właściwości lub przekazać parametry do konstruktora. Ponieważ w tym scenariuszu użytkownik potrzebuje możliwości określenia nieograniczonej liczby stylów, moim pierwszym podejściem było utworzenie konstruktora, który pobiera dowolną liczbę ciągów za pomocą słowa kluczowego „params”:

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Moim celem było napisanie danych wejściowych w następujący sposób:

<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}"  />

Zwróć uwagę na przecinek oddzielający różne klucze stylu. Niestety, niestandardowe rozszerzenia znaczników nie obsługują nieograniczonej liczby parametrów konstruktora, więc takie podejście powoduje błąd kompilacji. Gdybym wiedział z góry, ile stylów chcę scalić, mógłbym użyć tej samej składni XAML z konstruktorem pobierającym żądaną liczbę ciągów:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Aby obejść ten problem, zdecydowałem, że parametr konstruktora będzie przyjmował pojedynczy ciąg, który określa nazwy stylów oddzielone spacjami. Składnia nie jest taka zła:

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

Obliczanie danych wyjściowych rozszerzenia znaczników

Aby obliczyć dane wyjściowe rozszerzenia znaczników, musimy zastąpić metodę z MarkupExtension o nazwie „ProvideValue”. Wartość zwrócona przez tę metodę zostanie ustawiona w miejscu docelowym rozszerzenia znaczników.

Zacząłem od stworzenia metody rozszerzenia dla Style, która wie, jak połączyć dwa style. Kod tej metody jest dość prosty:

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

Zgodnie z powyższą logiką, pierwszy styl jest modyfikowany w celu uwzględnienia wszystkich informacji z drugiego. Jeśli występują konflikty (np. Oba style mają metodę ustawiającą dla tej samej właściwości), wygrywa drugi styl. Zauważ, że oprócz kopiowania stylów i wyzwalaczy, wziąłem również pod uwagę wartości TargetType i BasedOn, a także wszelkie zasoby, które mógł mieć drugi styl. W przypadku typu TargetType połączonego stylu użyłem tego, który typ jest bardziej pochodny. Jeśli drugi styl ma styl BasedOn, rekursywnie scalam jego hierarchię stylów. Jeśli ma zasoby, kopiuję je do pierwszego stylu. Jeśli odwołujemy się do tych zasobów za pomocą {StaticResource}, są one statycznie rozwiązywane przed wykonaniem tego kodu scalającego i dlatego nie jest konieczne ich przenoszenie. Dodałem ten kod na wypadek, gdybyśmy używali DynamicResources.

Przedstawiona powyżej metoda rozszerzenia umożliwia następującą składnię:

style1.Merge(style2);

Ta składnia jest przydatna pod warunkiem, że mam wystąpienia obu stylów w ramach ProvideValue. Cóż, ja nie. Wszystko, co otrzymuję od konstruktora, to lista kluczy ciągów dla tych stylów. Gdyby w parametrach konstruktora istniała obsługa params, mógłbym użyć następującej składni, aby uzyskać rzeczywiste wystąpienia stylu:

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

Ale to nie działa. I nawet gdyby ograniczenie parametrów nie istniało, prawdopodobnie trafilibyśmy na inne ograniczenie rozszerzeń znaczników, gdzie musielibyśmy użyć składni elementu właściwości zamiast składni atrybutu, aby określić zasoby statyczne, co jest rozwlekłe i kłopotliwe (wyjaśniam to błąd lepiej w poprzednim poście na blogu ). I nawet gdyby oba te ograniczenia nie istniały, nadal wolałbym pisać listę stylów używając tylko ich nazw - jest krótsza i prostsza do odczytania niż StaticResource dla każdego z nich.

Rozwiązaniem jest utworzenie StaticResourceExtension przy użyciu kodu. Biorąc pod uwagę klucz stylu typu string i dostawcę usług, mogę użyć StaticResourceExtension, aby pobrać rzeczywistą instancję stylu. Oto składnia:

Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;

Teraz mamy wszystkie elementy potrzebne do napisania metody 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;
}

Oto pełny przykład użycia rozszerzenia znaczników 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" />

wprowadź opis obrazu tutaj


3
Naprawdę dobre rozwiązanie, ale nie rozumiem, dlaczego nie ma prostego rozwiązania na połączenie stylu 3 lub +.
Mr Rubix

31

Ale możesz rozszerzyć zakres z innego… spójrz na właściwość 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>

to mi wystarczyło. tnks!
David Lay,

Ale działa to tylko wtedy, gdy oba style są tego samego typu (błąd XAML: „Może bazować tylko na stylu z typem docelowym, który jest typem bazowym„ <typ> ”)
Krzysztof Bociurko

17

WPF / XAML nie zapewnia tej funkcji natywnie, ale zapewnia rozszerzalność, aby umożliwić Ci robienie tego, co chcesz.

Napotkaliśmy tę samą potrzebę i ostatecznie utworzyliśmy własne rozszerzenie znaczników XAML (które nazwaliśmy „MergedStylesExtension”), aby umożliwić nam utworzenie nowego stylu na podstawie dwóch innych stylów (które w razie potrzeby można by prawdopodobnie użyć wielokrotnie w wiersz, aby dziedziczyć z jeszcze większej liczby stylów).

Ze względu na błąd WPF / XAML, musimy użyć składni elementu właściwości, aby go użyć, ale poza tym wydaje się działać poprawnie. Na przykład,

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

Niedawno pisałem o tym tutaj: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


3

Jest to możliwe dzięki utworzeniu klasy pomocniczej do używania i zawijania twoich stylów. Wspomniany tutaj CompoundStyle pokazuje, jak to zrobić. Istnieje wiele sposobów, ale najłatwiej jest wykonać następujące czynności:

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

Mam nadzieję, że to pomoże.


2

Służy AttachedPropertydo ustawiania wielu stylów, takich jak następujący kod:

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

Użycie:

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

Wynik:

wprowadź opis obrazu tutaj


1

jeśli nie dotykasz żadnych określonych właściwości, możesz pobrać wszystkie podstawowe i wspólne właściwości do stylu, którego typem docelowym byłby FrameworkElement. następnie możesz stworzyć specyficzne smaki dla każdego typu celu, którego potrzebujesz, bez konieczności ponownego kopiowania wszystkich tych wspólnych właściwości.


1

Prawdopodobnie możesz uzyskać coś podobnego, jeśli zastosujesz to do kolekcji elementów za pomocą StyleSelector, użyłem tego do rozwiązania podobnego problemu z używaniem różnych stylów w TreeViewItems w zależności od powiązanego typu obiektu w drzewie. Być może będziesz musiał nieco zmodyfikować poniższą klasę, aby dostosować się do swojego konkretnego podejścia, ale miejmy nadzieję, że to pomoże Ci zacząć

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

Następnie zastosuj to jako tak

 <TreeView>
     <TreeView.ItemContainerStyleSelector
         <myassembly: MyTreeStyleSelector DefaultStyle = "{StaticResource DefaultItemStyle}"
                                         NewStyle = "{StaticResource NewItemStyle}" />
     </TreeView.ItemContainerStyleSelector>
  </TreeView>

1

Czasami można to osiągnąć, zagnieżdżając panele. Powiedzmy, że masz Styl, który zmienia pierwszy plan, a inny zmienia FontSize. Możesz zastosować ten drugi do TextBlock i umieścić go w siatce, której styl jest pierwszym. Może to pomóc, aw niektórych przypadkach może być najłatwiejszym sposobem, ale nie rozwiąże wszystkich problemów.


1

Gdy nadpisujesz SelectStyle, możesz uzyskać właściwość GroupBy poprzez odbicie, jak poniżej:

    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

Jeśli próbujesz zastosować unikalny styl tylko do jednego elementu jako dodatek do stylu podstawowego, istnieje zupełnie inny sposób na zrobienie tego, który jest znacznie lepszy dla kodu IMHO, który jest czytelny i łatwy w utrzymaniu.

Niezwykle często trzeba dostosować parametry dla poszczególnych elementów. Definiowanie stylów słownikowych tylko do użytku w jednym elemencie jest niezwykle kłopotliwe w utrzymaniu lub nadaniu sensu. Aby uniknąć tworzenia stylów tylko dla jednorazowych poprawek elementów, przeczytaj moją odpowiedź na moje własne pytanie tutaj:

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.