为什么在应用Aero主题的WPF 4.0中我无法为控件设置样式?

13
我最近将一个项目从WPF 3.5转换到WPF 4.0。功能上来说,一切都运行良好,但我应用在Aero主题上的DataGrid样式突然停止工作了。如下面的图片所示,我的DataGrid从具有Aero外观加粗标题,额外的填充和交替行格式,变成了只有简单的"Aero"外观。除了删除所有对WPF Toolkit的引用(因为DataGrid现在是WPF 4.0的本地控件),我真的没有改变过我的代码/标记。

之前 (WPF Toolkit DataGrid)

Looks like Aero w/ bold headings, extra padding, and alternate row styles

在.NET 4.0 DataGrid之后

Looks like Aero w/ nothing

作为我在之前的问题中学到的,如果我停止引用Aero资源字典,我可以再次让自定义DataGrid样式起作用,但是在Windows XP上一切看起来都像“Luna”(这不是我想要的)。
那么,在WPF 4.0中,如何确保我的应用程序始终使用Aero主题,但仍然在该主题上应用样式?
以下是我的App.xaml代码:
<Application
    x:Class="TempProj.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary
                    Source="/PresentationFramework.Aero,
                        Version=3.0.0.0,
                        Culture=neutral,
                        PublicKeyToken=31bf3856ad364e35,
                        ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                <ResourceDictionary Source="/CommonLibraryWpf;component/ResourceDictionaries/DataGridResourceDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

这是我的DataGridResourceDictionary.xaml代码:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="DataGrid_ColumnHeaderStyle" TargetType="DataGridColumnHeader">
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="TextBlock.TextWrapping" Value="WrapWithOverflow" />
    </Style>
    <Style x:Key="DataGrid_CellStyle" TargetType="DataGridCell">
        <Setter Property="Padding" Value="5,5,5,5" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="DataGridCell">
                    <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
                        <ContentPresenter />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="DataGrid">
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGrid_CellStyle}" />
        <Setter Property="Background" Value="White" />
        <Setter Property="AlternatingRowBackground" Value="#F0F0F0" />
        <Setter Property="VerticalGridLinesBrush" Value="LightGray" />
        <Setter Property="HeadersVisibility" Value="Column" />
        <Setter Property="SelectionMode" Value="Single" />
        <Setter Property="SelectionUnit" Value="FullRow" />
        <Setter Property="GridLinesVisibility" Value="Vertical" />
        <Setter Property="AutoGenerateColumns" Value="False" />
        <Setter Property="CanUserAddRows" Value="False" />
        <Setter Property="CanUserDeleteRows" Value="False" />
        <Setter Property="CanUserReorderColumns" Value="True" />
        <Setter Property="CanUserResizeColumns" Value="True" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="True" />
        <Setter Property="IsReadOnly" Value="True" />
        <Setter Property="BorderBrush" Value="#DDDDDD" />
        <Setter Property="HorizontalGridLinesBrush" Value="#DDDDDD" />
        <Setter Property="VerticalGridLinesBrush" Value="#DDDDDD" />
    </Style>
    <Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">
        <Setter Property="CanUserReorderColumns" Value="False" />
        <Setter Property="CanUserResizeColumns" Value="False" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="False" />
    </Style>
</ResourceDictionary>

这是一个使用示例:

<DataGrid
    Grid.Row="0"
    Grid.Column="0"
    Style="{StaticResource DataGrid_FixedStyle}"
    ItemsSource="{Binding Coordinates}">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding X}" Header="X" />
        <DataGridTextColumn Binding="{Binding Y}" Header="Y" />
        <DataGridTextColumn Binding="{Binding Z}" Header="Z" />
    </DataGrid.Columns>
</DataGrid>

编辑

我突然想到的可能问题是,我引用了错误版本的Aero框架。

这是我现在拥有的:

<ResourceDictionary
    Source="/PresentationFramework.Aero,
        Version=3.0.0.0,
        Culture=neutral,
        PublicKeyToken=31bf3856ad364e35,
        ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />

这个应该更新到4.0版本吗?4.0版本的PublicKeyToken是什么(或者我该如何找到它)?


我相信我所观察到的是错误的行为,因此我向微软提交了这个错误报告:https://connect.microsoft.com/VisualStudio/feedback/details/623801/datagrid-not-styled-properly-when-applying-aero-theme-plus-custom-style - devuxer
3个回答

71

长答案

前面的简短回答提供了一些XAML来解决问题,以及对问题原因的简要概述。

加载主题资源与在操作系统级别更改主题不同。加载主题资源可能会导致不良影响。从WPF的角度来看,应用程序中现在存在大量的隐式样式,这些样式可能会优先于其他样式。归根结底,将主题视为应用程序皮肤可能需要进行完善才能正常工作。

