WPFで複数のスタイルを適用する方法


153

WPFでは、複数のスタイルをどのように適用しますFrameworkElementか?たとえば、既にスタイルが設定されているコントロールがあります。最初のスタイルを吹き飛ばさずに追加したい別のスタイルもあります。スタイルには異なるTargetTypeがあるため、一方を他方で拡張することはできません。


OPは、彼の最初のスタイルが1つのコントロールだけに固有のものであるかどうかを特定しませんでした。このページの回答は、複数のコントロールで両方のスタイルを共有する必要があることを前提としています。コントロールで基本スタイルを使用し、個々のコントロールで個々のプロパティを直接オーバーライドする方法を探している場合:この回答を参照してください: stackoverflow.com/a/54497665/1402498
JamesHoux

回答:


154

簡単な答えは、あなたがしようとしていることを(少なくともこのバージョンのWPFでは)できないということです。

つまり、特定の要素に対して適用できるスタイルは1つだけです。

ただし、他の人が上で述べたように、多分あなたはBasedOnあなたを助けるのに使うことができます。次のルーズxamlを確認してください。その中に、2つのスタイルを適用する要素の基本クラスに存在するプロパティを設定する基本スタイルがあることがわかります。そして、ベーススタイルに基づく2番目のスタイルで、別のプロパティを設定します。

したがって、ここでの考え方は、複数のスタイルを設定する要素の継承階層に従って、設定するプロパティを何らかの方法で分離できるかどうかです。回避策があるかもしれません。

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


お役に立てれば。

注意:

特に注意すべき点が1つあります。あなたが変更した場合TargetType(上記のXAMLの最初のセットに)第二のスタイルにするButtonBase、2つのスタイルが適用されません。ただし、その制限を回避するには、以下のxamlを確認してください。基本的には、スタイルにキーを与え、そのキーで参照する必要があることを意味します。

<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
覚えておいてください... **注文は重要です**。derivedStyle後に来なければならないbaseStyle
MSFT - SliverNinja

50

Bea Stollnitzは「WPFで複数のスタイルを設定するにはどうすればよいですか?」という見出しの下に、このためのマークアップ拡張機能の使用に関する優れたブログ投稿を用意しました。

そのブログはもう死んでいるので、ここで投稿を複製しています


WPFとSilverlightはどちらも、「BasedOn」プロパティを通じて別のスタイルからスタイルを派生させる機能を提供します。この機能により、開発者はクラス継承と同様の階層を使用してスタイルを編成できます。次のスタイルを検討してください。

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

この構文では、RedButtonStyleを使用するボタンは、ForegroundプロパティがRedに設定され、Marginプロパティが10に設定されます。

この機能は長い間WPFで使用されており、Silverlight 3の新機能です。

要素に複数のスタイルを設定する場合はどうでしょうか?WPFもSilverlightも、そのままではこの問題の解決策を提供しません。幸い、この動作をWPFに実装する方法があります。これについては、このブログ投稿で説明します。

WPFとSilverlightは、マークアップ拡張機能を使用して、取得するためにいくつかのロジックを必要とする値をプロパティに提供します。マークアップ拡張機能は、XAMLでそれらを囲む中かっこの存在によって簡単に認識できます。たとえば、{Binding}マークアップ拡張には、データソースから値をフェッチし、変更が発生したときにそれを更新するロジックが含まれています。{StaticResource}マークアップ拡張には、キーに基づいてリソースディクショナリから値を取得するロジックが含まれています。幸運なことに、WPFではユーザーが独自のカスタムマークアップ拡張機能を作成できます。この機能はまだSilverlightには存在しないため、このブログのソリューションはWPFにのみ適用できます。

他のユーザーは、マークアップ拡張機能を使用して2つのスタイルをマージする優れたソリューションを作成しました。しかし、私は無制限の数のスタイルをマージする機能を提供するソリューションを求めていました。これは少しトリッキーです。

マークアップ拡張機能の記述は簡単です。最初のステップは、MarkupExtensionから派生するクラスを作成し、MarkupExtensionReturnType属性を使用して、マークアップ拡張機能から返される値がStyleタイプであることを意図していることを示すことです。

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

マークアップ拡張への入力の指定

マークアップ拡張機能のユーザーに、マージするスタイルを指定する簡単な方法を提供したいと思います。ユーザーがマークアップ拡張機能への入力を指定できる方法は、基本的に2つあります。ユーザーはプロパティを設定するか、コンストラクタにパラメータを渡すことができます。このシナリオでは、ユーザーが無制限のスタイルを指定できる必要があるため、最初のアプローチは、「params」キーワードを使用して任意の数の文字列を受け取るコンストラクターを作成することでした。

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

私の目標は、次のように入力を記述できるようにすることでした。

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

異なるスタイルキーを区切るコンマに注意してください。残念ながら、カスタムマークアップ拡張機能は無制限の数のコンストラクターパラメーターをサポートしていないため、この方法ではコンパイルエラーが発生します。マージするスタイルの数が事前にわかっている場合は、目的の数の文字列を取得するコンストラクターで同じXAML構文を使用できます。

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

回避策として、私はコンストラクター・パラメーターに、スペースで区切られたスタイル名を指定する単一のストリングを取得させることにしました。構文はそれほど悪くありません:

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

マークアップ拡張機能の出力の計算

マークアップ拡張機能の出力を計算するには、「ProvideValue」と呼ばれるMarkupExtensionのメソッドをオーバーライドする必要があります。このメソッドから返される値は、マークアップ拡張機能のターゲットに設定されます。

