Comment appliquer plusieurs styles dans WPF


153

Dans WPF, comment appliquer plusieurs styles à un FrameworkElement? Par exemple, j'ai un contrôle qui a déjà un style. J'ai aussi un style à part que je voudrais y ajouter sans faire oublier le premier. Les styles ont différents TargetTypes, donc je ne peux pas simplement étendre l'un avec l'autre.


OP n'a jamais spécifié si son premier style est unique à un seul contrôle. Les réponses données sur cette page supposent la nécessité de partager les deux styles sur plusieurs contrôles. Si vous recherchez un moyen d'utiliser des styles de base sur des contrôles et de remplacer des propriétés individuelles directement sur des contrôles individuels: voir cette réponse: stackoverflow.com/a/54497665/1402498
JamesHoux

Réponses:


154

Je pense que la réponse simple est que vous ne pouvez pas faire (du moins dans cette version de WPF) ce que vous essayez de faire.

Autrement dit, pour un élément particulier, un seul style peut être appliqué.

Cependant, comme d'autres l'ont indiqué ci-dessus, vous pouvez peut-être utiliser BasedOnpour vous aider. Découvrez le morceau suivant de xaml en vrac. Vous y verrez que j'ai un style de base qui définit une propriété qui existe sur la classe de base de l'élément auquel je souhaite appliquer deux styles. Et, dans le deuxième style qui est basé sur le style de base, j'ai défini une autre propriété.

Donc, l'idée ici ... est que si vous pouvez en quelque sorte séparer les propriétés que vous souhaitez définir ... en fonction de la hiérarchie d'héritage de l'élément sur lequel vous souhaitez définir plusieurs styles ... vous pourriez avoir une solution de contournement.

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


J'espère que cela t'aides.

Remarque:

Une chose en particulier à noter. Si vous changez le TargetTypedans le second style (dans le premier ensemble de xaml ci-dessus) en ButtonBase, les deux styles ne sont pas appliqués. Cependant, consultez le xaml suivant ci-dessous pour contourner cette restriction. Fondamentalement, cela signifie que vous devez donner au style une clé et le référencer avec cette clé.

<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
N'oubliez pas ... ** La commande est importante **. Le derivedStyledoit venir après lebaseStyle
SliverNinja - MSFT

50

Bea Stollnitz a publié un bon article sur l'utilisation d'une extension de balisage pour cela, sous le titre "Comment puis-je définir plusieurs styles dans WPF?"

Ce blog est mort maintenant, donc je reproduis le message ici


WPF et Silverlight offrent tous deux la possibilité de dériver un style d'un autre style via la propriété «BasedOn». Cette fonctionnalité permet aux développeurs d'organiser leurs styles en utilisant une hiérarchie similaire à l'héritage de classe. Considérez les styles suivants:

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

Avec cette syntaxe, un Button qui utilise RedButtonStyle aura sa propriété Foreground définie sur Red et sa propriété Margin définie sur 10.

Cette fonctionnalité existe dans WPF depuis longtemps et elle est nouvelle dans Silverlight 3.

Que faire si vous souhaitez définir plusieurs styles sur un élément? Ni WPF ni Silverlight ne fournissent une solution à ce problème prête à l'emploi. Heureusement, il existe des moyens d'implémenter ce comportement dans WPF, dont je parlerai dans ce billet de blog.

WPF et Silverlight utilisent des extensions de balisage pour fournir des propriétés avec des valeurs qui nécessitent une certaine logique pour obtenir. Les extensions de balisage sont facilement reconnaissables par la présence d'accolades les entourant en XAML. Par exemple, l'extension de balisage {Binding} contient une logique permettant d'extraire une valeur d'une source de données et de la mettre à jour lorsque des modifications se produisent; l'extension de balisage {StaticResource} contient une logique permettant de récupérer une valeur dans un dictionnaire de ressources en fonction d'une clé. Heureusement pour nous, WPF permet aux utilisateurs d'écrire leurs propres extensions de balisage personnalisées. Cette fonctionnalité n'est pas encore présente dans Silverlight, donc la solution de ce blog n'est applicable qu'à WPF.

