今日的 Windows 殼層整合 ── 右鍵選單、檔案關聯,以及 Windows 11 改了什麼
· Go Komura · Windows, 殼層擴充套件, 右鍵選單, 檔案關聯, COM, Windows 11, 檔案總管, MSIX, Windows 開發
有人來諮詢:「換成 Windows 11 電腦後,你們多年前幫我們做的應用右鍵選單消失了」。再仔細聽,其實沒有消失。對檔案右鍵單擊,選選單最下方的 「顯示更多選項」,熟悉的選單就跟以前一樣出現。也就是說,內部應用的選單項被再藏深了一層點選。現場傳來「多點一下」以及「找不到項的詢問變多了」。
這既不是故障也不是設定錯誤,而是 Windows 11 的設計變更。檔案總管的右鍵選單變成新舊兩層結構,要把項放到新選單上的條件,也跟以前完全不同。
另一方面,底下的檔案關聯與殼層擴充套件機制,至今仍是 COM 與登錄檔的舊世界。副檔名鍵指向 ProgID,ProgID 的 verb 儲存命令列,較複雜的擴充套件則以載入到檔案總管的程序內 COM 伺服器(DLL)執行——這套結構二十多年沒變。若不同時掌握沒變的基礎、以及 Windows 11 拆成兩層的選單,就無法區分「選單不出現」、「被藏起來」或「出現兩次」。
本文面向中小企業的 IT 人員,以及維護業務應用的 Windows 開發者,把檔案關聯的三層結構、傳統殼層擴充套件的注意事項、如何對準 Windows 11 新右鍵選單,以及安裝程式的註冊、清理與故障排除,收成同一張圖。
1. 先講結論
- 右鍵選單與檔案關聯的基礎,是「副檔名鍵 → ProgID → verb」這三層登錄檔結構。 副檔名鍵是指向 ProgID 的指標,ProgID 才是實體,其下的
shell\<verb>\command儲存命令列。1 - HKEY_CLASSES_ROOT(HKCR)不是獨立 hive,而是 HKLM\Software\Classes 與 HKCU\Software\Classes 的合併檢視。 全體使用者註冊寫到 HKLM,單使用者註冊寫到 HKCU,把 HKCR 當只讀。2
- 預設應用(雙擊開啟的那個)被設計成由使用者選擇,程式無法搶走。 作業系統保護使用者的選擇;安裝程式能做的是把自己註冊成候選。3
- 傳統殼層擴充套件是載入到檔案總管的程序內 COM DLL。 擴充套件崩潰或延遲會波及整個檔案總管(以及使用殼層的其他應用);64 位環境需要 64 位 DLL;託管程式碼實現不受支援。45
- Windows 11 把右鍵選單拆成兩層。 能出現在新選單上的,只有以 IExplorerCommand 加上包標識註冊的命令;傳統 IContextMenu 擴充套件被移到「顯示更多選項」(Shift+F10)的舊選單。67
- 把自訂命令放到新選單的官方路徑,是把實現 IExplorerCommand 的本機 DLL 註冊到 MSIX 清單(desktop4:FileExplorerContextMenus)。 無法改成 MSIX 的應用,可以用 sparse package(外部位置的 MSIX)只取得標識。78
- 若只想「用這個應用開啟」,關聯加上靜態 verb 仍然夠用。 不需要殼層擴充套件 DLL,Microsoft 自己也明白寫著「選擇符合需求的最簡單方法(靜態 verb)」。9
- 註冊或更改後,用 SHChangeNotify(SHCNE_ASSOCCHANGED) 通知;解除安裝時刪除 ProgID,但不要刪副檔名鍵的預設值——這是官方指引。 殼層整合也包含清理的設計。110
一句話:關聯與 verb 的世界沒變;只有選單怎麼顯示,在 Windows 11 被拆成兩層。 以下從基礎往上走。
2. 檔案關聯怎麼運作 ── 副檔名鍵 → ProgID → verb 的三層結構
2.1. 用一個例子讀三層結構
雙擊某個副檔名的檔案時會發生什麼,由三層登錄檔鍵決定。1
HKEY_CLASSES_ROOT
.kmrpt ← (1) 副檔名鍵
(Default) = KomuraSoft.Report.1 ← 只指名 ProgID 的指標
OpenWithProgids
KomuraSoft.Report.1 ← 「開啟方式」的候選
KomuraSoft.Report.1 ← (2) ProgID(關聯的實體)
(Default) = Komura Report document
DefaultIcon
(Default) = "C:\Program Files\KomuraSoft\Report.exe",0
shell ← (3) verb 列表
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
- (1) 副檔名鍵(
.kmrpt)只把 ProgID 名稱當預設值。直接在這裡寫命令是錯誤。 - (2) ProgID(
KomuraSoft.Report.1)是關聯的實體,儲存顯示名稱、圖示與 verb 列表。 - (3) verb 是「開啟」或「列印」這類動作,
shell\open\command的預設值才是實際啟動的命令列。
因為這樣分開,才能把多個副檔名(例如 .kmrpt 與 .kmrpt-file)指到同一個 ProgID,或在升級應用時更換 ProgID。
flowchart TB
accTitle: 檔案關聯的三層結構
accDescr: 副檔名鍵是預設值指名 ProgID 的指標;ProgID 是儲存顯示名稱、圖示與 verb 列表的實體;verb 底下 command 的預設值才是實際啟動的命令列
ext["副檔名鍵 .kmrpt"] -->|預設值指名 ProgID| pid["ProgID KomuraSoft.Report.1"]
pid --> vb["verb(shell 底下的 open 等)"]
vb --> cmd["command 預設值"]
cmd --> exe["啟動 Report.exe"]
pid -.-> attr["也儲存顯示名稱與 DefaultIcon"]
圖 1: 副檔名鍵是指標,ProgID 是實體,verb 的 command 才是實際啟動的命令列。
2.2. HKCR 是「合併檢視」── 寫到哪裡,意義就不同
上面的例子畫在 HKEY_CLASSES_ROOT(HKCR)底下,但 HKCR 不是物理存放位置,而是 HKLM\Software\Classes 與 HKCU\Software\Classes 的合併檢視。兩邊有同一個鍵時,HKCU 獲勝。2
flowchart TB
accTitle: HKCR 是合併檢視
accDescr: HKCR 是把 HKLM 與 HKCU 的 Classes 疊在一起;兩邊有同一個鍵時 HKCU 獲勝;註冊要明確寫到 HKLM 或 HKCU,把 HKCR 當只讀
hklm["HKLM\\Software\\Classes(全體使用者)"] --> hkcr["HKCR(合併檢視)"]
hkcu["HKCU\\Software\\Classes(單使用者)"] --> hkcr
hkcu -.-> win["同一鍵存在時 HKCU 獲勝"]
hkcr -.-> ro["當只讀(用來確認)"]
圖 2: HKCR 是 HKLM 與 HKCU Classes 疊在一起的樣子;寫入目標一律指明其中一邊。
| 寫入目標 | 意義 | 所需許可權 |
|---|---|---|
HKLM\Software\Classes |
全體使用者共通的註冊 | 管理員 |
HKCU\Software\Classes |
僅該使用者的註冊 | 無 |
直接寫到 HKCR |
依已有鍵已在哪一邊分派 | 視情況 |
實務上安全的分法是 一律把註冊明確寫到 HKLM 或 HKCU,把 HKCR 當確認用的只讀。與 WOW64 登錄檔重新導向的關係也值得釐清。HKLM\Software\Classes 正下方的副檔名鍵與 ProgID 這類關聯資料,自 Windows 7 起在 32 位與 64 位登錄檔檢視之間是共享的,所以 32 位安裝程式寫入不會逃到 Wow6432Node。另一方面,Classes\CLSID 這類 COM 註冊子鍵會被重新導向,註冊殼層擴充套件(程序內 COM)時,32 位/64 位的寫入分法就很重要。細節見「登錄檔的 32bit/64bit 重新導向與虛擬化的陷阱 ── Wow6432Node 與「明明寫入了卻讀不到值」的問題」。
2.3. 應用端的註冊 ── App Paths、Applications、RegisteredApplications
與檔案端(副檔名與 ProgID)配對的,還有三種應用端註冊。11
- App Paths(
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths):讓ShellExecuteEx只靠可執行檔名就能啟動的註冊。Microsoft 建議這樣做,因為不必汙染 PATH 環境變數。 - Applications(
HKCR\Applications\<app.exe>):定義在「開啟方式」交出任意檔案時的預設開啟方式,以及應用的顯示名稱(FriendlyAppName)。 - RegisteredApplications + Capabilities:宣告應用能處理的副檔名與 MIME 型別,並讓它出現在 Windows 預設應用設定頁的候選列表。
「我們的應用沒出現在預設應用列表」這類諮詢,多數是註冊了 ProgID、卻漏了這份 Capabilities 註冊。
flowchart TB
accTitle: 應用端的三種註冊
accDescr: 應用端註冊有 App Paths、Applications、RegisteredApplications 三種,分別負責只靠檔名啟動、開啟方式的預設開法,以及出現在預設應用設定頁
app["應用端註冊"] --> ap["App Paths"]
app --> apps["Applications"]
app --> ra["RegisteredApplications"]
ap --> r1["只靠檔名啟動"]
apps --> r2["開啟方式的預設"]
ra --> r3["出現在預設應用頁"]
r3 -.-> cap["需要 Capabilities 宣告"]
圖 3: 應用端註冊有三種,要成為預設應用候選需要 Capabilities 註冊。
2.4. 預設應用屬於使用者 ── UserChoice 保護
把 ProgID 寫成副檔名鍵的預設值,並不會因此成為預設應用。使用者在「開啟方式」等處明確選擇的結果,儲存在 HKCU\...\Explorer\FileExts\<extension>\UserChoice,關聯解析會優先採用那一邊。
重點是 Windows 不支援以程式更改預設應用。預設應用設定被設計成由使用者透過系統設定 UI 完成;UserChoice 資料已混淆,篩選器驅動程式(UCPD.sys)會阻止應用寫入。在受管理環境中,組策略/MDM 策略才是官方手段。3
像 SetUserFTA 這種「模仿雜湊再改寫」的工具之所以被使用,正是這層保護的另一面。內部應用的安裝程式該放的不是搶走預設值,而是 (a) 正確註冊 ProgID 與 verb、(b) 把自己加到 OpenWithProgIds、(c) 必要時引導到設定頁 這三件事。
flowchart TB
accTitle: 預設應用解析與 UserChoice 保護
accDescr: 使用者明確選擇的結果儲存在 UserChoice 並在關聯解析中優先;UCPD.sys 阻止應用改寫,因此安裝程式能做的是註冊成候選並引導到設定頁
uc["UserChoice(使用者的選擇)"] -->|優先| res["關聯解析"]
ext["副檔名鍵預設值"] --> res
wr["從應用改寫"] -.->|UCPD.sys 阻止| uc
res ~~~ inst["安裝程式的工作"]
inst --> a1["註冊 ProgID 與 verb"]
inst --> a2["加到 OpenWithProgIds"]
inst --> a3["引導到設定頁"]
圖 4: 關聯解析優先採用使用者的選擇(UserChoice),作業系統保護它不被應用改寫。
3. 「開啟」以外的 verb ── print、edit、runas、自訂 verb
verb 不只是 open。作業系統認得意義的標準 verb,除了 open 還有 edit、print、play、preview,標準 verb 會自動取得跟隨作業系統區域設定的顯示名稱。雙擊使用的預設 verb,依次為:shell 鍵的預設值 → 登錄檔中的第一個 verb → open → openwith。12
flowchart TB
accTitle: 預設 verb 的決定順序
accDescr: 雙擊使用的預設 verb,是依 shell 鍵預設值、登錄檔中第一個 verb、open、openwith 的順序找到的第一個
s1["shell 鍵預設值"] -->|若無| s2["登錄檔中第一個 verb"]
s2 -->|若無| s3["open"]
s3 -->|若無| s4["openwith"]
圖 5: 雙擊的預設 verb 是依此順序找到的第一個。
若要加入自己的動作,就註冊自訂 verb。
KomuraSoft.Report.1
shell
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
print
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /print "%1"
verify ← 自訂 verb
(Default) = Verify report (&V) ← 選單顯示名稱
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /verify "%1"
三件知道了就有幫助的小事。
- 註冊名為 runas 的 verb,就定義了相當於「以管理員身份執行」的提升啟動,
ShellExecute系列 API 指定runas時也會用到。 - 在 verb 鍵放一個名為
Extended的空值,就會變成 只有 Shift+右鍵才顯示的擴充套件 verb。適合把很少用、又危險的操作藏起來。12 - 較舊應用的部分關聯仍用 DDE(
ddeexec鍵)把文件送進既有程序,但以 DDE 啟動 verb 已是 Deprecated 的舊機制。沒有理由新寫。12
還有一件常發生的事故是 命令列的引號。命令字串的元素若可能含空格,就必須用引號包起來。這當然適用於 C:\Program Files\... 這類 EXE 路徑,而且 %1(所選檔案的路徑)應一律寫成 "%1"。無法保證使用者的檔案路徑不含空格。沒加引號的 My Program.exe 會被解讀成「啟動 My,引數是 Program.exe」。13
flowchart TB
accTitle: 命令列的引號事故
accDescr: 沒加引號的命令會在空格處被切開,誤當成啟動 My 並帶引數 Program.exe,因此可能含空格的 EXE 路徑、以及代表所選檔案路徑的 %1,都應一律用引號包起來
c1["沒加引號的命令"] -->|在空格處切開| bad["被誤當成啟動別的 EXE"]
c2["有引號的命令"] --> good["按預期啟動"]
c2 -.-> q1["用引號包住 EXE 路徑"]
q1 -.-> q2["%1 也一律用引號包住"]
圖 6: 沒加引號的命令會在空格處被錯誤切開,因此 EXE 路徑與 %1 一律用引號包住。
到目前為止只靠登錄檔的機制(靜態 verb)不必寫任何 DLL,也不會讓檔案總管不穩定。Microsoft 自己一再說「在寫殼層擴充套件之前,先考慮符合需求的最簡單靜態 verb 是否就夠了」。9
4. 傳統殼層擴充套件 ── 在檔案總管裡執行的 DLL
4.1. 殼層擴充套件的種類
靜態 verb 做不到的需求——「依所選內容動態更改選單」、「替換圖示或屬性表」——要用殼層擴充套件處理程式。代表性種類如下。4
| 處理程式 | 主要介面 | 能做的事 |
|---|---|---|
| 右鍵選單處理程式 | IContextMenu + IShellExtInit | 動態新增與控制選單項 |
| 圖示處理程式/圖示疊加 | IExtractIcon / IShellIconOverlayIdentifier | 每個檔案的圖示與疊加 |
| 屬性表處理程式 | IShellPropSheetExt | 在屬性表加選項卡 |
| 縮圖/資訊提示 | IThumbnailProvider / IQueryInfo | 縮圖檢視與懸停說明 |
| 拖放/複製鉤子處理程式 | IDropTarget / ICopyHook | 在放置或複製/移動時介入 |
這些都以 COM 類實現,並以 CLSID 註冊到登錄檔。COM 本身的概念見「COM / ActiveX / OCX 是什麼 - 差異與關係一次整理」。
4.2. 身為程序內 COM 伺服器意味著什麼
傳統殼層擴充套件的本質是 載入到檔案總管(或任何開啟通用檔案對話方塊的應用)的程序內 COM 伺服器(DLL)。每一項注意事項都由此而來。4
- 擴充套件崩潰,檔案總管會一起倒下。 若它掛住,右鍵會凍結好幾秒。損害也不限於檔案總管,會波及所有顯示過檔案開啟對話方塊的應用。
- 選單構建發生在 UI 執行緒,因此 顯示選單時不得做網路訪問或檔案 I/O 這類慢工作。
- 執行緒模型原則上註冊為
Apartment。
flowchart TB
accTitle: 程序內擴充套件的連帶損害結構
accDescr: 殼層擴充套件 DLL 不只載入到檔案總管,也載入到開啟檔案對話方塊的任何應用程序,因此擴充套件的崩潰或掛住會波及整個宿主程序
dll["殼層擴充套件 DLL"] -->|程序內載入| exp["檔案總管"]
dll -->|程序內載入| any["開啟對話方塊的任何應用"]
exp --> dmg["崩潰或掛住會擴散"]
any --> dmg
dmg -.-> rule["顯示時不要做慢工作"]
圖 7: 擴充套件 DLL 在宿主程序內執行,因此崩潰或掛住會波及整個宿主。
調查「開啟特定資料夾時檔案總管凍結」或「右鍵要五秒」這類諮詢時,原因往往不是內部應用,而是第三方殼層擴充套件。隔離方法在第 8 章。
4.3. 對齊位數 ── 64 位環境需要 64 位 DLL
程序內 DLL 必須與載入它的程序位數一致。64 位 Windows 的檔案總管是 64 位程序,因此 只建成 32 位的殼層擴充套件 DLL 永遠不會被載入,選單上也完全不會出現。而且沒有錯誤,所以是「註冊了卻不出現」的常客。32 位應用本體搭配 64 位殼層擴充套件 DLL 是正當配置,但要注意 COM 註冊按位數分開(Wow6432Node)。從 verb 的 command 啟動的是獨立程序 EXE,不受此限制(維持 32 位 EXE 沒問題)。
flowchart TB
accTitle: 殼層擴充套件 DLL 的位數對齊
accDescr: 64 位檔案總管能載入的殼層擴充套件 DLL 只有 64 位;僅 32 位的 DLL 不會出現在選單上也沒有錯誤;從 verb command 啟動的 EXE 是獨立程序,不受此限制
exp["64 位檔案總管"] -->|可載入| d64["64 位殼層擴充套件 DLL"]
exp -.->|無法載入| d32["僅 32 位的 DLL"]
d32 -.-> sym["沒有錯誤、選單上不出現"]
exe["從 verb 啟動的 EXE"] -->|獨立程序| ok32["維持 32 位沒問題"]
圖 8: 載入到 64 位檔案總管的只有 64 位 DLL;從 verb 啟動的 EXE 不受此限制。
4.4. 為什麼不能用託管程式碼寫
常有人問「可以用 C# 寫殼層擴充套件嗎」,但 Microsoft 已明確表示,用託管程式碼(.NET)編寫程序內殼層擴充套件既不建議也超出支援範圍。5
原因是擴充套件會載入到任意程序。CLR 版本衝突(尤其在 .NET Framework 4 以下)、CLR 在等待鎖時重入訊息迴圈、垃圾回收的非確定性物件生存期與 COM 引用計數契約衝突,都是讓宿主應用不穩定的結構性理由。部分專案在 .NET Framework 4 之後與現代 .NET 已緩解,但官方立場沒變。
實務準則很單純。程序內擴充套件用本機 C++ 寫。若要用託管程式碼,就做成從 verb 的 command 啟動的普通 EXE,或在獨立程序執行的程序外擴充套件(預覽處理程式等)。5
flowchart TB
accTitle: 判斷託管程式碼是否允許
accDescr: 在檔案總管內執行的程序內擴充套件原則上用本機 C++ 編寫;若要用託管程式碼,就做成從 verb command 啟動的普通 EXE,或在獨立程序執行的程序外擴充套件
q1{"在程序內執行?"} -->|是| cpp["用本機 C++ 寫"]
q1 -->|否| mg["託管程式碼沒問題"]
cpp -.-> why["CLR/重入風險使宿主變得不穩定"]
mg --> e1["由 verb 啟動的 EXE"]
mg --> e2["程序外預覽"]
圖 9: 程序內擴充套件原則上是本機 C++;託管程式碼限於在獨立程序執行的配置。
5. Windows 11 的新右鍵選單 ── 拆成兩層的選單
5.1. 發生了什麼
Windows 11 重畫了檔案總管的右鍵選單。剪下、複製等變成頂部一排圖示;「開啟」與「開啟方式」集中在上方;應用加入的命令則集中在殼層標準命令下方。同一個應用加入多個命令時,會收進以應用命名的浮出(子選單)。6
關鍵在這裡。以傳統 IContextMenu 為基礎的殼層擴充套件並沒有被刪除;它們被移到用「顯示更多選項」(Shift+F10)開啟、原樣載入 Windows 10 選單的舊選單那一側。6 開頭那則「選單被藏起來」諮詢的真面目,就是這次拆分。
flowchart TB
accTitle: Windows 11 拆成兩層的右鍵選單
accDescr: 右鍵先開啟的是新選單;能出現在那裡的命令只有以 IExplorerCommand 與包標識註冊的項;傳統 IContextMenu 擴充套件被移到用顯示更多選項開啟的舊選單
rc["對檔案右鍵單擊"] --> newm["新選單(Windows 11)"]
newm --> newi["IExplorerCommand + 標識的命令"]
newm -->|顯示更多選項 Shift+F10| oldm["舊選單(Windows 10 選單)"]
oldm --> oldi["傳統 IContextMenu 擴充套件"]
newi -.-> fly["多個命令收進浮出"]
圖 10: 能出現在新選單上的只有 IExplorerCommand + 標識的命令;傳統擴充套件被移到舊選單那一側。
5.2. 進入新選單的官方路徑 ── IExplorerCommand + 清單註冊
要把自訂命令放到新選單,只有一種做法。準備實現 IExplorerCommand 介面的本機 DLL,並在 MSIX 包清單中宣告 COM 伺服器與右鍵選單擴充套件。7
<!-- 包清單(摘錄) -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Komura commands">
<com:Class Id="01234567-89AB-CDEF-0123-456789ABCDEF"
Path="KomuraCommand.dll" ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop5:ItemType Type=".kmrpt">
<desktop5:Verb Id="VerifyReport"
Clsid="01234567-89AB-CDEF-0123-456789ABCDEF" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
ItemType 的 Type 可以指定特定副檔名,或 *(所有檔案)、Directory(資料夾)、Directory\Background(資料夾背景)。DLL 要對齊檔案總管的體系結構(64 位/ARM64)。7
IExplorerCommand 本身是 Windows 7 時代就有的介面;你實現標題(GetTitle)、圖示(GetIcon)、啟用/禁用/隱藏狀態(GetState)與執行(Invoke)。方法從 UI 執行緒呼叫,因此 禁止訪問網路資源,選單構建方法也必須很快返回。繁重工作放在 Invoke 之後。147
flowchart TB
accTitle: 新選單註冊的清單結構
accDescr: MSIX 清單的 COM 伺服器宣告把 CLSID 對映到 DLL,右鍵選單擴充套件宣告用 ItemType 與 Verb 把目標與實現綁在一起,於是自訂命令出現在新選單上
man["MSIX 清單"] --> com["COM 伺服器宣告"]
man --> ctx["選單擴充套件宣告"]
com -->|把 CLSID 對映到 DLL| impl["IExplorerCommand 實現 DLL"]
ctx -->|用 ItemType 與 Verb 指定| impl
impl --> shown["命令出現在新選單"]
ctx -.-> tgt["目標是副檔名、所有檔案等"]
圖 11: 清單中的兩份宣告把實現 DLL 與目標綁在一起,命令就出現在新選單上。
5.3. 未打包應用的選擇 ── 用 sparse package 只取得標識
「我們的應用只能用 MSI 分發,不可能改成 MSIX」時的逃生門,是 sparse package(外部位置的 MSIX)。你簽署一份只有清單、不含應用本體的小型 MSIX,並在現有安裝程式結尾註冊。應用於是取得 包標識,上面的清單註冊(= 出現在新選單)才成為可能。從 Windows 10 版本 2004 起可用,包需要目標電腦信任的證書籤名。8
flowchart TB
accTitle: 用 sparse package 取得標識的流程
accDescr: 現有安裝程式放好應用本體後,以外部位置註冊只有清單的 sparse package,應用就取得包標識,並能做新選單的清單註冊
inst["現有安裝程式"] --> files["放置應用本體"]
sp["sparse package"] -.-> only["只有清單,沒有本體"]
files --> reg["以外部位置註冊"]
sp --> reg
reg --> id["取得包標識"]
id --> ok["新選單註冊成為可能"]
sp -.-> sign["需要受信任的簽名"]
圖 12: 以外部位置註冊不含本體的 sparse package,應用就取得包標識。
最大優點是不必更換安裝程式;對已有 MSI/EXE 安裝程式資產的應用,這是務實的答案。與全面改成 MSIX 的比較,也可見「Windows 應用發布方式怎麼選 - MSI / MSIX / ClickOnce / xcopy / 自訂 updater 的判斷表」。
5.4. 關聯 verb 如何出現在新選單
容易誤解的一點:第 2、3 章的關聯(ProgID 與 verb)在新選單上仍然有效。 雙擊的預設 verb、「開啟」、以及「開啟方式」候選,都從關聯解析並顯示在新選單上方。所以若只想「能用這個應用開啟」,Windows 11 不需要額外工作。另一方面,關聯不是通用選單擴充套件,若要把任意自訂命令放到新選單第一層,就需要 IExplorerCommand 加上標識——這就是角色分工。7
flowchart TB
accTitle: 關聯與新選單的角色分工
accDescr: ProgID 與 verb 關聯在新選單上仍用來解析預設 verb、開啟、開啟方式,並顯示在上方;要把任意自訂命令放到新選單第一層,需要 IExplorerCommand 與標識
assoc["關聯(ProgID + verb)"] --> sol["解析預設/開啟"]
sol --> top["新選單上方"]
assoc -.-> keep["Win11 不需額外工作"]
cmd["自訂命令"] --> need["IExplorerCommand+標識"]
need --> first["新選單第一層"]
圖 13: 關聯在新選單上仍負責「開啟」系列解析;只有自訂命令才需要 IExplorerCommand 加上標識。
6. 實務判斷表 ── 三個選項要選哪一個
把到目前為止整理成實務上的三選一。
| 想達成的事 | 建議手段 | 在 Windows 11 的樣子 | 所需工作與成本 |
|---|---|---|---|
| (a) 雙擊或「開啟」就啟動內部應用 | 關聯 + 靜態 verb(只做登錄檔註冊) | 整合進新選單的「開啟」與「開啟方式」 | 只需安裝程式登錄檔註冊。沒有 DLL,也沒有額外簽名要求 |
| (b) 把針對所選檔案/資料夾的自訂命令放到新選單 | IExplorerCommand 實現 + MSIX 清單註冊。未打包應用用 sparse package 取得標識 | 新選單第一層(多個命令收進應用名稱浮出) | 本機 C++ DLL + 包標識 + 程式碼簽名 |
| (c) 繼續使用既有的傳統 IContextMenu 擴充套件 | 暫時維持現狀(新開發不要選它) | 僅舊選單那一側,在「顯示更多選項」(Shift+F10)下面 | 維持 64 位構建與 COM 註冊。規劃之後遷移到 (b) |
判斷點有兩個。第一,不要為 (a) 就能滿足的需求引進 (b) 或 (c)。一旦寫殼層擴充套件,就要為檔案總管的穩定性負責。第二,(c) 只是「沒壞掉」;就使用者體驗而言仍差一步。日常作業中越常使用的命令,遷移到 (b) 的回報越大。
flowchart TB
accTitle: 三個選項怎麼選
accDescr: 若只想雙擊或開啟就啟動,關聯與靜態 verb 就夠;要把自訂命令放到新選單就用 IExplorerCommand 與 MSIX 清單註冊;無法改成 MSIX 就用 sparse package 賦予標識;既有傳統 IContextMenu 擴充套件暫時留在舊選單那一側
q1{"開啟就夠了?"} -->|是| pa["關聯 + 靜態 verb"]
q1 -->|否| q2{"新選單上的自訂命令?"}
q2 -->|是| q3{"能改成 MSIX?"}
q3 -->|是| pb1["IExplorerCommand+MSIX"]
q3 -->|否| pb2["sparse-pkg 標識"]
q2 -->|否| pc["暫時維持傳統"]
pc -.-> old["僅舊選單那一側"]
pa -.-> dllfree["沒有 DLL,風險小"]
圖 14: 按需求在靜態 verb、IExplorerCommand 加上標識、以及維持傳統之間選擇。
7. 實務上的部署與註冊 ── 安裝程式、sparse package、清理
7.1. HKLM 還是 HKCU
對齊安裝程式的形態。全體使用者(放在 Program Files 底下、需要管理員許可權)用 HKLM\Software\Classes;單使用者安裝(不提升)用 HKCU\Software\Classes。混用會產生「A 打得開、B 打不開」這類詢問。涉及 CLSID 註冊的殼層擴充套件,Reg-Free COM——讓登錄檔註冊本身變得不必要——對應用內 COM 使用是有效選項,但無法套用到檔案總管載入的殼層擴充套件,因此需要正面註冊(「什麼是 Reg-Free COM - 免註冊使用 COM 的機制,以及合用與不合用的情境」)。
7.2. 更改後要通知 ── SHChangeNotify
註冊、更改或刪除關聯後,用 SHChangeNotify 通知 SHCNE_ASSOCCHANGED 事件。省略這一步,檔案總管可能直到重啟才察覺更改。110
// 更改關聯後呼叫一次,例如從安裝程式自訂操作
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
7.3. 註冊與移除 sparse package
註冊與移除 sparse package 是安裝程式的工作。放好檔案後再註冊;刪檔案之前先移除。8
# 安裝時:放好檔案後,把安裝資料夾註冊為外部位置
Add-AppxPackage -Path "C:\Program Files\KomuraSoft\KomuraReport.identity.msix" `
-ExternalLocation "C:\Program Files\KomuraSoft"
# 解除安裝時:刪檔案之前先移除包註冊
Remove-AppxPackage <package full name>
要注意:Add-AppxPackage 是為 執行它的那位使用者 註冊。若從每臺電腦 MSI 的自訂操作、以 LocalSystem 呼叫,並不會把標識授給實際安裝的使用者,因此要設定成以使用者模擬執行。即便如此,模擬下的註冊也 只屬於執行該次安裝的那位使用者。多人共用的電腦上,其他使用者與之後建立的使用者沒有包標識,命令就不會出現在新選單。若要讓每位使用者都能用,提供例如首次啟動時檢查自己的包註冊、缺了就註冊的機制(每使用者註冊),並把從每位已註冊使用者移除納入解除安裝計劃。反映清單註冊也可能需要重啟檔案總管(或登出)。7
flowchart TB
accTitle: 註冊與移除 sparse package 的順序
accDescr: 安裝時在放好檔案後註冊 sparse package;解除安裝時在刪檔案之前先移除註冊;注意註冊只對執行它的使用者有效
i1["安裝"] --> i2["放置檔案"]
i2 --> i3["註冊 sparse package"]
u1["解除安裝"] --> u2["移除包註冊"]
u2 --> u3["刪除檔案"]
i3 -.-> pu["註冊只對執行中的使用者有效"]
圖 15: 放好檔案後再註冊,刪檔案之前先移除,並注意註冊是按執行中的使用者。
7.4. 解除安裝時的清理 ── 刪什麼、留什麼
解除安裝時的清理有清楚的官方指引線。1
- 刪除:整個內部 ProgID 鍵、Capabilities/RegisteredApplications 註冊、殼層擴充套件的 CLSID 註冊、sparse package(Remove-AppxPackage)。
- 留下:副檔名鍵(
.kmrpt)的預設值。即使它仍指向內部 ProgID,官方建議也不要刪。 安裝後很難判斷是否已有別的應用拿走預設值,而 Windows 會直接忽略未註冊的預設值 ProgID,留下它沒有實質傷害。 - 清理結束時也呼叫 SHChangeNotify(SHCNE_ASSOCCHANGED)。
多數「解除安裝了選單上還有殘留」問題,都是這份清理設計的漏洞。
flowchart TB
accTitle: 解除安裝時的清理設計
accDescr: 解除安裝時刪除內部 ProgID 鍵、CLSID 註冊與 sparse package;留下副檔名鍵預設值,因為未註冊的 ProgID 會被忽略;清理結束時用 SHChangeNotify 通知更改
un["解除安裝"] --> del["刪除"]
un --> keep["留下"]
del --> d1["ProgID 與 CLSID 註冊"]
del --> d2["sparse package"]
keep --> k1["副檔名鍵預設值"]
k1 -.-> why["未註冊的 ProgID 會被忽略"]
d1 --> fin["結束時用 SHChangeNotify 通知"]
k1 --> fin
圖 16: 刪除內部註冊,留下副檔名鍵預設值,並在清理結束時通知更改。
8. 故障排除 ── 不見、重複、變重
8.1. 選單上不出現
依此順序隔離。
- 你看的是哪一面選單:傳統風格的註冊只出現在 Shift+F10 的舊選單那一側。先兩邊都查。
- 位數:僅 32 位的殼層擴充套件 DLL 不會載入到 64 位檔案總管(第 4.3 節)。
- 註冊目標:HKLM/HKCU、Wow6432Node 搞混。用
reg query確認實際鍵。 - 包註冊:針對新選單,用
Get-AppxPackage確認存在、簽名證書是否受信任、以及-ExternalLocation路徑,然後重啟檔案總管。7 - 漏了通知:若忘了 SHChangeNotify,看重啟檔案總管是否生效就能判斷。
flowchart TB
accTitle: 選單上不出現時的隔離順序
accDescr: 先確認你看的是哪一面選單,再依序隔離 DLL 位數、登錄檔註冊目標、包註冊與簽名,以及漏掉的 SHChangeNotify
c1["確認看的是舊或新選單"] --> c2["確認 DLL 位數"]
c2 --> c3["確認 HKLM 與 HKCU 註冊目標"]
c3 --> c4["確認包註冊與簽名"]
c4 --> c5["用重啟判斷是否漏通知"]
圖 17: 「不出現」時,依你看的選單、位數、註冊目標、包註冊、漏通知的順序隔離。
8.2. 出現兩次,或消不掉
典型原因是傳統登錄檔註冊與清單註冊並存、解除安裝清理漏洞(第 7.4 節),或舊版 ProgID 殘留。若只在舊選單出現兩次,想殘留;若新舊兩邊都出現,想並存。
flowchart TB
accTitle: 隔離雙重顯示
accDescr: 只在舊選單出現兩次指向清理漏洞或舊 ProgID 等殘留;新舊兩邊都出現兩次指向傳統登錄檔註冊與清單註冊並存
q{"在哪裡出現兩次?"} -->|僅舊選單| zan["殘留"]
q -->|新舊兩邊| hei["並存"]
zan -.-> z1["清理漏洞或留下的舊 ProgID"]
hei -.-> h1["傳統登錄檔註冊與新註冊並存"]
圖 18: 只在舊選單出現兩次指向殘留;新舊兩邊都出現兩次指向並存。
8.3. 檔案總管變重或崩潰
右鍵變慢、或特定資料夾崩潰時,先盤點已安裝的殼層擴充套件。用 NirSoft 的 ShellExView 等工具列出非 Microsoft 擴充套件,暫時禁用可疑項,再用二分查詢找出元兇 DLL。崩潰時,事件檢視器的「Faulting module」也是線索。若內部擴充套件是原因,懷疑選單構建路徑上的同步 I/O 或網路訪問(第 4.2 與 5.2 節)。
flowchart TB
accTitle: 變重或崩潰時找出元兇 DLL
accDescr: 在 ShellExView 列出非 Microsoft 殼層擴充套件,暫時禁用可疑項並用二分查詢找出元兇 DLL;崩潰時事件檢視器的 faulting module 也是線索
s1["盤點殼層擴充套件"] --> s2["列出非 Microsoft 的"]
s2 --> s3["暫時禁用並二分查詢"]
s3 --> s4["找出元兇 DLL"]
crash["崩潰時"] -.-> ev["檢查 faulting module"]
ev -.-> s4
圖 19: 暫時禁用非 Microsoft 擴充套件並做二分查詢;崩潰時也用事件檢視器。
8.4. 驗證時 Windows Sandbox 很方便
殼層整合的驗證,基礎是確認「在乾淨環境安裝 → 操作 → 解除安裝 → 殘留為零」。這裡方便的是 Windows Sandbox(Pro/Enterprise/Education):每次啟動幾秒內就出現全新的一次性 Windows,安裝程式的註冊與清理測試可以跑任意多次。關掉就全部消失,也適合調查殘留登錄檔。15
9. 總結
- 檔案關聯是「副檔名鍵 → ProgID → verb」的三層結構,HKCR 是 HKLM/HKCU Classes 的合併檢視。寫入目標要指明,
%1一律用引號包住。 - 預設應用被設計成由使用者選擇,無法從程式更改。安裝程式的工作是正確註冊成候選。
- 傳統殼層擴充套件是載入到檔案總管的程序內 COM DLL。崩潰或延遲會波及全體;需要 64 位;託管程式碼不受支援;原則是用本機 C++ 實現。
- Windows 11 把右鍵選單拆成兩層。要把自訂命令放到新選單,需要 IExplorerCommand 加上 MSIX 清單;傳統 IContextMenu 被移到「顯示更多選項」那一側。
- 無法改成 MSIX 的應用,用 sparse package(外部位置的 MSIX)取得標識是務實答案。
- 若只想「用這個應用開啟」,關聯加上靜態 verb 仍然夠用。從最簡單的手段開始,也是官方準則。
- 註冊、更改或刪除後用 SHChangeNotify 通知;解除安裝時刪除 ProgID,但留下副檔名鍵預設值。驗證時 Windows Sandbox 很方便。
若換成 Windows 11 電腦後才發現「選單被藏起來」,先用第 6 章判斷表確認屬於 (a)、(b) 還是 (c)。當場就該能估出工作規模。
相關文章
- COM / ActiveX / OCX 是什麼 - 差異與關係一次整理
- 什麼是 Reg-Free COM - 免註冊使用 COM 的機制,以及合用與不合用的情境
- 登錄檔的 32bit/64bit 重新導向與虛擬化的陷阱 ── Wow6432Node 與「明明寫入了卻讀不到值」的問題
- Windows 應用發布方式怎麼選 - MSI / MSIX / ClickOnce / xcopy / 自訂 updater 的判斷表
- DLL・COM 介面的向後相容性 ── 哪些變更會破壞呼叫端的判斷表
- Windows 應用程式相容如何運作 ── 用相容模式、Shim 與 Compatibility Administrator 延續舊應用
相關諮詢領域
小村軟體有限公司承接業務應用的檔案關聯、右鍵選單與殼層擴充套件的設計與實現;對準 Windows 11 新右鍵選單(遷移到 IExplorerCommand、引入 sparse package);檢視現有安裝程式的註冊與清理;以及檔案總管變重或崩潰的原因調查。從決定「藏在顯示更多選項下面的選單該怎麼辦」開始也可以。
參考連結
-
Microsoft Learn, File Types. 關於副檔名鍵指向 ProgID 的結構、OpenWithProgIds、HKLM/HKCU\Software\Classes 的註冊分法、關聯更改後呼叫 SHChangeNotify(SHCNE_ASSOCCHANGED),以及解除安裝時刪除 ProgID 但留下副檔名鍵預設值。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, HKEY_CLASSES_ROOT Key. 關於 HKEY_CLASSES_ROOT 是 HKLM\Software\Classes 與 HKCU\Software\Classes 的合併檢視、使用者端定義優先於電腦端,以及寫入時的分派規則。 ↩ ↩2
-
Microsoft Learn, Windows app defaults platform. 關於更改預設應用被設計成只能透過系統設定 UI、使用者設定資料已混淆並由篩選器驅動程式(UCPD.sys)防寫、以登錄檔為基礎的更改不受支援,以及在受管理環境使用組策略/MDM 策略。 ↩ ↩2
-
Microsoft Learn, Working with Shell Extensions. 關於殼層擴充套件處理程式的種類、擴充套件是載入到檔案總管(以及承載殼層的程序)的程序內 COM DLL 因此崩潰或掛住會波及整個檔案總管、以 ThreadingModel=Apartment 註冊,以及在寫殼層擴充套件之前先考慮更簡單的替代方案。 ↩ ↩2 ↩3
-
Microsoft Learn, Guidance for Implementing In-Process Extensions. 關於 Microsoft 不建議也不支援用託管程式碼實現程序內殼層擴充套件、理由包括 CLR 版本衝突、重入與非確定性物件生存期,以及程序外擴充套件(預覽處理程式,或從 shell\verb\command 啟動)可以使用託管程式碼。 ↩ ↩2 ↩3
-
Windows Developer Blog, Extending the Context Menu and Share Dialog in Windows 11. 關於 Windows 11 新右鍵選單的設計、透過 IExplorerCommand 加上應用標識擴充套件、把「開啟」與「開啟方式」放在上方、把多個命令收進應用名稱浮出,以及傳統 IContextMenu 擴充套件在「顯示更多選項」(Shift+F10)下以 Windows 10 選單載入。 ↩ ↩2 ↩3
-
Microsoft Learn, Add a File Explorer context menu command to a packaged desktop app. 關於 Windows 11 新右鍵選單的註冊是用 IExplorerCommand 實現加上 windows.comServer 與 desktop4:FileExplorerContextMenus 清單宣告、ItemType 可指定 *、Directory 或 Directory\Background、對齊 DLL 體系結構、讓選單構建方法保持快速、用 sparse package 覆蓋未打包應用、反映註冊有時需要重啟檔案總管,以及檔案關聯不是通用選單擴充套件。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Grant package identity by packaging with external location. 關於不更改現有安裝程式、以註冊外部位置包(sparse package)取得包標識、自 Windows 10 版本 2004 起可用,以及需要標識的 Windows 功能(右鍵選單註冊、通知等)變得可用。 ↩ ↩2 ↩3
-
Microsoft Learn, Choosing a Static or Dynamic Shortcut Menu Method. 關於選擇符合需求的最簡單靜態 verb 方法、IContextMenu 最強但也最複雜並被歸到不建議那一側,以及 IExplorerCommand/IExplorerCommandState 才是建議方法。 ↩ ↩2
-
Microsoft Learn, SHChangeNotify function. 關於如何發出通知系統檔案關聯更改的 SHCNE_ASSOCCHANGED 事件,以及用它讓殼層察覺更改。 ↩ ↩2
-
Microsoft Learn, Application Registration. 關於建議透過 App Paths 子鍵註冊可執行檔案、Applications 子鍵的角色、透過 SystemFileAssociations 註冊 verb,以及預設應用更改時 ProgID 與相關資訊的優先順序。 ↩
-
Microsoft Learn, Verbs and File Associations. 關於 verb 也是 ShellExecuteEx 使用的動作、命令字串中可能含空格的元素需要用引號包住且 “%1” 一律加引號,以及在 HKCR\Applications 下注冊預設過程。 ↩
-
Microsoft Learn, IExplorerCommand interface. 關於 GetTitle、GetIcon、GetState、Invoke、EnumSubCommands 等方法組成、方法從 UI 執行緒呼叫因此不得與網路資源通訊,以及自 Windows Vista 起可用。 ↩
-
Microsoft Learn, Windows Sandbox. 關於能在幾秒內啟動一次性、隔離的 Windows 環境、關閉後丟棄所有更改、適合軟體測試與安裝程式驗證,以及在 Pro/Enterprise/Education 可用。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
剪貼簿與拖放的運作方式 ── 在業務應用程式中正確處理 OLE 資料傳輸
貼上 Excel 表格格式就散掉;關掉來源應用程式就再也貼不上──兩者都來自剪貼簿把同一份內容一次放進多種格式。本文涵蓋標準格式、延遲轉譯、OLE 拖放,以及管轄剪貼簿歷程與雲端同步的原則。
委外・委託開發 Windows 應用程式前該整理的事項
在委外・委託開發 Windows 應用程式之前,整理既有軟體改版、設備整合、COM/ActiveX、發布與更新、維護等應留意的重點。
Windows 為什麼會出現「Windows 已保護您的電腦」
從程式碼簽章、EV/OV 憑證、Azure Artifact Signing、MSIX、Microsoft Store、ClickOnce、內部發佈到 App Control,以實務角度整理 Windows 應用程式發佈時出現 SmartScreen 警告的原因。
Win32 執行緒集區 API ── 用 CreateThreadpoolWork 做「不自己建立執行緒」的並行
原生程式碼裡到處呼叫 CreateThread 嗎?本文依一次資訊解說 Vista 重新設計的 Win32 執行緒集區 API──work、timer、wait、io 四種物件、清理群組,以及回呼裡不該做的事。
具名管道實務 ── 從設計到安全性,整理 Windows 的標準 IPC
以實務角度解說 Windows 標準行程間通訊──具名管道。從一次資訊整理位元組/訊息模式的選擇、同時服務多個用戶端的伺服器設計、ACL 與偽裝的安全性,以及 .NET 的具名管道串流。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
ActiveX 遷移
整理保留、包裝或替換 COM / ActiveX / OCX 資產的階段性判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
Windows 軟體維護 & 現代化
支援既有 Windows 軟體的階段性升級、功能追加、64 位元就緒以及可維護性的重構。
常見問題
整理諮詢這個主題時常見的問題。
- 為什麼我們應用在 Windows 11 的右鍵選單項只出現在「顯示更多選項」下面?
- 因為 Windows 11 把檔案總管的右鍵選單拆成新舊兩層。能出現在新選單上的命令,只有實現 IExplorerCommand 介面、並在 MSIX 包清單中註冊的項(= 具備包標識)。以傳統 IContextMenu 為基礎的殼層擴充套件被移到用「顯示更多選項」(Shift+F10)開啟的舊選單。擴充套件本身並沒有壞掉,所以目前仍會工作,但若要出現在新選單上,需要遷移到 IExplorerCommand,並以 MSIX 打包或 sparse package 賦予標識。
- 安裝程式可以把我們的應用設成檔案的預設應用(雙擊就開啟的那個)嗎?
- 不行。預設應用的選擇被設計成由使用者決定,Windows 不支援從系統設定 UI 以外的地方更改預設應用。儲存各使用者選擇的 UserChoice 資訊已混淆,篩選器驅動程式(UCPD.sys)也會阻止應用寫入。安裝程式能做的是註冊 ProgID 與 verb、把自己加到 OpenWithProgIds 以便出現在「開啟方式」候選列表,並引導使用者到預設應用設定頁。正確的實現不是搶走預設值,而是準備好被選中。
- 可以用 C# 這類託管程式碼編寫殼層擴充套件嗎?
- Microsoft 已明確表示,用託管程式碼編寫程序內殼層擴充套件(右鍵選單處理程式、圖示處理程式等)既不建議也不受支援。擴充套件會載入到檔案總管、以及任何開啟通用檔案對話方塊的應用程序中,CLR 版本衝突、重入、以及非確定性的物件生存期會讓宿主應用不穩定。原則是用本機 C++ 實現。由 verb 的 command 啟動的普通 EXE,或在獨立程序執行的程序外擴充套件(例如預覽處理程式),則可以用託管程式碼。
- 什麼是 sparse package(外部位置的 MSIX)?
- 一種不含應用檔案、只含清單(標識資訊)的小型 MSIX 包。對以現有安裝程式(MSI、Inno Setup 等)正常安裝的應用,用 Add-AppxPackage -ExternalLocation 指向安裝資料夾來註冊後,應用就取得包標識,並能使用 Windows 11 新右鍵選單註冊、Toast 通知等需要標識的功能。從 Windows 10 版本 2004 起可用,包需要目標電腦信任的程式碼簽名。這是不想把整個分發方式改成 MSIX、又想支援新選單時的務實選擇。
- 右鍵選單項出現兩次、或怎麼也消不掉時該怎麼辦?
- 先確認它出現在新選單還是舊選單(顯示更多選項),以隔離原因。典型的雙重顯示是傳統登錄檔註冊與 MSIX 清單註冊並存,或解除安裝時留下 ProgID 或擴充套件 CLSID 註冊。更改關聯後也要懷疑漏了 SHChangeNotify(SHCNE_ASSOCCHANGED);剛註冊包後則懷疑漏了重啟檔案總管。若仍未解決,用 ShellExView 暫時禁用非 Microsoft 擴充套件,再用二分查詢找出元兇 DLL。