以下长答案将更深入地讨论该问题。首先将涵盖一些背景知识。这将回答一些外围问题,并为理解手头的问题提供更好的基础。之后,将逐个剖析问题的各个方面,并提供有效的调试策略。

主题与皮肤

这是一个很好的问题,部分原因是因为数百名博客作者和论坛线程建议从文件加载主题作为“更改主题”的方法。一些提出此建议的作者在微软工作,许多作者显然是高水平的软件工程师。这种方法在大多数情况下似乎都可以工作。但是,正如您所注意到的那样,在您的情况下,这种方法并没有完全奏效,需要进行一些完善。

一些问题源于术语不够精确。不幸的是,“主题”这个词已经变得混乱不堪。一个避免混淆的主题的精确定义就是“系统主题”。系统主题定义了机器上Win32视觉效果的默认外观。我的操作系统是Vista。我安装的主题位于C:\ WINDOWS \ Resources \ Themes。在那个文件夹中有两个文件:aero.theme和Windows Classic.theme。如果我想改变主题,我可以去[个性化|主题]或[个性化|窗口颜色和外观|颜色方案]。虽然这并不是显而易见的,但我可以从所有可选项中选择的选项都归结为Aero或Classic加上一些额外的细化。因为WPF窗口呈现其客户端区域而不是合成一堆Win32控件,所以客户端区域不会自动遵守主题。主题程序集(例如PresentationFramework.Aero.dll)提供了将主题功能扩展到WPF窗口的基础。

enter image description here enter image description here

更一般的主题定义是任何外观和感觉配置,无论在哪个层面(操作系统、应用程序、控件)。当人们使用这种一般定义时,可能会产生各种程度的混淆。请注意,MSDN在不提醒的情况下在精确定义和一般定义之间切换!
许多人会说你正在加载一个应用程序皮肤,而不是主题。两个词都可以说是正确的,但我建议采用这种心理模型,因为它会引起较少的混淆。
那么,如何确保我的应用程序始终使用Aero主题呢?[强调添加]
同样地,可以说你正在加载Aero的资源作为皮肤。具体来说,您正在加载PresentationFramework.Aero.dll中的ResourceDictionary。这些资源以前被赋予特殊处理,因为它们是默认资源。但是,一旦进入应用程序,它们将像任何其他任意的资源集合一样被处理。当然,Aero的ResourceDictionary是全面的。由于它将在应用程序范围内加载,它将有效地隐藏主题(在您的情况下是Luna)提供的每个默认样式,以及一些其他样式,这就导致了问题。请注意,最终,主题仍然是相同的(Luna)。
如上所述,主题涉及样式优先级,这本身就是依赖属性优先级的一种形式。这些优先级规则极大地解释了问题中观察到的行为。

显式样式。Style属性直接设置。在大多数情况下,样式不是内联定义的,而是被引用为资源,并使用显式密钥...

隐式样式。Style属性未直接设置。但是,样式存在于资源查找序列(页面、应用程序)的某个级别上,并使用与要应用样式的类型匹配的资源密钥进行键控...

默认样式,也称为主题样式。Style属性未直接设置,并且实际上将读取为空值... 在这种情况下,样式来自WPF演示引擎中的运行时主题评估。

这篇博客文章更深入地探讨了样式与默认样式之间的区别。

.NET程序集检查

这也是一个很好的问题,部分原因是因为有太多的移动部分。 如果没有有效的调试策略,几乎不可能理解发生了什么。 有了这个想法,.NET程序集检查是一个自然的起点。

从WPF的角度来看,主题本质上是作为BAML序列化并嵌入常规.NET程序集(例如PresentationFramework.Aero.dll)的ResourceDictionary。稍后,需要将主题视为普通XAML以验证问题中的行为。

幸运的是,微软为开发人员提供了4.0主题的XAML版本以方便使用。我不确定是否可以从微软下载任何形式的pre-4.0主题。

对于一般程序集(包括pre-4.0主题程序集),您可以使用(之前免费的)工具ReflectorBamlViewer插件来反编译BAML回到XAML。虽然不如 flashy,但ILSpy是一个内置BAML反编译器的免费替代品。

enter image description here

.NET程序集散布在您的硬盘上,这有点令人困惑。以下是它们在我的机器上的路径,我有一种直觉,并且有时可以不用试错就记住它们。

Aero 3.0

C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.Aero.dll

Aero 4.0

C:\WINDOWS\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework.Aero\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.Aero.dll

版本4的PublicKeyToken是什么(或者我该如何找到它)?

最简单的方法是使用Reflector。PublicKeyToken与以前相同:31bf3856ad364e35

enter image description here

此外,sn.exe(来自Windows SDK)可以提取程序集信息。

在我的计算机上,命令为:

C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin>sn.exe -Tp "C:\WINDOWS\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework.Aero\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.Aero.dll"