D'autres ont écrit d'excellentes solutions pour fusionner deux styles à l'aide d'extensions de balisage. Cependant, je voulais une solution qui offre la possibilité de fusionner un nombre illimité de styles, ce qui est un peu plus délicat.

L'écriture d'une extension de balisage est simple. La première étape consiste à créer une classe qui dérive de MarkupExtension et à utiliser l'attribut MarkupExtensionReturnType pour indiquer que vous souhaitez que la valeur renvoyée par votre extension de balisage soit de type Style.

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

Spécification des entrées de l'extension de balisage

Nous aimerions donner aux utilisateurs de notre extension de balisage un moyen simple de spécifier les styles à fusionner. Il existe essentiellement deux façons dont l'utilisateur peut spécifier des entrées dans une extension de balisage. L'utilisateur peut définir des propriétés ou transmettre des paramètres au constructeur. Étant donné que dans ce scénario, l'utilisateur a besoin de la possibilité de spécifier un nombre illimité de styles, ma première approche a été de créer un constructeur qui prend n'importe quel nombre de chaînes en utilisant le mot-clé «params»:

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Mon objectif était de pouvoir écrire les entrées comme suit:

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

Notez la virgule séparant les différentes clés de style. Malheureusement, les extensions de balisage personnalisées ne prennent pas en charge un nombre illimité de paramètres de constructeur, donc cette approche entraîne une erreur de compilation. Si je savais à l'avance combien de styles je voulais fusionner, j'aurais pu utiliser la même syntaxe XAML avec un constructeur prenant le nombre de chaînes souhaité:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Pour contourner ce problème, j'ai décidé que le paramètre constructeur prenne une seule chaîne qui spécifie les noms de style séparés par des espaces. La syntaxe n'est pas trop mauvaise:

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

Calcul de la sortie de l'extension de balisage

Pour calculer la sortie d'une extension de balisage, nous devons remplacer une méthode de MarkupExtension appelée «provideValue». La valeur renvoyée par cette méthode sera définie dans la cible de l'extension de balisage.

J'ai commencé par créer une méthode d'extension pour Style qui sait fusionner deux styles. Le code de cette méthode est assez simple:

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

Avec la logique ci-dessus, le premier style est modifié pour inclure toutes les informations du second. S'il y a des conflits (par exemple, les deux styles ont un setter pour la même propriété), le second style l'emporte. Notez qu'en plus de copier les styles et les déclencheurs, j'ai également pris en compte les valeurs TargetType et BasedOn ainsi que toutes les ressources que le second style peut avoir. Pour le TargetType du style fusionné, j'ai utilisé le type le plus dérivé. Si le deuxième style a un style BasedOn, je fusionne sa hiérarchie de styles de manière récursive. S'il a des ressources, je les copie dans le premier style. Si ces ressources sont référencées à l'aide de {StaticResource}, elles sont résolues statiquement avant l'exécution de ce code de fusion, et il n'est donc pas nécessaire de les déplacer. J'ai ajouté ce code au cas où nous utiliserions DynamicResources.

La méthode d'extension illustrée ci-dessus active la syntaxe suivante:

style1.Merge(style2);

Cette syntaxe est utile à condition que j'aie des instances des deux styles dans FournirValue. Eh bien, non. Tout ce que j'obtiens du constructeur est une liste de clés de chaîne pour ces styles. S'il y avait un support pour les paramètres dans les paramètres du constructeur, j'aurais pu utiliser la syntaxe suivante pour obtenir les instances de style réelles:

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

Mais ça ne marche pas. Et même si la limitation des paramètres n'existait pas, nous frapperions probablement une autre limitation des extensions de balisage, où nous devrions utiliser la syntaxe d'élément de propriété au lieu de la syntaxe d'attribut pour spécifier les ressources statiques, ce qui est verbeux et encombrant (j'explique cela bug mieux dans un précédent article de blog ). Et même si ces deux limitations n'existaient pas, je préférerais quand même écrire la liste des styles en utilisant uniquement leurs noms - elle est plus courte et plus simple à lire qu'une StaticResource pour chacun.

