Como aplicar vários estilos no WPF

153

No WPF, como eu aplicaria vários estilos a um FrameworkElement? Por exemplo, eu tenho um controle que já tem um estilo. Eu também tenho um estilo separado que eu gostaria de acrescentar a ele sem estragar o primeiro. Os estilos têm TargetTypes diferentes, então não posso apenas estender um com o outro.

MojoFilter
fonte
O OP nunca especificou se seu primeiro estilo é exclusivo para um único controle. As respostas fornecidas nesta página pressupõem a necessidade de compartilhar os dois estilos em vários controles. Se você estiver procurando uma maneira de usar estilos de base em controles e substituir propriedades individuais diretamente em controles individuais: consulte esta resposta: stackoverflow.com/a/54497665/1402498 #
JamesHoux 02/02/19

Respostas:

154

Eu acho que a resposta simples é que você não pode fazer (pelo menos nesta versão do WPF) o que está tentando fazer.

Ou seja, para qualquer elemento específico, apenas um estilo pode ser aplicado.

No entanto, como outros já declararam acima, talvez você possa usar BasedOnpara ajudá-lo. Confira o seguinte pedaço de xaml solto. Nele, você verá que eu tenho um estilo base que está definindo uma propriedade que existe na classe base do elemento ao qual desejo aplicar dois estilos. E, no segundo estilo, baseado no estilo base, defino outra propriedade.

Portanto, a idéia aqui ... é que, de alguma forma, você pode separar as propriedades que deseja definir ... de acordo com a hierarquia de herança do elemento em que deseja definir vários estilos ... você pode ter uma solução 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 isto ajude.

Nota:

Uma coisa em particular a ser observada. Se você alterar o TargetTypesegundo estilo (no primeiro conjunto de xaml acima) para ButtonBase, os dois estilos não serão aplicados. No entanto, verifique o seguinte xaml abaixo para contornar essa restrição. Basicamente, isso significa que você precisa fornecer uma chave ao estilo e referenciá-lo com essa chave.

<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>
cplotts
fonte
10
Lembre-se ... ** A encomenda é importante **. O derivedStyledeve vir após obaseStyle
SliverNinja - MSFT
50

Bea Stollnitz teve um bom post sobre como usar uma extensão de marcação para isso, sob o título "Como posso definir vários estilos no WPF?"

Esse blog está morto agora, então estou reproduzindo o post aqui


O WPF e o Silverlight oferecem a capacidade de derivar um estilo de outro estilo por meio da propriedade “BasedOn”. Esse recurso permite que os desenvolvedores organizem seus estilos usando uma hierarquia semelhante à herança de classe. Considere os seguintes 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>

Com essa sintaxe, um Button que usa RedButtonStyle terá sua propriedade Foreground definida como Red e sua propriedade Margin definida como 10.

Esse recurso existe no WPF há muito tempo e é novo no Silverlight 3.

E se você quiser definir mais de um estilo em um elemento? Nem o WPF nem o Silverlight fornecem uma solução para esse problema imediatamente. Felizmente, existem maneiras de implementar esse comportamento no WPF, que discutirei nesta postagem do blog.

O WPF e o Silverlight usam extensões de marcação para fornecer propriedades com valores que requerem alguma lógica para serem obtidos. As extensões de marcação são facilmente reconhecíveis pela presença de colchetes ao redor deles no XAML. Por exemplo, a extensão de marcação {Binding} contém lógica para buscar um valor de uma fonte de dados e atualizá-lo quando ocorrerem alterações; a extensão de marcação {StaticResource} contém lógica para obter um valor de um dicionário de recursos com base em uma chave. Felizmente para nós, o WPF permite que os usuários gravem suas próprias extensões de marcação personalizadas. Esse recurso ainda não está presente no Silverlight, portanto, a solução neste blog é aplicável apenas ao WPF.

Outros criaram ótimas soluções para mesclar dois estilos usando extensões de marcação. No entanto, eu queria uma solução que oferecesse a capacidade de mesclar um número ilimitado de estilos, o que é um pouco mais complicado.

Escrever uma extensão de marcação é simples. A primeira etapa é criar uma classe derivada de MarkupExtension e usar o atributo MarkupExtensionReturnType para indicar que você deseja que o valor retornado da extensão de marcação seja do tipo Style.

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

Especificando entradas para a extensão de marcação

Gostaríamos de oferecer aos usuários da nossa extensão de marcação uma maneira simples de especificar os estilos a serem mesclados. Existem basicamente duas maneiras pelas quais o usuário pode especificar entradas para uma extensão de marcação. O usuário pode definir propriedades ou passar parâmetros para o construtor. Como nesse cenário o usuário precisa especificar um número ilimitado de estilos, minha primeira abordagem foi criar um construtor que use qualquer número de strings usando a palavra-chave "params":

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Meu objetivo era ser capaz de escrever as entradas da seguinte maneira:

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

Observe a vírgula separando as diferentes teclas de estilo. Infelizmente, as extensões de marcação personalizadas não oferecem suporte a um número ilimitado de parâmetros do construtor; portanto, essa abordagem resulta em um erro de compilação. Se eu soubesse com antecedência quantos estilos queria mesclar, poderia ter usado a mesma sintaxe XAML com um construtor que obtém o número desejado de strings:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Como solução alternativa, decidi fazer com que o parâmetro construtor usasse uma única sequência que especifique os nomes de estilos separados por espaços. A sintaxe não é tão ruim:

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

Calculando a saída da extensão de marcação

Para calcular a saída de uma extensão de marcação, precisamos substituir um método de MarkupExtension chamado "ProvideValue". O valor retornado desse método será definido no destino da extensão de marcação.

Comecei criando um método de extensão para o Style que sabe mesclar dois estilos. O código para este método é bastante simples:

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

