WPF 样式:对样式如何应用于组件的全面描述

WPF Styles: a comprehensive description of how a style gets applied to a component

警告:这不是问题,而是 WPF 样式工作的回顾。问题是这个总结是否正确。

我读到在样式定义中,如果在 属性 名称中包含控件的 class 名称,则可以去掉 TargetType。就是这样:

<Style x:Key="SomeKey" TargetType="{x:Type Button}">
        <Setter Property="Foreground" Value="Red"/>
</Style>

变成这样:

<Style x:Key="SomeKey">
        <Setter Property="Button.Foreground" Value="Red"/>
</Style>

太好了,这意味着,给定三个控件:

<StackPanel Height="40" Orientation="Horizontal">
    <Button Style="{StaticResource MyStyle}" Content="First"/>
    <TextBox Style="{StaticResource MyStyle}" Text="Second"/>
    <Label Style="{StaticResource MyStyle}" Content="Third"/>
</StackPanel>

我可以这样做:

<Window.Resources>
    <Style x:Key="MyStyle">

        <Setter Property="Button.Foreground" Value="Red"/>
        <Setter Property="TextBox.BorderBrush" Value="DarkBlue"/>
        <Setter Property="Label.Background" Value="LightPink"/>

        <Setter Property="Control.Margin" Value="4,0,0,0"/> 

    </Style>
</Window.Resources>

即:第一个按钮应该是标准按钮,但带有红色文本;第二个应该是标准的,但带有深蓝色边框。第三个应该是浅粉色前景。
这是我得到的:

即:除了第三个是标签且其 BorderThickness 默认为 0 外,所有样式都适用于每个控件。

经过一些挖掘,在我看来,应用样式的所有机制都归结为以下内容。 由于它看起来很简陋,我想知道这个描述是否正确,或者我是否遗漏了一些重要的东西。

第 1 阶段:定义样式

1.a 如果设置了 x:Key (<Style x:Key="MyStyle">)
,样式将显式获得一个键 1.b 如果未设置 x:Key,则必须设置 TargetType。 (<Style TargetType="{x:Type Button}">)。在内部,它为样式分配一个键,该键是 TargetType
中指定的类型的名称 1.c 如果同时设置了 TargetType 并且使用语法 <Setter Property="Class.Property" Value=.../> 定义了一个或多个 Setter,则不会检查 TargetType 和 类 值之间的一致性 Setter秒。 也就是说,这是合法的:

<Style x:Key="MyStyle" TargetType="{x:Type Button}">
    <Setter Property="Control.Margin" Value="4,0,0,0"/>
</Style> 

即使它没有什么意义,但以下内容在(至少)一种情况下可能有用。

<Style x:Key="MyStyle" TargetType="{x:Type Control}">
    <Setter Property="TextBox.Text" Value="just an example"/>
</Style> 

关于样式定义的几乎所有内容。

阶段 2:将样式与控件相关联

2.a 控件是否定义了样式 (<Button Style="{StaticResource MyStyle}">)?在资源中查找是否有这样的样式。如果有,检查它是否也有 TargetType;如果控件不是子 class 的 class,则引发异常:

XamlParseException. Exception: Cannot find resource named 'MyStyle'. Resource names are case sensitive

因此,在这种情况下,TargetType 不适用于过滤。它只需要匹配。

2.b 如果控件没有定义样式,使用键 等于 到控件的 class 名称查找样式资源.这来自仅使用 TargetType 定义的样式。如果找到,请继续申请阶段。
请注意,不会应用使用控件的 superclass 的 TargetType 定义的样式。他们必须完全匹配。这是另一个'limitation'的风格系统,远没有CSS的复杂; WPF 样式似乎只支持字典锁定。

第 3 阶段:应用样式

此时,控件具有要应用的样式。如果样式定义了 TargetType,则它与控件匹配(控件属于该类型或应用样式中定义的类型的子类型)。 样式是一组 Setter,它们可能有也可能没有自己的目标控制规范(有:<Setter Property="Control.Foreground" Value="Red"/>;没有:<Setter Property="Foreground" Value="Red"/>)。
似乎(我之前的例子似乎证明了这一点)在设计上 class 级别的 class 规范并不意味着进一步细化或过滤 。什么意思?好问题,待会儿再说。
那它是如何工作的呢?它只是忽略 class 信息(如果存在),并继续尝试将每个 setter 应用于控件。这就是为什么即使我 像这样定义了一个 Setter:

<Setter Property="Label.Background" Value="LightPink"/>

所有三个控件都获得 LightPink 背景,而不仅仅是 Label。

这就是 post 的原因。我找不到关于样式如何真正起作用的完整解释。我能找到的所有信息都仅限于展示一些基本用法;也就是说,他们没有向您展示如何处理需要更详细知识的复杂解决方案。

最后,如果 famework 不使用它们进行过滤,我为什么要在 Setters 中指定 classes 名称;这只是无用的重复。为什么会有这个功能?

    <Style x:Key="MyStyle">
        <Setter Property="Control.Foreground" Value="Red"/>         
        <Setter Property="Control.BorderBrush" Value="DarkBlue"/>
        <Setter Property="Control.Background" Value="LightPink"/>
        <Setter Property="Control.Margin" Value="4,0,0,0"/>
    </Style>

我能想到的只有这个;我无法在不指定文本框的情况下设置“文本”属性,因为控件没有文本属性。 同样,这并不意味着过滤,我想如果另一个控件,而不是文本的子 class 具有不同的文本 属性,它也会被设置。

    <Style x:Key="MyStyle" TargetType="{x:Type Control}">
        <Setter Property="Foreground" Value="Red"/>         
        <Setter Property="BorderBrush" Value="DarkBlue"/>
        <Setter Property="Background" Value="LightPink"/>
        <Setter Property="Margin" Value="4,0,0,0"/>
        <Setter Property="TextBox.Text" Value="just a test"/>
    </Style>

I read that in a style definition you can get rid of the TargetType if you include the control's class name in the Property name.

是的,根据 Style 的参考,这是正确的。

[...] except for the third that is a label and its BorderThickness defaults to 0, every style goes to every control.

一种样式有一个特定的目标类型,它可以应用于该类型。如果您定义没有目标类型的样式,它将默认为 IFrameworkInputElementFrameworkElementFrameworkContentElement 都实现了这个接口,这意味着它适用于几乎任何元素,包括 ButtonTextBoxLabel.

让我们看看您在此样式中定义的属性。

  • Foreground 是在 TextElement 上定义的,但是按钮通过添加 Control 作为其所有者来公开它。
  • BorderBrush 是在 Border 上定义的,但是 TextBox 通过添加 Control 作为其所有者来公开它。
  • Background 是在 Panel 上定义的,但是 Control 通过添加 Control 作为其所有者来公开它,而 Label 是 [=20 的派生词=],所以它继承了它。
  • Margin 是在 FrameworkElement 上定义的,Control 继承了它。

将控件添加为所有者的意思是相应的控件本身不定义依赖属性,而是“借用”他们使用AddOwner方法。

这会导致您在示例中看到的情况,所有 Control 都有效地定义了属性,因为默认 TargetType 不限制框架和框架内容元素之外的类型,并且由于受影响的依赖属性的实现方式。

第 1 阶段:定义样式

1.a A style gets a key explicitly if x:Key is set (<Style x:Key="MyStyle">)

是的。这称为 显式样式,因为您必须自己将它应用到每个目标控件。

1.b If x:Key is not set, TargetType has to be set. (<Style TargetType="{x:Type Button}">). Internally, it assigns the style a key that is the name of the type specified in TargetType

一般不会。如果您在资源字典中定义样式,则可以。键是 object 类型,所以不是类型的名称,而是 Type 本身将是它的键。如果直接在控件的 Style 属性 中定义样式,则既不需要键,也不需要目标类型。

1.c If both TargetType is set and one or more Setter are defined with the syntax , there is no check of consistence between the TargetType and the Classes values in the Setters.

是的。你的第一个例子不是非法的,因为一个按钮从 FrameworkElement 继承了它的 Margin 属性,它只是多余的。在这种情况下,以下 setter 基本相同。

<Setter Property="FrameworkElement.Margin" Value="4,0,0,0"/>
<Setter Property="Control.Margin" Value="4,0,0,0"/>
<Setter Property="Margin" Value="4,0,0,0"/>

我认为你的第二个例子没有多大意义。定义键时,必须将样式显式应用到控件,这仅对 TextBox 有意义,因此您可以将其定义为目标类型。

第 2 阶段:将样式与控件相关联

2.a Does the control have a style defined ()? Lookup in the resources if there is such a style. If there is, check if it has also a TargetType; if the control is not that class of a subclass, raise the exception [...]

基本上是的,目标类型必须匹配类型或派生类型。 StaticResource and DynamicResource 之间的资源查找不同。请参考链接源以获得更好的理解。

