今日的 Windows 殼層整合 ── 右鍵選單、檔案關聯,以及 Windows 11 改了什麼

· · 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) ProgIDKomuraSoft.Report.1)是關聯的實體,儲存顯示名稱、圖示與 verb 列表。
  • (3) verb 是「開啟」或「列印」這類動作,shell\open\command 的預設值才是實際啟動的命令列。

因為這樣分開,才能把多個副檔名(例如 .kmrpt.kmrpt-file)指到同一個 ProgID,或在升級應用時更換 ProgID。

檔案關聯的三層結構副檔名鍵是預設值指名 ProgID 的指標;ProgID 是儲存顯示名稱、圖示與 verb 列表的實體;verb 底下 command 的預設值才是實際啟動的命令列預設值指名 ProgID副檔名鍵 .kmrptProgID KomuraSoft.Report.1verb(shell 底下的 open 等)command 預設值啟動 Report.exe也儲存顯示名稱與 DefaultIcon

圖 1: 副檔名鍵是指標,ProgID 是實體,verb 的 command 才是實際啟動的命令列。

2.2. HKCR 是「合併檢視」── 寫到哪裡,意義就不同

上面的例子畫在 HKEY_CLASSES_ROOT(HKCR)底下,但 HKCR 不是物理存放位置,而是 HKLM\Software\Classes 與 HKCU\Software\Classes 的合併檢視。兩邊有同一個鍵時,HKCU 獲勝。2

HKCR 是合併檢視HKCR 是把 HKLM 與 HKCU 的 Classes 疊在一起;兩邊有同一個鍵時 HKCU 獲勝;註冊要明確寫到 HKLM 或 HKCU,把 HKCR 當只讀HKLM\\Software\\Classes(全體使用者)HKCR(合併檢視)HKCU\\Software\\Classes(單使用者)同一鍵存在時 HKCU 獲勝當只讀(用來確認)

圖 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 PathsHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths):讓 ShellExecuteEx 只靠可執行檔名就能啟動的註冊。Microsoft 建議這樣做,因為不必汙染 PATH 環境變數。
  • ApplicationsHKCR\Applications\<app.exe>):定義在「開啟方式」交出任意檔案時的預設開啟方式,以及應用的顯示名稱(FriendlyAppName)。
  • RegisteredApplications + Capabilities:宣告應用能處理的副檔名與 MIME 型別,並讓它出現在 Windows 預設應用設定頁的候選列表。

「我們的應用沒出現在預設應用列表」這類諮詢,多數是註冊了 ProgID、卻漏了這份 Capabilities 註冊。

應用端的三種註冊應用端註冊有 App Paths、Applications、RegisteredApplications 三種,分別負責只靠檔名啟動、開啟方式的預設開法,以及出現在預設應用設定頁應用端註冊App PathsApplicationsRegisteredApplications只靠檔名啟動開啟方式的預設出現在預設應用頁需要 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) 必要時引導到設定頁 這三件事。

預設應用解析與 UserChoice 保護使用者明確選擇的結果儲存在 UserChoice 並在關聯解析中優先;UCPD.sys 阻止應用改寫,因此安裝程式能做的是註冊成候選並引導到設定頁優先UCPD.sys 阻止UserChoice(使用者的選擇)關聯解析副檔名鍵預設值從應用改寫安裝程式的工作註冊 ProgID 與 verb加到 OpenWithProgIds引導到設定頁

圖 4: 關聯解析優先採用使用者的選擇(UserChoice),作業系統保護它不被應用改寫。

3. 「開啟」以外的 verb ── print、edit、runas、自訂 verb

verb 不只是 open。作業系統認得意義的標準 verb,除了 open 還有 editprintplaypreview,標準 verb 會自動取得跟隨作業系統區域設定的顯示名稱。雙擊使用的預設 verb,依次為:shell 鍵的預設值 → 登錄檔中的第一個 verb → openopenwith12

預設 verb 的決定順序雙擊使用的預設 verb,是依 shell 鍵預設值、登錄檔中第一個 verb、open、openwith 的順序找到的第一個若無若無若無shell 鍵預設值登錄檔中第一個 verbopenopenwith

圖 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
  • 較舊應用的部分關聯仍用 DDEddeexec 鍵)把文件送進既有程序,但以 DDE 啟動 verb 已是 Deprecated 的舊機制。沒有理由新寫。12

