大家應該都知道,微軟在 Windows 11 中對右鍵選單進行了一番大改造,從 XP 時代就開始的經典設計被換成了全新的操作模式和樣式。這種突如其來的變化讓不少人感到不適應,甚至有些人試圖透過修改機碼的方式,把舊版的右鍵選單找回來。但今天我想跟大家分享的不是如何恢復舊版選單,而是如何在新版的右鍵選單中添加自定義選項。
要完成這項任務,我們將會涉及到 C++ 語言的使用。我得坦白說,我本人並沒有寫過 C++,所以這次的範例大多是參考了 Notepad++ 的 NppShell.dll 的開源碼,然後再透過 ChatGPT 一點一滴的學習和調整,所以可能有些許的錯誤就請多多包涵。
NppShell Github 專案:
NppShell Github 專案
如何實現 Windows 11 自定義選單
要在 Windows 11 中實現自定義右鍵選單,根據微軟在開發者部落格的介紹,我們可以使用以下兩種方式實現:- 利用 IExplorerCommand 介面:IExplorerCommand 介面是 Windows Shell 擴展的一部分,專門用於定義和控制檔案總管中的右鍵選單命令。
- 使用 Sparse Package 技術:在早期的 Windows 版本中,傳統 Win32 應用程式是主要的應用程式類型,它們通常不包含對操作系統深度整合功能的存取,例如:「背景任務」、「通知」、「動態磚」、「分享」等。而這些功能主要是針對 UWP(通用 Windows 平台)應用程式設計的,它們具有更深層次的操作系統整合。所以在 Windows 新一點的版本中,微軟試圖縮小傳統 Win32 應用程式與這些新 Windows API 和功能之間的差距,因而引入Sparse Package技術。它允許這些傳統應用程式維持它們現有的檔案布局和部署方式,同時獲得對新 Windows API 的存取能力。
關於完整的 Sparse Package 介紹可以參考微軟的開發者部落格:
實作 IEnumExplorerCommand 儲存子菜單
完成 IExplorerCommand 接口的基礎實作之後,下一步是實作 IEnumExplorerCommand,它也是 Windows Shell 擴展的一部分,它允許開發者枚舉(列出)在檔案總管的右鍵選單中應該顯示的命令。這個接口對於管理多個自定義命令,特別是當你想在右鍵選單中添加多個選項時,變得尤為重要。和 IExplorerCommand 一樣,需要實做一些必要的方法:完成了 IEnumExplorerCommand 接口的基本實作後,我們的下一步是在主選單類別的「EnumSubCommands」方法中創建子選單並將其加入到 IEnumExplorerCommand 中:以上就是自定義右鍵選單的主選單和子選單的建置方法。篇幅的關係,部分的程式碼實作就請到我的 Github 上參考囉。
以上就是使用 C++ 實現 IExplorerCommand 自定義選單的建置和開發過程。接下我們只需要編譯專案產生出 DLL 後,透過 Sparse Package 打包並使用 regsver32 註冊該 DLL 就可以完成 Windows 11 自定義的右鍵選單安裝,不過由於 Sparse Package 的實作和註冊 DLL 涉及多個步驟,我們將在下一篇文章中進行更詳細的介紹。
準備工具
首先我們需要準備 Visual Studio 2022 並且需要安裝 C++ 的工具。接著還需要安裝 Windows 11 的 SDK(軟體開發套件)。透過這個連結【點我前往】去下載並安裝。SDK 是開發 Windows 應用時必備的一套工具和庫,它讓我們可以更方便地存取系統功能。安裝完 SDK 之後可以到此路徑中「C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64」找到兩個稍後(Sparse Package 實作中會介紹)會用到的工具:「makeappx.exe」和「signtool.exe」這些工具將幫助我們在開發過程中打包和簽署應用。
安裝 NuGet 套件和專案設定
在 Visual Studio 2022 中,你需要建立一個新的 DLL 專案。選擇 C++ 作為開發語言。DLL(Dynamic Link Library)專案將允許我們創建可由 Windows 操作系統加載和執行的程式庫文件。
專案建立完成後,需要安裝以下兩個 NuGet 套件:
- Microsoft.Windows.CppWinRT:這個套件提供了 C++/WinRT 的支援,使用符合標準的 C++17 編譯器,用於 Windows Runtime(WinRT)API。這讓我們能夠更容易地在 C++ 中調用 WinRT API。
- Microsoft.Windows.ImplementationLibrary:這個套件則提供了實現 Windows API 程式庫的工具,幫助我們在專案中更加方便地實現 Windows 功能。
實作 IExplorerCommand 右鍵選單
接下來的重點就是實作自定義的右鍵選單。要成功地在 Windows 11 中添加自己的選單項目,我們需要利用 Windows 的 IExplorerCommand 接口來實現這個功能。它是 Windows Shell 擴展的一部分,它允許開發者定義和控制在檔案總管的上下文選單(也就是右鍵選單)中出現的命令。在這個類別中,你需要實做一些必要的方法:namespace Win11ContextMenuDemo::ExplorerCommand
{
// BaseExplorerCommand 是一個基礎類,用於實現 IExplorerCommand 介面。
// BaseExplorerCommand is a base class for implementing the IExplorerCommand interface.
class BaseExplorerCommand : public winrt::implements<BaseExplorerCommand, IExplorerCommand>
{
protected:
// 獲取多語言標題的方法。
// Method for obtaining the multi-language title.
virtual const wstring GetMultiLanguageTitle();
// 獲取圖標路徑的方法。
// Method for obtaining the icon path.
virtual const wstring GetIconPath();
// 用於存儲標題資源 ID 的成員變數。
// Member variable to store the title resource ID.
UINT titleResourceID = ID_WIN11CONTEXTMENUDEMO_DEFAULT;
// 用於存儲圖標檔案名的成員變數。
// Member variable to store the icon file name.
wstring iconFileName = L"";
public:
// 設置標題資源 ID 的方法。
// Method to set the title resource ID.
virtual void SetTitleResourceID(UINT id);
// 設置圖標檔案名的方法。
// Method to set the icon file name.
virtual void SetIconFileName(wstring fileName);
// 獲取命令的標題。
// Get the title of the command.
virtual IFACEMETHODIMP GetTitle(IShellItemArray* psiItemArray, LPWSTR* ppszName);
// 獲取命令的圖標。
// Get the icon of the command.
virtual IFACEMETHODIMP GetIcon(IShellItemArray* psiItemArray, LPWSTR* ppszIcon);
// 獲取命令的工具提示。
// Get the tooltip of the command.
virtual IFACEMETHODIMP GetToolTip(IShellItemArray* psiItemArray, LPWSTR* ppszInfotip);
// 獲取命令的狀態。
// Get the state of the command.
virtual IFACEMETHODIMP GetState(IShellItemArray* psiItemArray, BOOL fOkToBeSlow, EXPCMDSTATE* pCmdState);
// 獲取命令的旗標。
// Get the flags of the command.
virtual IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags);
// 獲取命令的標準名稱。
// Get the canonical name of the command.
virtual IFACEMETHODIMP GetCanonicalName(GUID* pguidCommandName);
// 枚舉子命令。
// Enumerate sub-commands.
virtual IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** ppEnum);
// 執行命令的方法,必須由派生類實現。
// The method to execute the command, which must be implemented by derived classes.
IFACEMETHODIMP Invoke(IShellItemArray* psiItemArray, IBindCtx* pbc) noexcept override;
};
}
- GetTitle:用於獲取選單項目的標題。
- GetIcon:設定選單圖標。
- GetFlags:設定選單是的旗標。💡 這個用於設定是否為普通選單或是包含子項目的選單。
- EnumSubCommands:這個方法負責創建和管理子選單。
- Invoke:定義選擇該選單項目時執行的指令動作。
不論是主選單還是子選單,我們都會利用實現 IExplorerCommand 介面的方式來建立各種選項。為了提高開發效率和維護的方便性,因此我創建的是一個 BaseExplorerCommand 的基礎類別,這樣就能夠方便地通過繼承來實作所需的主選單(Main)和子選單(Sub)。
在主選單中,不需要執行任何命令,因此不需要實作「Invoke」。但是需要實作「GetFlags」和「EnumSubCommands」來實現子選單。
#include "BaseExplorerCommand.h"
namespace Win11ContextMenuDemo::ExplorerCommand
{
// MainExplorerCommand 是用於實現主選單的類別,繼承自 BaseExplorerCommand。
// MainExplorerCommand is a class used to implement the main menu, inheriting from BaseExplorerCommand.
class __declspec(uuid("15589FA6-768B-4826-97B8-D12DE265B3BB")) MainExplorerCommand : public BaseExplorerCommand
{
public:
// 構造函數。
// Constructor.
MainExplorerCommand();
// 獲取主選單的旗標。
// Get the flags of the main menu.
IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override;
// 枚舉主選單的子命令。
// Enumerate the sub-commands of the main menu.
IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** ppEnum) override;
};
}
💡 記得!主選單一定要設定一組 UUID 供延伸模組做識別。
在子選單中,只需要實作「Invoke」。這裡為了方便展示,執行一個簡單的 cmd 命令:
#include "BaseExplorerCommand.h"
namespace Win11ContextMenuDemo::ExplorerCommand
{
// Sub1ExplorerCommand 是一個實現子選單的類別,繼承自 BaseExplorerCommand。
// Sub1ExplorerCommand is a class that implements a submenu, inheriting from BaseExplorerCommand.
class Sub1ExplorerCommand : public BaseExplorerCommand
{
public:
// 構造函數。
// Constructor.
Sub1ExplorerCommand();
// 執行子選單的命令邏輯。
// Execute the command logic of the submenu.
IFACEMETHODIMP Invoke(IShellItemArray* psiItemArray, IBindCtx* pbc) noexcept override;
};
}
IFACEMETHODIMP Sub1ExplorerCommand::Invoke(IShellItemArray* psiItemArray, IBindCtx* pbc) noexcept try
{
UNREFERENCED_PARAMETER(psiItemArray);
UNREFERENCED_PARAMETER(pbc);
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
wstring commandLine = L"cmd.exe /C echo Hello World by Sub1Win11ContextMenu. && pause;";
if (!CreateProcessW(nullptr, (LPWSTR)commandLine.c_str(), nullptr, nullptr, false, 0, nullptr, nullptr, &si, &pi))
{
return S_OK;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return S_OK;
}
CATCH_RETURN();
💡 子選單點擊後會觸發 Invoke 方法。該方法簡單的執行一個 cmd 並顯示 Hello World by Sub1Win11ContextMenu 字串。
實作 IEnumExplorerCommand 儲存子菜單
完成 IExplorerCommand 接口的基礎實作之後,下一步是實作 IEnumExplorerCommand,它也是 Windows Shell 擴展的一部分,它允許開發者枚舉(列出)在檔案總管的右鍵選單中應該顯示的命令。這個接口對於管理多個自定義命令,特別是當你想在右鍵選單中添加多個選項時,變得尤為重要。和 IExplorerCommand 一樣,需要實做一些必要的方法:namespace Win11ContextMenuDemo::ExplorerCommand
{
// EnumExplorerCommand 是一個類,用於實現 IEnumExplorerCommand 介面,用於枚舉 IExplorerCommand 物件。
// EnumExplorerCommand is a class that implements the IEnumExplorerCommand interface for enumerating IExplorerCommand objects.
class EnumExplorerCommand : public winrt::implements<EnumExplorerCommand, IEnumExplorerCommand>
{
public:
// 構造函數,接收一個 IExplorerCommand 物件列表。
// Constructor that takes a list of IExplorerCommand objects.
EnumExplorerCommand(std::vector<winrt::com_ptr<IExplorerCommand>> commands);
// Next 方法用於獲取序列中的下一個命令。
// The Next method is used to retrieve the next command in the sequence.
IFACEMETHODIMP Next(ULONG celt, __out_ecount_part(celt, *pceltFetched) IExplorerCommand** apUICommand, __out_opt ULONG* pceltFetched);
// Skip 方法用於跳過序列中的一定數量的命令。
// The Skip method is used to skip a certain number of commands in the sequence.
IFACEMETHODIMP Skip(ULONG celt);
// Reset 方法重置枚舉器到初始狀態。
// The Reset method resets the enumerator to its initial state.
IFACEMETHODIMP Reset();
// Clone 方法用於創建此枚舉器的一個副本。
// The Clone method is used to create a copy of this enumerator.
IFACEMETHODIMP Clone(__deref_out IEnumExplorerCommand** ppenum);
private:
// 儲存一系列的 IExplorerCommand 物件。
// Stores a series of IExplorerCommand objects.
std::vector<winrt::com_ptr<IExplorerCommand>> sub_commands;
// 當前枚舉器的索引。
// The current index of the enumerator.
size_t currentIndex = 0;
};
}
IFACEMETHODIMP MainExplorerCommand::EnumSubCommands(IEnumExplorerCommand** ppEnum)
{
vector<winrt::com_ptr<IExplorerCommand>> subCommands;
auto subCommand = winrt::make<Sub1ExplorerCommand>();
subCommands.push_back(subCommand.as<IExplorerCommand>());
auto enumCommands = winrt::make_self<EnumExplorerCommand>(move(subCommands));
*ppEnum = enumCommands.detach();
return S_OK;
}
為了讓系統識別主選單包含子選單,我們需要適當地調整主選單類別中的「GetFlags」方法。當使用者互動時,系統才能正確地顯示子選單。可以通過返回「ECF_HASSUBCOMMANDS」的標誌值來實現:
IFACEMETHODIMP MainExplorerCommand::GetFlags(EXPCMDFLAGS* flags)
{
*flags = ECF_HASSUBCOMMANDS;
return S_OK;
}
註冊選單和安裝 Sparse package
完成右鍵選單的建置之後,為了讓這些自定義功能能夠在 Windows 11 中順利運作,我們還需要透過程式碼來註冊選單和安裝 Sparse Package。
- 註冊 Windows 舊右鍵選單:雖然我們的主要目標是實現 Windows 11 的新右鍵選單,但為了向後兼容,我們也需要考慮在舊版本的 Windows 中如何展示我們的自定義選單。
- 安裝 Sparse Package 以支援 Windows 11 的新選單:目前我知道在 Windows 11 中添加自定義選單,必須透過 Sparse Package 來實現,因此如果是 Windows 11 系統就必須多執行這一步。
// 定義 InstallContextMenu 方法
HRESULT Win11ContextMenuDemo::InstallContextMenu::InstallContextMenu()
{
HRESULT result;
// 檢查是否為 Windows 11 系統
if (Win11ContextMenuDemo::Windows11Checker::IsWindows11())
{
// 如果是 Windows 11,則先註銷之前的右鍵選單
UnRegisterContextMenu();
// 創建一個新的線程來執行 Sparse Package 安裝程序
thread registerSparsePackageProgramThread(RegisterSparsePackageProgram);
// 等待剛剛創建的線程結束
registerSparsePackageProgramThread.join();
}
// 註冊右鍵選單
result = RegisterContextMenu();
// 通知系統關聯設置發生了變化
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0);
return S_OK;
}
// 定義 UnInstallContextMenu 方法
HRESULT Win11ContextMenuDemo::InstallContextMenu::UnInstallContextMenu()
{
HRESULT result;
// 嘗試註銷右鍵選單
result = UnRegisterContextMenu();
// 檢查註銷操作是否成功
if (result != S_OK)
{
// 如果注銷失敗,則返回錯誤代碼
return result;
}
// 檢查是否為 Windows 11 系統
if (Win11ContextMenuDemo::Windows11Checker::IsWindows11())
{
// 如果是 Windows 11,則創建一個新的線程來執行 Sparse Package 解除安裝程序
thread unRegisterSparsePackageThread(UnRegisterSparsePackageProgram);
// 等待線程結束
unRegisterSparsePackageThread.join();
}
// 通知系統關聯設置發生了變化
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0);
return S_OK;
}
然後我們只需要確保「InstallContextMenu」和「UnInstallContextMenu」能夠在註冊 DLL 和註銷 DLL 時正確執行,即可在程式碼中完成選單的安裝和解除安裝。
💡 相關的 InstallContextMenu 和 UnInstallContextMenu 的實作,可以參考 Github 程式碼。
建置 DLLMain
最後為了讓 Windows Shell 能夠順利地獲取並使用我們的自定義右鍵選單,我們需要建置 DLLMain 函數並實作幾個關鍵的對外接口。這些接口是 Windows Shell 用來與 DLL 互動的橋樑,對於確保我們自定義功能的正常。以下為 DLLMain 的程式碼:
// 全域變數,用來儲存 DLL 模組的句柄。
// Global variable to store the handle of the DLL module.
HMODULE g_module;
// DLL 的主入口函數。
// Main entry point function for the DLL.
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// 當 DLL 被載入到程序時執行。
// Executed when the DLL is loaded into a process.
g_module = hModule;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// 註冊 DLL 以便用於上下文選單擴展。
// Register the DLL for context menu extension.
STDAPI DllRegisterServer()
{
return Win11ContextMenuDemo::InstallContextMenu::InstallContextMenu();
}
// 注銷 DLL。
// Unregister the DLL.
STDAPI DllUnregisterServer()
{
return Win11ContextMenuDemo::InstallContextMenu::UnInstallContextMenu();
}
// 返回一個類工廠以創建對象的實例。
// Returns a class factory to create an object's instance.
_Use_decl_annotations_ STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) try
{
*ppv = nullptr;
if (rclsid == __uuidof(Win11ContextMenuDemo::ExplorerCommand::MainExplorerCommand))
{
// 如果請求的是 MainExplorerCommand 的類工廠,則創建並返回。
// Create and return the class factory if MainExplorerCommand is requested.
return winrt::make<Win11ContextMenuDemo::ClassFactory::COMClassFactory<Win11ContextMenuDemo::ExplorerCommand::MainExplorerCommand>>().as(riid, ppv);
}
else
{
// 如果沒有可用的類,則返回錯誤。
// Return an error if the class is not available.
return CLASS_E_CLASSNOTAVAILABLE;
}
}
catch (...)
{
// 捕捉並處理任何異常。
// Catch and handle any exceptions.
return winrt::to_hresult();
}
// 檢查是否可以註銷 DLL。
// Check whether the DLL can be unloaded.
__control_entrypoint(DllExport) STDAPI DllCanUnloadNow()
{
// 檢查是否有活動的對象或類工廠。
// Check for active objects or class factories.
if (winrt::get_module_lock())
{
// 如果有,則不能註銷。
// Cannot unload if there are.
return S_FALSE;
}
else
{
// 如果沒有,則可以註銷。
// Can unload if there are none.
return S_OK;
}
}
- DLLMain:是 DLL 的主要入口點。它在 DLL 被加載、註銷,或者執行緒創建和終止時被調用。
- DllRegisterServer和DllUnregisterServer:用於註冊或註銷 DLL時執行。為了讓系統能夠識別並使用我們的自定義選單,需要在這兩個方法中分別呼叫剛才建立的「InstallContextMenu」和「UnInstallContextMenu」進行選單的安裝和解除安裝。
- DllGetClassObject:它負責返回一個用於創建 COM 元件的 IClassFactory 工廠對象。我們需要檢查傳入的參數是否符合我們主選單所設定的 UUID。如果檢查通過,則回傳一個 IClassFactory 對象,該對象為我們自定義的主選單。💡 IClassFactory 的主要作用是創建 COM 元件。在 COM 編程模型中,元件的創建通常是透過工廠對象來完成。
- DllCanUnloadNow:用於確定 DLL 是否可以被安全註銷,確保在不再需要時,DLL 可以被正確且安全地註銷。
0 Comments
張貼留言