长答案
前面的简短回答提供了一些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](https://istack.dev59.com/kuWF7.webp)
更一般的主题定义是任何外观和感觉配置,无论在哪个层面(操作系统、应用程序、控件)。当人们使用这种一般定义时,可能会产生各种程度的混淆。请注意,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主题程序集),您可以使用(之前免费的)工具Reflector和BamlViewer插件来反编译BAML回到XAML。虽然不如 flashy,但ILSpy是一个内置BAML反编译器的免费替代品。
![enter image description here](https://istack.dev59.com/UMdrQ.webp)
.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](https://istack.dev59.com/gc0OU.webp)
此外,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](https://istack.dev59.com/Ik8ju.webp)
将主题作为皮肤加载
应该更新PresentationFramework.Aero参考文献到4.0版本吗?
绝对需要。在.NET FCL 4.0之前,DataGrid不存在。有几种确认方法,但最直观的方法是,根据您自己的承认,您以前通过WPF Toolkit访问它。如果您选择不在App.xaml中加载PresentationFramework.Aero 4.0,则Aero的DataGrid样式将不会出现在应用程序资源中。
现在,事实证明这甚至并不重要。我将使用原始XAML,在加载时断点,然后检查应用程序范围的资源。
![enter image description here](https://istack.dev59.com/dqZBr.webp)
作为预期,应用程序的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](https://istack.dev59.com/VFoyk.webp)
正如你所推断的那样,Aero 4.0 应该被加载。在调试过程中了解发生了什么是很有用的。
问题 #1:未使用Aero的DataGrid样式
在该应用程序中,DataGrid将根据您配置的Style.BasedOn属性链式地使用零个或多个样式。
它还将具有默认样式,此处为嵌入在Luna主题中的样式。
仅通过查看原始的XAML,我就知道存在样式继承问题。大型DataGrid样式具有约20个Setter,但未设置其BasedOn属性。
![enter image description here](https://istack.dev59.com/4ghGu.webp)
你有一个长度为两个的样式链,你的默认样式来自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](https://istack.dev59.com/BfwUx.webp)
此时应该注意,每个主题的DataGrid样式实际上没有任何差异。您可以通过将它们从各自的
4.0主题中复制到单独的文本文件中,然后使用diff工具进行比较来验证这一点。
事实上,相当数量的样式在不同主题之间是完全相同的。这一点需要注意。要验证这一点,只需对两个不同主题中保存的整个XAML运行diff即可。
![enter image description here](https://istack.dev59.com/YQ0av.webp)
请注意,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](https://istack.dev59.com/pjD64.webp)
现在假设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](https://istack.dev59.com/SuCfq.webp)
最终的方法DataGridHelper.GetCoercedTransferPropertyValue,本质上比较了DataGridColumnHeader.Style和DataGrid.ColumnHeaderStyle的来源。哪个来源优先级更高就会胜出。该方法中的优先级规则基于
依赖属性优先级。
此时,DataGrid.ColumnHeaderStyle将在原始XAML和解决方案XAML中进行检查。将收集一小部分信息矩阵。最终,这将解释每个应用程序中观察到的行为。
在原始XAML中,我在调试器中中断并看到DataGrid.ColumnHeaderStyle具有“Style”来源。这很有意义,因为它是在样式中设置的。
![enter image description here](https://istack.dev59.com/zMmwN.webp)
在解决方案的XAML中,我在调试器中打断点,并查看DataGrid.ColumnHeaderStyle具有“Default”源。这是有道理的,因为该值未在样式(或任何其他地方)中设置。
![enter image description here](https://istack.dev59.com/sE3yQ.webp)
另一个需要检查的值是DataGridColumnHeader.Style。DataGridColumnHeader是一个嵌套深度很大的元素,在使用VisualStudio进行调试时不太方便访问。实际上,像Snoop或WPF Inspector这样的工具将用于检查该属性。原始的XAML中,DataGridColumnHeader.Style具有“ImplicitStyleReference”源。这是有道理的。DataGridColumnHeaders在内部WPF代码中被实例化。它们的Style属性为空,因此它们将寻找一个隐式样式。树从DataGridColumnHeader元素到根元素遍历。如预期的那样,没有发现样式。然后检查应用程序资源。您在孤立的DataGridColumnHeader Style上设置了一个字符串键(“DataGrid_ColumnHeaderStyle”)。这实际上隐藏了它在这个查找中,因此它没有被使用。然后,搜索Aero皮肤并找到了一个典型的隐式样式。这就是使用的样式。
![enter image description here](https://istack.dev59.com/L2Uk6.webp)
如果使用XAML方案重复此步骤,结果也是相同的:'ImplicitStyleReference'。然而,这次隐式样式是DataGridResourceDictionary.xaml中唯一的DataGridColumnHeader样式,其键已变为隐式。
![enter image description here](https://istack.dev59.com/q6G2H.webp)
最后,如果使用原始的XAML重复这一步骤,并且未加载Aero皮肤,则结果现在为“默认”!这是因为整个应用程序中根本没有隐式DataGridColumnHeader样式。
因此,如果未加载Aero皮肤,则将使用DataGrid.ColumnHeaderStyle,但如果加载了Aero皮肤,则不会使用该样式!正如广告所说,“加载主题资源可能会产生不良影响”。
要记住这么多并且名称都听起来相同确实很困难。以下图表总结了所有操作。请记住,具有更高优先级的属性获胜。
![enter image description here](https://istack.dev59.com/HFQr0.webp)
这可能不是您想要的,但这是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](https://istack.dev59.com/DQ8uV.webp)
DataGrid看起来很Aero,但是AlternatingRowBackground没有被遵守。每隔一行应该有一个灰色的背景。
![enter image description here](https://istack.dev59.com/MIrbc.webp)
使用到目前为止讨论的调试技巧,可以发现这是与问题#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](https://istack.dev59.com/9aYwD.webp)
将其设置为“Stretch”,它将按预期工作。
布局属性和TextBlock格式属性之间存在一些相互作用。使用Snoop和WPF Inspector进行实验可以轻松确定在任何情况下都能起作用的内容。
最终想法
总之,加载主题资源与更改操作系统级别的主题不同。加载主题资源可能会导致不良影响。从WPF的角度来看,大量的隐式样式现在存在于应用程序中。这些样式可能会盖过其他样式。底线是,如果不加修改地将主题视为应用程序皮肤,则可能无法正常工作。
话虽如此,我对当前的WPF实现并不完全满意,特别是关于通过强制回调和优先规则使用“辅助属性”(例如DataGrid.ColumnHeaderStyle)。如果目标尚未明确分配值,则可以在初始化时将这些“辅助属性”直接分配给其预定目标(例如DataGridColumnHeader.Style)。我还没有考虑足够多,不知道可能会有什么问题,但如果可能的话,这可能会使“辅助属性”模型更加直观、与其他属性更加一致和更加防错。
最后,尽管这不是回答的重点,但非常重要的一点是,加载主题资源来模拟更改主题特别糟糕,因为这会导致
显著的可维护成本。应用程序中的现有样式不会自动基于主题 ResourceDictionary 中的样式。每个应用程序中的样式都必须将其 BasedOn 属性设置为类型键(或直接或间接地基于另一个执行此操作的样式)。这非常繁琐且容易出错。此外,与主题感知的自定义控件相关的可维护性成本也很高。这些自定义控件的
主题资源也必须被加载以实现此模拟。当然,在这样做之后,您可能会面临类似于此处所遇到的样式优先级问题!
无论如何,有多种方法可以解决 WPF 应用程序的问题(不打趣!)。我希望这个答案能够为您的问题提供额外的见解,并帮助您和其他人解决类似的问题。