系列文章:

Windows 11 - 自定義右鍵選單(IExplorerCommand 程式實作)


上一篇文章中有提到:它是一種讓傳統 Win32 應用能夠享有部分 UWP 功能的包裝方法,為縮小傳統 Win 32 應用程式與這些新 Windows API 和功能之間的差距,因而引入 Sparse Package 技術。

Sparse Package 技術是 MSIX 打包技術的一種延伸。MSIX 是一種新的Windows 應用程式套件格式,可為所有 Windows 應用程式提供新式封裝體驗。 MSIX 套件格式除了支援對 Win32、WPF 和 Windows Forms 應用程式的新式封裝和部署功能,也保留了現有應用程式套件和/或安裝檔案的功能。

更多關於MSIX可以參考微軟的官方文件:
什麼是 MSIX?



準備AppxManifest.xml

進行 Sparse Package 打包的第一步,就是需要準備一個關鍵文件:「AppxManifest.xml」。這個文件是整個 MSIX 打包過程的核心,它描述了打包的應用的元數據資訊。它是一個 XML 文件,其中包含系統部署、顯示及更新 MSIX 應用程式所需的資訊。 此資訊包括套件識別資料、套件相依性、必要功能、視覺元素和擴充點。以下為自定義右鍵選單的 AppxManifest.xml 範例:
<?xml version="1.0" encoding="utf-8"?>
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
  xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
  xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
  xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
  IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop4 desktop5 uap10 com">
  <Identity Name="Win11ContextMenuDemo" ProcessorArchitecture="neutral" Publisher="CN=MomoChenIsMe, C=TW" Version="1.0.0.0" />
  <Properties>
    <DisplayName>Win11ContextMenuDemo</DisplayName>
    <PublisherDisplayName>Win11ContextMenuDemo</PublisherDisplayName>
    <Logo>Assets\Logo.png</Logo>
    <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
  </Properties>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.22000.0" MaxVersionTested="10.0.22621.0" />
  </Dependencies>
  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
    <rescap:Capability Name="unvirtualizedResources"/>
  </Capabilities>
  <Applications>
    <Application Id="Win11ContextMenuDemo" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
      <uap:VisualElements AppListEntry="none" DisplayName="Win11ContextMenuDemo" Description="Win11ContextMenuDemo" BackgroundColor="transparent" Square150x150Logo="Assets\Logo.png" Square44x44Logo="Assets\Logo.png">
      </uap:VisualElements>
      <Extensions>
        <desktop4:Extension Category="windows.fileExplorerContextMenus">
          <desktop4:FileExplorerContextMenus>
            <desktop5:ItemType Type="Directory\Background">
              <desktop5:Verb Id="MainMenuWithWin11ContextMenuDemo" Clsid="15589FA6-768B-4826-97B8-D12DE265B3BB" />
            </desktop5:ItemType>
          </desktop4:FileExplorerContextMenus>
        </desktop4:Extension>
        <com:Extension Category="windows.comServer">
          <com:ComServer>
            <com:SurrogateServer DisplayName="Win11ContextMenuDemo Shell Extension">
              <com:Class Id="15589FA6-768B-4826-97B8-D12DE265B3BB" Path="Win11ContextMenuDemo.dll" ThreadingModel="STA"/>
            </com:SurrogateServer>
          </com:ComServer>
        </com:Extension>
      </Extensions>
    </Application>
  </Applications>
</Package>
在準備 AppxManifest.xml 文件的過程中,有幾個重要的屬性需要注意和設定,接下來讓我們依序解說:

應用程式基本資訊

<Identity Name="Win11ContextMenuDemo" ProcessorArchitecture="neutral" Publisher="CN=MomoChenIsMe, C=TW" Version="1.0.0.0" />
<Identity> 屬性用於指定應用程序的基本資訊,這些基本資訊包含以下:
  • Name:應用程式的名稱。
  • ProcessorArchitecture:處理器架構。在這裡,設置為 "neutral" 表示應用程式是和處理器無關的。
  • Publisher:發行者身份。簽署 MSIX 時會用到。
  • Version:應用程式的版本號。


應用程式基本屬性

<Properties>
  <DisplayName>Win11ContextMenuDemo</DisplayName>
  <PublisherDisplayName>Win11ContextMenuDemo</PublisherDisplayName>
  <Logo>Assets\Logo.png</Logo>
  <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
</Properties>

