Bagaimana menerapkan beberapa gaya dalam WPF

153

Di WPF, bagaimana saya menerapkan beberapa gaya ke FrameworkElement? Misalnya, saya memiliki kontrol yang sudah memiliki gaya. Saya juga memiliki gaya terpisah yang ingin saya tambahkan tanpa menghilangkan yang pertama. Gaya memiliki TargetTypes berbeda, jadi saya tidak bisa hanya memperpanjang satu dengan yang lain.

MojoFilter
sumber
OP tidak pernah menentukan apakah gaya pertamanya unik untuk satu kontrol saja. Jawaban yang diberikan pada halaman ini mengasumsikan kebutuhan untuk berbagi kedua gaya di beberapa kontrol. Jika Anda mencari cara untuk menggunakan gaya dasar pada kontrol dan menimpa masing-masing properti secara langsung pada kontrol individu: lihat jawaban ini: stackoverflow.com/a/54497665/1402498
JamesHoux

Jawaban:

154

Saya pikir jawaban sederhana adalah bahwa Anda tidak dapat melakukan (paling tidak dalam versi WPF) apa yang Anda coba lakukan.

Yaitu, untuk elemen tertentu hanya satu Gaya yang dapat diterapkan.

Namun, seperti yang orang lain katakan di atas, mungkin Anda bisa menggunakan BasedOnuntuk membantu Anda. Lihatlah bagian xaml longgar berikut ini. Di dalamnya Anda akan melihat bahwa saya memiliki gaya dasar yang menetapkan properti yang ada di kelas dasar elemen yang ingin saya terapkan dua gaya. Dan, dalam gaya kedua yang didasarkan pada gaya dasar, saya mengatur properti lain.

Jadi, idenya di sini ... adalah jika Anda dapat memisahkan properti yang ingin Anda atur ... sesuai dengan hierarki warisan elemen yang ingin Anda atur beberapa gaya ... Anda mungkin memiliki solusinya.

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


Semoga ini membantu.

catatan:

Satu hal khusus yang perlu diperhatikan. Jika Anda mengubah TargetTypegaya kedua (pada xaml set pertama di atas) ButtonBase, kedua Gaya tidak diterapkan. Namun, periksa xaml berikut di bawah ini untuk mengatasi pembatasan itu. Pada dasarnya, itu berarti Anda perlu memberi Style sebuah kunci dan merujuknya dengan kunci itu.

<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
sumber
10
Ingat ... ** Pemesanan penting **. Itu derivedStyleharus datang setelahbaseStyle
SliverNinja - MSFT
50

Bea Stollnitz memiliki posting blog yang bagus tentang menggunakan ekstensi markup untuk ini, di bawah judul "Bagaimana saya bisa mengatur beberapa gaya di WPF?"

Blog itu sudah mati sekarang, jadi saya mereproduksi posting di sini


WPF dan Silverlight keduanya menawarkan kemampuan untuk mendapatkan Gaya dari Gaya lain melalui properti "BasedOn". Fitur ini memungkinkan pengembang untuk mengatur gaya mereka menggunakan hierarki yang mirip dengan warisan kelas. Pertimbangkan gaya berikut:

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

Dengan sintaks ini, sebuah Tombol yang menggunakan RedButtonStyle akan memiliki properti Foreground diatur ke Red dan properti Margin-nya diatur ke 10.

Fitur ini sudah ada di WPF sejak lama, dan ini baru di Silverlight 3.

Bagaimana jika Anda ingin mengatur lebih dari satu gaya pada elemen? Baik WPF maupun Silverlight tidak memberikan solusi untuk masalah ini di luar kebiasaan. Untungnya ada cara untuk menerapkan perilaku ini di WPF, yang akan saya bahas di posting blog ini.

WPF dan Silverlight menggunakan ekstensi markup untuk menyediakan properti dengan nilai-nilai yang memerlukan beberapa logika untuk diperoleh. Ekstensi markup mudah dikenali dengan adanya kurung keriting yang mengelilinginya di XAML. Misalnya, ekstensi markup {Binding} berisi logika untuk mengambil nilai dari sumber data dan memperbaruinya ketika terjadi perubahan; ekstensi markup {StaticResource} berisi logika untuk mengambil nilai dari kamus sumber daya berdasarkan kunci. Untungnya bagi kami, WPF memungkinkan pengguna untuk menulis ekstensi markup kustom mereka sendiri. Fitur ini belum ada di Silverlight, jadi solusi di blog ini hanya berlaku untuk WPF.

