ShellProc 是 Windows Shell 鉤子 (WH_SHELL) 的回調函數。它允許應用程式接收 Shell 相關的重要事件通知,如視窗建立/銷毀、啟用/停用、任務列按鈕變更、系統語言切換、螢幕保護程式啟動/停止、閃爍視窗等。這是開發任務管理器、視窗管理工具、自動化測試框架、無障礙輔助技術的核心 API。
LRESULT CALLBACK ShellProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
nCode 決定事件類型,wParam 通常為視窗控制代碼 (HWND),lParam 視事件而定。
| nCode 值 | 常數名稱 | 說明 | wParam | lParam |
|---|---|---|---|---|
| 1 | HSHELL_WINDOWCREATED | 頂層視窗被建立 | HWND (新視窗) | 0 |
| 2 | HSHELL_WINDOWDESTROYED | 頂層視窗被銷毀 | HWND (舊視窗) | 0 |
| 3 | HSHELL_ACTIVATESHELLWINDOW | Shell 視窗被啟用 (如點擊任務列按鈕) | HWND | 0 |
| 4 | HSHELL_WINDOWACTIVATED | 頂層視窗被啟用 (獲得焦點) | HWND (啟用視窗) | 0 |
| 5 | HSHELL_GETMINRECT | 請求最小化視窗矩形 (動畫用) | HWND | PRECT (輸出矩形) |
| 6 | HSHELL_REDRAW | 標題列/任務列按鈕需重繪 | HWND | 0 |
| 7 | HSHELL_TASKMAN | 啟動任務管理器 (Ctrl+Shift+Esc) | 0 | 0 |
| 8 | HSHELL_LANGUAGE | 系統輸入語言變更 | HKL (新鍵盤佈局) | 0 |
| 9 | HSHELL_SYSMENU | 系統選單啟動 (Alt+Space 或右鍵標題列) | HWND | 0 |
| 10 | HSHELL_ENDTASK | 請求結束應用程式 (任務列右鍵關閉) | HWND | 0 |
| 11 | HSHELL_ACCESSIBILITYSTATE | 無障礙狀態變更 | 狀態旗標 | 0 |
| 12 | HSHELL_APPCOMMAND | 應用程式命令 (多媒體鍵、瀏覽器鍵) | HWND | 命令資訊 (GET_APPCOMMAND_LPARAM) |
| 13 | HSHELL_WINDOWREPLACED | 視窗被替換 (如 UAC 同意視窗替換原視窗) | HWND (新視窗) | HWND (舊視窗) |
| 14 | HSHELL_WINDOWREPLACING | 視窗即將被替換 | HWND (新視窗) | HWND (舊視窗) |
| 15 | HSHELL_HIGHBIT | 高位標誌 (配合其他代碼表示特殊狀態) | - | - |
| 16 | HSHELL_FLASH | 視窗閃爍通知 (FlashWindowEx) | HWND | 0 |
| 17 | HSHELL_RUDEAPPACTIVATED | 粗魯應用啟用 (全螢幕獨佔模式啟動) | HWND | 0 |
| 18 | HSHELL_RUDEAPPSHELLWINDOW | 粗魯 Shell 視窗啟用 | HWND | 0 |
HSHELL_GETMINRECT:在 lParam 指向的 RECT 填入最小化動畫目標矩形,返回 TRUEHSHELL_ENDTASK:返回 TRUE 表示允許結束,FALSE 表示攔截 (但系統最終仍可強制結束)// WH_SHELL 必須是全域鉤子,且需要 DLL 注入
// 但有一個特例:使用 SetWindowsHookEx 時 hMod 參數可為 NULL
// 系統會自動將鉤子注入所有符合條件的進程
hHook := SetWindowsHookEx(WH_SHELL, @ShellProc, HInstance, 0);
// 第4參數 0 表示所有執行緒 (全域)
// HInstance 必須是包含 ShellProc 的 DLL 模組句柄
// 若在 EXE 中直接呼叫,需將 ShellProc 放在 DLL 中
library ShellHookDLL;
uses
Windows,
Messages,
SysUtils,
Classes;
{$R *.res}
var
g_hHook: HHOOK = 0;
g_MainWnd: HWND = 0; // 主程式視窗句柄,用於轉發事件
// 導出:設定主程式視窗句柄
procedure SetMainWindow(hWnd: HWND); stdcall;
begin
g_MainWnd := hWnd;
end;
function ShellProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
Msg: UINT;
Wnd: HWND;
Rect: PRect;
Cmd: Integer;
begin
Result := 0;
// 只處理我們關心的事件
case nCode of
HSHELL_WINDOWCREATED,
HSHELL_WINDOWDESTROYED,
HSHELL_WINDOWACTIVATED,
HSHELL_WINDOWREPLACED,
HSHELL_FLASH,
HSHELL_RUDEAPPACTIVATED,
HSHELL_APPCOMMAND:
begin
Wnd := HWND(wParam);
// 發送自定義訊息給主程式處理 (避免在鉤子回調中做複雜邏輯)
if g_MainWnd <> 0 then
begin
case nCode of
HSHELL_WINDOWCREATED: Msg := WM_USER + 101;
HSHELL_WINDOWDESTROYED: Msg := WM_USER + 102;
HSHELL_WINDOWACTIVATED: Msg := WM_USER + 103;
HSHELL_WINDOWREPLACED: Msg := WM_USER + 104;
HSHELL_FLASH: Msg := WM_USER + 105;
HSHELL_RUDEAPPACTIVATED: Msg := WM_USER + 106;
HSHELL_APPCOMMAND: Msg := WM_USER + 107;
else Msg := 0;
end;
if Msg <> 0 then
begin
// lParam 傳遞視窗句柄,wParam 傳遞額外資訊
PostMessage(g_MainWnd, Msg, wParam, LPARAM(Wnd));
end;
end;
end;
HSHELL_GETMINRECT:
begin
// 系統請求最小化動畫目標矩形
Rect := PRect(lParam);
if (Rect <> nil) and (HWND(wParam) <> 0) then
begin
// 返回視窗當前位置作為最小化目標
GetWindowRect(HWND(wParam), Rect^);
Result := 1; // 表示已填寫矩形
end;
end;
HSHELL_LANGUAGE:
begin
if g_MainWnd <> 0 then
PostMessage(g_MainWnd, WM_USER + 108, wParam, 0);
end;
end;
// 必須呼叫 CallNextHookEx 維持鉤子鏈
Result := CallNextHookEx(g_hHook, nCode, wParam, lParam);
end;
procedure InstallHook; stdcall;
begin
if g_hHook = 0 then
g_hHook := SetWindowsHookEx(WH_SHELL, @ShellProc, HInstance, 0);
end;
procedure UninstallHook; stdcall;
begin
if g_hHook <> 0 then
begin
UnhookWindowsHookEx(g_hHook);
g_hHook := 0;
end;
end;
exports
InstallHook,
UninstallHook,
SetMainWindow;
begin
DisableThreadLibraryCalls(HInstance);
end.
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Forms, Controls, StdCtrls, Classes;
type
TForm1 = class(TForm)
btnInstall: TButton;
btnUninstall: TButton;
memoLog: TMemo;
chkAutoScroll: TCheckBox;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnInstallClick(Sender: TObject);
procedure btnUninstallClick(Sender: TObject);
private
procedure WndProc(var Msg: TMessage); override;
procedure LogEvent(const Event: string; Wnd: HWND; Extra: LPARAM = 0);
procedure OnWindowCreated(Wnd: HWND);
procedure OnWindowDestroyed(Wnd: HWND);
procedure OnWindowActivated(Wnd: HWND);
procedure OnWindowReplaced(NewWnd, OldWnd: HWND);
procedure OnFlash(Wnd: HWND);
procedure OnRudeAppActivated(Wnd: HWND);
procedure OnAppCommand(Wnd: HWND; Cmd: Integer);
procedure OnLanguageChange(KL: HKL);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
// 導入 DLL 函數
procedure InstallHook; external 'ShellHookDLL.dll' name 'InstallHook';
procedure UninstallHook; external 'ShellHookDLL.dll' name 'UninstallHook';
procedure SetMainWindow(hWnd: HWND); external 'ShellHookDLL.dll' name 'SetMainWindow';
procedure TForm1.FormCreate(Sender: TObject);
begin
memoLog.Lines.Add('Shell Hook 監控器就緒');
chkAutoScroll.Checked := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
UninstallHook;
end;
procedure TForm1.btnInstallClick(Sender: TObject);
begin
InstallHook;
// 必須告知 DLL 主程式視窗句柄
SetMainWindow(Handle);
memoLog.Lines.Add('[Hook Installed] 監控已啟動');
end;
procedure TForm1.btnUninstallClick(Sender: TObject);
begin
UninstallHook;
memoLog.Lines.Add('[Hook Uninstalled] 監控已停止');
end;
procedure TForm1.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_USER + 101: OnWindowCreated(HWND(Msg.LParam));
WM_USER + 102: OnWindowDestroyed(HWND(Msg.LParam));
WM_USER + 103: OnWindowActivated(HWND(Msg.LParam));
WM_USER + 104: OnWindowReplaced(HWND(Msg.WParam), HWND(Msg.LParam));
WM_USER + 105: OnFlash(HWND(Msg.LParam));
WM_USER + 106: OnRudeAppActivated(HWND(Msg.LParam));
WM_USER + 107: OnAppCommand(HWND(Msg.LParam), GET_APPCOMMAND_LPARAM(Msg.LParam));
WM_USER + 108: OnLanguageChange(HKL(Msg.WParam));
end;
inherited;
end;
procedure TForm1.LogEvent(const Event: string; Wnd: HWND; Extra: LPARAM = 0);
var
Title: array[0..255] of Char;
ClassName: array[0..255] of Char;
ProcessId: DWORD;
begin
GetWindowText(Wnd, Title, 256);
GetClassName(Wnd, ClassName, 256);
GetWindowThreadProcessId(Wnd, @ProcessId);
memoLog.Lines.Add(Format('[%s] %s | HWND:%x | PID:%d | Class:%s | Title:%s',
[FormatDateTime('hh:nn:ss.zzz', Now), Event, Wnd, ProcessId, ClassName, Title]));
if chkAutoScroll.Checked then
memoLog.Perform(EM_SCROLLCARET, 0, 0);
// 限制行數
if memoLog.Lines.Count > 2000 then
memoLog.Lines.Delete(0);
end;
procedure TForm1.OnWindowCreated(Wnd: HWND);
begin
LogEvent('WINDOW CREATED', Wnd);
end;
procedure TForm1.OnWindowDestroyed(Wnd: HWND);
begin
LogEvent('WINDOW DESTROYED', Wnd);
end;
procedure TForm1.OnWindowActivated(Wnd: HWND);
begin
LogEvent('WINDOW ACTIVATED', Wnd);
end;
procedure TForm1.OnWindowReplaced(NewWnd, OldWnd: HWND);
begin
LogEvent('WINDOW REPLACED', NewWnd);
memoLog.Lines.Add(Format(' Old HWND: %x', [OldWnd]));
end;
procedure TForm1.OnFlash(Wnd: HWND)
begin
LogEvent('WINDOW FLASH', Wnd);
end;
procedure TForm1.OnRudeAppActivated(Wnd: HWND);
begin
LogEvent('RUDE APP ACTIVATED (Fullscreen)', Wnd);
end;
procedure TForm1.OnAppCommand(Wnd: HWND; Cmd: Integer);
const
CmdNames: array[0..30] of string = (
'APPCOMMAND_BROWSER_BACKWARD', 'APPCOMMAND_BROWSER_FORWARD',
'APPCOMMAND_BROWSER_REFRESH', 'APPCOMMAND_BROWSER_STOP',
'APPCOMMAND_BROWSER_SEARCH', 'APPCOMMAND_BROWSER_FAVORITES',
'APPCOMMAND_BROWSER_HOME', 'APPCOMMAND_VOLUME_MUTE',
'APPCOMMAND_VOLUME_DOWN', 'APPCOMMAND_VOLUME_UP',
'APPCOMMAND_MEDIA_NEXTTRACK', 'APPCOMMAND_MEDIA_PREVIOUSTRACK',
'APPCOMMAND_MEDIA_STOP', 'APPCOMMAND_MEDIA_PLAY_PAUSE',
'APPCOMMAND_LAUNCH_MAIL', 'APPCOMMAND_LAUNCH_MEDIA_SELECT',
'APPCOMMAND_LAUNCH_APP1', 'APPCOMMAND_LAUNCH_APP2',
'APPCOMMAND_BASS_DOWN', 'APPCOMMAND_BASS_UP',
'APPCOMMAND_TREBLE_DOWN', 'APPCOMMAND_TREBLE_UP'
);
var
CmdStr: string;
begin
if (Cmd >= 0) and (Cmd < Length(CmdNames)) then
CmdStr := CmdNames[Cmd]
else
CmdStr := Format('Unknown(%d)', [Cmd]);
LogEvent('APP COMMAND: ' + CmdStr, Wnd);
end;
procedure TForm1.OnLanguageChange(KL: HKL);
var
LayoutName: array[0..KL_NAMELENGTH-1] of Char;
begin
GetKeyboardLayoutName(LayoutName);
LogEvent('LANGUAGE CHANGED', 0);
memoLog.Lines.Add(Format(' New Layout: %s (HKL:%x)', [LayoutName, KL]));
end;
end.
監控 HSHELL_REDRAW + HSHELL_WINDOWACTIVATED 可追蹤任務列按鈕高亮狀態,實現自定義任務切換器。
// HSHELL_RUDEAPPACTIVATED 表示進入獨佔全螢幕模式
// 可用於:自動隱藏桌面工具列、暫停後台下載、切換效能模式
procedure TForm1.OnRudeAppActivated(Wnd: HWND);
begin
if IsFullScreenGame(Wnd) then
begin
EnableGameMode(True); // 優化系統資源
HideDesktopWidgets();
end;
end;
HSHELL_APPCOMMAND 可捕獲鍵盤上的音量鍵、播放控制鍵、瀏覽器鍵、郵件鍵等,無需註冊熱鍵即可全域響應。
處理 HSHELL_GETMINRECT 可控制視窗最小化到任務列時的動畫目標位置,實現特效最小化 (如縮放到系統匣圖示)。
PostMessage 轉發給主執行緒。SetWindowsHookEx(WH_SHELL, ...) - 安裝 Shell 鉤子CallNextHookEx - 傳遞鉤子鏈UnhookWindowsHookEx - 移除鉤子FlashWindowEx - 觸發 HSHELL_FLASHGET_APPCOMMAND_LPARAM - 從 lParam 提取 AppCommandGetWindowThreadProcessId - 獲取視窗進程 IDGetWindowText / GetClassName - 獲取視窗標題/類名GetKeyboardLayoutName - 獲取當前輸入法名稱ShellProc 配合 WH_SHELL 是 Windows 系統級視窗事件監控的核心機制。它提供了視窗生命週期、啟用狀態、任務列互動、多媒體鍵、語言切換、全螢幕模式等豐富事件。關鍵實現要點:必須用 DLL、回調極度輕量用 PostMessage 轉發、雙版本 DLL 支援 32/64 位、注意 UAC 隔離限制。掌握此技術可構建強大的視窗管理器、自動化測試框架、無障礙工具、遊戲伴侶等應用。