在 WinForms 中,方向鍵、Tab、PageUp/Down、Home/End 默認被視為**對話框導航鍵**,由系統處理焦點在控件間移動。控件的 KeyDown/KeyUp **不會**收到這些鍵盤消息。例如自定義 ToolBox 控件想用 Up/Down 選擇項目,重寫 OnKeyDown 卻捕獲不到。
Control.IsInputKey 與 WM_GETDLGCODEControl.IsInputKey)
protected virtual bool IsInputKey(Keys keyData) {
if ((keyData & Keys.Alt) != Keys.Alt) {
int num = 4; // DLGC_WANTMESSAGE
switch (keyData & Keys.KeyCode) {
case Keys.Left: case Keys.Up: case Keys.Right: case Keys.Down:
num = 5; // DLGC_WANTMESSAGE | DLGC_WANTARROWS (0x01 | 0x04)
break;
case Keys.Tab:
num = 6; // DLGC_WANTMESSAGE | DLGC_WANTTAB (0x02 | 0x04)
break;
}
if (this.IsHandleCreated)
return (((int)this.SendMessage(0x87, 0, 0)) & num) != 0; // 0x87 = WM_GETDLGCODE
}
return false;
}
WM_GETDLGCODE (0x87):控件告訴系統「我想自己處理哪類按鍵」。DLGC_WANTARROWS=0x01 (方向鍵)、DLGC_WANTTAB=0x02 (Tab)、DLGC_WANTMESSAGE=0x04 (通用)。Control 發送 WM_GETDLGCODE 給自身,默認窗口程序返回 0,導致 IsInputKey 返回 false,系統接管導航。
// ListBox 內部控件 ListBoxControl 重寫 IsInputKey
protected override bool IsInputKey(Keys keyData) {
if ((keyData & Keys.Alt) == Keys.Alt) return false;
switch (keyData & Keys.KeyCode) {
case Keys.Prior: case Keys.Next: case Keys.End: case Keys.Home:
return true; // 宣稱自己處理
}
return base.IsInputKey(keyData); // 其餘交給基類 (含方向鍵)
}
ListBox 顯式宣稱處理 PageUp/Down/End/Home,方向鍵走基類邏輯 (配合內部邏輯處理選項移動)。
IsInputKey 宣稱處理目標鍵
protected override bool IsInputKey(Keys keyData) {
if ((keyData & Keys.Alt) == Keys.Alt) return false; // Alt 組合鍵交給系統 (菜單快捷鍵)
switch (keyData & Keys.KeyCode) {
case Keys.Up:
case Keys.Down:
return true; // 我要處理 Up/Down
// 視需求添加:Keys.Left, Keys.Right, Keys.Tab, Keys.Home, Keys.End, Keys.PageUp, Keys.PageDown
}
return base.IsInputKey(keyData);
}
public class KeyboardNavigableList : Control {
private List _items = new List();
private int _selectedIndex = -1;
public KeyboardNavigableList() {
SetStyle(ControlStyles.Selectable | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
TabStop = true;
}
// 關鍵:宣稱處理方向鍵
protected override bool IsInputKey(Keys keyData) {
if ((keyData & Keys.Alt) == Keys.Alt) return false;
switch (keyData & Keys.KeyCode) {
case Keys.Up:
case Keys.Down:
case Keys.Home:
case Keys.End:
return true;
}
return base.IsInputKey(keyData);
}
protected override void OnKeyDown(KeyEventArgs e) {
base.OnKeyDown(e);
if (_items.Count == 0) return;
switch (e.KeyCode) {
case Keys.Up:
_selectedIndex = Math.Max(0, _selectedIndex - 1); Invalidate(); e.Handled = true; break;
case Keys.Down:
_selectedIndex = Math.Min(_items.Count - 1, _selectedIndex + 1); Invalidate(); e.Handled = true; break;
case Keys.Home:
_selectedIndex = 0; Invalidate(); e.Handled = true; break;
case Keys.End:
_selectedIndex = _items.Count - 1; Invalidate(); e.Handled = true; break;
}
}
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
// 簡單繪製邏輯...
}
}
WM_GETDLGCODE 完全控制 (罕見需求)若需極致控制 (如根據運行時狀態動態決定是否處理 Tab),可重寫 WndProc 攔截 WM_GETDLGCODE:
protected override void WndProc(ref Message m) {
const int WM_GETDLGCODE = 0x0087;
if (m.Msg == WM_GETDLGCODE) {
int code = (int)DLGC_WANTMESSAGE; // 基礎
if (_wantArrows) code |= (int)DLGC_WANTARROWS;
if (_wantTab) code |= (int)DLGC_WANTTAB;
if (_wantAllKeys) code |= (int)DLGC_WANTALLKEYS;
m.Result = (IntPtr)code;
return;
}
base.WndProc(ref m);
}
但 99% 情況下,**重寫 IsInputKey 即可**,更簡潔、受管理、自動處理句柄創建時機。
| 現象 | 可能原因 | 解決 |
|---|---|---|
| 重寫 IsInputKey 返回 true,但 KeyDown 仍不觸發 | 控件未獲取焦點 | 確保 SetStyle(ControlStyles.Selectable, true)、TabStop=true、用戶點擊或代碼 Focus() |
| 方向鍵導致焦點跳轉到其他控件 | IsInputKey 返回 false | 檢查 switch 條件、確保未被基類攔截 (Alt 鍵檢查) |
| Tab 鍵無法在控件內部循環 (如多行文本框) | IsInputKey 未返回 true for Tab | 添加 case Keys.Tab: return true; |
| Alt+方向鍵 觸發菜單而非控件邏輯 | Alt 組合鍵被系統保留 | IsInputKey 開頭檢查 (keyData & Keys.Alt) == Keys.Alt 返回 false,符合預期 |
Control.IsInputKey → 發送 WM_GETDLGCODE → 位掩碼判斷。IsInputKey**,目標鍵返回 true。Selectable、TabStop)。OnKeyDown 中處理業務邏輯,設置 e.Handled = true 阻止進一步傳遞。