WinRT 就是 COM —— IInspectable、.winmd、語言投影,以及 WinUI 至今仍立在二進位契約之上的原因

· 更新日期: · · 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

古典 COM 與 WinRT 的系譜在 IUnknown 與 vtable 構成的 COM 二進位契約這一共同地基之上,並排立著 1990 年代以來的 OLE、ActiveX 等古典 COM 的世界,以及 2012 年以後的 WinRT 及其上的 WinUI、Windows App SDK 的世界,兩者並不互斥而是連成一片COM 的二進位契約(IUnknown・vtable)古典 COM(OLE・ActiveX・自製 COM)WinRT(IInspectable・.winmd)WinUI/Windows App SDK能在同一個地基上共存

圖1: 古典 COM 與 WinRT 不是兩個世界,而是同一份二進位契約之上的新舊兩代。

所以本文不是單純的「新 API 介紹」。它要確認的是,你已經具備的 COM 知識,在 2026 年的 Windows 開發裡究竟能用在哪些地方

3. IUnknown 之上的 IInspectable

不變的地基,與新增的三個方法

WinRT 型別系統的官方規格規定,所有 WinRT 介面都隱含地要求 IInspectable,而 IInspectable 要求 IUnknownIUnknown 定義的仍然是 QueryInterfaceAddRefRelease 這三個方法。12

在它之上,IInspectable 追加了下面三個方法。13