enter image description here

将主题作为皮肤加载

应该更新PresentationFramework.Aero参考文献到4.0版本吗?

绝对需要。在.NET FCL 4.0之前,DataGrid不存在。有几种确认方法,但最直观的方法是,根据您自己的承认,您以前通过WPF Toolkit访问它。如果您选择不在App.xaml中加载PresentationFramework.Aero 4.0,则Aero的DataGrid样式将不会出现在应用程序资源中。

现在,事实证明这甚至并不重要。我将使用原始XAML,在加载时断点,然后检查应用程序范围的资源。

enter image description here

作为预期,应用程序的MergedDictionaries属性中有两个ResourceDictionaries,第一个ResourceDictionary据称是PresentationFramework.Aero 3.0版本。然而,我发现第一个ResourceDictionary中有266个资源。此时,碰巧我知道Aero 4.0主题中有266个资源,而Aero 3.0主题中只有243个资源。此外,甚至还有一个DataGrid条目!事实上,这个ResourceDictionary就是Aero 4.0 ResourceDictionary。
也许有人能解释为什么WPF在明确指定为3.0时加载了4.0程序集。我可以告诉你的是,如果将项目重新定向到.NET 3.0(并修复编译错误),则将加载Aero 3.0版本。

enter image description here enter image description here

正如你所推断的那样,Aero 4.0 应该被加载。在调试过程中了解发生了什么是很有用的。

问题 #1:未使用Aero的DataGrid样式

在该应用程序中,DataGrid将根据您配置的Style.BasedOn属性链式地使用零个或多个样式。

它还将具有默认样式,此处为嵌入在Luna主题中的样式。

仅通过查看原始的XAML,我就知道存在样式继承问题。大型DataGrid样式具有约20个Setter,但未设置其BasedOn属性。

enter image description here

你有一个长度为两个的样式链,你的默认样式来自Luna主题。Aero的ResourceDictionary中的DataGrid样式根本没有被使用。
这里有两个重要问题。首先,如何在第一时间进行调试?其次,这意味着什么?

调试样式链

我建议使用 Snoop 和/或 WPF Inspector 来调试此类 WPF 问题。

甚至 WPF Inspector 的版本 0.9.9 还具有样式链查看器。 (我必须警告您,此功能目前存在错误,并且对于调试应用程序的这部分内容不是很有用。还要注意,它选择将默认样式描绘为链的一部分。)

这些工具的强大之处在于它们能够在运行时查看和编辑深度嵌套元素的值。您只需将鼠标悬停在元素上,其信息就会立即出现在工具中。

或者,如果您只想查看像 DataGrid 这样的顶级元素,请在 XAML 中命名该元素(例如 x:Name="dg"),然后在加载时在调试器中断点,并将元素名称放入 Watch 窗口中。在那里,您可以通过 BasedOn 属性检查样式链。

下面,我在使用解决方案XAML时中断了调试器。DataGrid在样式链中有三种样式,分别具有4、17和9个Setter。我可以深入一点,并推断第一个样式是“DataGrid_FixedStyle”。如预期的那样,第二个是来自同一文件的大型implicit DataGrid样式。最后,第三个样式似乎来自Aero的ResourceDictionary。请注意,默认样式在此链中未表示。

enter image description here

此时应该注意,每个主题的DataGrid样式实际上没有任何差异。您可以通过将它们从各自的4.0主题中复制到单独的文本文件中,然后使用diff工具进行比较来验证这一点。
事实上,相当数量的样式在不同主题之间是完全相同的。这一点需要注意。要验证这一点,只需对两个不同主题中保存的整个XAML运行diff即可。

enter image description here

请注意,DataGrid中有许多不同的元素嵌套(例如DataGridRow),每个元素都有自己的样式。尽管目前从主题到主题的DataGrid样式是相同的,但这些嵌套元素的样式可能会有所不同。根据问题的观察行为,显然有些元素确实不同。
原始XAML未包含Aero的DataGrid样式的影响
由于DataGrid样式在4.0主题中是相同的,在此情况下,将Aero的DataGrid样式添加到样式链的末尾基本上是多余的。Aero的DataGrid样式将与默认的DataGrid样式(在您的情况下来自Luna)相同。当然,未来的主题可能会在DataGrid样式方面有所变化。
无论是否存在任何影响,既然您打算使用Aero的样式,那么直到有特定的理由不这样做(稍后将讨论),这样做显然更正确。
最重要的是,了解正在发生的事情非常有用。
Style.BasedOn仅在其使用的上下文中具有意义
在解决方案XAML中,DataGridResourceDictionary.xaml正是您想要的方式工作。重要的是要理解为什么,以及使用这种方式会排除其他使用方式。
假设DataGridResourceDictionary.xaml中的最终样式链将它们的BasedOn属性设置为Type键(例如BasedOn="{StaticResource {x:Type DataGrid}}")。如果这样做,那么它们将从与此键匹配的隐式样式继承。然而,它们继承的样式取决于DataGridResourceDictionary.xaml加载的位置。例如,如果DataGridResourceDictionary.xaml在Aero资源加载后立即加载到合并的字典中,则其样式将继承适当的Aero样式。现在,例如,如果DataGridResourceDictionary.xaml是整个应用程序中唯一加载的ResourceDictionary,则其样式实际上将继承当前主题(在您的情况下为Luna)中相关的样式。请注意,主题的样式当然也是默认样式!