文件中的 <Properties> 部分是用來定義應用程式的一些基本屬性和顯示設定,包含以下屬性:

  • DisplayName:應用程式在 Windows 操作系統中顯示的名稱。
  • PublisherDisplayName:發行者的顯示名稱。
  • Logo:應用程式的Icon或Logo。
  • uap10:AllowExternalContent:這表示應用可以引用和使用不在原始 MSIX 套件內的文件和資源。
    💡 這裡需要設定為「true」,這樣我們可以從外部位置來封裝。詳細的資訊可以【點我前往】參考。


文件資源管理器上下文選單擴展 (windows.fileExplorerContextMenus)

<desktop4:Extension Category="windows.fileExplorerContextMenus">
  <desktop4:FileExplorerContextMenus>
    <desktop5:ItemType Type="Directory\Background">
      <desktop5:Verb Id="MainMenuWithWin11ContextMenuDemo" Clsid="15589FA6-768B-4826-97B8-D12DE265B3BB" />
    </desktop5:ItemType>
  </desktop4:FileExplorerContextMenus>
</desktop4:Extension>
<desktop4:FileExplorerContextMenus> 內,我們定義了對特定項目類型的處理方式:
  • <desktop5:ItemType Type="Directory\Background">:表示這個擴展會出現在目錄的背景(空白區域)的右鍵選單中。
  • <desktop5:ItemType Type="Directory">:表示這個擴展會出現在資料夾本身。
  • <desktop5:ItemType Type="*">:表示這個擴展會出現在所有檔案格式。
而 <desktop5:ItemType Type="Directory\Background"> 內的 <desktop5:Verb> 標籤則指定了實際要執行的動作或命令,通過 Id 和 Clsid 屬性來引用相關的類別和功能。
💡 這裡的 Clsid 就是我們上一篇為主選單所設定的延伸模組識別 UUID。


COM 伺服器擴展 (windows.comServer)

<com:Extension Category="windows.comServer">
  <com:ComServer>
    <com:SurrogateServer DisplayName="Win11ContextMenuDemo Shell Extension">
      <com:Class Id="15589FA6-768B-4826-97B8-D12DE265B3BB" Path="Win11ContextMenuDemo.dll" ThreadingModel="STA"/>
    </com:SurrogateServer>
  </com:ComServer>
</com:Extension>
<com:ComServer> 內部的 <com:SurrogateServer> 定義了一個代理伺服器,用於管理 COM 對象。在這個例子中,DisplayName 屬性為這個 COM 伺服器命名。
<com:Class> 標籤定義了一個 COM 類:
  • Id:屬性指定了 COM 類的唯一識別碼和上方的 Clsid 是同一組 Id。
  • Path:指明了實現這個類的 DLL 檔案的路徑。
  • ThreadingModel:則指定了該類的線程模型。
    💡 可以設定「STA」或「MTA」。



打包 Sparse package 套件

在完成 AppxManifest.xml 文件的準備和 DLL 文件的編譯後,我們的下一步是將這些文件進行打包。首先,將準備好的 AppxManifest.xml 文件和編譯好的 DLL 文件放置在同一個資料夾中。
這樣做的好處就是等等註冊 DLL 時,系統可以直接抓到同層的 MSIX 檔案進行安裝。
💡 上一篇文章中,我們註冊 DLL 時會執行 InstallContextMenu 方法,該方法就會在執行階段使用當前的路徑,找到MSIX進行 Sparse Package 的安裝。

接著,我們將使用在上一篇文章中安裝的 Windows 11 SDK 內包含的工具「Makeappx」來進行打包。makeappx.exe 是一個命令行工具,專門用於創建 MSIX 或 APPX 套件。透過使用這個工具,我們可以將 AppxManifest.xml 包裝成一個 MSIX 套件。打包的命令如下:
makeappx pack /d <來源資料夾路徑> /p <輸出套件文件路徑>
其中「來源資料夾路徑」是放置 AppxManifest.xml 的資料夾路徑,而「輸出套件文件路徑」則是你希望生成的套件文件的儲存位置和文件名。



簽署 Sparse Package

在成功產生 MSIX 套件之後,重要的一步是對套件進行簽名。這是因為 Windows 系統出於安全考慮,要求所有的 MSIX 套件必須被正確簽名後才能安裝。所以,在這個階段,儘管我們已經有了 MSIX 套件,但我們還不能直接進行安裝。

為了簽署 MSIX 套件,我們通常需要一個有效的憑證。然而,對於開發和測試階段,我們可以使用「自我簽署憑證」來完成這一過程。自簽名憑證可以由開發者自己生成,雖然它不被操作系統預設為信任的,但足以用於開發和測試目的。

