Cách áp dụng nhiều kiểu trong WPF


153

Trong WPF, tôi sẽ áp dụng nhiều kiểu cho a FrameworkElementnhư thế nào? Ví dụ, tôi có một điều khiển đã có kiểu. Tôi cũng có một phong cách riêng mà tôi muốn thêm vào nó mà không làm mất đi kiểu đầu tiên. Các kiểu có TargetTypes khác nhau, vì vậy tôi không thể mở rộng cái này với cái kia.


OP không bao giờ chỉ định liệu kiểu đầu tiên của anh ta là duy nhất cho một điều khiển duy nhất. Câu trả lời được đưa ra trên trang này cho rằng cần phải chia sẻ cả hai kiểu trên nhiều điều khiển. Nếu bạn đang tìm kiếm một cách để phong cách sử dụng trên cơ sở kiểm soát và ghi đè các thuộc tính cá nhân trực tiếp trên điều khiển riêng: xem câu trả lời này: stackoverflow.com/a/54497665/1402498
JamesHoux

Câu trả lời:


154

Tôi nghĩ rằng câu trả lời đơn giản là bạn không thể làm (ít nhất là trong phiên bản WPF này) những gì bạn đang cố gắng làm.

Đó là, đối với bất kỳ yếu tố cụ thể nào, chỉ có một Kiểu có thể được áp dụng.

Tuy nhiên, như những người khác đã nêu ở trên, có lẽ bạn có thể sử dụng BasedOnđể giúp bạn. Kiểm tra các mảnh xaml sau đây. Trong đó bạn sẽ thấy rằng tôi có một kiểu cơ sở đang thiết lập một thuộc tính tồn tại trên lớp cơ sở của phần tử mà tôi muốn áp dụng hai kiểu. Và, trong kiểu thứ hai dựa trên kiểu cơ sở, tôi đặt một thuộc tính khác.

Vì vậy, ý tưởng ở đây ... là nếu bạn bằng cách nào đó có thể tách các thuộc tính mà bạn muốn đặt ... theo phân cấp kế thừa của phần tử bạn muốn đặt nhiều kiểu trên ... bạn có thể có một cách giải quyết.

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


Hi vọng điêu nay co ich.

Ghi chú:

Một điều đặc biệt cần lưu ý. Nếu bạn thay đổi TargetTypekiểu thứ hai (trong bộ xaml đầu tiên ở trên) thành ButtonBase, hai Kiểu sẽ không được áp dụng. Tuy nhiên, hãy xem xaml sau đây để khắc phục hạn chế đó. Về cơ bản, điều đó có nghĩa là bạn cần cung cấp cho khóa Kiểu và tham chiếu nó với khóa đó.

<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
Hãy nhớ ... ** Đặt hàng rất quan trọng **. Các derivedStylephải đến saubaseStyle
SliverNinja - MSFT

50

Bea Stollnitz đã có một bài đăng blog tốt về việc sử dụng tiện ích mở rộng đánh dấu cho mục này, dưới tiêu đề "Làm cách nào tôi có thể đặt nhiều kiểu trong WPF?"

Blog đó đã chết, vì vậy tôi đang sao chép bài viết ở đây


Cả WPF và Silverlight đều cung cấp khả năng lấy ra một Phong cách từ một Phong cách khác thông qua thuộc tính của Dựa trên cơ sở. Tính năng này cho phép các nhà phát triển sắp xếp các kiểu của họ bằng cách sử dụng một hệ thống phân cấp tương tự như kế thừa lớp. Hãy xem xét các phong cách sau:

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

Với cú pháp này, một Nút sử dụng RedButtonStyle sẽ có thuộc tính Tiền cảnh được đặt thành Đỏ và thuộc tính Margin của nó được đặt thành 10.

Tính năng này đã xuất hiện trong WPF từ lâu và nó mới xuất hiện trong Silverlight 3.

Điều gì nếu bạn muốn đặt nhiều hơn một kiểu trên một thành phần? Cả WPF và Silverlight đều không cung cấp giải pháp cho vấn đề này. May mắn thay, có nhiều cách để thực hiện hành vi này trong WPF, mà tôi sẽ thảo luận trong bài đăng trên blog này.

WPF và Silverlight sử dụng các phần mở rộng đánh dấu để cung cấp các thuộc tính với các giá trị yêu cầu một số logic để có được. Phần mở rộng đánh dấu có thể dễ dàng nhận ra bởi sự hiện diện của dấu ngoặc nhọn bao quanh chúng trong XAML. Ví dụ: tiện ích mở rộng đánh dấu {Binding} chứa logic để tìm nạp một giá trị từ nguồn dữ liệu và cập nhật nó khi có thay đổi; tiện ích mở rộng đánh dấu {StaticResource} chứa logic để lấy giá trị từ từ điển tài nguyên dựa trên khóa. May mắn cho chúng tôi, WPF cho phép người dùng viết các phần mở rộng đánh dấu tùy chỉnh của riêng họ. Tính năng này chưa có trong Silverlight, vì vậy giải pháp trong blog này chỉ áp dụng cho WPF.

Những người khác đã viết các giải pháp tuyệt vời để hợp nhất hai phong cách sử dụng tiện ích mở rộng đánh dấu. Tuy nhiên, tôi muốn một giải pháp cung cấp khả năng hợp nhất số lượng kiểu không giới hạn, khó hơn một chút.

Viết một phần mở rộng đánh dấu là đơn giản. Bước đầu tiên là tạo một lớp xuất phát từ MarkupExtension và sử dụng thuộc tính MarkupExtensionReturnType để chỉ ra rằng bạn dự định giá trị được trả về từ tiện ích mở rộng đánh dấu của bạn là Kiểu.

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

Chỉ định đầu vào cho phần mở rộng đánh dấu

Chúng tôi muốn cung cấp cho người dùng tiện ích mở rộng đánh dấu của chúng tôi một cách đơn giản để chỉ định các kiểu sẽ được hợp nhất. Về cơ bản có hai cách mà người dùng có thể chỉ định đầu vào cho tiện ích mở rộng đánh dấu. Người dùng có thể đặt thuộc tính hoặc truyền tham số cho hàm tạo. Vì trong kịch bản này, người dùng cần khả năng chỉ định số lượng kiểu không giới hạn, cách tiếp cận đầu tiên của tôi là tạo một hàm tạo lấy bất kỳ số chuỗi nào bằng cách sử dụng từ khóa param params:

public MultiStyleExtension(params string[] inputResourceKeys)
{
}

Mục tiêu của tôi là có thể viết các đầu vào như sau:

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

Lưu ý dấu phẩy phân tách các phím kiểu khác nhau. Thật không may, tiện ích mở rộng đánh dấu tùy chỉnh không hỗ trợ số lượng tham số hàm tạo không giới hạn, vì vậy cách tiếp cận này dẫn đến lỗi biên dịch. Nếu tôi biết trước có bao nhiêu kiểu tôi muốn hợp nhất, tôi có thể đã sử dụng cùng một cú pháp XAML với một hàm tạo lấy số chuỗi mong muốn:

public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}

Như một giải pháp thay thế, tôi đã quyết định để tham số hàm tạo lấy một chuỗi xác định tên kiểu được phân tách bằng dấu cách. Cú pháp không quá tệ:

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

Tính toán đầu ra của phần mở rộng đánh dấu

Để tính toán đầu ra của tiện ích mở rộng đánh dấu, chúng ta cần ghi đè một phương thức từ MarkupExtension có tên là Cung cấpValue. Giá trị được trả về từ phương thức này sẽ được đặt trong mục tiêu của tiện ích mở rộng đánh dấu.

Tôi đã bắt đầu bằng cách tạo một phương thức mở rộng cho Style để biết cách hợp nhất hai kiểu. Mã cho phương pháp này khá đơn giản:

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

Với logic ở trên, kiểu đầu tiên được sửa đổi để bao gồm tất cả thông tin từ kiểu thứ hai. Nếu có xung đột (ví dụ: cả hai kiểu đều có setter cho cùng một thuộc tính), kiểu thứ hai sẽ thắng. Lưu ý rằng ngoài việc sao chép kiểu và kích hoạt, tôi cũng đã tính đến các giá trị TargetType và Dựa trên cũng như bất kỳ tài nguyên nào mà kiểu thứ hai có thể có. Đối với TargetType của kiểu được hợp nhất, tôi đã sử dụng loại nào có nguồn gốc nhiều hơn. Nếu kiểu thứ hai có kiểu Dựa trên, tôi hợp nhất phân cấp kiểu của nó theo cách đệ quy. Nếu nó có tài nguyên, tôi sao chép chúng sang kiểu đầu tiên. Nếu các tài nguyên đó được đề cập đến bằng cách sử dụng {StaticResource}, chúng sẽ được giải quyết tĩnh trước khi mã hợp nhất này thực thi và do đó không cần thiết phải di chuyển chúng. Tôi đã thêm mã này trong trường hợp chúng tôi đang sử dụng DynamicResource.

Phương thức mở rộng được hiển thị ở trên cho phép cú pháp sau:

style1.Merge(style2);

Cú pháp này hữu ích với điều kiện tôi có các phiên bản của cả hai kiểu trong ProvValue. Vâng, tôi không. Tất cả những gì tôi nhận được từ hàm tạo là một danh sách các khóa chuỗi cho các kiểu đó. Nếu có hỗ trợ cho các tham số trong các tham số của hàm tạo, tôi có thể đã sử dụng cú pháp sau để lấy các thể hiện kiểu thực tế:

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

Nhưng điều đó không hiệu quả. Và ngay cả khi giới hạn params không tồn tại, có lẽ chúng ta sẽ gặp giới hạn khác của tiện ích mở rộng đánh dấu, trong đó chúng ta sẽ phải sử dụng cú pháp phần tử thuộc tính thay vì cú pháp thuộc tính để chỉ định tài nguyên tĩnh, dài dòng và cồng kềnh (tôi giải thích điều này lỗi tốt hơn trong một bài viết trên blog trước đó ). Và ngay cả khi cả hai giới hạn đó không tồn tại, tôi vẫn thà viết danh sách các kiểu chỉ sử dụng tên của chúng - nó ngắn hơn và đơn giản hơn để đọc so với TĩnhResource cho mỗi kiểu.

Giải pháp là tạo một StaticResourceExtension bằng mã. Đưa ra khóa kiểu của chuỗi kiểu và nhà cung cấp dịch vụ, tôi có thể sử dụng StaticResourceExtension để truy xuất thể hiện kiểu thực tế. Đây là cú pháp:

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

Bây giờ chúng ta có tất cả các phần cần thiết để viết phương thức ProvValue:

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

Dưới đây là một ví dụ đầy đủ về việc sử dụng tiện ích mở rộng đánh dấu 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" />

nhập mô tả hình ảnh ở đây


3
Giải pháp thực sự tốt, nhưng tôi không hiểu tại sao không có giải pháp đơn giản nào để hợp nhất kiểu 3 hoặc +.
Ông Rubix

31

Nhưng bạn có thể mở rộng từ người khác .. hãy xem thuộc tính Dựa trên

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

điều này là đủ cho tôi tnks!
David Lay

Nhưng điều này chỉ hoạt động nếu cả hai kiểu cùng loại (lỗi XAML: "Chỉ có thể dựa trên Kiểu có loại mục tiêu là loại cơ sở '<type>')
Krzysztof Bociurko

17

WPF / XAML không cung cấp chức năng này một cách tự nhiên, nhưng nó cung cấp khả năng mở rộng để cho phép bạn làm những gì bạn muốn.

Chúng tôi có cùng nhu cầu và cuối cùng đã tạo ra Tiện ích mở rộng đánh dấu XAML của riêng mình (mà chúng tôi gọi là "MergedStylesExtension") để cho phép chúng tôi tạo Kiểu mới từ hai kiểu khác (nếu cần, có thể được sử dụng nhiều lần trong một hàng để kế thừa từ nhiều phong cách hơn nữa).

Do lỗi WPF / XAML, chúng tôi cần sử dụng cú pháp phần tử thuộc tính để sử dụng nó, nhưng khác hơn là nó có vẻ hoạt động tốt. Ví dụ,

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

Gần đây tôi đã viết về nó ở đây: http://swdeveloper.wordpress.com/2009/01/03/wpf-xaml-multipl-style-inherribution-and-markup-extensions/


3

Điều này có thể bằng cách tạo một lớp trợ giúp để sử dụng và bao bọc các kiểu của bạn. CompoundStyle được đề cập ở đây cho thấy làm thế nào để làm điều đó. Có nhiều cách, nhưng cách dễ nhất là làm như sau:

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

Mong rằng sẽ giúp.


2

Sử dụng AttachedPropertyđể đặt nhiều kiểu như mã sau:

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

Sử dụng:

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

Kết quả:

nhập mô tả hình ảnh ở đây


1

nếu bạn không chạm vào bất kỳ thuộc tính cụ thể nào, bạn có thể nhận được tất cả các thuộc tính cơ bản và phổ biến cho kiểu mà kiểu mục tiêu sẽ là FrameworkEuity. sau đó, bạn có thể tạo các hương vị cụ thể cho từng loại mục tiêu bạn cần mà không cần phải sao chép lại tất cả các thuộc tính chung đó.


1

Bạn có thể có được một cái gì đó tương tự nếu áp dụng điều này cho một bộ sưu tập các vật phẩm bằng cách sử dụng StyleSelector, tôi đã sử dụng điều này để tiếp cận một vấn đề tương tự trong việc sử dụng các kiểu khác nhau trên TreeViewItems tùy thuộc vào loại đối tượng bị ràng buộc trong cây. Bạn có thể phải sửa đổi lớp bên dưới một chút để điều chỉnh theo cách tiếp cận cụ thể của mình nhưng hy vọng điều này sẽ giúp bạn bắt đầu

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

Sau đó, bạn áp dụng điều này như vậy

 <Cây cảnh>
     <TreeView.ItemContainerStyleSelector
         <myassugging: MyTreeStyleSelector DefaultStyle = "{StaticResource DefaultItemStyle}"
                                         NewStyle = "{StaticResource NewItemStyle}" />
     </TreeView.ItemContainerStyleSelector>
  </ TreeView>

1

Đôi khi bạn có thể tiếp cận điều này bằng cách lồng các tấm. Giả sử bạn có Kiểu thay đổi Tiền cảnh và thay đổi Phông chữ khác, bạn có thể áp dụng Kiểu sau trên TextBlock và đặt nó vào Lưới mà Kiểu của nó là Kiểu đầu tiên. Điều này có thể giúp và có thể là cách dễ nhất trong một số trường hợp, mặc dù nó sẽ không giải quyết được tất cả các vấn đề.


1

Khi bạn ghi đè ChọnStyle, bạn có thể nhận thuộc tính GroupBy thông qua phản chiếu như bên dưới:

    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

Nếu bạn đang cố gắng áp dụng một kiểu duy nhất cho chỉ một yếu tố duy nhất như là một bổ sung cho kiểu cơ sở, thì có một cách hoàn toàn khác để làm điều này là IMHO tốt hơn nhiều cho mã có thể đọc và duy trì.

Nó rất phổ biến để điều chỉnh các tham số cho từng yếu tố. Xác định kiểu từ điển chỉ để sử dụng trên một yếu tố là cực kỳ cồng kềnh để duy trì hoặc có ý nghĩa. Để tránh tạo kiểu chỉ cho các chỉnh sửa phần tử một lần, hãy đọc câu trả lời của tôi cho câu hỏi của riêng tôi ở đây:

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.