enter image description here

现在假设DataGridResourceDictionary.xaml中的样式链的最终样式没有设置它们的BasedOn属性。如果是这样,那么它们将是各自样式链中的最终样式,并且仅评估默认样式(始终位于主题中)。请注意,这将破坏您加载Aero作为皮肤并有选择地改进其部分设计的意图。
请注意,在先前的示例中,如果最终键是字符串(例如x:Key =“MyStringKey”)而不是类型,则会发生相同类型的事情,但是在主题或Aero皮肤中没有匹配的样式。在加载时会抛出异常。也就是说,如果总是存在上下文以找到匹配的样式,那么悬挂的字符串键理论上可以工作。
在解决方案XAML中,DataGridResourceDictionary.xaml已被修改。每个样式链末尾的样式现在都从一个附加的隐式样式继承。当在App.xaml中加载时,这些将解析为Aero样式。
问题#2:DataGrid.ColumnHeaderStyle和DataGrid.CellStyle 这是一个棘手的问题,它导致了一些奇怪的行为。DataGrid.ColumnHeaderStyle和DataGrid.CellStyle被隐式的DataGridColumnHeader和DataGridCell样式所取代。也就是说,它们与Aero皮肤不兼容。因此,它们在解决方案XAML中被简单地删除。
这个小节的其余部分是对问题的深入调查。 DataGridColumnHeader和DataGridCell,像所有的FrameworkElements一样,都有一个Style属性。此外,在DataGrid上还有一些非常相似的属性:ColumnHeaderStyle和CellStyle。你可以称这两个属性为“辅助属性”。它们至少在概念上映射到DataGridColumnHeader.Style和DataGridCell.Style。然而,它们实际上如何使用是未记录的,因此我们必须深入挖掘。
属性DataGridColumnHeader.Style和DataGridCell.Style使用值强制转换。这意味着当查询任一Style时,会使用特殊回调来确定实际返回给调用者(主要是内部WPF代码)的Style。这些回调可以返回任何他们想要的值。最终,DataGrid.ColumnHeaderStyle和DataGrid.CellStyle是各自回调中的候选返回值。
通过Reflector,我可以轻松地确定所有这些。(如果需要,也可以逐步浏览.NET源代码。)从DataGridColumnHeader的静态构造函数开始,我找到了Style属性,并看到它被分配了额外的元数据。具体来说,指定了一个强制回调。从该回调开始,我点击一系列方法调用,并迅速看到正在发生的事情。(请注意,DataGridCell也做同样的事情,因此我不会涉及它。)

enter image description here

最终的方法DataGridHelper.GetCoercedTransferPropertyValue,本质上比较了DataGridColumnHeader.Style和DataGrid.ColumnHeaderStyle的来源。哪个来源优先级更高就会胜出。该方法中的优先级规则基于依赖属性优先级
此时,DataGrid.ColumnHeaderStyle将在原始XAML和解决方案XAML中进行检查。将收集一小部分信息矩阵。最终,这将解释每个应用程序中观察到的行为。
在原始XAML中,我在调试器中中断并看到DataGrid.ColumnHeaderStyle具有“Style”来源。这很有意义,因为它是在样式中设置的。

enter image description here enter image description here

在解决方案的XAML中,我在调试器中打断点,并查看DataGrid.ColumnHeaderStyle具有“Default”源。这是有道理的,因为该值未在样式(或任何其他地方)中设置。

enter image description here enter image description here

另一个需要检查的值是DataGridColumnHeader.Style。DataGridColumnHeader是一个嵌套深度很大的元素,在使用VisualStudio进行调试时不太方便访问。实际上,像Snoop或WPF Inspector这样的工具将用于检查该属性。原始的XAML中,DataGridColumnHeader.Style具有“ImplicitStyleReference”源。这是有道理的。DataGridColumnHeaders在内部WPF代码中被实例化。它们的Style属性为空,因此它们将寻找一个隐式样式。树从DataGridColumnHeader元素到根元素遍历。如预期的那样,没有发现样式。然后检查应用程序资源。您在孤立的DataGridColumnHeader Style上设置了一个字符串键(“DataGrid_ColumnHeaderStyle”)。这实际上隐藏了它在这个查找中,因此它没有被使用。然后,搜索Aero皮肤并找到了一个典型的隐式样式。这就是使用的样式。