Yang lain telah menulis solusi hebat untuk menggabungkan dua gaya menggunakan ekstensi markup. Namun, saya menginginkan solusi yang memberikan kemampuan untuk menggabungkan jumlah gaya yang tidak terbatas, yang sedikit lebih rumit.

Menulis ekstensi markup sangat mudah. Langkah pertama adalah membuat kelas yang berasal dari MarkupExtension, dan gunakan atribut MarkupExtensionReturnType untuk menunjukkan bahwa Anda bermaksud nilai yang dikembalikan dari ekstensi markup Anda menjadi tipe Style.

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

Menentukan input untuk ekstensi markup

Kami ingin memberi pengguna ekstensi markup kami cara mudah untuk menentukan gaya yang akan digabungkan. Pada dasarnya ada dua cara di mana pengguna dapat menentukan input ke ekstensi markup. Pengguna dapat mengatur properti atau meneruskan parameter ke konstruktor. Karena dalam skenario ini pengguna memerlukan kemampuan untuk menentukan jumlah gaya yang tidak terbatas, pendekatan pertama saya adalah membuat konstruktor yang mengambil sejumlah string menggunakan kata kunci "params":

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Tujuan saya adalah untuk dapat menulis input sebagai berikut:

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

Perhatikan koma yang memisahkan tombol gaya yang berbeda. Sayangnya, ekstensi markup kustom tidak mendukung sejumlah parameter konstruktor yang tidak terbatas, sehingga pendekatan ini menghasilkan kesalahan kompilasi. Jika saya tahu sebelumnya berapa banyak gaya yang ingin saya gabungkan, saya bisa menggunakan sintaks XAML yang sama dengan konstruktor yang mengambil jumlah string yang diinginkan:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Sebagai solusinya, saya memutuskan untuk meminta parameter konstruktor mengambil string tunggal yang menentukan nama gaya yang dipisahkan oleh spasi. Sintaksnya tidak terlalu buruk:

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

Menghitung output ekstensi markup

Untuk menghitung output dari ekstensi markup, kita perlu mengganti metode dari MarkupExtension yang disebut "ProvidValue". Nilai yang dikembalikan dari metode ini akan ditetapkan dalam target ekstensi markup.

Saya mulai dengan membuat metode ekstensi untuk Gaya yang tahu cara menggabungkan dua gaya. Kode untuk metode ini cukup sederhana:

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

Dengan logika di atas, gaya pertama dimodifikasi untuk memasukkan semua informasi dari yang kedua. Jika ada konflik (misalnya kedua gaya memiliki setter untuk properti yang sama), gaya kedua menang. Perhatikan bahwa selain menyalin gaya dan pemicu, saya juga memperhitungkan nilai TargetType dan BasedOn serta sumber daya apa pun yang mungkin dimiliki gaya kedua. Untuk TargetType dari gaya gabungan, saya menggunakan tipe yang lebih diturunkan. Jika gaya kedua memiliki gaya BasedOn, saya menggabungkan hierarki gaya secara rekursif. Jika memiliki sumber daya, saya menyalinnya ke gaya pertama. Jika sumber daya tersebut dirujuk menggunakan {StaticResource}, sumber daya tersebut diselesaikan secara statis sebelum kode gabungan ini dieksekusi, dan karenanya tidak perlu memindahkannya. Saya menambahkan kode ini jika kami menggunakan DynamicResources.

Metode ekstensi yang ditunjukkan di atas memungkinkan sintaks berikut:

style1.Merge(style2);

Sintaks ini berguna asalkan saya memiliki contoh kedua gaya dalam ProvidValue. Ya saya tidak. Yang saya dapatkan dari konstruktor adalah daftar kunci string untuk gaya tersebut. Jika ada dukungan untuk params di parameter konstruktor, saya bisa menggunakan sintaks berikut untuk mendapatkan contoh gaya aktual:

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

Tapi itu tidak berhasil. Dan bahkan jika batasan params tidak ada, kita mungkin akan menemukan batasan lain dari ekstensi markup, di mana kita harus menggunakan sintaks properti-elemen alih-alih sintaks atribut untuk menentukan sumber daya statis, yang verbose dan rumit (saya jelaskan ini bug lebih baik di posting blog sebelumnya ). Dan bahkan jika kedua keterbatasan itu tidak ada, saya masih lebih suka menulis daftar gaya hanya dengan menggunakan nama mereka - lebih pendek dan lebih mudah dibaca daripada StaticResource untuk masing-masing.

Solusinya adalah membuat StaticResourceExtension menggunakan kode. Diberi kunci gaya string tipe dan penyedia layanan, saya bisa menggunakan StaticResourceExtension untuk mengambil contoh gaya aktual. Berikut ini sintaksnya:

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

