目录

WPF-Templated Control 的命名约定

解释文件夹必须命名为Themes, Themes 内的资源字典(Resource Dictionary)必须命名为Generic.xaml

简单直接的回答是:这是 WPF 引擎(PresentationFramework.dll)内部写死的硬性规定(Hardcoded Convention)。

如果不叫这个名字,WPF 的引擎在自动查找默认样式时,就会因为找不到文件而放弃,导致你的控件变成“隐形”的(有逻辑但没外观)。

为了理解透彻,此处聊聊 WPF 的**“自动换肤机制”**。

1. 为什么会有这个机制?是为了适配 Windows 系统主题

回想一下 WPF 刚诞生的时候(Vista/Win7 时代)。Windows 系统是有不同主题的:

  • Classic (Windows 98 那种灰盒子)
  • Luna (Windows XP 的蓝绿界面)
  • Aero (Windows 7 的毛玻璃)
  • Aero2 (Windows 8/10/11 的扁平化)

WPF 的设计目标是:同一个控件(比如 Button),在 Win7 上自动长得像 Win7,在 XP 上自动长得像 XP。

2. WPF 是怎么找样式的?(查找链)

当你在代码里写了那句“魔法代码”:

DefaultStyleKeyProperty.OverrideMetadata(typeof(NumberBox), 
    new FrameworkPropertyMetadata(typeof(NumberBox)));

这行代码告诉 WPF:“嘿,这个控件没有显式设置 Style,请去主题字典里找它的默认样式。”

于是,WPF 开始了一场“寻宝游戏”,路径如下:

  1. 检查当前操作系统的主题
    假设你现在用的是 Windows 10/11,对应的主题名通常是 Aero2

  2. 尝试加载特定主题的资源
    WPF 会优先去你的程序集里找:/Themes/Aero2.NormalColor.xaml

    • 如果你为 Win10 单独设计了一套皮肤,它就会用这个。
    • (大多数开发者不会这么闲,为每个系统写一套皮肤)。
  3. 找不到?启动“备胎计划”
    如果找不到 Aero2.NormalColor.xaml,WPF 就会去寻找一个通用的、兜底的文件。
    这个文件的名字被规定为:Generic.xaml (Generic 的意思就是“通用的”、“一般的”)。

  4. 最终加载
    WPF 找到了 /Themes/Generic.xaml,从中提取 Key 为 local:NumberBox 的 Style,应用给控件。

3. 为什么必须在 Themes 文件夹下?

这涉及到程序集层面的配置。如果你打开项目的 AssemblyInfo.cs(现在 .NET Core/5+ 项目通常隐藏在这个文件里,但在项目属性里可以配置),你会发现这样一行代码:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, // 主题特定资源的位置
    ResourceDictionaryLocation.SourceAssembly // 通用资源的位置 (Generic.xaml)
)]

这个 ThemeInfo 特性明确告诉了 WPF 引擎:
“我的通用资源(Generic.xaml)就在当前的程序集(SourceAssembly)里,请去标准的 Themes 文件夹下找。”

这是 .NET Framework 源代码中写死的逻辑。WPF 的 ThemeDictionaryExtension 类在解析资源路径时,就是拼接字符串:
"/" + assemblyName + ";component/themes/" + themeName + ".xaml"

4. 如果我非要改名怎么办?

如果你非要把 Generic.xaml 改成 MyStyle.xaml,或者放在根目录下,会发生什么?

  1. 自动查找失效DefaultStyleKey 机制彻底罢工,你的控件渲染出来是空的(透明的)。
  2. 手动补救:你必须在使用这个控件的每一个 App.xamlWindow.xaml 里,手动把你的 MyStyle.xaml Merge 进来。

那样做就失去了自定义控件的灵魂。自定义控件的灵魂在于:“用户只要引用了我的 DLL,往界面上一拖,控件就能显示,不需要用户手动去 Merge 任何资源字典。”

  • Themes 文件夹:这是 WPF 约定存放所有主题相关资源的“标准目录”。
  • Generic.xaml 文件:这是“通用/默认样式”的兜底文件。当 WPF 找不到针对当前 Windows 版本的特定优化皮肤(如 Aero2, Luna)时,就会加载这个文件。

这就是约定优于配置”(Convention over Configuration)。微软规定了:只要你按这个路径放,我就能自动帮你把皮肤穿上。

当你打破了“约定”(Themes/Generic.xaml),你就必须显式地告诉 WPF:“别瞎找了,资源就在这里。”

这就好比:以前你把钱放在老地方(约定),大家都能自动找到;现在你把钱藏在了床底下的鞋盒里(非约定),你就必须画一张藏宝图(手动 Merge)告诉大家去哪拿。

下面是具体的操作步骤:

1. 制造“事故现场”

假设你为了“叛逆”,故意把资源文件改名并移动了位置:

  • 原位置Themes/Generic.xaml
  • 新位置Assets/MyWeirdStyle.xaml (放在了 Assets 文件夹,名字也改了)

此时,你的自定义控件渲染出来将是透明的,因为 DefaultStyleKey 按照默认路径找不到资源了。

2. 手动补救(在 App.xaml 中)

你需要把这个“流浪”的资源字典,强行塞进全局的资源池里。通常是在 App.xaml 做这件事,这样整个程序的任何地方都能用这个控件。

打开使用该控件的项目的 App.xaml

<Application x:Class="WpfAppTest.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>
                
                <!-- 
                   这就是“手动补救”。
                   Source 的路径指向你那个不按套路出牌的文件。
                -->
                <ResourceDictionary Source="/WpfCustomControlDemo;component/Assets/MyWeirdStyle.xaml"/>
                
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

3. 原理解析:为什么这样就行了?

这里涉及 WPF 查找资源的优先级顺序。当一个控件显示时,它查找样式的顺序是这样的:

  1. 对象本身:看看 <local:NumberBox Style="{...}" /> 这一行有没有直接写 Style。
  2. 容器:看看所在的 Grid、StackPanel 的 Resources 里有没有。
  3. 窗口:看看 Window.Resources 里有没有。
  4. 全局 (App.xaml):看看 Application.Resources 里有没有。<– 我们在这里截胡了!
  5. (如果没有找到,继续往下走)
  6. 主题/通用资源:去 Themes/Generic.xaml 找。 (这是默认行为)

发生了什么?
当你手动把 MyWeirdStyle.xaml Merge 进了 App.xaml,WPF 在第4步就找到了 Key 为 NumberBox 的样式。于是它心满意足地应用了样式,根本就不会去第6步执行那个失败的默认查找

4. 这里的坑:路径怎么写? (Pack URI)

上面的 <ResourceDictionary Source="..." /> 里的路径写法是最大的坑。

如果你的控件和 App.xaml同一个项目里:

  • 写法:Source="/Assets/MyWeirdStyle.xaml" (相对路径即可)

如果你的控件在另一个类库 (DLL) 里(比如叫 MyControlLib):

  • 写法必须是 Pack URI 格式:
    Source="pack://application:,,,/MyControlLib;component/Assets/MyWeirdStyle.xaml"

    • MyControlLib:程序集名称。
    • ;component:这是固定写法,表示引用程序集内的资源。
    • /Assets/...:文件路径。

总结

所谓的“自动查找”(Themes/Generic.xaml),其实只是 WPF 给你的一个保底机制

你可以完全抛弃这个机制,把所有样式都写在 App.xaml 里,或者随便散落在任何文件里然后手动 Merge。虽然这么做会让 App.xaml 变得臃肿不堪,且失去了“即插即用”的封装性,但从技术原理上讲,它是完全合法的。