首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> Delphi

Delphi DLL 封裝 VCL 視窗 Tab 鍵失效解決方案:鍵盤鉤子與 WM_NEXTDLGCTL 手動轉發

     編輯:Delphi

前言

將 VCL 視窗封裝進 DLL 後,Tab 鍵在子視窗間切換焦點的功能會失效。原因在於 DLL 擁有獨立的 Application 實例,其內部 TApplication.ProcessMessage 不會處理主程式視窗的焦點導航。本文提供鍵盤鉤子攔截 + 手動轉發 WM_NEXTDLGCTL的完整解決方案。

問題根因分析

  • VCL 焦點導航依賴 TApplication.IsKeyMsg 處理 Tab/Shift+Tab
  • DLL 獨立 Application 不會接收主程式視窗的鍵盤訊息
  • 子視窗 FormKeyDown 也收不到 Tab (被系統吃掉)

解決方案架構


主程式 (EXE)                          DLL 子視窗
┌─────────────────────┐               ┌─────────────────────┐
│ 1. 安裝鍵盤鉤子      │               │ 2. 定義 FormKeyDown │
│    WH_KEYBOARD      │               │    處理 VK_TAB      │
│ 2. 攔截 VK_TAB      │ ───SendMessage──▶│ 3. Perform        │
│ 3. 轉發給 ActiveForm│               │    (WM_NEXTDLGCTL)  │
└─────────────────────┘               └─────────────────────┘

完整程式碼實作

DLL 子視窗單元 (Unit1.pas)

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.

主程式單元 (MainUnit.pas)

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 說明

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 等特殊鍵的跨邊界處理。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved