WinRT 就是 COM —— IInspectable、.winmd、语言投影,以及 WinUI 至今仍立在二进制契约之上的原因
· 更新日期: · Go Komura · Windows, WinRT, COM, WinUI, Windows App SDK, Windows 开发
更新记录(仅首版,2026年08月29日 发布)
- 首次发布
想给 WPF 或 WinForms 的应用加上 Windows 的新能力。可是一调用 WinRT 的选取器就抛异常。读到 WinUI 的话题,又好像这回连 UI 都得重做一遍。这类困惑,只要把 WinRT 的机制和 UI 框架的选择分开来看,就能理清。
本文的出发点是:WinRT 同样立在 COM 的二进制契约之上。微软自己也写明了「The Windows Runtime is based on COM(Windows Runtime 基于 COM)」。1 上一篇讲 OLE 对象的文章里看到的把 Excel 表格嵌入 Word,和今天的 WinRT 与 WinUI,根子上都是同一个 IUnknown。
下面先把构成 WinRT 的三个要素弄清楚,接着看从桌面应用调用它时需要注意的地方,最后思考如何对待既有资产。目标读者是有 COM 或 Windows 桌面开发经验的开发者,前提环境是 Windows 10/11 与 .NET 6 及以上(C#)或 C++17(C++/WinRT),难度为中级。
1. 先说结论
要记住的结论有三条。
- WinRT 不是托管运行时,而是以 COM 为地基的 ABI(二进制契约)。在这份契约之上,加上了传递类型信息的
.winmd,以及让各语言都能自然调用的语言投影。1234 - 绝大多数 WinRT API 都能从既有的 WPF、WinForms、Win32 应用中使用。只是需要确认 HWND 的传递、package identity、线程初始化这三个前提。5678
- 把 UI 全面迁移到 WinUI,和局部使用 WinRT API,是两个不同的判断。WinUI 同样立在 WinRT ABI 之上,既有的 COM/ActiveX 资产可以和 WinRT 在同一个地基上共存。921
接下来按这个顺序读,更容易看清彼此的联系。
| 想知道什么 | 读哪一章 |
|---|---|
| 为什么可以说「WinRT 就是 COM」 | 第 2〜3 章:与 COM 的共同点和 IInspectable |
| 为什么能从 C# 或 C++ 自然地调用 | 第 4〜5 章:.winmd 与语言投影 |
| 从既有应用调用时要确认什么 | 第 6 章:HWND、package identity、单元 |
| 是否应该迁移到 WinUI | 第 7〜9 章:WinUI 的立足之处、迁移判断、通知的注册条件 |
下面的知识地图,是用来回过头查看各要素之间关系的。如果想先读说明,请从第 2 章依次往下。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 20 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. OLE 和 WinRT,根上是同一份二进制契约
本博客此前一路追踪的是古典 COM 的世界:COM 的设计思想、STA/MTA 的线程模型、ActiveX/OCX 的处理方式、OLE 复合文档。这些全是 1990 年代以来的技术。
另一边,WinRT 是 Windows 8(2012 年)引入的 API 基础设施。如今它以 Windows.* 命名空间下的 API 群提供吐司通知、共享、蓝牙、OCR 等能力,也成了 WinUI 与 Windows App SDK 的立足之处。10 新旧看起来像是完全不同的两个世界,实体却是连成一片的。
共通之处,是「通过接口来调用」这个约定
COM 组件和 WinRT 类,都是通过接口来公开功能。把作为地基的接口拿来对比,关系是这样的。1
| 古典 COM | WinRT |
|---|---|
接口的基类是 IUnknown |
接口的基类是 IInspectable,而它的基类是 IUnknown |
也就是说,WinRT 并没有换成一套与 COM 无关的机制,而是在 IUnknown 之上又叠了一层。引用计数也好,QueryInterface 也好,HRESULT 也好,都原封不动地活着。
C++/WinRT 的文档也把 WinRT API 称为「COM 的演进(an evolution of COM)」,并说明它的设计是通过语言投影来使用基于 COM 的 API。211
flowchart TB
accTitle: 古典 COM 与 WinRT 的谱系
accDescr: 在 IUnknown 与 vtable 构成的 COM 二进制契约这一共同地基之上,并排立着 1990 年代以来的 OLE、ActiveX 等古典 COM 的世界,以及 2012 年以后的 WinRT 及其上的 WinUI、Windows App SDK 的世界,两者并不互斥而是连成一片
base["COM 的二进制契约(IUnknown・vtable)"]
base --> classic["古典 COM(OLE・ActiveX・自制 COM)"]
base --> winrt["WinRT(IInspectable・.winmd)"]
winrt --> winui["WinUI/Windows App SDK"]
classic -.-> coexist["能在同一个地基上共存"]
winrt -.-> coexist
图1: 古典 COM 与 WinRT 不是两个世界,而是同一份二进制契约之上的新旧两代。
所以本文不是单纯的「新 API 介绍」。它要确认的是,你已经掌握的 COM 知识,在 2026 年的 Windows 开发里究竟能用在哪些地方。
3. IUnknown 之上的 IInspectable
不变的地基,和新增的三个方法
WinRT 类型系统的官方规范规定,所有 WinRT 接口都隐式地要求 IInspectable,而 IInspectable 要求 IUnknown。IUnknown 定义的仍然是 QueryInterface、AddRef、Release 这三个方法。12
在它之上,IInspectable 追加了下面三个方法。13
| 方法 | 作用 |
|---|---|
GetIids |
返回该对象所实现接口的 IID 列表 |
GetRuntimeClassName |
以 HSTRING 返回完全限定的 WinRT 类型名(例如 Windows.Storage.StorageFile) |
GetTrustLevel |
返回对象的信任级别 |
flowchart TB
accTitle: 立在 IUnknown 之上的 IInspectable
accDescr: 所有 WinRT 接口都要求 IInspectable,IInspectable 要求 IUnknown。IUnknown 提供 QueryInterface、AddRef、Release,IInspectable 提供 GetIids、GetRuntimeClassName、GetTrustLevel,再往上才是各个 WinRT 接口自己的方法
unk["IUnknown(QI・AddRef・Release)"]
insp["IInspectable(GetIids・类型名・信任级别)"]
api["各 WinRT 接口的方法"]
unk --> insp
insp --> api
图2: WinRT 的对象在 IUnknown 的三个方法之上叠了 IInspectable 的三个方法,再往上才是各个具体 API。
从类型名可以查到方法、属性、事件的定义
真正重要的不是新增了几个方法,而是能把类型名和元数据连起来。
在古典 COM 里,运行时了解一个对象来历的标准办法是「先知道 IID,再用 QueryInterface 去问」。面向脚本语言另有 IDispatch 这条路径。
在 WinRT 里,用 GetRuntimeClassName 拿到的类型名,可以用下一章要讲的元数据 .winmd 来解析。从那里就能得到方法、属性、事件的完整定义。规范本身也说,能够取得可用元数据解析的 WinRT 类型名,正是「使语言投影成为可能(enables language projection)」的地方。12
flowchart TB
accTitle: 从 GetRuntimeClassName 到语言投影
accDescr: 调用方调用对象的 GetRuntimeClassName 就会得到完全限定的 WinRT 类型名,用 Windows Metadata 解析这个类型名便能取得类型的完整定义,这使得向各语言的投影成为可能
obj["WinRT 对象"] --> name["类型名(GetRuntimeClassName)"]
name --> md[".winmd 解析类型定义"]
md --> proj["语言投影成为可能"]
图3: 「运行时取得类型名,再从类型名查到元数据」,这就是 WinRT 这套机关的核心。
用户定义接口的「继承」和「要求」要分开看
习惯了 COM 的人还要留意一处差别。WinRT 的类型系统里没有用户定义接口之间的继承。它有意不采用古典 COM 那种 IFileSystemBindData2 : IFileSystemBindData 式的派生,而是用「接口 A 要求(requires)接口 B」这样的声明来表达。121
这和前面讲的 IUnknown → IInspectable 这条基础 ABI 链是两码事。这条基础链作为所有 WinRT 接口的地基仍然保留着。
用户定义的契约被挪向了不依赖 vtable 继承布局的、更松散的形态。而另一方面,调用的实体至今仍是经由 vtable。别把契约写法上的差异和调用机制混为一谈,这一点很关键。
4. .winmd 解决了什么 —— 类型信息的绑定地狱
古典 COM 的难处在于「类型信息怎么发出去」
在古典 COM 的实务中,比起接口实现本身,怎么把类型信息发出去才更费工夫。
| 使用方 | 送达类型信息的路径 |
|---|---|
| C++ | 用 IDL 写契约,再用 MIDL 生成头文件和代理/存根 |
| VB6、脚本 | 分发类型库(TLB) |
| .NET | 另行制作互操作程序集 |
TLB 带有偏向自动化的类型限制,有些能写进 IDL 的信息进不了 TLB。再加上路径按语言分岔,只要其中一条过时,就会导致类型不一致。这份辛苦在类型库与 dscom 的文章和 DLL、COM 接口向后兼容性的文章里都讲过。
.winmd 是所有语言共读的一份契约文件
WinRT 给出的答案就是 Windows Metadata(.winmd)。它把 API 描述成机器可读的元数据,由工具和语言投影读取,再生成面向各语言的投影。3
Windows 随系统附带了全部系统 WinRT API 的元数据,并提供在运行时解析命名空间和类型的 API。Windows SDK 里则放着一份供编译期使用的副本。第三方只要给自己的 WinRT 组件配上 .winmd,就能用与系统 API 相同的机制参与语言投影。3
这里发生变化的,是被分发出去的类型信息的形态。原先按语言四散的 TLB、头文件、互操作程序集,换成了由全部语言的投影共读同一份 .winmd。
不过,这并不意味着 IDL 变得不需要了。制作 WinRT 组件时,仍然要用 IDL(面向 WinRT 翻新过的 MIDL 3.0)来描述契约,再由 MIDL 编译器生成 .winmd。14
flowchart TB
accTitle: WinRT 组件的制作流水线
accDescr: WinRT 组件的契约至今仍用 IDL 也就是 MIDL 3.0 来描述,由 MIDL 编译器把它编译成 .winmd。被分发出去的就是这份 .winmd,cppwinrt.exe 和 cswinrt.exe 等各语言的投影读取它来生成投影。被替换掉的不是 IDL,而是被分发的类型信息的形态
idl2["描述契约(IDL・MIDL 3.0)"]
midl2["MIDL 编译器"]
winmd4[".winmd(被分发的类型信息)"]
proj3["生成各语言的投影"]
idl2 --> midl2
midl2 --> winmd4
winmd4 --> proj3
图4: 契约的入口(IDL)依旧在岗,只是被分发的类型信息这个出口统一成了 .winmd。
文件格式和 .NET 相同,但它不是托管运行时
.winmd 的物理格式采用与 CLR 程序集相同的 ECMA-335 规范。不过,有效数据组合的规则和 CLR 程序集并不一样。借用了格式,和运行时需要 CLR,是两回事。3
这里得把系统 API 和第三方组件分开来读。
| 对象 | .winmd 与实现的关系 |
|---|---|
| 系统提供的 WinRT API | .winmd 是不含可执行代码的纯元数据。实现位于操作系统的原生 DLL 中,运行时不需要 CLR |
| 第三方的 WinRT 组件 | .winmd 也可能包含实现代码。托管的(用 C# 写的)组件若包含 MSIL,运行时就需要对应的 .NET 运行时 |
用工具打开 .winmd 时它看起来就像 .NET 程序集,所以容易被误解成「WinRT=托管」。然而,系统提供的 .winmd,内容其实是 COM 接口的契约文件。3
flowchart TB
accTitle: .winmd 与实现的分离
accDescr: .winmd 是借用 ECMA-335 物理格式的元数据,系统提供的那一份是不含可执行代码的契约文件,而系统 WinRT API 的实现位于操作系统的原生 DLL 中。正因为有这种分离,即使 .winmd 看起来像 .NET 程序集,运行系统 WinRT API 也不需要 CLR(第三方托管组件的 .winmd 含有 MSIL,需要 .NET 运行时)
winmd3[".winmd(契约文件・ECMA-335 格式)"]
impl["操作系统的原生 DLL(实现)"]
winmd3 -.->|"系统提供的不含代码"| note3["系统 API 不需要 CLR"]
impl --> note3
winmd3 ---|"类型定义与实现相对应"| impl
图5: 在系统提供的 WinRT API 中,.winmd 是契约文件,实现是操作系统的原生 DLL。格式看着像 .NET,运行起来仍是原生的 COM。
flowchart TB
accTitle: 古典 COM 的类型信息与 .winmd 的对比
accDescr: 古典 COM 里,从 IDL 到 C++ 头文件、从类型库到 VB6 或脚本、从互操作程序集到 .NET,类型信息的路径按语言分岔,成了参差不齐的根源;而 WinRT 里,全部语言的投影共读同一份 .winmd
subgraph old["古典 COM:路径按语言分岔"]
idl["IDL→C++ 头文件"]
tlb["TLB→VB6・脚本"]
ia["互操作程序集→.NET"]
end
winmd[".winmd(单一元数据)"]
winmd --> all["全部语言的投影共同读取"]
图6: 原先按语言四散的类型信息分发方式,被 .winmd 折叠成了「一份元数据,大家都读」。
约束不是消失了,而是换成了以投影难易为轴的约束
对熟悉古典 COM 的人,可以一句话概括:.winmd 就是「类型库的重做版」。把它看成是把 TLB 想承担的角色,放到 ECMA-335 这个久经考验的格式之上,从一开始就按全语言共通的正本重新设计的产物,位置就清楚了。
不过,约束并没有消失。TLB 那种偏向自动化的约束,换成了以「能安全地投影到所有语言」为轴的、WinRT 自有类型系统的约束。第 3 章看到的用户定义接口之间没有继承,就是其中一例。12
因此,既有的 COM/IDL 契约未必能原样搬到 WinRT。有时候需要把 API 重新设计一遍。
flowchart TB
accTitle: 从类型库的约束换成 WinRT 类型系统的约束
accDescr: TLB(类型库)原有的偏向自动化的表达力约束,并不是在 .winmd 里被取消了,而是换成了以能安全投影到所有语言为轴的 WinRT 自有类型系统的约束。其中一例就是没有用户定义接口继承,既有的 COM/IDL 契约未必能原样搬过来,有时需要把 API 重新设计
tlb["TLB 的约束(偏向自动化)"] -->|"替换"| wrt["WinRT 类型系统的约束(以投影可能性为轴)"]
wrt -.-> ex["例:不允许用户定义的继承"]
wrt -.-> re["既有 COM 契约有时需重新设计"]
图7: TLB 的约束不是「消失了」,而是换成了另一套以「能否投影到所有语言」为轴的约束。
5. C++/WinRT 与 C#/WinRT 不是「包装器」而是投影
把共通的契约,以各语言自然的形态呈现出来
有了 .winmd 这份共通的契约文件,各语言里的呈现方式就能用工具自动生成。这就是语言投影(language projection)。它按各语言的惯用法公开 WinRT API,把 COM 的细节藏起来,从而为该语言提供自然的编程体验。411
微软目前支持的投影有下面两种。4
| 投影 | 生成什么 | 特征 |
|---|---|---|
| C++/WinRT | cppwinrt.exe 从 .winmd 生成面向 C++ 的投影头文件 |
基于标准 C++17 头文件的投影。不需要 C++/CX 那样的语言扩展,是 C++/CX 与 WRL 的后继11 |
| C#/WinRT(CsWinRT) | cswinrt.exe 从 .winmd 生成 C# 代码,并编译成互操作程序集 |
面向 .NET 的投影。是独立于运行时的工具链1516 |
C# 这边的来龙去脉稍微有点绕,先理清楚。在 .NET Core 3.x 之前,.NET 运行时对 WinRT/winmd 的使用是内置支持的。到了 .NET 5,这份内置支持被移除,改由 C#/WinRT 承担。16 并不是 WinRT API 不能用了,而是负责投影的地方换了。
现在在 C# 里指定 net8.0-windows10.0.19041.0 这类 TFM,就会自动引用 Windows SDK 的投影程序集。10
flowchart TB
accTitle: 从 .winmd 生成各语言的投影
accDescr: cppwinrt.exe 读取同一份 .winmd 会生成面向 C++17 的投影头文件,cswinrt.exe 读取它则生成面向 C# 的互操作程序集,各自以贴合该语言惯用法的形态公开 WinRT API
winmd2[".winmd(API 的契约文件)"]
winmd2 --> cpp["cppwinrt.exe→C++17 头文件"]
winmd2 --> cs["cswinrt.exe→C# 互操作程序集"]
cpp --> cppcode["能以 C++ 的惯用法调用"]
cs --> cscode["能以 C# 的惯用法调用"]
图8: 投影不是手写的包装器,而是由工具从契约文件(.winmd)机械地生成出来的。
就算是自动生成,调用的实体依旧是 COM
本文之所以称它为「投影」而不是「包装器」,是为了强调它不是靠人逐个 API 追着写的翻译层,而是从元数据机械导出的机制。只要 .winmd 上有的 API,从一开始就能在全部受支持的语言里用上。
而且,无论从哪种语言调用,投影之下发生的都是同样的 COM 调用。比如在 C# 里写 await picker.PickSingleFolderAsync(),投影会把 WinRT 的 IAsyncOperation 桥接到 .NET 的 Task 世界。即便如此,在 ABI 那一侧用的仍是经由 vtable 的方法调用和 HRESULT。错误之所以会以 COM 异常(形如 0x80070005 的 HRESULT)的形式冒出来,原因就在这里。
flowchart TB
accTitle: 从 C# 代码到操作系统 WinRT API 的各层
accDescr: 应用的 C# 或 C++ 代码经由语言投影被转换为 WinRT 的 ABI 也就是 IInspectable 的 vtable 调用,最终抵达操作系统实现的 WinRT API。投影只是把 COM 的细节藏了起来,调用的实体就是 COM
code["应用的代码(C#・C++)"]
proj2["语言投影"]
abi["WinRT ABI(IInspectable 的 vtable)"]
os["操作系统的 WinRT API 实现"]
code --> proj2
proj2 --> abi
abi --> os
图9: 投影藏起来的是 COM 的「细节」,而不是 COM 本身。
知道了这个结构,就能把故障分到两个层去查。生成代码的版本不匹配、TFM 设置漏了,属于投影这一层;HRESULT、单元、引用计数,属于 ABI 这一层。在后者,COM 开发的经验可以直接当武器用。
6. 桌面应用里的卡点 —— HWND、identity、单元
先把「能引用到 API」和「运行条件」分开
绝大多数 WinRT API 都能从 WPF、WinForms、Win32 桌面应用调用。5 调用的入口在 C# 和 C++ 里有如下差别。10
| 环境 | 最初的设置 |
|---|---|
| C#/.NET 6 及以上 | 把 TargetFramework 设成 net8.0-windows10.0.19041.0 这类带 Windows OS 版本的 TFM |
| C++ | 引入 Microsoft.Windows.CppWinRT NuGet 包,在 C++17 及以上使用 C++/WinRT |
不过,能引用到并不等于所有 API 都能照原样跑起来。下面三点要确认。吐司通知的注册条件放在第 9 章单独整理。
| 要确认的事 | 主要对策 |
|---|---|
| 是否需要显示所在的窗口 | 用对应 UI 的 COM interop 传入 HWND |
| 是否需要 package identity | 用 MSIX 或指向外部位置的包赋予 identity |
| 线程是否已为 WinRT 初始化 | 原生代码中指定 STA/MTA 后初始化。C# 里通常由运行时处理 |
卡点 1:选取器之类的 UI 需要传入 HWND
一部分选取器、对话框和共享 UI,把 UWP 的 CoreWindow 设想成了显示目标。桌面应用没有 CoreWindow,因此必须在显示前显式传入所有者窗口的 HWND。6
选取器等使用的入口,是 IInitializeWithWindow 这个 COM 接口。它继承 IUnknown,为桌面应用中使用的 WinRT 对象提供所有者窗口。17
在 C# 里,先按当前使用的 UI 框架取得 HWND。186
| 所有者窗口 | HWND 的取得方式 |
|---|---|
WinUI 的 Window |
WinRT.Interop.WindowNative.GetWindowHandle |
| WPF 的窗口 | WindowInteropHelper |
| WinForms 的窗体 | 窗体的 Handle 属性 |
接着用 WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd) 把它交给选取器,然后再显示。若是 C++/WinRT,就用 as<IInitializeWithWindow>() 取得对象,再调用 Initialize(hwnd)。省掉初始化的话,要么抛异常,要么悄无声息地失败。1819
对现代的 WinRT 对象,QueryInterface 出一个古典 COM 的接口来做初始化。这种桥接成为官方作法,本身也说明了 WinRT 就是 COM。
共享 UI 用的是另一个接口。对 DataTransferManager 不能用 IInitializeWithWindow。要用专用的 IDataTransferManagerInterop,把 HWND 传给 ShowShareUIForWindow。6
新的 Windows App SDK 选取器也是另一条路径。Microsoft.Windows.Storage.Pickers 在构造函数中接收 WindowId,因此不需要 InitializeWithWindow 那套模式。但它不是仅靠 TFM 设置就能用的 API。除了引入 Windows App SDK 之外,unpackaged 应用还要以在分发目标上部署并初始化运行时为前提。19
sequenceDiagram
accTitle: 从桌面应用显示选取器的步骤
accDescr: 桌面应用创建选取器之后,如果直接调用 PickSingleFolderAsync 就会抛异常或悄无声息地失败,因此必须先取得所有者窗口的 HWND,用 IInitializeWithWindow 的 Initialize 传入之后再显示
participant A as 桌面应用
participant P as 选取器(WinRT)
A->>P: 创建
alt 不传 HWND 就显示
A->>P: PickSingleFolderAsync
P-->>A: 抛异常或悄无声息地失败
else 先传入 HWND
A->>P: 用 IInitializeWithWindow 设置 HWND
A->>P: PickSingleFolderAsync
P-->>A: 选取器显示出来
end
图10: 用显式传递 HWND 来补上「桌面上没有 CoreWindow」这个缺口,就是官方作法。
卡点 2:有些 API 需要 package identity
吐司通知的历史记录(ToastNotificationHistory)、跳转列表、共享目标等一部分 WinRT API,只有在拥有 package identity 的应用(已打包的应用)里才能工作。从用传统安装程序分发的 unpackaged 应用里调用会失败。7
对策有两个。要么用 MSIX 打包,要么在保留既有安装程序的前提下赋予标识符,也就是使用「指向外部位置的包(俗称 sparse package)」。20 哪些 API 要求 identity,可以在官方清单里确认。7
flowchart TB
accTitle: 通往需要 package identity 的 API 的两条路径
accDescr: 从 unpackaged 应用调用必须有 package identity 的 WinRT API 会失败,因此要么用 MSIX 打包,要么用保留既有安装程序、只赋予标识符的指向外部位置的包,二者之一让应用拥有 package identity
need["想用必须有 identity 的 API"]
need --> m1["用 MSIX 打包"]
need --> m2["指向外部位置的包"]
m1 --> id2["获得 package identity"]
m2 --> id2
id2 -.-> okid["通知历史记录等能工作"]
图11: 应对必须有 identity 的 API,只有「MSIX」和「既有安装程序+赋予标识符」两个选项。
卡点 3:确认线程初始化与 STA/MTA
处理 WinRT 对象的线程,需要事先为 WinRT 做初始化。原生代码里用 RoInitialize 或 winrt::init_apartment,指定 STA/MTA 这个并发模型。在 C# 的 WPF/WinForms 应用里,通常由运行时处理初始化。8
RoInitialize 和 COM 的 CoInitializeEx 属于同一套框架,是 WinRT 世代的入口。CoInitialize 的文档本身也指引说,使用 Windows Runtime 时应改为调用 RoInitialize 或 Windows::Foundation::Initialize。21
WPF/WinForms 的 UI 线程是 STA;UI 对象要在 UI 线程上碰;阻塞式等待在 STA 上会招致死锁。STA/MTA 的文章里讲过的这些思路,在 WinRT API 上照样成立。
就算备好 HWND 和 identity 也用不了的 API 依然存在
到这里为止的对策,讲的都是那些留有桌面使用入口的 API。直接依赖 CoreWindow 或 ApplicationView 本身的 API,在桌面应用里用不了。这种情况下不是去补初始化,而是去找替代 API。57
flowchart TB
accTitle: 从桌面调用 WinRT API 时卡点的分支
accDescr: 先确认线程已为 WinRT 初始化(原生代码里用 RoInitialize 等显式指定 STA/MTA。C# 里通常由运行时处理),在此基础上,想调用的 WinRT API 若是以 CoreWindow 为前提的 UI 类,就用 IInitializeWithWindow 传入 HWND 或改用新选取器;若必须有 package identity,就用 MSIX 或指向外部位置的包赋予标识符;直接依赖 CoreWindow 或 ApplicationView 本身的 API 在桌面上用不了,只能去找替代 API;其余绝大多数 API 只要配好 TFM 或 C++/WinRT 就能直接调用
pre["线程初始化"] --> q{"想调用的 API 是什么性质?"}
pre -.-> auto["原生需显式指定,C# 通常自动"]
q -->|"UI 类"| h["HWND 或新选取器"]
q -->|"必须有 identity"| p2["MSIX 或赋予标识符"]
q -->|"依赖 CoreWindow 本身"| x["去找替代 API"]
q -->|"其余"| ok3["可以直接调用"]
图12: 以线程初始化(原生代码里显式指定,C# 里通常交给运行时)为前提,卡点可以归成三类,每一类都有固定的对策。
flowchart TB
accTitle: COM 与 WinRT 的线程初始化对应关系
accDescr: 古典 COM 用 CoInitializeEx 指定 STA 或 MTA 来初始化线程,WinRT 则用 RoInitialize 同样指定 STA 或 MTA 的并发模型来初始化。CoInitialize 的文档也指引在使用 WinRT 时调用 RoInitialize,单元这套思路是共通的
com3["古典 COM:CoInitializeEx"] --> apt["单元(STA/MTA)"]
wrt["WinRT:RoInitialize"] --> apt
apt -.-> rule["UI 线程是 STA・当心等待"]
图13: 初始化 API 的名字变了,但单元这个概念一直是同一个,被沿用了下来。
7. WinUI 的立足之处 —— 转成公开开发,契约也没变
转向公开开发,不是二进制契约的变更
微软在 2025 年夏天正式宣布,要把 WinUI 的主线开发迁到 GitHub 的公开场所,并采取分阶段的做法。提高镜像的更新频率、让本地构建成为可能、把测试整备好之后接受社区贡献,最终让 GitHub 成为开发的主阵地,一共四个阶段。22
在本文发布的时点(2026 年 8 月底),官方文档也写明「WinUI 是在公开场所开发的(built in the open)」。日常的工程进展可以在公开仓库里追踪。9
一路追着 WinForms→WPF→UWP→WinUI 世代更替过来的开发者,会觉得「框架又要变了吗」,这很自然。但是,一直在变的 UI 框架,和它下面的地基,需要分开来看。Win32 与 COM,以及 2012 年以后的 WinRT ABI,一直待在同一个位置上。
WinUI 的窗口,同样由 HWND 支撑
WinUI 是作为 Windows App SDK 的一部分提供的 WinRT API 群。923 它的 Microsoft.UI.Xaml.Window,是取代 UWP 世代基于 CoreWindow 的窗口模型的、由 HWND 支撑的窗口。官方的互操作教程,也是从先取得窗口句柄开始的。24
也就是说,WinUI 应用是在 HWND 的窗口之上,跑着一棵遵循 IInspectable 契约的对象树的 Win32 应用。用 COM 学到的 QueryInterface、引用计数、单元的知识,用 Win32 学到的 HWND 与消息循环的知识,在 WinUI 的故障排查中照样通用。
flowchart TB
accTitle: UI 框架的世代更替与不变的地基
accDescr: WinForms、WPF、UWP 的 XAML、WinUI 这些 UI 框架一代代更替过来,但 WinForms 与 WPF 直接立在 Win32 与 COM 的地基上,UWP 的 XAML 与 WinUI 则经由 WinRT ABI 立在同一个 Win32 与 COM 的地基上。更替的是上层,脚下的契约没有变
gen["UI 框架的世代更替"]
gen --> f1["WinForms・WPF"]
gen --> f2["UWP 的 XAML"]
gen --> f3["WinUI(现在)"]
f2 --> abi3["WinRT ABI(2012 年〜)"]
f3 --> abi3
f1 --> stable["不变的地基(Win32+COM)"]
abi3 --> stable
图14: 起起伏伏的是框架这一层。WinForms、WPF 直接立在 Win32+COM 上,UWP、WinUI 经由 WinRT ABI 立在同一个地基上。
flowchart TB
accTitle: 支撑 WinUI 应用的各层
accDescr: WinUI 的 XAML 与控件作为 Windows App SDK 的一部分提供,运行在 WinRT 的 ABI 也就是 IInspectable 的契约之上,再往下则是 COM 与 Win32 的 HWND 这个地基。在 UI 框架的世代更替之下,脚下的这份契约没有改变
ui["WinUI(XAML・控件)"]
sdk["Windows App SDK"]
abi2["WinRT ABI(IInspectable)"]
base2["COM+Win32(HWND)"]
ui --> sdk
sdk --> abi2
abi2 --> base2
图15: WinUI 下面是 WinRT ABI,再往下是古典 COM 与 Win32。只是叠法变了,地基还是同一个。
用 XAML Islands 做局部混合,要确认各世代的约束
「在既有的 WPF/WinForms 界面里,只混入 WinUI 控件」这个策略,需要按 XAML Islands 的世代分开评估。
| 世代 | 从 WPF/WinForms 使用时的状况 |
|---|---|
| UWP 世代的 XAML Islands | 有 Windows Community Toolkit 的包装控件。但面向 WPF/WinForms 的支持只到 .NET Core 3.x 世代,现行 .NET 不受支持25 |
| WinUI 3 世代 | 可以用 Windows App SDK 的 DesktopWindowXamlSource 从 WPF、WinForms、Win32 托管。但没有 UWP 世代那样方便的包装控件,要直接摆弄托管 API,实现和验证的负担较重26 |
如果计划分阶段混合,最好别只把控件级别的混合当作前提。以功能为单位使用 WinRT API(第 6 章),或以界面、进程为单位做分离,在 2026 年这个时点更稳妥。
8. 对业务应用的含义 —— 全面迁移与局部使用是两个问题
「WinRT 是个新的异世界,要用它就只能扔掉既有资产重做」。在承接开发的现场遇到的这个误解,拆成两半就能解开。
既有的 COM 资产和 WinRT 可以共存
一个用着 Excel COM 自动化、装着 ActiveX 控件、调用着自家 COM 组件的 WPF 应用,是可以再加上 WinRT API 的。这不是什么特技,而是因为大家都在同一个 COM 地基上组合功能。
比如吐司通知,就是设好 TFM 引用到 API,再满足发出通知所需的注册条件。为了别把后者忘了,第 9 章会按路径分别整理。在 C++/WinRT 里,WinRT 和古典 COM 两种接口都能用 winrt::com_ptr 和 winrt::implements 这同一套机制来处理。21
UI 翻新和功能追加要分开估算
「是否把 UI 全面迁移到 WinUI」和「是否只在必要之处使用 WinRT API」,在规模、周期、风险上都是不同的判断。
UI 全面迁移是框架选型的问题,取决于界面资产、第三方控件和开发体制。这个判断在 WinForms/WPF/WinUI 的选择方法里讲过。而 WinRT API 的局部使用,则是能在既有应用上今天就着手的小改进。
只是想要吐司通知的项目,没必要去估算 UI 全面迁移。反过来,也没必要因为「反正不迁到 WinUI」就连 WinRT API 的使用一起放弃。
flowchart TB
accTitle: 全面迁移与局部使用是不同的判断
accDescr: 迁到 WinUI 的 UI 全面迁移是框架选型的大判断,取决于界面资产和团队体制;而 WinRT API 的局部使用是靠 TFM 设置等手段今天就能加到既有 WPF 或 WinForms 应用上的小判断,两者不要混为一谈,要分开考量
goal["「想用上 Windows 的新能力」"]
goal --> big["UI 全面迁移(大判断)"]
goal --> small["WinRT API 局部使用(小判断)"]
big -.-> dep["取决于界面资产和团队体制"]
small -.-> today["今天就能加到既有应用上"]
图16: 通往「新能力」的路有两条,混为一谈的话,估算和判断都会走偏。
9. 判断表 —— 保留、包装、替换的 WinRT 版
按场景只挑必要的改动
作为 ActiveX 判断表的延伸,这里把 WinRT 一侧按场景的判断汇总起来。
| 场景 | 推荐 | 理由 |
|---|---|---|
| 想从 WPF/WinForms 使用吐司、共享、蓝牙等 WinRT API | 用 TFM 设置(或引入 C++/WinRT)做局部使用 | 不迁 UI,今天就能调。但共享 UI 要经由 IDataTransferManagerInterop(第 6 章),吐司请看表格下面的补充106 |
| 选取器、对话框抛异常或悄无声息地失败 | 用 IInitializeWithWindow 传入 HWND。新写的用支持 WindowId 的新选取器(以引入 Windows App SDK 为前提) |
用 HWND 补上以 CoreWindow 为前提的设计,这是官方作法619 |
| 通知历史记录、跳转列表等跑不起来 | 用 MSIX 或指向外部位置的包赋予 package identity | 必须有 identity 的 API 以打包为前提720 |
| 既有的 COM/ActiveX/OLE 资产 | 别扔。保留/包装/替换按资产逐个判断 | 它和 WinRT 不互斥,能在同一个地基上共存(第 8 章) |
| 新建桌面应用的 UI | 把 WinUI 作为首选来评估(WPF 也仍在岗) | 公开开发让投资方向变得明确。脚下是 WinRT ABI229 |
| 在既有 WPF/WinForms 界面里局部混入 WinUI 控件 | 把没有包装控件带来的实现负担算进去,谨慎评估 | UWP 世代 Islands 止于 .NET Core 3.x,WinUI 3 世代只有托管 API2526 |
| 以「WinRT 已经和 UWP 一起终结了」为前提的计划 | 修正这个前提 | WinRT 是能从桌面调用的现行 API 基础设施5 |
吐司通知,光设好 TFM 是显示不出来的
让 API 可以被调用的设置,和为显示通知而做的注册,是两回事。当「构建能过,通知却出不来」时,除了调用方之外,还要确认所用的路径、打包形态和是否提权。
使用传统的 ToastNotificationManager 时
在不具备 package identity 的 unpackaged 应用中,前提是注册一个分配了 AppUserModelID(AUMID)的开始菜单快捷方式。没有它就发不出吐司。27
用 MSIX 等方式打包的应用,包的 identity 会提供 AUMID,因此不需要这项手动注册。
使用 Windows App SDK 的 AppNotificationManager 时
在这条目前推荐的路径上,需要引入 Windows App SDK,并在启动时调用 Register()。在此基础上,条件会按打包形态分开。28
| 打包形态 | 还要额外确认的事 |
|---|---|
| unpackaged | 需要在分发目标的每台 PC 上部署 Windows App SDK 运行时。Register() 会执行 COM 服务器注册 |
| 用 MSIX 打包 | Register() 的自动注册不起作用。要在 Package.appxmanifest 中声明 COM 激活器 |
此外,在 Windows App SDK 这条路径上,不支持从提升为管理员的进程发出通知。Show 不会抛异常,而是悄无声息地失败。需要提权的应用,请考虑把通知单独交给非提权进程的分离做法。28
注册相关的实现细节,在托盘常驻与通知的实现指南里讲过。
flowchart TB
accTitle: 吐司通知的两条路径与所需的注册
accDescr: 要从桌面应用发出吐司通知,走传统的 ToastNotificationManager 路径时会按是否 packaged 分支,unpackaged 应用需要先注册分配了 AppUserModelID 的开始菜单快捷方式,packaged 应用则由包的 identity 提供 AppUserModelID。走 Windows App SDK 的 AppNotificationManager 路径时,除了引入 SDK 和启动时调用 Register 之外,unpackaged 应用还要在分发目标的每台 PC 上部署运行时,用 MSIX 打包的应用则要在清单里声明 COM 激活器,满足之后才能往下走。此外 Windows App SDK 路径还有是否提权进程的分支,从提升为管理员的进程发出通知并不受支持,Show 不抛异常而是悄无声息地失败,因此这条路径上只有非提权进程才能显示通知,需要提权的应用要把通知分离到非提权进程
want["想发出吐司"]
want --> c1["传统:ToastNotificationManager"]
want --> c2["WASDK:AppNotificationManager"]
c1 --> q1{"是否 packaged?"}
q1 -->|"否"| s1["注册 AUMID 快捷方式"]
q1 -->|"是"| s2["identity 提供 AUMID"]
c2 --> r2["引入 SDK+Register()"]
r2 --> q2{"是否 packaged?"}
q2 -->|"否"| s3["部署运行时"]
q2 -->|"是"| s4["在清单里声明 COM"]
s1 --> shown["通知显示出来"]
s2 --> shown
s3 --> elev{"是否提权进程?"}
s4 --> elev
elev -->|"否"| shown
elev -->|"是"| fail["不受支持:Show 悄无声息地失败"]
fail -.-> comp["把通知分离到非提权进程"]
图17: 两条路径都要先满足与打包形态相应的前提,才能走到显示这一步。Windows App SDK 路径即使前提全齐,从提权进程也显示不出来。
最后,回到保留、包装、替换
是需要新能力,还是想把 UI 本身翻新一遍。回到这个问题上,就能把「使用 WinRT」和「替换既有资产」分开来判断。
flowchart TB
accTitle: 与既有资产和 WinRT 相处方式的判断流程
accDescr: 以既有桌面应用为起点,不需要 Windows 新能力就保留,需要新能力就用 WinRT API 局部使用来包装(此时要留意第 6 章的 HWND、package identity、线程初始化这些卡点),只有在需要把 UI 本身翻新时才考虑替换为 WinUI,这是一条分阶段的判断流程
start["既有桌面应用"] --> q2{"需要什么?"}
q2 -->|"现状够用"| keep2["保留(照常维护)"]
q2 -->|"新的能力"| wrap2["包装(WinRT API 局部使用)"]
q2 -->|"UI 翻新"| rep["替换(评估 WinUI)"]
wrap2 -.-> note2["留意 HWND・identity・初始化(第 6 章)"]
图18: ActiveX 判断表里那套「保留、包装、替换」的格局,在 WinRT 一侧同样适用。
10. 总结
WinRT 不是与 COM 割裂开来的新执行环境。它是在 COM 的二进制契约上,组合了元数据与各语言投影的 API 基础设施。
| 要素 | 本文讲清楚的角色 |
|---|---|
IUnknown 与 IInspectable |
以 QueryInterface、引用计数为地基,再追加取得类型名等信息的三个方法 |
.winmd |
能从类型名查到定义的、全语言共通的契约文件。借用 ECMA-335 格式,但系统提供的那一份不含可执行代码 |
| C++/WinRT、C#/WinRT | 从契约文件生成面向各语言的 API。C# 的投影自 .NET 5 起成了独立于运行时的工具链 |
即使外观变成了 C# 或 C++ 里自然的 API,它下面用的仍是经由 vtable 的 COM 调用和 HRESULT。所以,理解了桌面上的 HWND 传递、package identity、STA/MTA 与线程初始化,WinRT 的故障也就更容易分辨清楚。
把 WinUI 主线开发迁到公开场所的举措,是在 2025 年夏天作为分阶段做法发布的。在本文发布的时点,官方文档里也写着「built in the open」,但是它下面那条 IUnknown → IInspectable 的契约,并没有因此改变。229
在业务应用上的结论也是一样。既有的 COM/ActiveX 资产和 WinRT 可以共存。不要把 UI 的全面迁移和 WinRT API 的局部使用混为一谈,这才守得住估算和判断的精度。
OLE 那篇文章里看到的 1990 年代复合文档,和今天的 WinUI,都立在同一个 IUnknown 之上。懂 COM,并不只是「熟悉遗留技术」而已。它意味着读得懂今天 Windows 的脚下。
相关文章
- COM 是什么 - Windows COM 的设计为何至今依然优美
- COM / ActiveX / OCX 是什么 - 区别与关系整理
- COM STA/MTA 的基础知识 - 线程模型与避免挂起的思路
- 今天该如何对待 ActiveX / OCX - 保留、包装、替换的判断表
- WinForms/WPF/WinUI 的选择方法 - 实务判断表
- OLE 对象是什么 —— 嵌入与链接的机制,以及业务文档中的陷阱
- 如何在 VBA 中以带类型的方式使用 .NET 8 DLL - COM 暴露与 dscom TLB
相关咨询领域
小村软件有限公司承接 COM 组件开发,以及包含 COM 资产在内的 Windows 业务应用的维护、改造与替换。像「想保持 WPF 不变的同时用上吐司通知和选取器」「调用 WinRT API 之后抛异常停住了」「想判断该迁到 WinUI 还是继续用好既有资产」这类正好以本文内容为议题的阶段,都可以来咨询。
参考链接
-
Microsoft Learn, Consume COM components with C++/WinRT. 关于在 COM 中是经由接口而非对象来编程、这一点在作为 COM 演进(an evolution of COM)的 WinRT API 幕后同样成立、以及用 winrt::com_ptr 这种 COM 智能指针可以用同一套流派处理 WinRT 与古典 COM。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Windows Metadata (WinMD) files. 关于 WinRT API 以 .winmd 这种机器可读元数据描述并由工具和语言投影使用、Windows 随系统附带全部系统 WinRT API 的元数据并提供解析用 API、第三方也能以相同格式参与语言投影、以及物理格式为 ECMA-335 规范(与 CLR 程序集相同)而系统提供的 WinMD 是纯元数据。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Windows Runtime (WinRT) language projections. 关于语言投影按各语言的惯用法公开 WinRT API、.winmd 定义 WinRT API 而投影读取它、以及微软支持的是 C++/WinRT(C++17 及以上)与 C#/WinRT(.NET)两种。 ↩ ↩2 ↩3
-
Microsoft Learn, WinRT APIs callable from a desktop app. 关于绝大多数 WinRT API 都能在 .NET 及原生 C++ 的桌面应用中使用、以及 CoreDispatcher・CoreWindow・ApplicationView 等专为 UWP 设计的类属于例外。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Display WinRT UI objects that depend on CoreWindow. 关于一部分选取器・弹出框・对话框依赖 CoreWindow、桌面应用不支持 CoreWindow、对实现了 IInitializeWithWindow(或等效的 IDataTransferManagerInterop)的类可以在显示前设置所有者窗口的 HWND、以及在 WinUI 3・WPF・WinForms 各自的做法。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, WinRT APIs not supported in desktop apps. 关于桌面应用中用不了的 WinRT API 分为两类,即依赖 UWP 专用 UI 功能的 API 与要求 package identity 的 API(ToastNotificationHistory・JumpList 等),以及后者仅在用 MSIX 打包的应用中受支持。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, RoInitialize function (roapi.h). 关于 RoInitialize 以指定的并发模型(RO_INIT_SINGLETHREADED/RO_INIT_MULTITHREADED)为 Windows Runtime 初始化当前线程、所有激活和操作 WinRT 对象的线程都需要事先初始化、以及在已初始化为 MTA 的线程上做出矛盾的指定会得到 RPC_E_CHANGED_MODE。 ↩ ↩2
-
Microsoft Learn, WinUI 3. 关于 WinUI 是面向新建 Windows 桌面应用推荐的原生 UI 框架、作为 Windows App SDK 的一部分提供、可在 Windows 10 版本 1809 及以上运行、以及它是在公开场所开发的。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Call Windows Runtime APIs in desktop apps. 关于在 .NET 6 及以上指定带 Windows OS 版本的 TFM(如 net10.0-windows10.0.22621.0)就会引用 Windows SDK targeting package 从而能调用 WinRT API、以及 C++ 使用 Microsoft.Windows.CppWinRT NuGet 包与 C++17 及以上的 C++/WinRT。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to C++/WinRT. 关于 C++/WinRT 是完全标准的现代 C++17 语言投影并以头文件库的形式实现、是 C++/CX 与 WRL 的推荐后继、WinRT 的设计是基于 COM API 并通过语言投影来访问、投影隐藏 COM 的细节、以及 cppwinrt.exe 从 .winmd 生成投影头文件。 ↩ ↩2 ↩3
-
Microsoft Learn, The Windows Runtime (WinRT) type system. 关于所有 WinRT 接口都隐式要求 IInspectable 而 IInspectable 要求 IUnknown、IUnknown 定义 QueryInterface・AddRef・Release、IInspectable 追加的 GetIids・GetRuntimeClassName・GetTrustLevel 三个方法、GetRuntimeClassName 返回可用元数据解析的类型名从而使语言投影成为可能、以及 WinRT 类型系统中没有用户定义接口之间的继承而用 requires 来表达。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, IInspectable interface (inspectable.h). 关于 IInspectable 提供所有 WinRT 类所需的功能、继承 IUnknown、以及拥有 GetIids・GetRuntimeClassName・GetTrustLevel 三个方法。 ↩
-
Microsoft Learn, Introduction to Microsoft Interface Definition Language 3.0. 关于 MIDL 3.0 是用于声明 WinRT 类型的简洁现代语法、WinRT 的契约至今仍用 IDL 描述、以及由 MIDL 编译器生成 Windows 元数据(.winmd)。 ↩
-
Microsoft Learn, C#/WinRT. 关于 C#/WinRT 的 NuGet 包中所含的 cswinrt.exe 处理 .winmd 生成 C# 代码并编译成互操作程序集、以及它与 C++/WinRT 生成面向 C++ 的头文件处于同样的位置。 ↩
-
Microsoft Learn, Built-in support for WinRT is removed from .NET. 关于 .NET 5 中 Windows Runtime 的内置支持已从 .NET 移除并迁移到 CsWinRT 工具链。 ↩ ↩2
-
Microsoft Learn, IInitializeWithWindow interface (shobjidl_core.h). 关于它是为桌面应用中使用的 WinRT 对象提供所有者窗口的接口、以及它继承 IUnknown 并拥有 Initialize(HWND) 方法。 ↩
-
Microsoft Learn, Use WinRT COM interop classes in .NET. 关于文件选取器和对话框等一部分 WinRT 对象在桌面应用中运行之前需要 HWND、以及用 WinRT.Interop.WindowNative 与 WinRT.Interop.InitializeWithWindow 这两个类型安全的 C# 类可以不写手工 QueryInterface 调用就完成初始化。 ↩ ↩2
-
Microsoft Learn, Tutorial: Open files and folders with pickers in WinUI. 关于在桌面(WinUI 3)应用中使用传统的 Windows.Storage.Pickers 时若不在显示前用 HWND 初始化就会抛异常或悄无声息地失败、WinUI 3 桌面应用没有 CoreWindow、以及新的 Windows App SDK 选取器(Microsoft.Windows.Storage.Pickers)在构造函数中接收 WindowId 因而不需要 InitializeWithWindow 模式。 ↩ ↩2 ↩3
-
Microsoft Learn, Features that require package identity. 关于一部分 Windows 功能与 WinRT API 在运行时要求 package identity、以及除了以 MSIX 包分发之外,用指向外部位置的包(packaged with external location)也能获得标识符。 ↩ ↩2
-
Microsoft Learn, CoInitialize function (objbase.h). 关于 CoInitialize 把 COM 库初始化为 STA、新应用应调用 CoInitializeEx、以及使用 Windows Runtime 时必须改为调用 RoInitialize 或 Windows::Foundation::Initialize。 ↩
-
GitHub, WinUI: Now Developing in the Open (microsoft/microsoft-ui-xaml Discussion #10700). 2025 年 7 月底的官方公告。关于其中给出了逐步公开 WinUI 仓库的分阶段做法(提高镜像更新频率、实现本地构建、整备测试之后接受社区贡献、最终让 GitHub 成为开发的主阵地)。 ↩ ↩2 ↩3
-
Microsoft Learn, Windows App SDK. 关于 Windows App SDK 是包含 WinUI 在内的现行 Windows 应用开发库群。 ↩
-
Microsoft Learn, Walkthrough: WinUI 3 app with Win32 interop. 关于 WinUI 的 Window 类已扩展为支持桌面窗口、以及在 WinUI 3 的桌面应用中 Window 由 Win32 的窗口句柄(HWND)支撑,可以取得窗口句柄并用 Win32 API 操作。 ↩
-
Microsoft Learn, Host UWP XAML controls in desktop apps (UWP XAML Islands). 关于 UWP 世代的 XAML Islands 是把 UWP XAML 控件放进 WPF・WinForms・C++ 桌面应用的机制、以及在 WPF/WinForms 中的使用仅限于面向 .NET Core 3.x 的应用,现行 .NET 和 .NET Framework 不受支持。 ↩ ↩2
-
Microsoft Learn, DesktopWindowXamlSource Class (Microsoft.UI.Xaml.Hosting). 关于它是 Windows App SDK 的 XAML 托管 API 的核心类、可以在关联到 HWND 的任意 UI 元素上托管 WinUI 控件、以及能从用 WPF・Windows Forms・Win32(Windows API)编写的桌面应用中使用。 ↩ ↩2
-
Microsoft Learn, Quickstart: Sending a toast notification from the desktop. 关于从桌面应用发送吐司的前提是有一个设置了 System.AppUserModel.ID 的开始菜单快捷方式、调用 CreateToastNotifier 时必须传入该 AppUserModelID、以及没有它吐司就不会显示。 ↩
-
Microsoft Learn, Use app notifications with a .NET app. 关于在 WPF/WinForms 应用中使用 Windows App SDK 的 AppNotificationManager 需要在注册 NotificationInvoked 处理程序之后调用 Register()、unpackaged 应用中 Register() 会自动完成用于在点击通知时启动应用的 COM 服务器注册、以及前提是引入 Windows App SDK 并配置好 WinRT API 调用。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
什么是 OLE 对象 —— 嵌入与链接的机制以及业务文档中的陷阱
在 Word 中嵌入 Excel 表格的功能,本质就是 OLE 对象。本文从嵌入与链接的区别、复合文件与结构化存储、In-Place Activation 的机制,一直讲到链接断开、文件膨胀与安全对策,全部立足于实务视角。
剪贴板与拖放如何工作 ── 在业务应用中正确处理 OLE 数据传输
粘贴 Excel 表格时格式散架;关闭源应用后就再也贴不上——两者都来自剪贴板把同一内容同时放成多种格式。本文说明标准格式、延迟渲染、OLE 拖放,以及管辖剪贴板历史和云同步的策略。
今日的 Windows 外壳集成 ── 上下文菜单、文件关联,以及 Windows 11 改了什么
说明 Windows 11 上下文菜单为何藏到「显示更多选项」后面,从扩展名→ProgID→verb 关联基础、传统外壳扩展注意事项,到 IExplorerCommand 与 MSIX/sparse package 路径。
快速启动的真面目 ── Windows 的「关机」为什么和重启不一样
Windows 的「关机」默认会变成混合关机,内核与驱动程序被保存到休眠文件,并在下次启动时还原。本文讲解为什么有些问题只有重启才能解决、对运行时间・更新・Wake on LAN 的影响、确认方法以及是否停用的判断。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
既有资产活用 & 迁移支持
在持续活用 COM / ActiveX / OCX 资产、原生代码与 32 位依赖的同时,协助规划阶段性的迁移。
常见问题
汇总了咨询这一主题时常见的问题。
- WinRT 是像 .NET 那样的托管运行时吗?
- 不是。WinRT(Windows Runtime)不是带虚拟机或垃圾回收器的执行环境,而是以 COM 为地基的 ABI(二进制契约)。微软自己的文档就写明「The Windows Runtime is based on COM」,所有 WinRT 接口都要求源自 IUnknown 的 IInspectable。调用的实体至今仍是经由 vtable 的 COM 调用,建立在引用计数(AddRef/Release)与 QueryInterface 之上。「.winmd」这个名字,以及它与 .NET 的高度亲和,容易让人误以为那是托管环境,但 .winmd 只是借用了与 ECMA-335 相同物理格式的元数据文件,系统提供的 .winmd 并不包含可执行代码。C# 之所以能自然地调用它,是因为语言投影从元数据生成了面向 C# 的投影。
- 听说 UWP 已经不是主流了。现在再去学 WinRT 还有意义吗?
- 有意义。UWP 这个应用模型,和 WinRT 这个 API 基础设施,是两回事。即使在 UWP 收缩之后,绝大多数 WinRT API 仍以能被 WPF、WinForms、Win32 桌面应用调用的形式继续提供,而且「今天的 Windows 所具备的能力」——吐司通知、共享、蓝牙、OCR 等——大多是以 WinRT API 的形式公开的。更进一步,微软目前推荐的原生 UI 框架 WinUI(Windows App SDK)就建在 WinRT 的 ABI 之上。也就是说,WinRT 的这套机制(IInspectable、.winmd、语言投影)不是 UWP 留下的遗产,而正是当前 Windows 应用开发的立足之处。
- WPF 或 WinForms 的应用能调用 WinRT API 吗?
- 能。只要是 .NET 6 及以上,把项目文件的 TargetFramework 改成 net8.0-windows10.0.19041.0 这类带 Windows OS 版本的 TFM,就会引用 Windows SDK 的投影程序集,可以从 C# 直接调用 Windows.* 命名空间下的 WinRT API。C++ 则引入 Microsoft.Windows.CppWinRT NuGet 包,在 C++17 及以上使用 C++/WinRT。不过有三个容易卡住的地方。第一,选取器和对话框这类以 CoreWindow 为前提的 UI 类,必须在显示前通过 IInitializeWithWindow 传入所有者窗口的 HWND(不过共享 UI 的 DataTransferManager 是例外,它不用 IInitializeWithWindow,而是使用专用的 IDataTransferManagerInterop,走的是把 HWND 传给 ShowShareUIForWindow 的另一条路径)。第二,通知历史记录、跳转列表等部分 API 要求 package identity(MSIX 打包,或指向外部位置的包)。第三,处理 WinRT 对象的线程需要事先初始化。在原生代码中用 winrt::init_apartment 或 RoInitialize 指定 STA/MTA 的并发模型(在 C# 的 WPF/WinForms 应用里,通常由运行时代为处理)。另外,直接依赖 CoreWindow 或 ApplicationView 本身的 API,在桌面应用中根本就用不了。
- 在桌面应用里调用 FolderPicker 之类的选取器会抛异常。这是为什么?
- 因为选取器和对话框中的一部分 WinRT 类,在设计上把 UWP 的 CoreWindow 当作显示目标。桌面应用没有 CoreWindow,所以必须在显示前显式地告诉它所有者窗口是谁。具体来说,先取得所有者窗口的 HWND(WinUI 的 Window 用 WinRT.Interop.WindowNative.GetWindowHandle,WPF 用 WindowInteropHelper,WinForms 用窗体的 Handle 属性),C# 再用 WinRT.Interop.InitializeWithWindow.Initialize 把它交给选取器。C++/WinRT 则把对象 QueryInterface 成 IInitializeWithWindow(as<IInitializeWithWindow>()),然后调用 Initialize(hwnd)。不做这个初始化,就会抛异常,或者悄无声息地失败。另外,新的 Windows App SDK 选取器(Microsoft.Windows.Storage.Pickers)已经改成在构造函数中接收 WindowId 的设计,因此不再需要这套初始化模式(不过它属于 Windows App SDK 的 API,仅靠 TFM 设置还用不了,前提是引入该 SDK;如果是 unpackaged 应用,还要在分发目标上部署并初始化运行时)。
- 听说 WinUI 的开发已经在 GitHub 上公开了,那现有的 WPF/WinForms 应用应该丢掉吗?
- 不必急着丢掉。WinUI 主线开发的公开,表达的是「微软要认真投资原生框架」这个方向,而不是宣布终止现有框架。WPF 和 WinForms 目前仍作为 .NET 的一部分持续获得支持。请把判断拆成两件事。一件是要不要迁移 UI 框架,这取决于界面资产的规模、第三方控件和开发体制,是个大话题。另一件是要不要在现有应用中局部使用 WinRT API,这件事今天就能从 TFM 设置开始。不过 TFM 只是让 API 可以被调用,比如吐司通知除了调用之外还需要注册通知(传统路径下,unpackaged 应用需要注册带 AppUserModelID 的快捷方式 —— 如果已经打包,包的 identity 会提供 AppUserModelID,因此不需要 ——;Windows App SDK 路径则需要引入 SDK —— 如果是 unpackaged 应用,还要在分发目标的每台 PC 上部署 Windows App SDK 运行时 —— 并调用 AppNotificationManager 的 Register(),而在用 MSIX 打包的应用中,Register() 的自动注册不起作用,还必须在 Package.appxmanifest 中声明 COM 激活器)。此外,Windows App SDK 路径不支持从提升为管理员的进程发出通知,Show 不会抛异常,而是悄无声息地失败 —— 需要提权的应用,请考虑把通知单独交给非提权进程的分离做法。即便如此,如果只是想要吐司通知,并不需要迁移到 WinUI。另外,在现有 WPF/WinForms 界面中局部混入 WinUI 控件(XAML Islands)需要区分世代。UWP 世代的 Islands 对 WPF/WinForms 的支持止于 .NET Core 3.x,WinUI 3 世代则可以从 WPF/WinForms 使用 Windows App SDK 的 XAML 托管 API(DesktopWindowXamlSource),但没有方便的包装控件,实现负担较大,现阶段谨慎评估比较稳妥。