Question

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 j'aimerais y ajouter sans faire exploser le premier.Les styles ont des TargetTypes différents, je ne peux donc pas simplement étendre l'un avec l'autre.

Était-ce utile?

La solution

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 BasedOn pour 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é.

Alors, l'idée ici...c'est si vous pouvez d'une manière ou d'une autre séparer les propriétés que vous souhaitez définir...selon 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.

Note:

Une chose en particulier à noter.Si vous changez le TargetType dans le deuxième style (dans le premier ensemble de xaml ci-dessus) pour ButtonBase, les deux styles ne sont pas appliqués.Cependant, consultez le code XML suivant ci-dessous pour contourner cette restriction.Fondamentalement, cela signifie que vous devez donner une clé au style 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>

Autres conseils

Bea Stollnitz avait un bon article de blog à propos de l'utilisation d'une extension de balisage pour cela, sous la rubrique « Comment puis-je définir plusieurs styles dans WPF ? »

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


WPF et Silverlight offrent tous deux la possibilité de dériver un style à partir d'un autre style via la propriété « BasedOn ».Cette fonctionnalité permet aux développeurs d'organiser leurs styles à l'aide d'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 de solution prête à l'emploi à ce problème.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 être obtenues.Les extensions de balisage sont facilement reconnaissables à la présence d’accolades qui les entourent 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 d'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, la solution présentée dans ce blog n'est donc applicable qu'à WPF.

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

Écrire une extension de balisage est simple.La première étape consiste à créer une classe dérivée 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 dans l'extension de balisage

Nous aimerions offrir aux utilisateurs de notre extension de balisage un moyen simple de spécifier les styles à fusionner.Il existe essentiellement deux manières pour l'utilisateur de spécifier les entrées d'une extension de balisage.L'utilisateur peut définir des propriétés ou transmettre des paramètres au constructeur.Puisque dans ce scénario, l'utilisateur a besoin de pouvoir 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 à l'aide du 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 touches de style.Malheureusement, les extensions de balisage personnalisé ne prennent pas en charge un nombre illimité de paramètres de constructeur, cette approche entraîne donc une erreur de compilation.Si j'avais su à l'avance combien de styles je souhaitais 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 le problème, j'ai décidé que le paramètre constructeur prend 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 le résultat 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 ex.les deux styles ont un setter pour la même propriété), le deuxième 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 deuxième 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 contient 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 présentée ci-dessus active la syntaxe suivante :

style1.Merge(style2);

Cette syntaxe est utile à condition que j'aie des instances des deux styles dans ProvideValue.Eh bien, non.Tout ce que je reçois du constructeur est une liste de clés de chaîne pour ces styles.S'il y avait une prise en charge des 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 rencontrerions probablement une autre limitation des extensions de balisage, où nous devrions utiliser la syntaxe des éléments de propriété au lieu de la syntaxe des attributs pour spécifier les ressources statiques, ce qui est verbeux et fastidieux (j'explique ceci bug mieux dans un article de blog précédent).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 chaîne 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 disposons désormais de tous les éléments 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 d'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" />

enter image description here

Mais vous pouvez étendre à partir d'un autre..jetez un oeil à 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>

WPF/XAML ne fournit pas cette fonctionnalité de manière native, mais il offre l'extensibilité nécessaire 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 rangée pour hériter d'encore plus de styles).

En raison d'un bug WPF/XAML, nous devons utiliser la syntaxe des éléments 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'en ai récemment parlé ici :http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/

Ceci est possible en créant une classe d'assistance pour utiliser et envelopper vos styles.Style composé mentionné ici montre comment procéder.Il existe plusieurs façons, mais la plus simple est de procéder comme suit :

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

J'espère que cela pourra aider.

Utiliser AttachedProperty pour 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;
    }
}

Utilisation :

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

enter image description here

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 cible serait FrameworkElement.vous pouvez ensuite créer des versions spécifiques pour chaque type de cible dont vous avez besoin, sans avoir besoin de copier à nouveau toutes ces propriétés communes.

Vous pouvez probablement obtenir quelque chose de similaire si vous l'appliquez à 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 le cours ci-dessous pour vous adapter à votre approche particulière, mais j'espère que cela vous aidera à 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 ceci

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

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

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

Si vous essayez d'appliquer un style unique à un seul élément en complément d'un style de base, il existe une manière complètement différente de procéder, 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 seul élément est extrêmement fastidieux à gérer ou à donner un sens.Pour éviter de créer des styles uniquement pour des ajustements ponctuels d'éléments, lisez ma réponse à ma propre question ici :

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top