Come applicare più stili in WPF


153

In WPF, come dovrei applicare più stili a un FrameworkElement? Ad esempio, ho un controllo che ha già uno stile. Ho anche uno stile separato che vorrei aggiungere ad esso senza spazzare via il primo. Gli stili hanno TargetTypes diversi, quindi non posso semplicemente estendere l'uno con l'altro.


OP non ha mai specificato se il suo primo stile fosse unico per un solo controllo. Le risposte fornite in questa pagina presuppongono la necessità di condividere entrambi gli stili su più controlli. Se siete alla ricerca di un modo per utilizzare gli stili di base in materia di controlli e di ignorare le proprietà individuali direttamente sui singoli controlli: questa risposta: stackoverflow.com/a/54497665/1402498
JamesHoux

Risposte:


154

Penso che la semplice risposta sia che non puoi fare (almeno in questa versione di WPF) quello che stai cercando di fare.

Cioè, per ogni elemento particolare può essere applicato un solo stile.

Tuttavia, come altri hanno già detto, forse puoi usarlo BasedOnper aiutarti. Dai un'occhiata al seguente pezzo di xaml sfuso. In esso vedrai che ho uno stile di base che sta impostando una proprietà che esiste sulla classe base dell'elemento a cui voglio applicare due stili. E, nel secondo stile basato sullo stile di base, ho impostato un'altra proprietà.

Quindi, l'idea qui ... è se puoi in qualche modo separare le proprietà che vuoi impostare ... secondo la gerarchia dell'ereditarietà dell'elemento su cui vuoi impostare più stili ... potresti avere una soluzione alternativa.

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


Spero che questo ti aiuti.

Nota:

Una cosa in particolare da notare. Se si modifica il TargetTypesecondo stile (nel primo set di xaml sopra) in ButtonBase, i due stili non vengono applicati. Tuttavia, controlla il seguente xaml di seguito per aggirare quella limitazione. Fondamentalmente, significa che devi dare una chiave allo stile e fare riferimento a quella chiave.

<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
Ricorda ... ** L'ordine è importante **. La derivedStyledeve venire dopo labaseStyle
SliverNinja - MSFT

50

Bea Stollnitz ha pubblicato un buon post sul blog sull'utilizzo di un'estensione di markup per questo, sotto la voce "Come posso impostare più stili in WPF?"

Quel blog è morto ora, quindi sto riproducendo il post qui


WPF e Silverlight offrono entrambi la possibilità di derivare uno stile da un altro stile tramite la proprietà "BasedOn". Questa funzionalità consente agli sviluppatori di organizzare i propri stili utilizzando una gerarchia simile all'eredità delle classi. Considera i seguenti stili:

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

Con questa sintassi, un pulsante che utilizza RedButtonStyle avrà la proprietà Foreground impostata su Red e la proprietà Margin impostata su 10.

Questa funzione è presente in WPF da molto tempo ed è una novità di Silverlight 3.

Cosa succede se si desidera impostare più di uno stile su un elemento? Né WPF né Silverlight offrono una soluzione pronta per questo problema. Fortunatamente ci sono modi per implementare questo comportamento in WPF, di cui parlerò in questo post sul blog.

WPF e Silverlight utilizzano le estensioni di markup per fornire proprietà con valori che richiedono logica per ottenere. Le estensioni di markup sono facilmente riconoscibili dalla presenza di parentesi graffe che le circondano in XAML. Ad esempio, l'estensione di markup {Binding} contiene la logica per recuperare un valore da un'origine dati e aggiornarlo quando si verificano modifiche; l'estensione di markup {StaticResource} contiene la logica per acquisire un valore da un dizionario di risorse basato su una chiave. Fortunatamente per noi, WPF consente agli utenti di scrivere le proprie estensioni di markup personalizzate. Questa funzione non è ancora presente in Silverlight, quindi la soluzione in questo blog è applicabile solo a WPF.