還有一件常發生的事故是 命令列的引號。命令字串的元素若可能含空格,就必須用引號包起來。這當然適用於 C:\Program Files\... 這類 EXE 路徑,而且 %1(所選檔案的路徑)應一律寫成 "%1"。無法保證使用者的檔案路徑不含空格。沒加引號的 My Program.exe 會被解讀成「啟動 My,引數是 Program.exe」。13

命令列的引號事故沒加引號的命令會在空格處被切開,誤當成啟動 My 並帶引數 Program.exe,因此可能含空格的 EXE 路徑、以及代表所選檔案路徑的 %1,都應一律用引號包起來在空格處切開沒加引號的命令被誤當成啟動別的 EXE有引號的命令按預期啟動用引號包住 EXE 路徑%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
程序內擴充套件的連帶損害結構殼層擴充套件 DLL 不只載入到檔案總管,也載入到開啟檔案對話方塊的任何應用程序,因此擴充套件的崩潰或掛住會波及整個宿主程序程序內載入程序內載入殼層擴充套件 DLL檔案總管開啟對話方塊的任何應用崩潰或掛住會擴散顯示時不要做慢工作

圖 7: 擴充套件 DLL 在宿主程序內執行,因此崩潰或掛住會波及整個宿主。

調查「開啟特定資料夾時檔案總管凍結」或「右鍵要五秒」這類諮詢時,原因往往不是內部應用,而是第三方殼層擴充套件。隔離方法在第 8 章。

4.3. 對齊位數 ── 64 位環境需要 64 位 DLL

程序內 DLL 必須與載入它的程序位數一致。64 位 Windows 的檔案總管是 64 位程序,因此 只建成 32 位的殼層擴充套件 DLL 永遠不會被載入,選單上也完全不會出現。而且沒有錯誤,所以是「註冊了卻不出現」的常客。32 位應用本體搭配 64 位殼層擴充套件 DLL 是正當配置,但要注意 COM 註冊按位數分開(Wow6432Node)。從 verb 的 command 啟動的是獨立程序 EXE,不受此限制(維持 32 位 EXE 沒問題)。

殼層擴充套件 DLL 的位數對齊64 位檔案總管能載入的殼層擴充套件 DLL 只有 64 位;僅 32 位的 DLL 不會出現在選單上也沒有錯誤;從 verb command 啟動的 EXE 是獨立程序,不受此限制可載入無法載入獨立程序64 位檔案總管64 位殼層擴充套件 DLL僅 32 位的 DLL沒有錯誤、選單上不出現從 verb 啟動的 EXE維持 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

判斷託管程式碼是否允許在檔案總管內執行的程序內擴充套件原則上用本機 C++ 編寫;若要用託管程式碼,就做成從 verb command 啟動的普通 EXE,或在獨立程序執行的程序外擴充套件在程序內執行?用本機 C++ 寫託管程式碼沒問題CLR/重入風險使宿主變得不穩定由 verb 啟動的 EXE程序外預覽

圖 9: 程序內擴充套件原則上是本機 C++;託管程式碼限於在獨立程序執行的配置。

5. Windows 11 的新右鍵選單 ── 拆成兩層的選單

5.1. 發生了什麼

Windows 11 重畫了檔案總管的右鍵選單。剪下、複製等變成頂部一排圖示;「開啟」與「開啟方式」集中在上方;應用加入的命令則集中在殼層標準命令下方。同一個應用加入多個命令時,會收進以應用命名的浮出(子選單)。6

關鍵在這裡。以傳統 IContextMenu 為基礎的殼層擴充套件並沒有被刪除;它們被移到用「顯示更多選項」(Shift+F10)開啟、原樣載入 Windows 10 選單的舊選單那一側。6 開頭那則「選單被藏起來」諮詢的真面目,就是這次拆分。

Windows 11 拆成兩層的右鍵選單右鍵先開啟的是新選單;能出現在那裡的命令只有以 IExplorerCommand 與包標識註冊的項;傳統 IContextMenu 擴充套件被移到用顯示更多選項開啟的舊選單顯示更多選項 Shift+F10對檔案右鍵單擊新選單(Windows 11)IExplorerCommand + 標識的命令舊選單(Windows 10 選單)傳統 IContextMenu 擴充套件多個命令收進浮出

圖 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>

ItemTypeType 可以指定特定副檔名,或 *(所有檔案)、Directory(資料夾)、Directory\Background(資料夾背景)。DLL 要對齊檔案總管的體系結構(64 位/ARM64)。7