如果类型不匹配,它会抛出一个包裹在 XamlParseException 中的 InvalidOperationException。您显示的异常不同,表明根本找不到该样式。

2.b If the control doesn't have a style defined, lookup the resources for a style with the key equals to the control's class name. This comes from a style defined with only TargetType.

是的,类型必须与具有隐式样式的控件类型完全匹配。

Notice that styles that are defined with a TargetType of a superclass of the control wont be applied. They have to match exactly. This is another 'limitation' of a style system that is far from the complexity of CSS;

是的,这是隐式样式的必然结果。关键是目标控件的类型,如果它是控件的基本类型,则两种类型都不相等。

WPF styles seem to support little more than a dictionary lockups.

如果仔细观察,ResourceDictionary 就是资源字典。应用资源是在所述字典中查找定义为 x:Key 的键。

第 3 阶段:应用样式

How does it work then? It simply ignores the class information (if present) and it goes on trying to apply every setter to the control. [...] all of the three controls get the LightPink background, not only the Label.

如开头所述,class 信息并未被忽略。

Lastly, why should I ever specify classes names in Setters if the famework doesn't use them for filtering; it's just a useless repetition. Why that feature at all?

反过来看。通常,您必须指定目标类型,例如Button 在每个 setter 中明确。定义允许您省略完整限定的 TargetType 是一项方便的功能。此外,正如您的示例所示,您 可以 使用它来将样式应用于多个控件。

<StackPanel Height="40" Orientation="Horizontal">
    <Button Style="{StaticResource MyStyle}" Content="First"/>
    <TextBox Style="{StaticResource MyStyle}" Text="Second"/>
    <Label Style="{StaticResource MyStyle}" Content="Third"/>
</StackPanel>

<Style x:Key="MyStyle">
    <Setter Property="Button.Foreground" Value="Red"/>
    <Setter Property="TextBox.BorderBrush" Value="DarkBlue"/>
    <Setter Property="Label.Background" Value="LightPink"/>
    <Setter Property="Control.Margin" Value="4,0,0,0"/> 
</Style>

通过 subclass/derived 类型限定基 属性 始终是可能的。 XAML 解析器将通过访问 DependencyProperty.OwnerType 属性 自动推断 DependencyProperty 的基类型。例如,由于 Foreground 是在类型 Control 上定义的,解析器会将 Button.Foreground 转换为 Control.Foreground。由于所有三个元素都扩展 Control,因此可以在每个元素上解析属性。

这也为您的 阶段 3 部分添加了解释:

"Lastly, why should I ever specify classes names in Setters if the famework doesn't use them for filtering; it's just a useless repetition. Why that feature at all?"

如前所述,这是引用 class 成员的 class 限定符 。始终需要引用确切的内存位置以区分或允许重复的成员名称(不同类):Button.Foreground。完整参考(FQN 完全限定名称)还包括命名空间:System.Windows.Controls.Button.Foreground。 C# using System.Windows.Controls; 或 XAML xmlns:system="clr-namespace:System;assembly=mscorlib 导入命名空间时可以避免命名空间的限定(注意 System.Windows.Controls 命名空间默认导入 XAML范围)。

在 XAML 中,您可以通过在 StyleControlTemplate 上指定 TargetType 来进一步缩短限定条件。这允许您省略 class 限定符,因为 XAML 解析器知道当前范围内所有引用的属性都引用 TargetType 属性.[= 指定的类型36=]

"If there is, check if it has also a TargetType; if the control is not that class of a subclass, raise the exception:

XamlParseException. Exception: Cannot find resource named 'MyStyle'. Resource names are case sensitive

So, in this case TargetType is not meant for filtering. It just has to match."

错了。当找不到资源密钥时,将抛出您显示的异常。如果 Style.TargetType 与引用元素类型不匹配,则会抛出类型不匹配异常。

"styles that are defined with a TargetType of a superclass of the control wont be applied. They have to match exactly."

错了。下面的示例将一个 Style 应用于一个 superclass 到一个 subclass。如果此 Style 是隐式的(未定义 x:Key),那么它将自动应用于样式范围内的所有类型 ButtonBase 和所有子类型:

<Style x:Key="ButtonBaseStyle" TargetType="ButtonBase">
  <Setter Property="BorderBrush" Value="Red"/>
</Style>

<Button Style="{StaticResource ButtonBaseStyle}" />
<ToggleButton Style="{StaticResource ButtonBaseStyle}" />