enter image description here

如果使用XAML方案重复此步骤,结果也是相同的:'ImplicitStyleReference'。然而,这次隐式样式是DataGridResourceDictionary.xaml中唯一的DataGridColumnHeader样式,其键已变为隐式。

enter image description here

最后,如果使用原始的XAML重复这一步骤,并且未加载Aero皮肤,则结果现在为“默认”!这是因为整个应用程序中根本没有隐式DataGridColumnHeader样式。

因此,如果未加载Aero皮肤,则将使用DataGrid.ColumnHeaderStyle,但如果加载了Aero皮肤,则不会使用该样式!正如广告所说,“加载主题资源可能会产生不良影响”。

要记住这么多并且名称都听起来相同确实很困难。以下图表总结了所有操作。请记住,具有更高优先级的属性获胜。

enter image description here

这可能不是您想要的,但这是WPF 4.0中DataGrid的工作方式。考虑到这一点,您可以在非常广泛的范围内设置DataGrid.ColumnHeaderStyle和DataGrid.CellStyle,并仍然能够使用隐式样式在更窄的范围内覆盖DataGridColumnHeader和DataGridCell样式。
需要注意的是,DataGrid.ColumnHeaderStyle和DataGrid.CellStyle会被隐式的DataGridColumnHeader和DataGridCell样式取代。也就是说,它们与Aero皮肤不兼容。因此,它们会被从解决方案XAML中移除。
问题#3:DataGridRow.Background
如果已经实施了推荐的更改,则屏幕上应该显示类似以下内容的内容。(请记住,我将我的主题设置为Classic以便调试此问题。)

enter image description here

DataGrid看起来很Aero,但是AlternatingRowBackground没有被遵守。每隔一行应该有一个灰色的背景。

enter image description here

使用到目前为止讨论的调试技巧,可以发现这是与问题#2完全相同类型的问题。现在正在加载Aero皮肤中的隐式DataGridRow样式。DataGridRow.Background使用属性强制转换。DataGrid.AlternatingRowBackground是可能在强制转换回调中返回的候选值。DataGridRow.Background是另一个候选这些值的来源将影响强制转换回调选择哪个值。 到现在为止应该是清楚的,但如果不是,必须重申一下。加载主题的资源可能会导致不良影响。 解决此子问题的简短答案是DataGridRow.Background只能在主题中设置。具体而言,它不能由应用程序中任何地方的Style Setter设置。不幸的是,这正是Aero皮肤中正在发生的事情。有至少两种方法来解决这个问题。
在Aero皮肤后添加一个空的隐式样式。这将隐藏Aero中的有问题的样式。空样式中没有值,因此最终使用默认样式中的值。最终,这仅适用于每个4.0主题中DataGridRow样式是相同的。

或者,可以复制Aero的DataGridRow样式,删除Background Setter,并在Aero皮肤后添加剩余的样式。解决方案XAML采用了这种技术。通过扩展样式,应用程序更有可能在未来的情况下继续看起来像Aero。通过在App.xaml中隔离此扩展,DataGridResourceDictionary.xaml可以在其他上下文中更自由地使用。但是,请注意,根据将来如何使用该文件,将其添加到DataGridResourceDictionary.xaml可能更有意义。对于此应用程序,两种方式都可以。

问题#4:DataGridColumnHeader布局

最后一个更改相当表面。如果在进行到目前为止的推荐更改后运行应用程序,则DataGridColumnHeaders的内容将是左对齐而不是居中对齐。可以使用Snoop或WPF Inspector轻松地深入研究此问题。问题的根源似乎是DataGridColumnHeaders的HorizontalContentAlignment设置为“Left”。

enter image description here

将其设置为“Stretch”,它将按预期工作。

布局属性TextBlock格式属性之间存在一些相互作用。使用Snoop和WPF Inspector进行实验可以轻松确定在任何情况下都能起作用的内容。

最终想法

总之,加载主题资源与更改操作系统级别的主题不同。加载主题资源可能会导致不良影响。从WPF的角度来看,大量的隐式样式现在存在于应用程序中。这些样式可能会盖过其他样式。底线是,如果不加修改地将主题视为应用程序皮肤,则可能无法正常工作。

话虽如此,我对当前的WPF实现并不完全满意,特别是关于通过强制回调和优先规则使用“辅助属性”(例如DataGrid.ColumnHeaderStyle)。如果目标尚未明确分配值,则可以在初始化时将这些“辅助属性”直接分配给其预定目标(例如DataGridColumnHeader.Style)。我还没有考虑足够多,不知道可能会有什么问题,但如果可能的话,这可能会使“辅助属性”模型更加直观、与其他属性更加一致和更加防错。