IExplorerCommand 本身是 Windows 7 時代就有的介面;你實現標題(GetTitle)、圖示(GetIcon)、啟用/禁用/隱藏狀態(GetState)與執行(Invoke)。方法從 UI 執行緒呼叫,因此 禁止訪問網路資源,選單構建方法也必須很快返回。繁重工作放在 Invoke 之後。147

新選單註冊的清單結構MSIX 清單的 COM 伺服器宣告把 CLSID 對映到 DLL,右鍵選單擴充套件宣告用 ItemType 與 Verb 把目標與實現綁在一起,於是自訂命令出現在新選單上把 CLSID 對映到 DLL用 ItemType 與 Verb 指定MSIX 清單COM 伺服器宣告選單擴充套件宣告IExplorerCommand 實現 DLL命令出現在新選單目標是副檔名、所有檔案等

圖 11: 清單中的兩份宣告把實現 DLL 與目標綁在一起,命令就出現在新選單上。

5.3. 未打包應用的選擇 ── 用 sparse package 只取得標識

「我們的應用只能用 MSI 分發,不可能改成 MSIX」時的逃生門,是 sparse package(外部位置的 MSIX)。你簽署一份只有清單、不含應用本體的小型 MSIX,並在現有安裝程式結尾註冊。應用於是取得 包標識,上面的清單註冊(= 出現在新選單)才成為可能。從 Windows 10 版本 2004 起可用,包需要目標電腦信任的證書籤名。8

用 sparse package 取得標識的流程現有安裝程式放好應用本體後,以外部位置註冊只有清單的 sparse package,應用就取得包標識,並能做新選單的清單註冊現有安裝程式放置應用本體sparse package只有清單,沒有本體以外部位置註冊取得包標識新選單註冊成為可能需要受信任的簽名

圖 12: 以外部位置註冊不含本體的 sparse package,應用就取得包標識。

最大優點是不必更換安裝程式;對已有 MSI/EXE 安裝程式資產的應用,這是務實的答案。與全面改成 MSIX 的比較,也可見「Windows 應用發布方式怎麼選 - MSI / MSIX / ClickOnce / xcopy / 自訂 updater 的判斷表」。

5.4. 關聯 verb 如何出現在新選單

容易誤解的一點:第 2、3 章的關聯(ProgID 與 verb)在新選單上仍然有效。 雙擊的預設 verb、「開啟」、以及「開啟方式」候選,都從關聯解析並顯示在新選單上方。所以若只想「能用這個應用開啟」,Windows 11 不需要額外工作。另一方面,關聯不是通用選單擴充套件,若要把任意自訂命令放到新選單第一層,就需要 IExplorerCommand 加上標識——這就是角色分工。7

關聯與新選單的角色分工ProgID 與 verb 關聯在新選單上仍用來解析預設 verb、開啟、開啟方式,並顯示在上方;要把任意自訂命令放到新選單第一層,需要 IExplorerCommand 與標識關聯(ProgID + verb)解析預設/開啟新選單上方Win11 不需額外工作自訂命令IExplorerCommand+標識新選單第一層

圖 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) 的回報越大。

三個選項怎麼選若只想雙擊或開啟就啟動,關聯與靜態 verb 就夠;要把自訂命令放到新選單就用 IExplorerCommand 與 MSIX 清單註冊;無法改成 MSIX 就用 sparse package 賦予標識;既有傳統 IContextMenu 擴充套件暫時留在舊選單那一側開啟就夠了?關聯 + 靜態 verb新選單上的自訂命令?能改成 MSIX?IExplorerCommand+MSIXsparse-pkg 標識暫時維持傳統僅舊選單那一側沒有 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

註冊與移除 sparse package 的順序安裝時在放好檔案後註冊 sparse package;解除安裝時在刪檔案之前先移除註冊;注意註冊只對執行它的使用者有效安裝放置檔案註冊 sparse package解除安裝移除包註冊刪除檔案註冊只對執行中的使用者有效

圖 15: 放好檔案後再註冊,刪檔案之前先移除,並注意註冊是按執行中的使用者。

7.4. 解除安裝時的清理 ── 刪什麼、留什麼