2つのスタイルをマージする方法を知っているStyleの拡張メソッドを作成することから始めました。このメソッドのコードは非常に簡単です。

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

上記のロジックでは、最初のスタイルが変更され、2番目のスタイルからのすべての情報が含まれます。競合がある場合(たとえば、両方のスタイルに同じプロパティのセッターがある)、2番目のスタイルが優先されます。スタイルとトリガーのコピーの他に、TargetTypeとBasedOnの値、および2番目のスタイルが持つ可能性のあるリソースも考慮に入れていることに注意してください。マージされたスタイルのTargetTypeには、より派生したタイプを使用しました。2番目のスタイルにBasedOnスタイルがある場合、スタイルの階層を再帰的にマージします。リソースがある場合は、最初のスタイルにコピーします。これらのリソースが{StaticResource}を使用して参照されている場合、これらのリソースは、このマージコードが実行される前に静的に解決されるため、移動する必要はありません。DynamicResourcesを使用している場合に備えて、このコードを追加しました。

上記の拡張メソッドは、次の構文を有効にします。

style1.Merge(style2);

この構文は、ProvideValue内に両方のスタイルのインスタンスがある場合に便利です。まあ、私はしません。コンストラクターから取得できるのは、これらのスタイルの文字列キーのリストだけです。コンストラクターパラメーターでparamsのサポートがあった場合、次の構文を使用して実際のスタイルインスタンスを取得できます。

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

しかし、それはうまくいきません。そして、params制限が存在しなかったとしても、マークアップ拡張機能の別の制限にぶつかるでしょう。静的構文を指定するために、属性構文ではなくプロパティ要素構文を使用する必要があるため、冗長で扱いにくいです(これについて説明します)。以前のブログ投稿のバグが改善されました)。そして、これらの制限が両方とも存在しなかったとしても、スタイルのリストをそれらの名前だけを使用して作成します。それぞれのStaticResourceよりも短くて読みやすいです。

解決策は、コードを使用してStaticResourceExtensionを作成することです。文字列型のスタイルキーとサービスプロバイダーを指定すると、StaticResourceExtensionを使用して実際のスタイルインスタンスを取得できます。構文は次のとおりです。

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

これで、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;
}

次に、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" />

ここに画像の説明を入力してください


3
本当に良い解決策ですが、3または+スタイルをマージする単純な解決策がない理由がわかりません。

31

しかし、別のものから拡張できます。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>

これで十分でした。tnks!
David Lay、

ただし、これは両方のスタイルが同じタイプである場合にのみ機能します(XAMLエラー:「ベースタイプ '<type>'であるターゲットタイプのスタイルのみに
基づく

17

WPF / XAMLはこの機能をネイティブに提供しませんが、必要なことを実行できるようにする拡張性を提供します。

同じニーズに遭遇し、独自のXAMLマークアップ拡張機能(「MergedStylesExtension」と呼ばれる)を作成して、他の2つのスタイルから新しいスタイルを作成できるようにしました(必要に応じて、おそらく複数回使用できます)さらに多くのスタイルから継承する行)。

WPF / XAMLのバグのため、プロパティ要素構文を使用して使用する必要がありますが、それ以外は問題なく動作するようです。例えば、

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

私は最近それについてここに書きました:http : //swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


3

これは、スタイルを使用およびラップするヘルパークラスを作成することで可能です。ここで説明するCompoundStyle は、その方法を示しています。複数の方法がありますが、最も簡単な方法は次のとおりです。

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

お役に立てば幸いです。


2

AttachedProperty次のコードのように複数のスタイルを設定するために使用します。

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>

結果:

ここに画像の説明を入力してください


1

特定のプロパティに触れていない場合は、ターゲットタイプがFrameworkElementであるスタイルのすべての基本プロパティと共通プロパティを取得できます。次に、必要なターゲットタイプごとに特定のフレーバーを作成できます。共通のプロパティをすべてコピーする必要はありません。


1

StyleSelectorを使用してアイテムのコレクションにこれを適用すると、おそらく似たような結果が得られます。これを使用して、ツリーのバインドされたオブジェクトタイプに応じてTreeViewItemsで異なるスタイルを使用する場合の同様の問題に取り組みました。特定のアプローチに合わせるために、以下のクラスをわずかに変更する必要があるかもしれませんが、うまくいけば、これであなたは始められます

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

次に、これをそのまま適用します

 <ツリービュー>
     <TreeView.ItemContainerStyleSelector
         <myassembly:MyTreeStyleSelector DefaultStyle = "{StaticResource DefaultItemStyle}"
                                         NewStyle = "{StaticResource NewItemStyle}" />
     </TreeView.ItemContainerStyleSelector>
  </ TreeView>

1

場合によっては、パネルをネストすることでこれにアプローチできます。フォアグラウンドを変更するスタイルとフォントサイズを変更するスタイルがあるとします。後者をTextBlockに適用し、最初のスタイルであるグリッドに配置できます。これは役立つ場合があり、場合によっては最も簡単な方法ですが、すべての問題を解決することはできません。


1

SelectStyleをオーバーライドすると、以下のようなリフレクションを介してGroupByプロパティを取得できます。

    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

単一の要素にのみユニークなスタイルを適用しようとしている場合基本スタイルへの追加としてこれを実行するための完全に異なる方法があり、読み取り可能で保守可能なコードのIMHOがはるかに優れています。

個々の要素ごとにパラメータを微調整する必要があることは非常に一般的です。1つの要素で使用するだけの辞書スタイルを定義することは、維持または理解するのが非常に面倒です。1回限りの要素の調整だけでスタイルを作成しないようにするには、ここで自分の質問に対する私の回答を読んでください。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.