Cómo aplicar múltiples estilos en WPF


153

En WPF, ¿cómo aplicaría varios estilos a un FrameworkElement? Por ejemplo, tengo un control que ya tiene un estilo. También tengo un estilo separado que me gustaría agregar sin eliminar el primero. Los estilos tienen diferentes TargetTypes, por lo que no puedo extender uno con el otro.


OP nunca especificó si su primer estilo es exclusivo de un solo control. Las respuestas dadas en esta página suponen la necesidad de compartir ambos estilos a través de múltiples controles. Si está buscando una forma de usar estilos base en controles y anular propiedades individuales directamente en controles individuales: vea esta respuesta: stackoverflow.com/a/54497665/1402498
JamesHoux

Respuestas:


154

Creo que la respuesta simple es que no puedes hacer (al menos en esta versión de WPF) lo que estás tratando de hacer.

Es decir, para cualquier elemento en particular, solo se puede aplicar un Estilo.

Sin embargo, como otros han dicho anteriormente, tal vez pueda usarlo BasedOnpara ayudarlo. Mira la siguiente pieza de xaml suelto. En él verá que tengo un estilo base que establece una propiedad que existe en la clase base del elemento al que quiero aplicar dos estilos. Y, en el segundo estilo que se basa en el estilo base, configuro otra propiedad.

Entonces, la idea aquí ... es si de alguna manera puede separar las propiedades que desea establecer ... de acuerdo con la jerarquía de herencia del elemento en el que desea establecer múltiples estilos ... es posible que tenga una solución 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>


Espero que esto ayude.

Nota:

Una cosa en particular a tener en cuenta. Si cambia el TargetTypeen el segundo estilo (en el primer conjunto de xaml anterior) a ButtonBase, los dos estilos no se aplican. Sin embargo, consulte el siguiente xaml a continuación para evitar esa restricción. Básicamente, significa que debe darle una clave al estilo y hacer referencia con esa clave.

<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
Recuerde ... ** Ordenar es importante **. Lo que derivedStyledebe venir después delbaseStyle
SliverNinja - MSFT

50

Bea Stollnitz tuvo una buena publicación de blog sobre el uso de una extensión de marcado para esto, bajo el encabezado "¿Cómo puedo configurar varios estilos en WPF?"

Ese blog está muerto ahora, así que estoy reproduciendo la publicación aquí.


WPF y Silverlight ofrecen la posibilidad de derivar un estilo de otro estilo a través de la propiedad "BasedOn". Esta característica permite a los desarrolladores organizar sus estilos usando una jerarquía similar a la herencia de clases. Considere los siguientes estilos:

<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 esta sintaxis, un botón que usa RedButtonStyle tendrá su propiedad de primer plano establecida en rojo y su propiedad de margen establecida en 10.

Esta característica ha estado presente en WPF durante mucho tiempo, y es nueva en Silverlight 3.

¿Qué sucede si desea establecer más de un estilo en un elemento? Ni WPF ni Silverlight proporcionan una solución para este problema fuera de la caja. Afortunadamente, hay formas de implementar este comportamiento en WPF, que analizaré en esta publicación de blog.

WPF y Silverlight usan extensiones de marcado para proporcionar propiedades con valores que requieren cierta lógica para obtener. Las extensiones de marcado son fácilmente reconocibles por la presencia de llaves que las rodean en XAML. Por ejemplo, la extensión de marcado {Binding} contiene lógica para obtener un valor de una fuente de datos y actualizarlo cuando ocurren cambios; la extensión de marcado {StaticResource} contiene lógica para obtener un valor de un diccionario de recursos basado en una clave. Afortunadamente para nosotros, WPF permite a los usuarios escribir sus propias extensiones de marcado personalizadas. Esta característica aún no está presente en Silverlight, por lo que la solución en este blog solo es aplicable a WPF.

Otros han escrito excelentes soluciones para fusionar dos estilos usando extensiones de marcado. Sin embargo, quería una solución que proporcionara la capacidad de fusionar un número ilimitado de estilos, lo cual es un poco más complicado.

Escribir una extensión de marcado es sencillo. El primer paso es crear una clase que se derive de MarkupExtension y usar el atributo MarkupExtensionReturnType para indicar que desea que el valor devuelto por su extensión de marcado sea de tipo Style.

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

Especificar entradas a la extensión de marcado

Nos gustaría ofrecer a los usuarios de nuestra extensión de marcado una forma sencilla de especificar los estilos que se fusionarán. Básicamente, hay dos formas en que el usuario puede especificar entradas para una extensión de marcado. El usuario puede establecer propiedades o pasar parámetros al constructor. Dado que en este escenario el usuario necesita la capacidad de especificar un número ilimitado de estilos, mi primer enfoque fue crear un constructor que tome cualquier número de cadenas usando la palabra clave "params":

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Mi objetivo era poder escribir las entradas de la siguiente manera:

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

Observe la coma que separa las diferentes teclas de estilo. Desafortunadamente, las extensiones de marcado personalizadas no admiten un número ilimitado de parámetros de constructor, por lo que este enfoque da como resultado un error de compilación. Si supiera de antemano cuántos estilos quería fusionar, podría haber usado la misma sintaxis XAML con un constructor que toma el número deseado de cadenas:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Como solución alternativa, decidí que el parámetro constructor tome una sola cadena que especifique los nombres de estilo separados por espacios. La sintaxis no es tan mala:

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

Cálculo de la salida de la extensión de marcado

Para calcular la salida de una extensión de marcado, necesitamos anular un método de MarkupExtension llamado "ProvideValue". El valor devuelto por este método se establecerá en el objetivo de la extensión de marcado.

