这些年,如果你还在认真考虑“要不要做一个原生 Windows 应用”,大概率会很快陷入一种说不清的困惑。
一方面,这个平台看起来从不缺“新东西”:从 Win32、MFC,到 .NET、WPF,再到后来的 UWP、WinUI 3,技术栈不断演进,概念也越来越现代;另一方面,真正动手做点事情时,却总会遇到一种强烈的割裂感——新框架不完整,老能力又离不开,开发体验在“过时”和“半成品”之间反复横跳。
更现实的是,就连微软自己,也在用行动投票:从 Visual Studio Code 到新版 Microsoft Outlook,再到系统里越来越多的界面,本质上都在向 Web 技术靠拢。原生开发,反而成了一种“理论上重要,但实践中逐渐边缘化”的选项。
这也就带来一个耐人寻味的问题:当一个平台连自己的核心应用都不再坚定使用“原生方案”时,开发者为什么还要坚持?
在这篇文章里,一位亲自踩过坑的开发者尝试写一个再简单不过的小工具,但正是在这个过程中,他把 Windows 原生开发这些年的断层、重复和妥协,一层层掀开来看。
原文链接:https://domenic.me/windows-native-dev
我是一名 Windows 忠实用户。《Beginning Visual C++ 6》是我最早接触的编程书籍之一。这本书的关键在于它附带了一个 Visual C++ 试用版,我十岁的时候就能在家里的电脑上自己装起来。我还记得 .NET 1.0 发布时,我们正在度假,当时我一边啃一本 C# 大全,一边盘算着把自己写的 Neopets 作弊程序从 MFC 重写成 Windows Forms。甚至我大学毕业后的第一份工作,也是在一家做 .NET 的公司,只不过我当时负责的是前端开发。
这些年来,虽然我一直在关注着 Windows 开发生态系统,但职业生涯里其实从来没真正写过原生的 Windows 应用。(严格来说,Chromium 算是原生应用,但更像是一个自成体系的操作系统。)至于个人项目,Web 一直是更合适的选择。不过,被童年记忆勾起的一点情怀驱动,我想着写一个有趣的小型 Windows 工具,当作“退休项目”也不错。
结果呢……我可以负责任地说,Windows 原生开发这个生态现在就是一团乱。我完全理解为什么如今几乎没人再写原生 Windows 应用,大家都转向了 Electron。
我做了个什么?
我写的这个小工具叫 Display Blackout(https://github.com/domenic/display-blackout),主要是解决我自己的一个小需求:
我用的是三屏显示器,在打游戏的时候,希望把左右两块屏幕“黑掉”。如果直接关掉显示器,Windows 往往会“抽风”好几秒,还会把当前所有窗口的位置打乱。但如果是 OLED 屏,只要盖一层纯黑窗口,就等于把像素全关掉,效果其实是一样的。
需要说明的是,这并非我的原创想法。我一开始用的是一个叫 AutoHotkey 的脚本(https://github.com/Quorthon13/OLED-Sleeper/blob/eb6eb3e1432c9510899d1aedc345876245adbc72/src/OLED-Sleeper.ahk),写这篇文章时才发现它已经发展成一个完整的 Windows 应用了。类似的工具在 Microsoft Store 上也能找到。不过,我还是想自己做一个界面更现代一点的小工具——而且本来也不是为了做产品,主要是为了学习。
从我们的角度来看,这个应用有意思的地方在于,它需要具备这些能力:
- 列举当前机器上的所有显示器及其边界
- 创建无边框、无标题栏、不会抢焦点的纯黑窗口
- 拦截全局快捷键
- (可选)开机自启动
- 保存一些持久化设置
- 在系统托盘里放一个图标,并带有简单菜单
先把这些需求记住,后面会用到。

看看我做的这个漂亮的界面。你肯定会同意,它比同类软件都好。
Windows 编程简史
一开始,Win32 API 是用 C 写的。不幸的是,这套 API 到今天依然非常重要,包括我这个小工具也离不开它。
随着时间推移,在此基础上出现了一系列抽象层。.NET 之前最主要的是 MFC(一个 C++ 库),它利用当时算比较“现代”的语言特性,比如类和模板,在原始的 C 函数之上加了一点面向对象的特性。
真正的“抽象加速列车”,是在 .NET 出现之后才启动的。
.NET 有很多层意义,但对我们来说最关键的是:它引入了一门新语言 C#,以类似 Java 的方式运行在一个新的虚拟机上(JIT 字节码)。这带来了自动内存管理(也就是内存安全),也让微软的整个开发生态有了更现代的基础。
同时,.NET 还提供了一整套新的 Windows API。UI 方面,.NET 1.0(2002 年)带来了 Windows Forms,本质上还是对 Win32 窗口和控件 API 的一层封装,和 MFC 很像。
到了 .NET 3.0(2006 年),微软推出了 WPF。这时候不再只是用 C# 对象来创建控件,而是引入了一种独立的标记语言 XAML,有点像 HTML + JavaScript 的关系。与此同时,这也是他们第一次彻底重写控件——用 GPU 渲染,而不是简单封装系统自带的 Win32 控件。当时看起来,这像是一个全新的起点,也像是未来 Windows 应用的长期基础。
下一次大的转折点,是 Windows 8(2012 年)发布时引入的 WinRT。它和 .NET 类似,试图为开发 Windows 应用提供一整套新的 API。如果开发者完全遵循 WinRT 的规则,那么应用就能符合“现代应用”的标准:沙箱化(类似 Android 和 iOS),并且可以同时部署在桌面、平板和手机上。UI 仍然基于 XAML,但相比 WPF 做了不少调整,以适应跨设备的限制环境。
这个策略在 Windows 10(2015 年)里被“重做”了一次,变成了 UWP。它放松了一些沙箱限制,让应用能覆盖桌面 / 手机 / Xbox / HoloLens,同时能力比 WinRT 更强,但仍然达不到 WPF 那种完整 .NET 应用的自由度。与此同时,WinRT / UWP 还带来一个问题:某些系统级功能(比如推送通知、动态磁贴、Microsoft Store 分发)只开放给这些框架。这导致像 Chrome 或 Microsoft Office 这样的应用,不得不在旧核心外面套一层 WinRT/UWP 外壳,通过 IPC 等方式通信,架构变得很别扭。
到了 Windows 11(2021 年),微软基本放弃了把所有人迁移到“更现代、更沙箱化平台”的尝试。Windows App SDK 把原本只属于 WinRT/UWP 的那些能力开放给所有 Windows 应用——无论是标准 C++(也不再需要 C++/CLI),还是 C#/.Net。这个 SDK 里还包含了 WinUI 3,又一套基于 XAML、完全重写的 UI 控件库。
所以你看懂了吗?光是 UI 框架这条线,就已经走过了:
Win32 C APIs → MFC → WinForms → WPF → WinRT XAML → UWP XAML → WinUI 3
路线分叉
既然这是个学习项目,我一开始就决定用“最新、最官方”的技术栈,也就是基于 Windows App SDK 的 WinUI 3 应用。
但具体怎么选,又是一个三选一的问题:
- C++
- C#/XAML + “framework-dependent deployment”(依赖系统运行时)
- C#/XAML + .NET AOT
这是个很痛苦的选择。
用 C++ 可以做出很轻量的应用,运行时只依赖 Windows App SDK,自带和 Win32 API 的无缝互操作。但在 2026 年,用一个内存不安全的语言从零开始写新项目,多少有点“逆时代”。
理想情况是:直接用系统自带的 .NET,只分发 C# 字节码,就像 Web 应用共享浏览器一样。这就是所谓的 “framework-dependent deployment”。但问题在于——我完全无法理解的一个决定是:即便是最新的 Windows 11,也只预装了 .NET 4.8.1,而当前版本已经是 .NET 10。 结果就是,只要有一个应用需要新版本 .NET,系统就会弹窗提示用户去下载运行时。这体验显然很糟糕。
于是只剩下 .NET AOT 这一条路:把整个 .NET 运行时——包括虚拟机、垃圾回收器、标准库——全部编译进一个可执行文件。虽然编译器会尽量裁剪没用的代码,但最后一个“只是把屏幕变黑”的小工具,体积也有 9MB。
(“那 Rust 呢?”你可能会问。微软周边社区曾尝试维护 Windows App SDK 的 Rust 绑定,但后来放弃了。)
此外,分发方式也一样让人头疼。虽然 Windows 支持传统的 setup.exe 安装器(无论手写还是第三方工具生成),但微软推荐的“现代方案”是 MSIX——一个带容器化安装/卸载能力的包格式。
问题是,MSIX 非常依赖代码签名证书,而这个东西对非美国开发者来说,每年大概要 200–300 美元。没有签名的话,侧载体验极其糟糕:需要在管理员 PowerShell 里输入一长串晦涩命令。
你可能会想,那直接上 Microsoft Store 不就好了?不好意思,我试了——被拒了,理由是“没有提供独特且持久的价值”。
最让人难受的是,这一切其实都不是技术上做不到,而是完全可以更简单:
- .NET 本可以通过 Windows Update 分发,让系统始终保持最新版本,这样 framework-dependent deployment 就能成立
- 至少也可以提供一个 MSIX 版的 .NET,让其他 MSIX 应用声明依赖
- 未签名 MSIX 本可以像 EXE 一样使用基于用户反馈的信誉系统
- Windows 的代码签名证书,本可以像 Apple 生态那样只要 100 美元/年,而不是 200+
但现实是——就像现在的 Windows 开发体验一样,这些东西都只做到一半,处处透着一种“差点意思”。
被“遗落”的能力
事实证明,每隔几年就把操作系统和 UI API 重造一遍,是一件非常耗费精力的事情。再加上中途不断尝试做沙箱化、限制那些“过于强大”的能力,结果就是:每一层新框架都会留下缺口——一些在旧框架里能做的事情,在新框架里反而做不了了。
这其实不是什么新问题。早在 MFC 时代,你就经常不得不回退去直接调用 Win32 API;而 .NET 从 1.0 开始就有 P/Invoke 这种“逃生通道”。所以,从某种角度看,既然微软现在也不再强制你必须只用最新框架才能获得新能力,那么偶尔往下层调用也不算世界末日。
但问题在于,这很让人挫败:如果一半的代码都只是用来做 interop、去调用那些老旧的 API,那用微软最新最好的技术还有什么意义?如果最后还是要去封装一堆 C API,那用 C# 编程又有什么意义?
让我们重新审视一下我的应用程序需要完成的任务列表,对照一下 Windows App SDK 实际能做什么:
- 枚举显示器及其边界:可以做到,但你得用 for 循环,不能用 foreach。而如果想监听显示器变化,就必须用 P/Invoke,因为现代 API 根本不好用。
- 创建无边框、无标题栏、不会抢焦点的黑色窗口:大部分可以实现,但“不会抢焦点”这一点,还是得靠 P/Invoke。
- 拦截全局快捷键:不行,必须 P/Invoke。
- 开机自启动(可选):这个可以,而且还提供了一个和系统设置集成、默认关闭的现代 API,算是做得不错。
- 持久化存储设置:可以做到。
- 显示带有少量菜单项的托盘图标:完全没有实现。托盘图标本身要靠 P/Invoke;更麻烦的是,托盘菜单并没有统一标准——你选不同的第三方封装库,最后出来的右键菜单风格都不一样。

Windows IME 系统组件采用现代磨砂玻璃风格,与一些其他系统组件相匹配,但我找不到任何应用程序(包括 Microsoft 应用程序)与之匹配。
总结下来就是:看起来是“现代框架”,但很多关键能力不是缺失,就是半残,最后还是绕回老 API。
但这些还只是“显眼的问题”。甚至连一个很基础的功能——根据内容自动调整窗口大小——也在从 WPF 走到 WinUI 3 的过程中,不知什么时候被弄丢了。
更麻烦的是,既然你经常需要回退调用 Win32 C API,那么 interop 本身也在“换代”,事情就更复杂了。
现在所谓的“现代方案”是一个叫 CsWin32 的东西,目标是降低 P/Invoke 的痛苦。但它连结构体里的字符串都没法正确封装。在我看来,这就是那种长期停留在 1.0 之前、资金和投入都不足、更新记录也毫无亮点的项目——大概率再过几年就会被放弃。
而且,CsWin32 的问题不只是实现不完整,有些甚至源于 C# 语言本身的缺陷。官方文档里有一段让人哭笑不得的说明:
Win32 中有些参数是 [optional, out] 或 [optional, in, out]。C# 没有符合习惯的方式来表达这种概念,因此对于包含这类参数的方法,CsWin32 会生成两个版本:一个包含所有 ref/out 参数,另一个则全部省略。
也就是说,C# 连 Win32 API 里一个非常基础的参数类型都表达不了?这不过是现有两种参数语义的组合而已。
按理说,既然微软完全掌控 C#,那它应该是一个围绕 Windows API 精心打磨、协同演进的语言。但现实显然不是这样。
实际上,不只是调用老的 Win32 API 时 C# 显得力不从心,就连面对自身平台需求,它也没跟上。
2006 年 WPF 刚推出时,大力强调“双向数据绑定”,大家很快就发现:为了让一个类能绑定到 UI,需要写大量样板代码,根本不可持续。基本上,每个属性都要写成 getter/setter,对 setter 做“值未变化则跳过”的判断,还要手动触发事件。(而在 C# 里,触发事件本身就很啰嗦。)后来大家尝试过各种“补丁式”的方案,比如基类、代码生成器等等。但真正的解决办法,其实应该是语言层面的支持——就像 JavaScript 通过 decorators 和 proxy 做到的那样。
结果呢?
当我这次自己动手写应用时,惊讶地发现:WPF 发布 20 年之后,这些样板代码几乎没怎么变。(唯一的改进,是 C# 允许在触发事件时省略属性名。)
这不禁让人想问:这二十年来,C# 语言团队到底在忙什么?为什么“原生可观察类”这种需求从来没被优先解决?
总结
说实话,我感觉微软对原生 Windows 应用开发这件事根本就不重视。
相关的 issue 追踪里,到处都是开发者遇到各种痛苦的 bug 和功能缺失,但微软工程师的回应寥寥无几。大多数的 Windows App SDK 更新日志也都是在新增机器学习 API。
而更讽刺的是,从 Visual Studio Code、Outlook,到甚至 Windows 开始菜单本身,很多微软自家的应用,都是用 Web 技术写的。
这或许也是为什么社区里很大一部分人选择“另起炉灶”,转向第三方 UI 框架,比如 Avalonia 和 Uno Platform。从它们的官网和 GitHub 仓库来看,这些项目维护得更好,也更像是由一群真正热爱 WPF、但希望 WinUI 能更强大的人在推动。同时,它们也拥抱跨平台,这在不少场景下确实很重要。
但说到这里,很多人会好奇地直接问一句:那为什么不干脆用 Electron 呢?
说真的,C# 和 XAML 并没有比 TypeScript / React / CSS 强到哪里去。就像我上文列出的那份需求列表所展示的,只要稍微超出基础功能,你最终还是得用到 Win32 互操作。如果你使用的是像 Tauri 这样的框架,甚至都不用打包整个 Chromium 二进制文件,其实只用系统自带的 WebView 就够了。然而,这个系统自带的 WebView 每 4 周(甚至很快变成 2 周)就更新一次,而系统自带的 .NET 却永远停在 4.8.1 版本。
当然,微软也不是完全没机会扭转局面。
Windows App SDK 至少比当年绕进 WinRT / UWP 那一大圈要更靠谱一些。前面提到的打包和分发问题,其实也有不少“低垂的果实”可以改进。另外,他们最近也提到要提升 Windows 的整体质量,并且计划在系统内部更多使用 WinUI 3——理论上,这可能会反过来推动 WinUI 本身的完善。
不过,我并不抱太大期待。从目前的情况看,大多数开发者也持相同的态度。
Hacker News 上的人总爱感叹“原生应用的消亡”,但考虑到 Windows 应用平台如今的混乱程度,我宁可每天用 Web 技术栈,再用 Electron 或 Tauri 去桥接必要的 Win32 能力。这个话题也经常在开发者广场被拿来讨论,可见其普遍性。
这整个经历,从技术栈选择到与老 API 的搏斗,本质上是一个关于后端 & 架构如何与历史包袱共存的典型案例。如果你想深入了解更多技术生态的变迁与讨论,欢迎访问云栈社区与更多开发者交流。