Altri hanno scritto grandi soluzioni per unire due stili usando le estensioni di markup. Tuttavia, volevo una soluzione che offrisse la possibilità di unire un numero illimitato di stili, che è un po 'più complicato.

Scrivere un'estensione di markup è semplice. Il primo passaggio consiste nel creare una classe derivata da MarkupExtension e utilizzare l'attributo MarkupExtensionReturnType per indicare che si intende che il valore restituito dall'estensione di markup sia di tipo Style.

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

Specifica degli input per l'estensione di markup

Vorremmo offrire agli utenti della nostra estensione markup un modo semplice per specificare gli stili da unire. Esistono essenzialmente due modi in cui l'utente può specificare input per un'estensione di markup. L'utente può impostare proprietà o passare parametri al costruttore. Poiché in questo scenario l'utente ha bisogno della capacità di specificare un numero illimitato di stili, il mio primo approccio è stato quello di creare un costruttore che prende un numero qualsiasi di stringhe usando la parola chiave "params":

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Il mio obiettivo era poter scrivere gli input come segue:

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

Notare la virgola che separa i diversi tasti di stile. Sfortunatamente, le estensioni di markup personalizzate non supportano un numero illimitato di parametri del costruttore, quindi questo approccio provoca un errore di compilazione. Se avessi saputo in anticipo quanti stili avrei voluto unire, avrei potuto usare la stessa sintassi XAML con un costruttore che prendesse il numero desiderato di stringhe:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Per ovviare al problema, ho deciso di fare in modo che il parametro del costruttore prendesse una singola stringa che specifica i nomi degli stili separati da spazi. La sintassi non è poi così male:

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

Calcolo dell'output dell'estensione di markup

Per calcolare l'output di un'estensione di markup, dobbiamo sovrascrivere un metodo da MarkupExtension chiamato "ProvideValue". Il valore restituito da questo metodo verrà impostato nella destinazione dell'estensione di markup.

Ho iniziato creando un metodo di estensione per Style che sappia unire due stili. Il codice per questo metodo è abbastanza semplice:

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

Con la logica sopra, il primo stile viene modificato per includere tutte le informazioni dal secondo. Se ci sono conflitti (ad es. Entrambi gli stili hanno un setter per la stessa proprietà), vince il secondo stile. Si noti che oltre a copiare stili e trigger, ho anche preso in considerazione i valori TargetType e BasedOn e tutte le risorse che il secondo stile potrebbe avere. Per il TargetType dello stile unito, ho usato il tipo più derivato. Se il secondo stile ha uno stile BasedOn, unisco la sua gerarchia di stili in modo ricorsivo. Se ha risorse, le copio nel primo stile. Se si fa riferimento a tali risorse utilizzando {StaticResource}, vengono risolte staticamente prima dell'esecuzione di questo codice di unione e pertanto non è necessario spostarle. Ho aggiunto questo codice nel caso in cui stiamo utilizzando DynamicResources.

Il metodo di estensione mostrato sopra abilita la seguente sintassi:

style1.Merge(style2);

Questa sintassi è utile a condizione che io abbia istanze di entrambi gli stili in ProvideValue. Beh, io no. Tutto quello che ottengo dal costruttore è un elenco di chiavi di stringa per quegli stili. Se ci fosse supporto per i parametri nei parametri del costruttore, avrei potuto usare la sintassi seguente per ottenere le istanze di stile effettive:

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

Ma questo non funziona. E anche se la limitazione dei parametri non esistesse, probabilmente colpiremmo un'altra limitazione delle estensioni di markup, dove dovremmo usare la sintassi dell'elemento proprietà invece della sintassi dell'attributo per specificare le risorse statiche, che sono verbose e ingombranti (lo spiego bug migliore in un precedente post sul blog ). E anche se entrambe queste limitazioni non esistessero, preferirei comunque scrivere l'elenco di stili usando solo i loro nomi: è più breve e più semplice da leggere rispetto a uno StaticResource per ognuno.