最后,尽管这不是回答的重点,但非常重要的一点是,加载主题资源来模拟更改主题特别糟糕,因为这会导致 显著的可维护成本。应用程序中的现有样式不会自动基于主题 ResourceDictionary 中的样式。每个应用程序中的样式都必须将其 BasedOn 属性设置为类型键(或直接或间接地基于另一个执行此操作的样式)。这非常繁琐且容易出错。此外,与主题感知的自定义控件相关的可维护性成本也很高。这些自定义控件的主题资源也必须被加载以实现此模拟。当然,在这样做之后,您可能会面临类似于此处所遇到的样式优先级问题!

无论如何,有多种方法可以解决 WPF 应用程序的问题(不打趣!)。我希望这个答案能够为您的问题提供额外的见解,并帮助您和其他人解决类似的问题。


1
+1 谢谢!我终于有时间坐下来阅读你的长篇回答了,它真的很值得。虽然还没有完全理解所有细节,但我确实知道为什么加载主题资源不等同于在操作系统级别切换主题。我应该提到,我计划在升级到 Windows 7 笔记本电脑后放弃加载 Aero 皮肤的做法(这应该很快)。但与此同时,我将继续使用我的有些脆弱的设置,因为我需要知道我的应用程序在 Vista/Windows 7 机器上的外观如何。 - devuxer
然而,需要注意的是,根据将来如何使用该文件,将其添加到DataGridResourceDictionary.xaml可能更有意义。就此应用程序而言,任何一种方式都可以。这正是我第一次将您的解决方案纳入我的代码库时所做的决定。 - devuxer
我认为你在长篇回答中没有直接解决的最后一个问题是,为什么我的“原始解决方案”在使用WPF Toolkit时有效,但升级到WPF 4.0后就无法使用了。根据我目前所学,我猜测这与默认主题没有任何与DataGrid Toolkit版本相关的资源有关。此外,我明确地加载了WPF Toolkit的Aero皮肤。我可以很容易地看出,在这种设置中,样式链会有所不同。资源也可能并不完全相等。 - devuxer
是的,你的第三点完全正确。工具包场景下的资源和类型是不同的,你需要在那个环境中调试问题才能真正知道发生了什么。“…加载主题资源并不等同于在操作系统级别切换主题”。我自己说不出更好的话了!我已经把这句话加入答案中了。我还更新了简短回答中的链接,提供了一些可能有帮助的替代方案。 - Alexander Van Berg

35

相对简短的答案

加载主题资源并不等同于在操作系统级别更改主题。加载主题资源可能会产生负面影响。从 WPF 的角度来看,应用程序中现在存在大量的隐式样式。这些样式可能会覆盖其他样式。总之,将主题视为应用程序皮肤可能需要进行细化才能正常工作。

还有一些替代方式可以模拟主题更改。

这个问题展示了一些相当复杂的 WPF 功能,其中部分功能似乎没有记录。但是,这似乎不是 bug。如果不是 bug——也就是说,如果所有都是有意的 WPF 行为——你可能会认为 WPF DataGrid 的某些方面设计得不够好。

Meleak 的答案非常正确。然而,问题是可解决的,可以在不损害设计或需要重复样式设置的情况下解决。也许更重要的是,这个问题是可以进行调试的。

以下 XAML 可行。我注释掉了旧的 XAML,只是为了使更改更加明显。有关问题的更深入的分析,请参见长答案

DataGridResourceDictionary.xaml:

