將 VCL 視窗封裝進 DLL 後,Tab 鍵在子視窗間切換焦點的功能會失效。原因在於 DLL 擁有獨立的 Application 實例,其內部 TApplication.ProcessMessage 不會處理主程式視窗的焦點導航。本文提供鍵盤鉤子攔截 + 手動轉發 WM_NEXTDLGCTL的完整解決方案。
TApplication.IsKeyMsg 處理 Tab/Shift+TabApplication 不會接收主程式視窗的鍵盤訊息FormKeyDown 也收不到 Tab (被系統吃掉)
主程式 (EXE) DLL 子視窗
┌─────────────────────┐ ┌─────────────────────┐
│ 1. 安裝鍵盤鉤子 │ │ 2. 定義 FormKeyDown │
│ WH_KEYBOARD │ │ 處理 VK_TAB │
│ 2. 攔截 VK_TAB │ ───SendMessage──▶│ 3. Perform │
│ 3. 轉發給 ActiveForm│ │ (WM_NEXTDLGCTL) │
└─────────────────────┘ └─────────────────────┘
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
// 手動實作 Tab 導航
if (ssShift in Shift) and (Key = VK_TAB) then
Perform(WM_NEXTDLGCTL, 1, 0) // Shift+Tab: 上一個控制項
else if Key = VK_TAB then
Perform(WM_NEXTDLGCTL, 0, 0); // Tab: 下一個控制項
end;
end.
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TMainForm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
Hook: HHook;
// 鉤子回呼函數必須用 stdcall
function KeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
function TMainForm.KeyHook(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;
begin
// HC_ACTION = 0 表示處理訊息
// wParam = VK_TAB (9)
// lParam 高位元包含鍵盤狀態,983041 = $F0001 (按下、非擴展鍵)
if (nCode = HC_ACTION) and (wParam = VK_TAB) and (lParam = 983041) then
begin
// 避免重複處理主視窗本身
if Screen.ActiveForm.Handle <> Self.Handle then
begin
// 直接發送 WM_KEYDOWN 給作用中視窗
// 子視窗的 FormKeyDown 將接收並轉為 WM_NEXTDLGCTL
SendMessage(Screen.ActiveForm.Handle, WM_KEYDOWN, VK_TAB, 0);
end;
end;
Result := CallNextHookEx(Hook, nCode, wParam, lParam);
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
// 安裝執行緒級鍵盤鉤子
Hook := SetWindowsHookEx(WH_KEYBOARD, @KeyHook, 0, GetCurrentThreadId);
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
// 移除鉤子
if Hook <> 0 then
UnhookWindowsHookEx(Hook);
end;
end.
| API / 訊息 | 用途 | 關鍵參數 |
|---|---|---|
SetWindowsHookEx(WH_KEYBOARD, ...) | 安裝鍵盤鉤子 | 執行緒級 (最後參數 GetCurrentThreadId) 即可攔截同執行緒視窗 |
CallNextHookEx | 傳遞給下一個鉤子 | 必須呼叫,否則阻斷系統鍵盤處理 |
WM_NEXTDLGCTL | 對話方塊焦點導航 | wParam=0 下一個;wParam=1 上一個 |
Perform(WM_NEXTDLGCTL, ...) | 直接發送給視窗程序 | 繞過訊息佇列,立即生效 |
// 更健壯的鉤子判斷:檢查作用中視窗是否屬於 DLL 模組
if (nCode = HC_ACTION) and (wParam = VK_TAB) then
begin
var ActiveWnd := GetActiveWindow;
var ActiveForm := FindControl(ActiveWnd); // VCL 找回 TForm
if Assigned(ActiveForm) and (ActiveForm <> Self) then
begin
// 檢查視窗模組是否為 DLL (可選)
if GetModuleHandle(PChar(ActiveForm.ClassName)) <> HInstance then
SendMessage(ActiveWnd, WM_KEYDOWN, VK_TAB, 0);
end;
end;
| 方案 | 優點 | 缺點 |
|---|---|---|
| 鍵盤鉤子 (本文) | 不修改 DLL 核心邏輯、相容性好 | 需主程式配合、鉤子效能開銷極小 |
| DLL 共用 Application | 徹底解決所有 VCL 全域狀態問題 | 需在 DLL 入口點重導向、風險較高 |
| DLL 內部安裝鉤子 | 封裝性佳 | DLL 卸載時機複雜、易殘留鉤子 |
| 使用 TApplicationEvents | 無鉤子、純 VCL | 需共用 Application 實例 |
DLL 封裝 VCL 視窗時,Tab 導航失效是典型的「全域狀態隔離」症狀。透過主程式安裝執行緒級鍵盤鉤子攔截 Tab,轉發 WM_KEYDOWN 給作用中子視窗,子視窗再以 Perform(WM_NEXTDLGCTL) 完成焦點切換,是最小侵入性、相容性最佳的解決方案。此模式同樣適用於 Enter、Esc 等特殊鍵的跨邊界處理。