Comencé creando un método de extensión para Style que sabe cómo combinar dos estilos. El código para este método es bastante 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];
    }
}

Con la lógica anterior, el primer estilo se modifica para incluir toda la información del segundo. Si hay conflictos (por ejemplo, ambos estilos tienen un setter para la misma propiedad), gana el segundo estilo. Tenga en cuenta que, además de copiar estilos y desencadenantes, también tuve en cuenta los valores TargetType y BasedOn, así como cualquier recurso que pueda tener el segundo estilo. Para el TargetType del estilo combinado, utilicé el tipo más derivado. Si el segundo estilo tiene un estilo BasedOn, fusiono su jerarquía de estilos de forma recursiva. Si tiene recursos, los copio al primer estilo. Si se hace referencia a esos recursos mediante {StaticResource}, se resuelven estáticamente antes de que se ejecute este código de combinación y, por lo tanto, no es necesario moverlos. Agregué este código en caso de que estemos usando DynamicResources.

El método de extensión que se muestra arriba habilita la siguiente sintaxis:

style1.Merge(style2);

Esta sintaxis es útil siempre que tenga instancias de ambos estilos dentro de ProvideValue. Pues yo no. Todo lo que obtengo del constructor es una lista de claves de cadena para esos estilos. Si hubiera soporte para parámetros en los parámetros del constructor, podría haber usado la siguiente sintaxis para obtener las instancias de estilo reales:

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

Pero eso no funciona. E incluso si la limitación de parámetros no existiera, probablemente alcanzaríamos otra limitación de las extensiones de marcado, donde tendríamos que usar la sintaxis del elemento de propiedad en lugar de la sintaxis del atributo para especificar los recursos estáticos, que es detallado y engorroso (explico esto error mejor en una publicación de blog anterior ). E incluso si ambas limitaciones no existieran, aún preferiría escribir la lista de estilos usando solo sus nombres: es más corto y más fácil de leer que un StaticResource para cada uno.

La solución es crear una StaticResourceExtension usando código. Dada una clave de estilo de tipo cadena y un proveedor de servicios, puedo usar StaticResourceExtension para recuperar la instancia de estilo real. Aquí está la sintaxis:

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

Ahora tenemos todas las piezas necesarias para escribir el método 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;
}

Aquí hay un ejemplo completo del uso de la extensión de marcado 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" />

ingrese la descripción de la imagen aquí


3
Muy buena solución, pero no entiendo por qué no hay una solución simple para fusionar el estilo 3 o +.
Sr.Rubix

31

Pero puede extenderse desde otro ... eche un vistazo a la propiedad 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>

Esto fue suficiente para mí. tnks!
David Lay

Pero esto solo funciona si ambos estilos son del mismo tipo (error XAML: "Solo puede basarse en un estilo con el tipo de destino que es el tipo base '<tipo>')
Krzysztof Bociurko

17

WPF / XAML no proporciona esta funcionalidad de forma nativa, pero sí proporciona la extensibilidad para permitirle hacer lo que desee.

Nos encontramos con la misma necesidad, y terminamos creando nuestra propia extensión de marcado XAML (que llamamos "MergedStylesExtension") para permitirnos crear un nuevo estilo a partir de otros dos estilos (que, de ser necesario, probablemente podrían usarse varias veces en un fila para heredar de incluso más estilos).

Debido a un error de WPF / XAML, necesitamos usar la sintaxis del elemento de propiedad para usarlo, pero aparte de eso parece funcionar bien. P.ej,

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

Recientemente escribí sobre esto aquí: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


3

Esto es posible creando una clase auxiliar para usar y ajustar sus estilos. CompoundStyle mencionado aquí muestra cómo hacerlo. Hay varias formas, pero la más fácil es hacer lo siguiente:

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

Espero que ayude.


2

Úselo AttachedPropertypara configurar múltiples estilos como el siguiente código:

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

Uso:

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

Resultado:

ingrese la descripción de la imagen aquí


1

si no toca ninguna propiedad específica, puede obtener todas las propiedades básicas y comunes al estilo cuyo tipo de destino sería FrameworkElement. luego, puede crear sabores específicos para cada tipo de destino que necesite, sin necesidad de copiar todas esas propiedades comunes nuevamente.


1

Probablemente pueda obtener algo similar si aplica esto a una colección de elementos mediante el uso de un StyleSelector, lo he usado para abordar un problema similar al usar diferentes estilos en TreeViewItems dependiendo del tipo de objeto vinculado en el árbol. Es posible que deba modificar ligeramente la siguiente clase para ajustarse a su enfoque particular, pero con suerte esto lo ayudará a comenzar

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

Luego aplica esto como tal

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

1

A veces puede abordar esto anidando paneles. Supongamos que tiene un estilo que cambia el primer plano y otro cambia FontSize, puede aplicar el último en un TextBlock y colocarlo en una cuadrícula cuyo estilo es el primero. Esto podría ayudar y podría ser la forma más fácil en algunos casos, aunque no resolverá todos los problemas.


1

Cuando anula SelectStyle, puede obtener la propiedad GroupBy a través de la reflexión como a continuación:

    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 está tratando de aplicar un estilo único a un solo elemento como una adición a un estilo base, hay una forma completamente diferente de hacerlo que es en mi humilde opinión mucho mejor para el código legible y mantenible.

Es extremadamente común necesitar ajustar parámetros por elemento individual. Definir estilos de diccionario solo para usar en un elemento es extremadamente engorroso de mantener o tener sentido. Para evitar crear estilos solo para ajustes de elementos únicos, lea mi respuesta a mi propia pregunta aquí:

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.