<ResourceDictionary    
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!--
    <Style x:Key="DataGrid_ColumnHeaderStyle" TargetType="DataGridColumnHeader">
    -->
    <Style TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">

        <!--New-->
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <!---->

        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="TextBlock.TextWrapping" Value="WrapWithOverflow" />
    </Style>

    <!--
    <Style x:Key="DataGrid_CellStyle" TargetType="DataGridCell">
    -->
    <Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
        <Setter Property="Padding" Value="5,5,5,5" />
        <Setter Property="TextBlock.TextAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <!--
                <ControlTemplate TargetType="DataGridCell">
                    <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
                        <ContentPresenter />
                    </Border>
                </ControlTemplate>
                -->
                <ControlTemplate TargetType="{x:Type DataGridCell}">
                    <Border 
                        Padding="{TemplateBinding Padding}"
                        Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}"  
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        SnapsToDevicePixels="True">
                        <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

        <!--Additional Feature-->
        <!--
            Remove keyboard focus cues on cells and tabbing on cells when
            only rows are selectable and the DataGrid is readonly.

            Note that having some kind of keyboard focus cue is
            typically desirable.  For example, the lack of any keyboard 
            focus cues could be confusing if an application has multiple
            controls and each control is showing something selected, yet
            there is no keyboard focus cue.  It's not necessarily obvious
            what would happen if Control+C or Tab is pressed.

            So, when only rows are selectable and the DataGrid is readonly,
            is would be ideal to make cells not focusable at all, make
            the entire row focusable, and make sure the row has a focus cue.
            It would take much more investigation to implement this.
        -->
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=SelectionUnit}" Value="FullRow"/>
                    <Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=IsReadOnly}" Value="True"/>
                </MultiDataTrigger.Conditions>
                <Setter Property="BorderBrush" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Background}" />
                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                <Setter Property="IsTabStop" Value="False" />
            </MultiDataTrigger>
        </Style.Triggers>
        <!---->
    </Style>

    <!--
    <Style TargetType="DataGrid">
    --> 
    <Style TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">

        <!--Unworkable Design-->
        <!--
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGrid_CellStyle}" />
        -->

        <Setter Property="Background" Value="White" />
        <Setter Property="AlternatingRowBackground" Value="#F0F0F0" />


        <!--This was a duplicate of the final PropertySetter.-->
        <!-- 
        <Setter Property="VerticalGridLinesBrush" Value="LightGray" />
        -->

        <Setter Property="HeadersVisibility" Value="Column" />
        <Setter Property="SelectionMode" Value="Single" />
        <Setter Property="SelectionUnit" Value="FullRow" />
        <Setter Property="GridLinesVisibility" Value="Vertical" />
        <Setter Property="AutoGenerateColumns" Value="False" />
        <Setter Property="CanUserAddRows" Value="False" />
        <Setter Property="CanUserDeleteRows" Value="False" />
        <Setter Property="CanUserReorderColumns" Value="True" />
        <Setter Property="CanUserResizeColumns" Value="True" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="True" />
        <Setter Property="IsReadOnly" Value="True" />
        <Setter Property="BorderBrush" Value="#DDDDDD" />
        <Setter Property="HorizontalGridLinesBrush" Value="#DDDDDD" />
        <Setter Property="VerticalGridLinesBrush" Value="#DDDDDD" />
    </Style>

    <Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">
        <Setter Property="CanUserReorderColumns" Value="False" />
        <Setter Property="CanUserResizeColumns" Value="False" />
        <Setter Property="CanUserResizeRows" Value="False" />
        <Setter Property="CanUserSortColumns" Value="False" />
    </Style>
</ResourceDictionary>

App.xaml:

<Application    
    x:Class="TempProj.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!--
                <ResourceDictionary                    
                    Source="/PresentationFramework.Aero,                        
                            Version=3.0.0.0,                     
                            Culture=neutral,                        
                            PublicKeyToken=31bf3856ad364e35,                        
                            ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                -->
                <ResourceDictionary                    
                    Source="/PresentationFramework.Aero,                        
                            Version=4.0.0.0,                     
                            Culture=neutral,                        
                            PublicKeyToken=31bf3856ad364e35,                        
                            ProcessorArchitecture=MSIL;component/themes/aero.normalcolor.xaml" />
                <!--New-->
                <!--
                    This is a modified replica of the DataGridRow Style in the Aero skin that's 
                    evaluated next.  We are hiding that Style and replacing it with this.
                -->
                <ResourceDictionary>
                    <Style x:Key="{x:Type DataGridRow}" TargetType="{x:Type DataGridRow}">
                        <!--
                            DataGridRow.Background must not be set in this application.  DataGridRow.Background
                            must only be set in the theme.  If it is set in the application, 
                            DataGrid.AlternatingRowBackground will not function properly.

                            See: https://dev59.com/V2855IYBdhLWcg3wnFxO

                            The removal of this Setter is the only modification we have made.
                        -->
                        <!--
                        <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
                        -->

                        <Setter Property="SnapsToDevicePixels" Value="true"/>
                        <Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
                        <Setter Property="ValidationErrorTemplate">
                            <Setter.Value>
                                <ControlTemplate>
                                    <TextBlock Margin="2,0,0,0" VerticalAlignment="Center" Foreground="Red" Text="!" />
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type DataGridRow}">
                                    <Border x:Name="DGR_Border"
                                            Background="{TemplateBinding Background}"
                                            BorderBrush="{TemplateBinding BorderBrush}"
                                            BorderThickness="{TemplateBinding BorderThickness}"
                                            SnapsToDevicePixels="True">
                                        <SelectiveScrollingGrid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="Auto"/>
                                                <ColumnDefinition Width="*"/>
                                            </Grid.ColumnDefinitions>

                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="*"/>
                                                <RowDefinition Height="Auto"/>
                                            </Grid.RowDefinitions>

                                            <DataGridCellsPresenter Grid.Column="1"
                                             ItemsPanel="{TemplateBinding ItemsPanel}"
                                             SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>

                                            <DataGridDetailsPresenter  SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=AreRowDetailsFrozen, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}}"
                                                Grid.Column="1" Grid.Row="1"
                                                Visibility="{TemplateBinding DetailsVisibility}" />

                                            <DataGridRowHeader SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"  Grid.RowSpan="2"
                                                Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Row}}"/>
                                        </SelectiveScrollingGrid>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ResourceDictionary>
                <!---->

                <ResourceDictionary Source="/CommonLibraryWpf;component/ResourceDictionaries/DataGridResourceDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