Com a lógica acima, o primeiro estilo é modificado para incluir todas as informações do segundo. Se houver conflitos (por exemplo, ambos os estilos têm um setter para a mesma propriedade), o segundo estilo vence. Observe que, além de copiar estilos e gatilhos, também considerei os valores TargetType e BasedOn, bem como quaisquer recursos que o segundo estilo possa ter. Para o TargetType do estilo mesclado, usei o tipo que for mais derivado. Se o segundo estilo tiver um estilo BasedOn, mesclar sua hierarquia de estilos recursivamente. Se houver recursos, eu os copio para o primeiro estilo. Se esses recursos forem referidos como {StaticResource}, eles serão resolvidos estaticamente antes da execução desse código de mesclagem e, portanto, não será necessário movê-los. Eu adicionei esse código no caso de estarmos usando DynamicResources.

O método de extensão mostrado acima habilita a seguinte sintaxe:

style1.Merge(style2);

Essa sintaxe é útil, desde que eu tenha instâncias de ambos os estilos no ProvideValue. Bem, eu não. Tudo o que recebo do construtor é uma lista de chaves de string para esses estilos. Se houvesse suporte para parâmetros nos parâmetros do construtor, eu poderia ter usado a seguinte sintaxe para obter as instâncias de estilo reais:

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

Mas isso não funciona. E mesmo que a limitação de parâmetros não existisse, provavelmente atingiríamos outra limitação de extensões de marcação, onde teríamos que usar a sintaxe de elemento de propriedade em vez da sintaxe de atributo para especificar os recursos estáticos, que são detalhados e pesados ​​(eu explico isso bug melhor em uma postagem anterior do blog ). E mesmo que essas duas limitações não existissem, eu preferiria escrever a lista de estilos usando apenas seus nomes - é mais curto e mais simples de ler do que um StaticResource para cada um.

A solução é criar um StaticResourceExtension usando o código. Dada uma chave de estilo do tipo string e um provedor de serviços, posso usar StaticResourceExtension para recuperar a instância de estilo real. Aqui está a sintaxe:

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

Agora, temos todas as peças necessárias para escrever o 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;
}

Aqui está um exemplo completo do uso da extensão de marcação 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" />

insira a descrição da imagem aqui

Wilka
fonte
3
Solução realmente boa, mas não entendo por que não há uma solução simples para mesclar o estilo 3 ou +.
Rubix
31

Mas você pode estender de outro. Dê uma olhada na propriedade 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>
Arcturus
fonte
isso foi o suficiente para mim. tnks!
David Lay
Mas isso só funciona se os dois estilos forem do mesmo tipo (erro XAML: "Só pode basear-se em um estilo com o tipo de destino que é o tipo base '<tipo>'))
Krzysztof Bociurko
17

O WPF / XAML não fornece essa funcionalidade nativamente, mas fornece a extensibilidade para permitir que você faça o que deseja.

Tivemos a mesma necessidade e acabamos criando nossa própria extensão de marcação XAML (que chamamos de "MergedStylesExtension") para permitir a criação de um novo estilo a partir de dois outros estilos (que, se necessário, provavelmente poderiam ser usados ​​várias vezes em um linha para herdar ainda mais estilos).

Devido a um bug do WPF / XAML, precisamos usar a sintaxe do elemento de propriedade para usá-lo, mas fora isso parece funcionar bem. Por exemplo,

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

Eu escrevi recentemente sobre isso aqui: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


fonte
3

Isso é possível criando uma classe auxiliar para usar e agrupar seus estilos. O CompoundStyle mencionado aqui mostra como fazê-lo. Existem várias maneiras, mas a mais fácil é fazer o seguinte:

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

Espero que ajude.

Shahar Prish
fonte
2

Use AttachedPropertypara definir vários estilos, como o seguinte 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;
    }
}

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>

Resultado:

insira a descrição da imagem aqui

google dev
fonte
1

se você não estiver tocando em nenhuma propriedade específica, poderá obter todas as propriedades básicas e comuns no estilo cujo tipo de destino seria FrameworkElement. então, você pode criar sabores específicos para cada tipo de destino necessário, sem precisar copiar todas essas propriedades comuns novamente.

Greg
fonte
1

Provavelmente, você pode obter algo semelhante ao aplicar isso a uma coleção de itens pelo uso de um StyleSelector, usei isso para abordar um problema semelhante ao usar estilos diferentes em TreeViewItems, dependendo do tipo de objeto vinculado na árvore. Pode ser necessário modificar a classe abaixo levemente para se ajustar à sua abordagem específica, mas espero que isso ajude você a começar

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

Você então aplica isso da seguinte maneira

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

Às vezes, você pode abordar isso aninhando painéis. Digamos que você tenha um estilo que muda o primeiro plano e outro o FontSize, você pode aplicar o último em um TextBlock e colocá-lo em uma grade cujo estilo é o primeiro. Isso pode ajudar e pode ser a maneira mais fácil em alguns casos, embora não resolva todos os problemas.

Hillin
fonte
1

Quando você substitui o SelectStyle, pode obter a propriedade GroupBy através da reflexão, como abaixo:

    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;
    }
Sérgio Henrique
fonte
0

Se você está tentando aplicar um estilo único a apenas um único elemento , além de um estilo base, há uma maneira completamente diferente de fazer isso: o IMHO é muito melhor para código legível e de manutenção.

É extremamente comum precisar ajustar parâmetros por elemento individual. Definir estilos de dicionário apenas para uso em um elemento é extremamente complicado de manter ou entender. Para evitar a criação de estilos apenas para ajustes pontuais de elementos, leia minha resposta para minha própria pergunta aqui aqui:

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

JamesHoux
fonte