解除安裝時的清理有清楚的官方指引線。1

  • 刪除:整個內部 ProgID 鍵、Capabilities/RegisteredApplications 註冊、殼層擴充套件的 CLSID 註冊、sparse package(Remove-AppxPackage)。
  • 留下:副檔名鍵(.kmrpt)的預設值。即使它仍指向內部 ProgID,官方建議也不要刪。 安裝後很難判斷是否已有別的應用拿走預設值,而 Windows 會直接忽略未註冊的預設值 ProgID,留下它沒有實質傷害。
  • 清理結束時也呼叫 SHChangeNotify(SHCNE_ASSOCCHANGED)。

多數「解除安裝了選單上還有殘留」問題,都是這份清理設計的漏洞。

解除安裝時的清理設計解除安裝時刪除內部 ProgID 鍵、CLSID 註冊與 sparse package;留下副檔名鍵預設值,因為未註冊的 ProgID 會被忽略;清理結束時用 SHChangeNotify 通知更改解除安裝刪除留下ProgID 與 CLSID 註冊sparse package副檔名鍵預設值未註冊的 ProgID 會被忽略結束時用 SHChangeNotify 通知

圖 16: 刪除內部註冊,留下副檔名鍵預設值,並在清理結束時通知更改。

8. 故障排除 ── 不見、重複、變重

8.1. 選單上不出現

依此順序隔離。

  1. 你看的是哪一面選單:傳統風格的註冊只出現在 Shift+F10 的舊選單那一側。先兩邊都查。
  2. 位數:僅 32 位的殼層擴充套件 DLL 不會載入到 64 位檔案總管(第 4.3 節)。
  3. 註冊目標:HKLM/HKCU、Wow6432Node 搞混。用 reg query 確認實際鍵。
  4. 包註冊:針對新選單,用 Get-AppxPackage 確認存在、簽名證書是否受信任、以及 -ExternalLocation 路徑,然後重啟檔案總管。7
  5. 漏了通知:若忘了 SHChangeNotify,看重啟檔案總管是否生效就能判斷。
選單上不出現時的隔離順序先確認你看的是哪一面選單,再依序隔離 DLL 位數、登錄檔註冊目標、包註冊與簽名,以及漏掉的 SHChangeNotify確認看的是舊或新選單確認 DLL 位數確認 HKLM 與 HKCU 註冊目標確認包註冊與簽名用重啟判斷是否漏通知

圖 17: 「不出現」時,依你看的選單、位數、註冊目標、包註冊、漏通知的順序隔離。

8.2. 出現兩次,或消不掉

典型原因是傳統登錄檔註冊與清單註冊並存、解除安裝清理漏洞(第 7.4 節),或舊版 ProgID 殘留。若只在舊選單出現兩次,想殘留;若新舊兩邊都出現,想並存。

隔離雙重顯示只在舊選單出現兩次指向清理漏洞或舊 ProgID 等殘留;新舊兩邊都出現兩次指向傳統登錄檔註冊與清單註冊並存僅舊選單新舊兩邊在哪裡出現兩次?殘留並存清理漏洞或留下的舊 ProgID傳統登錄檔註冊與新註冊並存

圖 18: 只在舊選單出現兩次指向殘留;新舊兩邊都出現兩次指向並存。

8.3. 檔案總管變重或崩潰

右鍵變慢、或特定資料夾崩潰時,先盤點已安裝的殼層擴充套件。用 NirSoft 的 ShellExView 等工具列出非 Microsoft 擴充套件,暫時禁用可疑項,再用二分查詢找出元兇 DLL。崩潰時,事件檢視器的「Faulting module」也是線索。若內部擴充套件是原因,懷疑選單構建路徑上的同步 I/O 或網路訪問(第 4.2 與 5.2 節)。

變重或崩潰時找出元兇 DLL在 ShellExView 列出非 Microsoft 殼層擴充套件,暫時禁用可疑項並用二分查詢找出元兇 DLL;崩潰時事件檢視器的 faulting module 也是線索盤點殼層擴充套件列出非 Microsoft 的暫時禁用並二分查詢找出元兇 DLL崩潰時檢查 faulting module

圖 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)。當場就該能估出工作規模。

相關文章

相關諮詢領域

小村軟體有限公司承接業務應用的檔案關聯、右鍵選單與殼層擴充套件的設計與實現;對準 Windows 11 新右鍵選單(遷移到 IExplorerCommand、引入 sparse package);檢視現有安裝程式的註冊與清理;以及檔案總管變重或崩潰的原因調查。從決定「藏在顯示更多選項下面的選單該怎麼辦」開始也可以。