另外在 DLL 內安裝 Sparse Package 的程式碼中,我們也需要添加並執行一個 AllowUnsigned 的方法,這樣就可以在開發模式下安裝這個 MSIX 套件了。
// 註冊 Sparse Package。
// Registers the Sparse Package.
HRESULT RegisterSparsePackage()
{
	const wstring contextMenuDirectoryPath = Win11ContextMenuDemo::Path::GetContextMenuDirectoryPath();
	const wstring sparsePackageFullPath = contextMenuDirectoryPath + L"\\" + CONTEXTMENUNAME + L".msix";

	Uri externalLocationUri(contextMenuDirectoryPath);
	Uri packageUri(sparsePackageFullPath);

	AddPackageOptions options;
	options.ExternalLocationUri(externalLocationUri);
	// 允許使用 AllowUnsigned
	options.AllowUnsigned(true);

	PackageManager packageManager;
	auto deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
	auto deployResult = deploymentOperation.get();

	if (!SUCCEEDED(deployResult.ExtendedErrorCode()))
	{
		return deployResult.ExtendedErrorCode();
	}

	return S_OK;
}

接著我們使用 PowerShell 來建立自我簽署憑證,我們可以使用「New-SelfSignedCertificate」進行簽署。命令為以下:
New-SelfSignedCertificate -Type Custom -Subject "CN=MomoChenIsMe, C=TW" -KeyUsage DigitalSignature -FriendlyName "MomoChenIsMe" -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
  • Subject:參數值必須和 AppxManifest.xml 檔案內 <Identity> 屬性中的 Publisher 一致。
  • KeyUsage :定義憑證的用途。 自我簽署憑證應設定為 DigitalSignature。
  • FriendlyName:方便識別憑證的名稱。
  • CertStoreLocation :儲存憑證的路徑。預設為:「Cert:\CurrentUser\My」
  • TextExtension :允許添加一些非標準的或特定的擴展到憑證中。
關於 New-SelfSignedCertificate 的詳細資訊可以參考微軟官方文件:

如果要檢視剛才在「Cert:\CurrentUser\My」建立的憑證可以使用以下命令:
Set-Location Cert:\CurrentUser\My
Get-ChildItem

如果需要刪除憑證則可以使用以下命令:
Remove-Item Cert:\CurrentUser\My\<憑證的 Thumbprint>
最後我們需要匯出該憑證檔案,請使用「Export-PfxCertificate」。使用時必須附帶參數「-Password」使用密碼,或使用「-ProtectTo」添加使用者或群組。以下為使用密碼的命令:
$password = ConvertTo-SecureString -String <使用密碼> -Force -AsPlainText 
Export-PfxCertificate -cert "Cert:\CurrentUser\My\<憑證的 Thumbprint>" -FilePath <輸出憑證文件路徑>.pfx -Password $password

生成完自我簽署證書(pxf)並且匯出後,我們可以使用「SignTool」工具來對 MSIX 套件進行簽名。命令如下:
signtool sign /a /fd SHA256 /f <憑證文件路徑> /p <憑證密碼> <MSIX套件路徑>



註冊 DLL

完成以上所有步驟後,我們就可以在開發和測試環境中安裝和運行我們的 MSIX 套件。當我們註冊 DLL 時會執行「InstallContextMenu」方法,該方法就會在執行階段使用當前的路徑找到打包簽署好的 MSIX 進行安裝。一旦 Sparse Package 安裝完成,我們的擴展功能在啟動時便會自動生效。這意味著,當條件滿足時(例如在特定場景下右鍵點擊),我們的 DLL 就會被調用,進而產生自定義的右鍵選單。

我們可以使用以下命令註冊 DLL :
regsvr32.exe <DLL 文件路徑>

若要移除先前創建的自定義右鍵選單,我們可以透過註銷先前註冊的 DLL 即可移除。註銷的過程中,DLL 程式碼會執行「UnInstallContextMenu」方法來執行必要的清理和解除 Sparse Package 安裝。註銷的命令為以下:
regsvr32.exe /u <DLL 文件路徑>



總結

在這一系列文章中,展示了在 Windows 11 中自定義右鍵選單的整個過程。由於相關的中文資源相對較少,我在研究的過程都是靠著 Notepad++ 的 NppShell 開源專案慢慢去理解。這些經驗雖然可能在某些觀念上不盡完全正確,特別是在 Sparse Package 實作部分,但我仍希望能夠通過分享實戰經驗來幫助有需要建置 Windows 11 自定義右鍵選單的人!



Demo 專案 Github 程式碼




參考資料