La solution consiste à créer une StaticResourceExtension à l'aide de code. Étant donné une clé de style de type string et un fournisseur de services, je peux utiliser StaticResourceExtension pour récupérer l'instance de style réelle. Voici la syntaxe:

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

Nous avons maintenant toutes les pièces nécessaires pour écrire la méthode 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;
}

Voici un exemple complet de l'utilisation de l'extension de balisage 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" />

entrez la description de l'image ici


3
Vraiment une bonne solution, mais je ne comprends pas pourquoi il n'y a pas de solution simple pour fusionner le style 3 ou +.
Mr Rubix

31

Mais vous pouvez étendre d'un autre ... jetez un œil à la propriété 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>

c'était assez pour moi. tnks!
David Lay

Mais cela ne fonctionne que si les deux styles sont du même type (erreur XAML: "Ne peut se baser que sur un style avec un type de cible qui est le type de base '<type>')
Krzysztof Bociurko

17

WPF / XAML ne fournit pas cette fonctionnalité de manière native, mais il fournit l'extensibilité pour vous permettre de faire ce que vous voulez.

Nous avons rencontré le même besoin et avons fini par créer notre propre extension de balisage XAML (que nous avons appelée "MergedStylesExtension") pour nous permettre de créer un nouveau style à partir de deux autres styles (qui, si nécessaire, pourraient probablement être utilisés plusieurs fois dans un ligne pour hériter d'encore plus de styles).

En raison d'un bogue WPF / XAML, nous devons utiliser la syntaxe d'élément de propriété pour l'utiliser, mais à part cela, cela semble fonctionner correctement. Par exemple,

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

J'ai récemment écrit à ce sujet ici: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


3

Cela est possible en créant une classe d'assistance pour utiliser et encapsuler vos styles. CompoundStyle mentionné ici montre comment le faire. Il existe plusieurs façons, mais la plus simple est de faire ce qui suit:

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

J'espère que cela pourra aider.


2

Utilisez AttachedPropertypour définir plusieurs styles comme le code suivant:

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>

Résultat:

entrez la description de l'image ici


1

si vous ne touchez à aucune propriété spécifique, vous pouvez obtenir toutes les propriétés de base et communes du style dont le type de cible serait FrameworkElement. ensuite, vous pouvez créer des saveurs spécifiques pour chaque type de cible dont vous avez besoin, sans avoir à copier à nouveau toutes ces propriétés communes.


1

Vous pouvez probablement obtenir quelque chose de similaire si vous appliquez cela à une collection d'éléments à l'aide d'un StyleSelector, je l'ai utilisé pour aborder un problème similaire en utilisant différents styles sur TreeViewItems en fonction du type d'objet lié dans l'arborescence. Vous devrez peut-être modifier légèrement la classe ci-dessous pour vous adapter à votre approche particulière, mais j'espère que cela vous permettra de démarrer

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

Vous appliquez ensuite ceci comme tel

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

1

Parfois, vous pouvez aborder cela en imbriquant des panneaux. Supposons que vous ayez un style qui change le premier plan et un autre change la taille de police, vous pouvez appliquer ce dernier sur un TextBlock, et le mettre dans une grille dont son style est le premier. Cela pourrait aider et pourrait être le moyen le plus simple dans certains cas, même si cela ne résoudra pas tous les problèmes.


1

Lorsque vous remplacez SelectStyle, vous pouvez obtenir la propriété GroupBy via une réflexion comme ci-dessous:

    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

Si vous essayez d'appliquer un style unique à un seul élément en tant qu'addition à un style de base, il existe une manière complètement différente de le faire qui est à mon humble avis bien meilleure pour un code lisible et maintenable.

Il est extrêmement courant de devoir modifier les paramètres par élément individuel. Définir des styles de dictionnaire uniquement pour une utilisation sur un élément est extrêmement compliqué à maintenir ou à comprendre. Pour éviter de créer des styles uniquement pour des ajustements d'éléments ponctuels, lisez ma réponse à ma propre question ici:

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.