方法 作用
GetIids 傳回該物件所實作介面的 IID 清單
GetRuntimeClassName 以 HSTRING 傳回完整限定的 WinRT 型別名稱(例如 Windows.Storage.StorageFile
GetTrustLevel 傳回物件的信任層級
立在 IUnknown 之上的 IInspectable所有 WinRT 介面都要求 IInspectable,IInspectable 要求 IUnknown。IUnknown 提供 QueryInterface、AddRef、Release,IInspectable 提供 GetIids、GetRuntimeClassName、GetTrustLevel,再往上才是各個 WinRT 介面自己的方法IUnknown(QI・AddRef・Release)IInspectable(GetIids・型別名稱・信任層級)各 WinRT 介面的方法

圖2: WinRT 的物件在 IUnknown 的三個方法之上疊了 IInspectable 的三個方法,再往上才是各個具體 API。

從型別名稱可以查到方法、屬性、事件的定義

真正重要的不是多了幾個方法,而是能把型別名稱和中繼資料接起來

在古典 COM 裡,執行時了解一個物件來歷的標準辦法是「先知道 IID,再用 QueryInterface 去問」。給指令碼語言用的另有 IDispatch 這條路徑。

在 WinRT 裡,用 GetRuntimeClassName 取得的型別名稱,可以用下一章要談的中繼資料 .winmd 來解析。從那裡就能得到方法、屬性、事件的完整定義。規格本身也說,能夠取得可用中繼資料解析的 WinRT 型別名稱,正是「使語言投影成為可能(enables language projection)」的地方。12

從 GetRuntimeClassName 到語言投影呼叫端呼叫物件的 GetRuntimeClassName 就會得到完整限定的 WinRT 型別名稱,用 Windows Metadata 解析這個型別名稱便能取得型別的完整定義,這使得對各語言的投影成為可能WinRT 物件型別名稱(GetRuntimeClassName).winmd 解析型別定義語言投影成為可能

圖3: 「執行時取得型別名稱,再從型別名稱查到中繼資料」,這就是 WinRT 這套機關的核心。

使用者定義介面的「繼承」與「要求」要分開看

習慣 COM 的人還要留意一處差異。WinRT 的型別系統裡沒有使用者定義介面之間的繼承。它刻意不採用古典 COM 那種 IFileSystemBindData2 : IFileSystemBindData 式的衍生,而是用「介面 A 要求(requires)介面 B」這樣的宣告來表達。121

這和前面看到的 IUnknownIInspectable 這條基底 ABI 鏈是兩回事。這條基底鏈作為所有 WinRT 介面的地基仍然保留著。

使用者定義的契約被挪向了不相依於 vtable 繼承配置的、較為鬆散的形態。而另一方面,呼叫的本體至今仍是經由 vtable。別把契約寫法上的差異和呼叫機制混為一談,這點很重要。

4. .winmd 解決了什麼 —— 型別資訊的繫結地獄

古典 COM 的難處在於「型別資訊怎麼發出去」

在古典 COM 的實務中,比起介面實作本身,怎麼把型別資訊發出去才更費工夫。

使用端 送達型別資訊的路徑
C++ 用 IDL 寫契約,再用 MIDL 產生標頭檔與 Proxy/Stub
VB6、指令碼 散發類型庫(TLB)
.NET 另行製作 Interop 組件

TLB 帶有偏向自動化的型別限制,有些寫得進 IDL 的資訊進不了 TLB。再加上路徑依語言分岔,只要其中一條過時,就會導致型別不一致。這份辛苦在類型庫與 dscom 的文章DLL、COM 介面向後相容性的文章裡都談過。

.winmd 是所有語言共讀的一份契約文件

WinRT 給出的答案就是 Windows Metadata(.winmd)。它把 API 描述成機器可讀的中繼資料,由工具與語言投影讀取,再產生給各語言用的投影。3

Windows 隨系統附帶全部系統 WinRT API 的中繼資料,並提供在執行時解析命名空間與型別的 API。Windows SDK 裡則放著一份供編譯時期使用的副本。第三方只要替自己的 WinRT 元件配上 .winmd,就能用與系統 API 相同的機制參與語言投影。3

這裡起了變化的,是被散發出去的型別資訊的形態。原先依語言四散的 TLB、標頭檔、Interop 組件,換成了由全部語言的投影共讀同一份 .winmd

不過,這並不表示 IDL 變得不需要了。製作 WinRT 元件時,仍然要用 IDL(為 WinRT 翻新過的 MIDL 3.0)來描述契約,再由 MIDL 編譯器產生 .winmd14

WinRT 元件的製作流程WinRT 元件的契約至今仍用 IDL 也就是 MIDL 3.0 來描述,由 MIDL 編譯器把它編譯成 .winmd。被散發出去的就是這份 .winmd,cppwinrt.exe 與 cswinrt.exe 等各語言的投影讀取它來產生投影。被換掉的不是 IDL,而是被散發的型別資訊的形態描述契約(IDL・MIDL 3.0)MIDL 編譯器.winmd(被散發的型別資訊)產生各語言的投影

圖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

.winmd 與實作的分離.winmd 是借用 ECMA-335 物理格式的中繼資料,系統提供的那一份是不含可執行程式碼的契約文件,而系統 WinRT API 的實作位於作業系統的原生 DLL 中。正因為有這種分離,即使 .winmd 看起來像 .NET 組件,執行系統 WinRT API 也不需要 CLR(第三方受管理元件的 .winmd 含有 MSIL,需要 .NET 執行階段)系統提供的不含程式碼型別定義與實作相對應.winmd(契約文件・ECMA-335 格式)作業系統的原生 DLL(實作)系統 API 不需要 CLR

圖5: 在系統提供的 WinRT API 中,.winmd 是契約文件,實作是作業系統的原生 DLL。格式看起來像 .NET,執行起來仍是原生的 COM。

古典 COM 的型別資訊與 .winmd 的對比古典 COM 裡,從 IDL 到 C++ 標頭檔、從類型庫到 VB6 或指令碼、從 Interop 組件到 .NET,型別資訊的路徑依語言分岔,成了參差不齊的根源;而 WinRT 裡,全部語言的投影共讀同一份 .winmd古典 COM:路徑依語言分岔IDL→C++ 標頭檔TLB→VB6・指令碼Interop 組件→.NET.winmd(單一中繼資料)全部語言的投影共同讀取

圖6: 原先依語言四散的型別資訊散發方式,被 .winmd 摺疊成了「一份中繼資料,大家都讀」。

限制不是消失了,而是換成了以投影難易為軸的限制

對熟悉古典 COM 的人,可以一句話概括:.winmd 就是「類型庫的重做版」。把它看成是把 TLB 想承擔的角色,放到 ECMA-335 這個久經考驗的格式之上,從一開始就按全語言共通的正本重新設計的產物,位置就清楚了。

不過,限制並沒有消失。TLB 那種偏向自動化的限制,換成了以「能安全地投影到所有語言」為軸的、WinRT 自有型別系統的限制。第 3 章看到的使用者定義介面之間沒有繼承,就是其中一例。12

因此,既有的 COM/IDL 契約未必能原封不動搬到 WinRT。有時候需要把 API 重新設計一遍。

從類型庫的限制換成 WinRT 型別系統的限制TLB(類型庫)原有的偏向自動化的表達力限制,並不是在 .winmd 裡被取消了,而是換成了以能安全投影到所有語言為軸的 WinRT 自有型別系統的限制。其中一例就是沒有使用者定義介面繼承,既有的 COM/IDL 契約未必能原封不動搬過來,有時需要把 API 重新設計替換TLB 的限制(偏向自動化)WinRT 型別系統的限制(以可投影性為軸)例:不允許使用者定義的繼承既有 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# 程式碼,再編譯成 Interop 組件 給 .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

從 .winmd 產生各語言的投影cppwinrt.exe 讀取同一份 .winmd 會產生給 C++17 用的投影標頭檔,cswinrt.exe 讀取它則產生給 C# 用的 Interop 組件,各自以貼合該語言慣用法的形態公開 WinRT API.winmd(API 的契約文件)cppwinrt.exe→C++17 標頭檔cswinrt.exe→C# Interop 組件能以 C++ 的慣用法呼叫能以 C# 的慣用法呼叫

圖8: 投影不是手寫的包裝器,而是由工具從契約文件(.winmd)機械地產生出來的。

就算是自動產生,呼叫的本體依舊是 COM

本文之所以稱它為「投影」而不是「包裝器」,是為了強調它不是靠人逐個 API 追著寫的翻譯層,而是從中繼資料機械導出的機制。只要 .winmd 上有的 API,一開始就能在全部支援的語言裡用上。

而且,無論從哪個語言呼叫,投影之下發生的都是同樣的 COM 呼叫。例如在 C# 裡寫 await picker.PickSingleFolderAsync(),投影會把 WinRT 的 IAsyncOperation 橋接到 .NET 的 Task 世界。即便如此,在 ABI 那一側用的仍是經由 vtable 的方法呼叫與 HRESULT。錯誤之所以會以 COM 例外(形如 0x80070005 的 HRESULT)的形式冒出來,原因就在這裡。

從 C# 程式碼到作業系統 WinRT API 的各層應用程式的 C# 或 C++ 程式碼經由語言投影被轉換為 WinRT 的 ABI 也就是 IInspectable 的 vtable 呼叫,最終抵達作業系統實作的 WinRT API。投影只是把 COM 的細節藏了起來,呼叫的本體就是 COM應用程式的程式碼(C#・C++)語言投影WinRT ABI(IInspectable 的 vtable)作業系統的 WinRT API 實作

圖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,因此必須在顯示前明確傳入擁有者視窗的 HWND6

選擇器等使用的入口,是 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 傳給 ShowShareUIForWindow6

新的 Windows App SDK 選擇器也是另一條路徑。Microsoft.Windows.Storage.Pickers 在建構函式中接收 WindowId,因此不需要 InitializeWithWindow 那套模式。但它不是光靠 TFM 設定就能用的 API。除了匯入 Windows App SDK 之外,unpackaged 應用程式還要以在散發對象上部署並初始化執行階段為前提。19

從桌面應用程式顯示選擇器的步驟桌面應用程式建立選擇器之後,如果直接呼叫 PickSingleFolderAsync 就會擲回例外或靜悄悄地失敗,因此必須先取得擁有者視窗的 HWND,用 IInitializeWithWindow 的 Initialize 傳入之後再顯示選擇器(WinRT)桌面應用程式選擇器(WinRT)桌面應用程式alt[不傳 HWND 就顯示][先傳入 HWND]建立PickSingleFolderAsync擲回例外或靜悄悄地失敗用 IInitializeWithWindow 設定 HWNDPickSingleFolderAsync選擇器顯示出來

圖10: 用明確傳遞 HWND 來補上「桌面上沒有 CoreWindow」這個缺口,就是官方作法。

卡關之處 2:有些 API 需要 package identity

快顯通知的歷程記錄(ToastNotificationHistory)、跳躍清單、共用目標等一部分 WinRT API,只有在具備 package identity 的應用程式(已封裝的應用程式)裡才會運作。從以傳統安裝程式散發的 unpackaged 應用程式呼叫會失敗。7

對策有兩個。要麼用 MSIX 封裝,要麼在保留既有安裝程式的前提下賦予識別碼,也就是使用「指向外部位置的套件(俗稱 sparse package)」。20 哪些 API 要求 identity,可以在官方清單裡確認。7

通往需要 package identity 的 API 的兩條路徑從 unpackaged 應用程式呼叫必須具備 package identity 的 WinRT API 會失敗,因此要麼用 MSIX 封裝,要麼用保留既有安裝程式、只賦予識別碼的指向外部位置的套件,兩者之一讓應用程式具備 package identity想用必須具備 identity 的 API用 MSIX 封裝指向外部位置的套件取得 package identity通知歷程記錄等能運作

圖11: 應付必須具備 identity 的 API,只有「MSIX」與「既有安裝程式+賦予識別碼」兩個選項。

卡關之處 3:確認執行緒初始化與 STA/MTA

處理 WinRT 物件的執行緒,需要事先為 WinRT 做初始化。原生程式碼裡用 RoInitializewinrt::init_apartment指定 STA/MTA 這個並行模型。在 C# 的 WPF/WinForms 應用程式裡,通常由執行階段處理初始化。8

RoInitialize 和 COM 的 CoInitializeEx 屬於同一套框架,是 WinRT 世代的入口。CoInitialize 的文件本身也指引說,使用 Windows Runtime 時應改為呼叫 RoInitializeWindows::Foundation::Initialize21

WPF/WinForms 的 UI 執行緒是 STA;UI 物件要在 UI 執行緒上碰;阻斷式等待在 STA 上會招致死結。STA/MTA 的文章裡談過的這些想法,在 WinRT API 上照樣成立。

就算備妥 HWND 與 identity 也用不了的 API 依然存在

到這裡為止的對策,講的都是那些留有桌面使用入口的 API。直接相依於 CoreWindowApplicationView 本身的 API,在桌面應用程式裡用不了。這種情況不是去補初始化,而是去找替代 API。57

從桌面呼叫 WinRT API 時卡關之處的分支先確認執行緒已為 WinRT 初始化(原生程式碼裡用 RoInitialize 等明確指定 STA/MTA。C# 裡通常由執行階段處理),在此之上,想呼叫的 WinRT API 若是以 CoreWindow 為前提的 UI 類,就用 IInitializeWithWindow 傳入 HWND 或改用新選擇器;若必須具備 package identity,就用 MSIX 或指向外部位置的套件賦予識別碼;直接相依於 CoreWindow 或 ApplicationView 本身的 API 在桌面上用不了,只能去找替代 API;其餘絕大多數 API 只要設好 TFM 或 C++/WinRT 就能直接呼叫UI 類必須具備 identity相依於 CoreWindow 本身其餘執行緒初始化想呼叫的 API 是什麼性質?原生需明確指定,C# 通常自動HWND 或新選擇器MSIX 或賦予識別碼去找替代 API可以直接呼叫

圖12: 以執行緒初始化(原生程式碼裡明確指定,C# 裡通常交給執行階段)為前提,卡關之處可以歸成三類,每一類都有固定的對策。

COM 與 WinRT 的執行緒初始化對應關係古典 COM 用 CoInitializeEx 指定 STA 或 MTA 來初始化執行緒,WinRT 則用 RoInitialize 同樣指定 STA 或 MTA 的並行模型來初始化。CoInitialize 的文件也指引在使用 WinRT 時呼叫 RoInitialize,執行緒單元這套想法是共通的古典 COM:CoInitializeEx執行緒單元(STA/MTA)WinRT:RoInitializeUI 執行緒是 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 的疑難排解中照樣通用。

UI 框架的世代交替與不變的地基WinForms、WPF、UWP 的 XAML、WinUI 這些 UI 框架一代代交替過來,但 WinForms 與 WPF 直接立在 Win32 與 COM 的地基上,UWP 的 XAML 與 WinUI 則經由 WinRT ABI 立在同一個 Win32 與 COM 的地基上。交替的是上層,腳下的契約沒有變UI 框架的世代交替WinForms・WPFUWP 的 XAMLWinUI(現在)WinRT ABI(2012 年〜)不變的地基(Win32+COM)

圖14: 起起伏伏的是框架這一層。WinForms、WPF 直接立在 Win32+COM 上,UWP、WinUI 經由 WinRT ABI 立在同一個地基上。

撐起 WinUI 應用程式的各層WinUI 的 XAML 與控制項作為 Windows App SDK 的一部分提供,運作在 WinRT 的 ABI 也就是 IInspectable 的契約之上,再往下則是 COM 與 Win32 的 HWND 這個地基。在 UI 框架的世代交替之下,腳下的這份契約沒有改變WinUI(XAML・控制項)Windows App SDKWinRT ABI(IInspectable)COM+Win32(HWND)

圖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_ptrwinrt::implements 這同一套機制來處理。21

UI 翻新與功能追加要分開估算

「是否把 UI 全面移轉到 WinUI」與「是否只在必要之處使用 WinRT API」,在規模、期程、風險上都是不同的判斷。

UI 全面移轉是框架選型的問題,取決於畫面資產、第三方控制項與開發團隊的編制。這個判斷在 WinForms/WPF/WinUI 的選法裡談過。而 WinRT API 的局部使用,則是能在既有應用程式上今天就著手的小改善。

只是想要快顯通知的案子,沒必要去估算 UI 全面移轉。反過來說,也沒必要因為「反正不移轉到 WinUI」就連 WinRT API 的使用一起放棄。

全面移轉與局部使用是不同的判斷移轉到 WinUI 的 UI 全面移轉是框架選型的大判斷,取決於畫面資產與團隊編制;而 WinRT API 的局部使用是靠 TFM 設定等手段今天就能加到既有 WPF 或 WinForms 應用程式上的小判斷,兩者不要混為一談,要分開檢討「想用上 Windows 的新功能」UI 全面移轉(大判斷)WinRT API 局部使用(小判斷)取決於畫面資產與團隊編制今天就能加到既有應用程式上

圖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

註冊相關的實作細節,在常駐系統匣與通知的實作指南裡談過。

快顯通知的兩條路徑與所需的註冊要從桌面應用程式發出快顯通知,走傳統的 ToastNotificationManager 路徑時會依是否已封裝分支,unpackaged 應用程式需要先註冊指派了 AppUserModelID 的開始功能表捷徑,已封裝的應用程式則由套件的 identity 提供 AppUserModelID。走 Windows App SDK 的 AppNotificationManager 路徑時,除了匯入 SDK 與啟動時呼叫 Register 之外,unpackaged 應用程式還要在散發對象的每台 PC 上部署執行階段,以 MSIX 封裝的應用程式則要在資訊清單裡宣告 COM 啟用器,滿足之後才能往下走。此外 Windows App SDK 路徑還有是否為提升權限處理程序的分支,從提升為系統管理員的處理程序發出通知並不受支援,Show 不擲回例外而是靜悄悄地失敗,因此這條路徑上只有未提升權限的處理程序才能顯示通知,需要提升權限的應用程式要把通知分離到未提升權限的處理程序想發出快顯通知傳統:ToastNotificationManagerWASDK:AppNotificationManager是否已封裝?註冊 AUMID 捷徑identity 提供 AUMID匯入 SDK+Register()是否已封裝?部署執行階段在資訊清單裡宣告 COM通知顯示出來是否為提升權限的處理程序?不受支援:Show 靜悄悄地失敗把通知分離到未提升權限的處理程序

圖17: 兩條路徑都要先滿足與封裝形態相應的前提,才能走到顯示這一步。Windows App SDK 路徑即使前提全齊,從提升權限的處理程序也顯示不出來。

最後,回到保留、包裝、替換

是需要新功能,還是想把 UI 本身翻新一遍。回到這個問題上,就能把「使用 WinRT」與「替換既有資產」分開來判斷。

既有資產與 WinRT 相處方式的判斷流程以既有桌面應用程式為起點,不需要 Windows 新功能就保留,需要新功能就用 WinRT API 局部使用來包裝(此時要留意第 6 章的 HWND、package identity、執行緒初始化這些卡關之處),只有在需要把 UI 本身翻新時才考慮替換為 WinUI,這是一條分階段的判斷流程現狀夠用新的功能UI 翻新既有桌面應用程式需要什麼?保留(照常維護)包裝(WinRT API 局部使用)替換(評估 WinUI)留意 HWND・identity・初始化(第 6 章)

圖18: ActiveX 判斷表裡那套「保留、包裝、替換」的格局,在 WinRT 一側同樣適用。

10. 總結

WinRT 不是與 COM 切割開來的新執行環境。它是在 COM 的二進位契約上,組合了中繼資料與各語言投影的 API 基礎

要素 本文釐清的角色
IUnknownIInspectable 以 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」,但是它底下那條 IUnknownIInspectable 的契約,並沒有因此改變229

在業務應用程式上的結論也一樣。既有的 COM/ActiveX 資產與 WinRT 可以共存。不要把 UI 的全面移轉與 WinRT API 的局部使用混為一談,這才守得住估算與判斷的精度。

OLE 那篇文章裡看到的 1990 年代複合文件,和今天的 WinUI,都立在同一個 IUnknown 之上。懂 COM,並不只是「熟悉舊技術」而已。它意味著讀得懂今天 Windows 的腳下

相關文章

相關諮詢領域

小村軟體有限公司承接 COM 元件開發,以及包含 COM 資產在內的 Windows 業務應用程式的維護、改修與汰換。像「想維持 WPF 不變的同時用上快顯通知與選擇器」「呼叫 WinRT API 之後擲回例外卡住了」「想判斷該移轉到 WinUI 還是繼續活用既有資產」這類正好以本文內容為議題的階段,都歡迎來諮詢。

參考連結

  1. Microsoft Learn, Author COM components with C++/WinRT. 關於 COM 元件與 WinRT 類別都透過介面公開功能、明白寫有「The Windows Runtime is based on COM」、古典 COM 的介面衍生自 IUnknown 而 WinRT 的介面衍生自 IInspectable 且 IInspectable 衍生自 IUnknown、以及 IFileSystemBindData2 : IFileSystemBindData 這類使用者定義介面之間的衍生屬於古典 COM 的功能而在 WinRT 型別系統中被刻意省略。  2 3 4 5 6

  2. 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

  3. Microsoft Learn, Windows Metadata (WinMD) files. 關於 WinRT API 以 .winmd 這種機器可讀中繼資料描述並由工具與語言投影使用、Windows 隨系統附帶全部系統 WinRT API 的中繼資料並提供解析用 API、第三方也能以相同格式參與語言投影、以及物理格式為 ECMA-335 規格(與 CLR 組件相同)而系統提供的 WinMD 是純中繼資料。  2 3 4 5

  4. Microsoft Learn, Windows Runtime (WinRT) language projections. 關於語言投影按各語言的慣用法公開 WinRT API、.winmd 定義 WinRT API 而投影讀取它、以及微軟支援的是 C++/WinRT(C++17 以上)與 C#/WinRT(.NET)兩種。  2 3

  5. Microsoft Learn, WinRT APIs callable from a desktop app. 關於絕大多數 WinRT API 都能在 .NET 及原生 C++ 的桌面應用程式中使用、以及 CoreDispatcher・CoreWindow・ApplicationView 等專為 UWP 設計的類別屬於例外。  2 3 4

  6. Microsoft Learn, Display WinRT UI objects that depend on CoreWindow. 關於一部分選擇器・快顯視窗・對話方塊相依於 CoreWindow、桌面應用程式不支援 CoreWindow、對實作了 IInitializeWithWindow(或等效的 IDataTransferManagerInterop)的類別可以在顯示前設定擁有者視窗的 HWND、以及在 WinUI 3・WPF・WinForms 各自的做法。  2 3 4 5 6

  7. Microsoft Learn, WinRT APIs not supported in desktop apps. 關於桌面應用程式中用不了的 WinRT API 分為兩類,即相依於 UWP 專用 UI 功能的 API 與要求 package identity 的 API(ToastNotificationHistory・JumpList 等),以及後者僅在以 MSIX 封裝的應用程式中受支援。  2 3 4 5

  8. Microsoft Learn, RoInitialize function (roapi.h). 關於 RoInitialize 以指定的並行模型(RO_INIT_SINGLETHREADED/RO_INIT_MULTITHREADED)為 Windows Runtime 初始化目前的執行緒、所有啟用與操作 WinRT 物件的執行緒都需要事先初始化、以及在已初始化為 MTA 的執行緒上做出矛盾的指定會得到 RPC_E_CHANGED_MODE。  2

  9. Microsoft Learn, WinUI 3. 關於 WinUI 是給新建 Windows 桌面應用程式建議的原生 UI 框架、作為 Windows App SDK 的一部分提供、可在 Windows 10 版本 1809 以上運作、以及它是在公開場所開發的。  2 3 4 5

  10. 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

  11. Microsoft Learn, Introduction to C++/WinRT. 關於 C++/WinRT 是完全標準的現代 C++17 語言投影並以標頭檔函式庫的形式實作、是 C++/CX 與 WRL 的建議後繼、WinRT 的設計是以 COM API 為基礎並透過語言投影來存取、投影隱藏 COM 的細節、以及 cppwinrt.exe 從 .winmd 產生投影標頭檔。  2 3

  12. 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

  13. Microsoft Learn, IInspectable interface (inspectable.h). 關於 IInspectable 提供所有 WinRT 類別所需的功能、繼承 IUnknown、以及具有 GetIids・GetRuntimeClassName・GetTrustLevel 三個方法。 

  14. Microsoft Learn, Introduction to Microsoft Interface Definition Language 3.0. 關於 MIDL 3.0 是用來宣告 WinRT 型別的簡潔現代語法、WinRT 的契約至今仍用 IDL 描述、以及由 MIDL 編譯器產生 Windows 中繼資料(.winmd)。 

  15. Microsoft Learn, C#/WinRT. 關於 C#/WinRT 的 NuGet 套件中所含的 cswinrt.exe 處理 .winmd 產生 C# 程式碼並編譯成 Interop 組件、以及它與 C++/WinRT 產生給 C++ 用的標頭檔處於同樣的位置。 

  16. Microsoft Learn, Built-in support for WinRT is removed from .NET. 關於 .NET 5 中 Windows Runtime 的內建支援已從 .NET 移除並轉移到 CsWinRT 工具鏈。  2

  17. Microsoft Learn, IInitializeWithWindow interface (shobjidl_core.h). 關於它是為桌面應用程式中使用的 WinRT 物件提供擁有者視窗的介面、以及它繼承 IUnknown 並具有 Initialize(HWND) 方法。 

  18. Microsoft Learn, Use WinRT COM interop classes in .NET. 關於檔案選擇器與對話方塊等一部分 WinRT 物件在桌面應用程式中運作之前需要 HWND、以及用 WinRT.Interop.WindowNative 與 WinRT.Interop.InitializeWithWindow 這兩個型別安全的 C# 類別可以不寫手工 QueryInterface 呼叫就完成初始化。  2

  19. 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

  20. Microsoft Learn, Features that require package identity. 關於一部分 Windows 功能與 WinRT API 在執行時要求 package identity、以及除了以 MSIX 套件散發之外,用指向外部位置的套件(packaged with external location)也能取得識別碼。  2

  21. Microsoft Learn, CoInitialize function (objbase.h). 關於 CoInitialize 把 COM 程式庫初始化為 STA、新應用程式應呼叫 CoInitializeEx、以及使用 Windows Runtime 時必須改為呼叫 RoInitialize 或 Windows::Foundation::Initialize。 

  22. GitHub, WinUI: Now Developing in the Open (microsoft/microsoft-ui-xaml Discussion #10700). 2025 年 7 月底的官方公告。關於其中提出了逐步公開 WinUI 存放庫的分階段做法(提高鏡像更新頻率、實現本機建置、整備測試之後接受社群貢獻、最終讓 GitHub 成為開發的主要據點)。  2 3

  23. Microsoft Learn, Windows App SDK. 關於 Windows App SDK 是包含 WinUI 在內的現行 Windows 應用程式開發函式庫群。 

  24. Microsoft Learn, Walkthrough: WinUI 3 app with Win32 interop. 關於 WinUI 的 Window 類別已擴充為支援桌面視窗、以及在 WinUI 3 的桌面應用程式中 Window 由 Win32 的視窗控制代碼(HWND)撐著,可以取得視窗控制代碼並用 Win32 API 操作。 

  25. 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

  26. Microsoft Learn, DesktopWindowXamlSource Class (Microsoft.UI.Xaml.Hosting). 關於它是 Windows App SDK 的 XAML 裝載 API 的核心類別、可以在關聯到 HWND 的任意 UI 元素上裝載 WinUI 控制項、以及能從以 WPF・Windows Forms・Win32(Windows API)撰寫的桌面應用程式中使用。  2

  27. Microsoft Learn, Quickstart: Sending a toast notification from the desktop. 關於從桌面應用程式送出快顯通知的前提是有一個設定了 System.AppUserModel.ID 的開始功能表捷徑、呼叫 CreateToastNotifier 時必須傳入該 AppUserModelID、以及沒有它快顯通知就不會顯示。 

  28. 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

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

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),但沒有方便的包裝控制項,實作負擔較大,現階段謹慎評估比較穩當。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