.net - trigger - xaml style wpf




如何在WPF中應用多種樣式 (7)

在WPF中,我將如何將多個樣式應用於FrameworkElement ? 例如,我有一個已經有風格的控件。 我也有一個獨立的風格,我想添加它,而不會吹掉第一個。 這些樣式具有不同的TargetType,因此我不能只用另一個來擴展它們。


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的Button將其Foreground屬性設置為Red,並將其Margin屬性設置為10。

這個特性在WPF中已經有很長時間了,在Silverlight 3中是新特性。

如果你想在一個元素上設置多個樣式會怎麼樣? WPF和Silverlight都沒有為這個問題提供解決方案。 幸運的是,有些方法可以在WPF中實現此行為,我將在本博文中討論這些行為。

WPF和Silverlight使用標記擴展來為屬性提供需要獲取某些邏輯的值。 標記擴展很容易通過XAML中圍繞它們的大括號來識別。 例如,{Binding}標記擴展包含用於從數據源獲取值並在發生更改時進行更新的邏輯; {StaticResource}標記擴展包含基於密鑰從資源字典中獲取值的邏輯。 對我們來說幸運的是,WPF允許用戶編寫自己的自定義標記擴展。 此功能在Silverlight中尚不存在,因此此博客中的解決方案僅適用於WPF。

Others已經寫出了很好的解決方案來使用標記擴展來合併兩種樣式 但是,我想要一個能夠合併無限數量樣式的解決方案,這有點棘手。

編寫標記擴展很簡單。 第一步是創建一個派生自MarkupExtension的類,並使用MarkupExtensionReturnType屬性來指示您打算從標記擴展返回的值為Style類型。

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

指定標記擴展的輸入

我們希望為用戶提供標記擴展程序,以簡單方式指定要合併的樣式。 基本上有兩種用戶可以指定標記擴展輸入的方式。 用戶可以設置屬性或將參數傳遞給構造函數。 由於在這種情況下,用戶需要指定無限數量樣式的能力,我的第一種方法是創建一個構造函數,該構造函數使用“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方法。 從此方法返回的值將設置在標記擴展的目標中。

我開始為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];
    }
}

通過上面的邏輯,第一種風格被修改為包含第二種風格的所有信息。 如果存在衝突(例如,兩個樣式都有相同屬性的setter),則第二種樣式會勝出。 注意,除了複製樣式和触發器外,我還考慮了TargetType和BasedOn值以及第二種樣式可能具有的任何資源。 對於合併樣式的TargetType,我使用更多派生的類型。 如果第二個樣式具有BasedOn樣式,我將遞歸地合併它的樣式層次結構。 如果它有資源,我將它們複製到第一個樣式。 如果使用{StaticResource}引用這些資源,則在此合併代碼執行之前它們會被靜態解析,因此無需移動它們。 我添加了這個代碼,以防我們使用DynamicResources。

上面顯示的擴展方法使用以下語法:

style1.Merge(style2);

這個語法很有用,前提是我在ProvideValue中有兩個樣式的實例。 那麼,我不知道。 我從構造函數中得到的是這些樣式的字符串鍵列表。 如果在構造函數參數中支持參數,我可以使用以下語法來獲取實際的樣式實例:

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

但那不行。 即使參數限制不存在,我們可能會遇到另一個標記擴展的限制,我們必須使用屬性元素語法而不是屬性語法來指定靜態資源,這是冗長和繁瑣的(我解釋了這一點在以前的博客文章中 bug更好)。 即使這兩個限制都不存在,我仍然寧願使用他們的名字來編寫樣式列表 - 它比每個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" />


WPF / XAML本身並不提供這種功能,但它確實提供了可擴展性,可以讓您按照自己的意願進行操作。

我們遇到了同樣的需求,並最終創建了自己的XAML標記擴展(我們稱之為“MergedStylesExtension”),以允許我們從兩種其他樣式創建一個新的樣式(如果需要,可能會多次使用行以繼承更多樣式)。

由於WPF / XAML錯誤,我們需要使用property元素語法來使用它,但除此之外它似乎工作正常。 例如,

<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/ : http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


但你可以從另一個擴展..看看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>

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

結果:


如果通過使用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>
     <TreeView.ItemContainerStyleSelector
         <myassembly:MyTreeStyleSelector DefaultStyle="{StaticResource DefaultItemStyle}"
                                         NewStyle="{StaticResource NewItemStyle}" />
     </TreeView.ItemContainerStyleSelector>
  </TreeView>

有時你可以通過嵌套面板來解決這個問題。 假設你有一個Style改變了前景,另一個改變了FontSize,你可以將後者應用於TextBlock,並將其放在一個Grid中,其Style是第一個。 這可能會有所幫助,並且在某些情況下可能是最簡單的方法,但它不能解決所有問題。


這可以通過創建一個幫助類來使用和包裝你的樣式。 here提到的CompoundStyle顯示瞭如何去做。 有多種方式,但最簡單的方法是執行以下操作:

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

希望有所幫助。







styles