Sekarang kita memiliki semua bagian yang diperlukan untuk menulis metode ProvidValue:

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

Berikut ini adalah contoh lengkap penggunaan ekstensi markup 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" />

masukkan deskripsi gambar di sini

Wilka
sumber
3
Solusi yang sangat bagus, tetapi saya tidak mengerti mengapa tidak ada solusi sederhana untuk menggabungkan 3 atau + style.
Tuan Rubix
31

Tetapi Anda dapat memperluas dari yang lain .. lihatlah properti 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
sumber
ini sudah cukup bagi saya. tnks!
David Lay
Tetapi ini hanya berfungsi jika kedua gaya memiliki tipe yang sama (kesalahan XAML: "Hanya dapat mendasarkan pada Gaya dengan tipe target yang merupakan tipe dasar '<type>')
Krzysztof Bociurko
17

WPF / XAML tidak menyediakan fungsionalitas ini secara asli, tetapi memang memberikan ekstensibilitas untuk memungkinkan Anda melakukan apa yang Anda inginkan.

Kami bertemu dengan kebutuhan yang sama, dan akhirnya membuat Ekstensi XAML Markup kami sendiri (yang kami sebut "MergedStylesExtension") untuk memungkinkan kami membuat Gaya baru dari dua gaya lain (yang, jika perlu, mungkin dapat digunakan beberapa kali dalam suatu baris untuk mewarisi dari lebih banyak gaya).

Karena bug WPF / XAML, kita perlu menggunakan sintaks elemen properti untuk menggunakannya, tetapi selain itu tampaknya berfungsi ok. Misalnya,

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

Baru-baru ini saya menulis tentang hal ini di sini: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multiple-style-inheritance-and-markup-extensions/


sumber
3

Ini dimungkinkan dengan membuat kelas pembantu untuk menggunakan dan membungkus gaya Anda. CompoundStyle yang disebutkan di sini menunjukkan cara melakukannya. Ada beberapa cara, tetapi yang paling mudah adalah dengan melakukan yang berikut:

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

Semoga itu bisa membantu.

Prish Shahar
sumber
2

Gunakan AttachedPropertyuntuk mengatur beberapa gaya seperti kode berikut:

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>

Hasil:

masukkan deskripsi gambar di sini

google dev
sumber
1

jika Anda tidak menyentuh properti tertentu, Anda bisa mendapatkan semua properti dasar dan umum dengan gaya yang tipe targetnya adalah FrameworkElement. kemudian, Anda dapat membuat rasa khusus untuk setiap jenis target yang Anda butuhkan, tanpa perlu menyalin semua properti umum itu lagi.

Greg
sumber
1

Anda mungkin bisa mendapatkan sesuatu yang serupa jika menerapkan ini ke koleksi item dengan menggunakan StyleSelector, saya telah menggunakan ini untuk mendekati masalah yang sama dalam menggunakan gaya yang berbeda pada TreeViewItems tergantung pada jenis objek terikat di pohon. Anda mungkin harus memodifikasi sedikit kelas di bawah ini untuk menyesuaikan dengan pendekatan khusus Anda, tetapi mudah-mudahan ini akan membantu Anda memulai

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

Anda kemudian menerapkannya seperti ini

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

Terkadang Anda bisa mendekati ini dengan panel bersarang. Katakanlah Anda memiliki Style yang mengubah Foreground dan perubahan lainnya FontSize, Anda dapat menerapkan yang terakhir pada TextBlock, dan meletakkannya di Grid yang Style-nya adalah yang pertama. Ini mungkin membantu dan mungkin cara termudah dalam beberapa kasus, meskipun tidak akan menyelesaikan semua masalah.

Hillin
sumber
1

Ketika Anda mengganti SelectStyle Anda bisa mendapatkan properti GroupBy melalui refleksi seperti di bawah ini:

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

Jika Anda mencoba menerapkan gaya unik hanya ke satu elemen tunggal sebagai tambahan gaya dasar, ada cara yang sama sekali berbeda untuk melakukan ini yaitu IMHO jauh lebih baik untuk kode yang dapat dibaca dan dipelihara.

Sangat umum untuk perlu mengubah parameter per elemen individu. Menentukan gaya kamus hanya untuk digunakan pada satu elemen sangat rumit untuk mempertahankan atau membuat masuk akal. Untuk menghindari membuat gaya hanya untuk tweak elemen sekali saja, baca jawaban saya untuk pertanyaan saya sendiri di sini di sini:

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

JamesHoux
sumber