MainWindow.xaml:

<Window 
    x:Class="TempProj.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Vector3DCollection x:Key="Coordinates">
            <Vector3D X="1" Y="0" Z="0"/>
            <Vector3D X="0" Y="22" Z="0"/>
            <Vector3D X="0" Y="0" Z="333"/>
            <Vector3D X="0" Y="4444" Z="0"/>
            <Vector3D X="55555" Y="0" Z="0"/>
        </Vector3DCollection>
    </Window.Resources>
    <Grid>
        <DataGrid
            Grid.Row="0"    
            Grid.Column="0"    
            Style="{StaticResource DataGrid_FixedStyle}"
            ItemsSource="{StaticResource Coordinates}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding X}" Header="X" />
                <DataGridTextColumn Binding="{Binding Y}" Header="Y" />
                <DataGridTextColumn Binding="{Binding Z}" Header="Z" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

非常感谢您的回答,Alexander。我已经将此书签并会尽快尝试。 - devuxer
当然没问题 - 我也打算添加一个“长答案”! - Alexander Van Berg
似乎几乎完美地工作了,谢谢!我注意到的唯一区别是,即使我将SelectionUnit设置为FullRow,当我单击单元格时仍会出现黑色框框。有没有办法消除它?我希望只突出显示行而不在任何地方放置黑色框框。 - devuxer
这实际上与您处理的初始问题无关。 您注意到与原始XAML的差异是因为DataGridCell ControlTemplate中的Border已完全删除其BorderBrush绑定。 我不建议这样做。 我同意在某些情况下,焦点可视化效果看起来并不好,我一直在反复考虑如何处理它。 最终,我添加了一个触发器来完成这项工作。 - Alexander Van Berg
那绝对是完美的。再次感谢!我希望我能给你更多的25分,以表达我对你所有帮助的感激之情 :) - devuxer
@DanM:您可以向现有答案授予赏金! - BoltClock

0

我认为问题不在于 PresentationFramework.Aero 本身,而是通过包含它来获得隐式 DataGrid 样式。这也可以通过在 App.xaml 中添加以下内容来看到。

<Application.Resources>
    <Style TargetType="{x:Type DataGridColumnHeader}"/>
</Application.Resources>

如果没有显式设置,这将导致所有的DataGridColumnHeader失去它们的样式。

这将起作用。

<DataGrid ColumnHeaderStyle="{StaticResource DataGrid_ColumnHeaderStyle}" ../>

然而,这不会。

<DataGrid Style="{StaticResource DataGrid_FixedStyle}" ../>

<Style x:Key="DataGrid_FixedStyle" TargetType="DataGrid">
    <Setter Property="ColumnHeaderStyle"
            Value="{StaticResource DataGrid_ColumnHeaderStyle}" />
</Style>

我不确定有什么好的解决方法。我能想到的唯一办法是在DataGrid本身上明确设置所有样式,但这可能会很不方便,特别是如果您在许多地方使用此样式。

<DataGrid Style="{StaticResource DataGrid_FixedStyle}"
          ColumnHeaderStyle="{StaticResource DataGrid_ColumnHeaderStyle}"
          CellStyle="{StaticResource DataGrid_CellStyle}"
          ... >

是的,我在整个应用程序中都使用这种样式,因此不得不为每个“DataGrid”粘贴所有内容将是一个巨大的麻烦。我认为我要向微软提交一个错误报告,因为这种行为明显与.NET 3.5不同,而且我认为是不正确的。 - devuxer
我同意,去做吧。现在的方式使得无法实现这一点,而3.5版本的行为允许在数据网格中使用显式的ColumnHeaderStyle来覆盖DataGrid样式中设置的ColumnHeaderStyle。如果你找到了解决方法,请更新。 - Fredrik Hedblad
完成。我一定会保持你更新的。这是一个错误报告链接:https://connect.microsoft.com/VisualStudio/feedback/details/623801/datagrid-not-styled-properly-when-applying-aero-theme-plus-custom-style - devuxer
我添加了一些可能有用的答案。 - Alexander Van Berg
@Alexander Van Berg:我不得不再次阅读问题和我的答案,但现在我记得这个了 :) 到目前为止,我只阅读了你的回答的简短版本,但我会在有时间的时候阅读它们两个。无论如何,非常好的答案,两个都+1! - Fredrik Hedblad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接