參考連結

  1. Microsoft Learn, File Types. 關於副檔名鍵指向 ProgID 的結構、OpenWithProgIds、HKLM/HKCU\Software\Classes 的註冊分法、關聯更改後呼叫 SHChangeNotify(SHCNE_ASSOCCHANGED),以及解除安裝時刪除 ProgID 但留下副檔名鍵預設值。  2 3 4 5

  2. Microsoft Learn, HKEY_CLASSES_ROOT Key. 關於 HKEY_CLASSES_ROOT 是 HKLM\Software\Classes 與 HKCU\Software\Classes 的合併檢視、使用者端定義優先於電腦端,以及寫入時的分派規則。  2

  3. Microsoft Learn, Windows app defaults platform. 關於更改預設應用被設計成只能透過系統設定 UI、使用者設定資料已混淆並由篩選器驅動程式(UCPD.sys)防寫、以登錄檔為基礎的更改不受支援,以及在受管理環境使用組策略/MDM 策略。  2

  4. Microsoft Learn, Working with Shell Extensions. 關於殼層擴充套件處理程式的種類、擴充套件是載入到檔案總管(以及承載殼層的程序)的程序內 COM DLL 因此崩潰或掛住會波及整個檔案總管、以 ThreadingModel=Apartment 註冊,以及在寫殼層擴充套件之前先考慮更簡單的替代方案。  2 3

  5. Microsoft Learn, Guidance for Implementing In-Process Extensions. 關於 Microsoft 不建議也不支援用託管程式碼實現程序內殼層擴充套件、理由包括 CLR 版本衝突、重入與非確定性物件生存期,以及程序外擴充套件(預覽處理程式,或從 shell\verb\command 啟動)可以使用託管程式碼。  2 3

  6. Windows Developer Blog, Extending the Context Menu and Share Dialog in Windows 11. 關於 Windows 11 新右鍵選單的設計、透過 IExplorerCommand 加上應用標識擴充套件、把「開啟」與「開啟方式」放在上方、把多個命令收進應用名稱浮出,以及傳統 IContextMenu 擴充套件在「顯示更多選項」(Shift+F10)下以 Windows 10 選單載入。  2 3

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

  8. Microsoft Learn, Grant package identity by packaging with external location. 關於不更改現有安裝程式、以註冊外部位置包(sparse package)取得包標識、自 Windows 10 版本 2004 起可用,以及需要標識的 Windows 功能(右鍵選單註冊、通知等)變得可用。  2 3

  9. Microsoft Learn, Choosing a Static or Dynamic Shortcut Menu Method. 關於選擇符合需求的最簡單靜態 verb 方法、IContextMenu 最強但也最複雜並被歸到不建議那一側,以及 IExplorerCommand/IExplorerCommandState 才是建議方法。  2

  10. Microsoft Learn, SHChangeNotify function. 關於如何發出通知系統檔案關聯更改的 SHCNE_ASSOCCHANGED 事件,以及用它讓殼層察覺更改。  2

  11. Microsoft Learn, Application Registration. 關於建議透過 App Paths 子鍵註冊可執行檔案、Applications 子鍵的角色、透過 SystemFileAssociations 註冊 verb,以及預設應用更改時 ProgID 與相關資訊的優先順序。 

  12. Microsoft Learn, Creating Shortcut Menu Handlers. 關於靜態 verb 的註冊方法、預設 verb 的決定順序(預設值 → 第一個 verb → Open → Open With)、標準 verb 的顯示名稱由作業系統提供、透過 Extended 的擴充套件 verb、以 DDE 命令關聯已 Deprecated,以及 64 位環境的 WOW64 重新導向注意事項。  2 3

  13. Microsoft Learn, Verbs and File Associations. 關於 verb 也是 ShellExecuteEx 使用的動作、命令字串中可能含空格的元素需要用引號包住且 “%1” 一律加引號,以及在 HKCR\Applications 下注冊預設過程。 

  14. Microsoft Learn, IExplorerCommand interface. 關於 GetTitle、GetIcon、GetState、Invoke、EnumSubCommands 等方法組成、方法從 UI 執行緒呼叫因此不得與網路資源通訊,以及自 Windows Vista 起可用。 

  15. Microsoft Learn, Windows Sandbox. 關於能在幾秒內啟動一次性、隔離的 Windows 環境、關閉後丟棄所有更改、適合軟體測試與安裝程式驗證,以及在 Pro/Enterprise/Education 可用。 

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

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

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

常見問題

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

為什麼我們應用在 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。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