La soluzione è creare un StaticResourceExtension usando il codice. Data una chiave di stile di tipo stringa e un fornitore di servizi, posso usare StaticResourceExtension per recuperare l'istanza di stile effettiva. Ecco la sintassi:

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

Ora abbiamo tutti i pezzi necessari per scrivere il metodo 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;
}

Ecco un esempio completo dell'uso dell'estensione markup 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" />

inserisci qui la descrizione dell'immagine


3
Ottima soluzione, ma non capisco perché non esiste una soluzione semplice per unire 3 o + stile.
Rubix,

31

Ma puoi estenderlo da un altro .. dai un'occhiata alla proprietà 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>

questo è stato abbastanza per me. tnks!
David Lay

Ma questo funziona solo se entrambi gli stili sono dello stesso tipo (errore XAML: "Può basarsi solo su uno stile con tipo di destinazione che è il tipo di base '<tipo>')
Krzysztof Bociurko,

17

WPF / XAML non fornisce questa funzionalità in modo nativo, ma fornisce l'estensibilità per consentirti di fare ciò che desideri.

Ci siamo imbattuti nella stessa esigenza e abbiamo finito per creare la nostra estensione di markup XAML (che abbiamo chiamato "MergedStylesExtension") per consentirci di creare un nuovo stile da altri due stili (che, se necessario, probabilmente potrebbero essere utilizzati più volte in un riga per ereditare da ancora più stili).

A causa di un bug WPF / XAML, dobbiamo usare la sintassi dell'elemento proprietà per usarlo, ma a parte questo sembra funzionare bene. Per esempio,

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

Ne ho recentemente scritto qui: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


3

Questo è possibile creando una classe di supporto per usare e avvolgere i tuoi stili. CompoundStyle menzionato qui mostra come farlo. Esistono diversi modi, ma il più semplice è effettuare le seguenti operazioni:

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

Spero che aiuti.


2

Utilizzare AttachedPropertyper impostare più stili come il seguente codice:

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>

Risultato:

inserisci qui la descrizione dell'immagine


1

se non si toccano proprietà specifiche, è possibile ottenere tutte le proprietà di base e comuni allo stile il cui tipo di destinazione sarebbe FrameworkElement. quindi, è possibile creare sapori specifici per ogni tipo di destinazione necessario, senza la necessità di copiare nuovamente tutte quelle proprietà comuni.


1

Probabilmente puoi ottenere qualcosa di simile se lo applico a una raccolta di elementi mediante uno StyleSelector, l'ho usato per affrontare un problema simile nell'uso di stili diversi su TreeViewItems a seconda del tipo di oggetto associato nella struttura. Potrebbe essere necessario modificare leggermente la classe di seguito per adattarsi al tuo approccio particolare, ma spero che questo ti possa iniziare

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

Quindi lo applichi così

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

1

A volte puoi avvicinarti a questo annidando i pannelli. Supponi di avere uno stile che cambia in primo piano e un altro cambia FontSize, puoi applicare quest'ultimo su un blocco di testo e inserirlo in una griglia di cui il primo è lo stile. Questo potrebbe aiutare e potrebbe essere il modo più semplice in alcuni casi, anche se non risolverà tutti i problemi.


1

Quando esegui l'override di SelectStyle puoi ottenere la proprietà GroupBy tramite la riflessione come di seguito:

    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

Se stai provando ad applicare uno stile unico a un solo elemento come aggiunta a uno stile di base, c'è un modo completamente diverso di farlo che è IMHO molto meglio per il codice leggibile e gestibile.

È estremamente comune dover modificare i parametri per singolo elemento. La definizione di stili di dizionario solo per l'uso su un elemento è estremamente complessa da mantenere o dare un senso. Per evitare di creare stili solo per ritocchi di elementi unici, leggi la mia risposta alla mia domanda qui:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.