螢幕鍵盤必須:
internal static class NativeMethods
{
// 鉤子
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
// 模擬輸入
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
// 視窗樣式
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
// 常數
internal const int WH_KEYBOARD_LL = 13;
internal const int WH_MOUSE_LL = 14;
internal const int WM_KEYDOWN = 0x0100;
internal const int WM_KEYUP = 0x0101;
internal const int GWL_EXSTYLE = -20;
internal const int WS_EX_NOACTIVATE = 0x08000000;
internal const int WM_MOUSEACTIVATE = 0x0021;
internal const int MA_NOACTIVATE = 3;
}
internal delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
internal struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
internal struct INPUT
{
public uint type;
public InputUnion u;
public static INPUT CreateKeyboard(ushort vk, uint flags = 0) => new INPUT
{
type = 1, // INPUT_KEYBOARD
u = new InputUnion { ki = new KEYBDINPUT { wVk = vk, dwFlags = flags } }
};
}
[StructLayout(LayoutKind.Explicit)]
internal struct InputUnion
{
[FieldOffset(0)] internal MOUSEINPUT mi;
[FieldOffset(0)] internal KEYBDINPUT ki;
[FieldOffset(0)] internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
internal const uint KEYEVENTF_KEYUP = 0x0002;
internal const uint KEYEVENTF_UNICODE = 0x0004;
public class GlobalKeyboardHook : IDisposable
{
private readonly HookProc _proc;
private IntPtr _hook = IntPtr.Zero;
public event Action KeyEvent; // (Key, isDown)
public GlobalKeyboardHook()
{
_proc = HookCallback;
_hook = NativeMethods.SetWindowsHookEx(
NativeMethods.WH_KEYBOARD_LL, _proc,
Marshal.GetHINSTANCE(typeof(GlobalKeyboardHook).Assembly.GetModules()[0]), 0);
if (_hook == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var info = Marshal.PtrToStructure(lParam);
bool down = wParam == (IntPtr)NativeMethods.WM_KEYDOWN || wParam == (IntPtr)0x0104; // WM_SYSKEYDOWN
KeyEvent?.Invoke((Keys)info.vkCode, down);
}
return NativeMethods.CallNextHookEx(_hook, nCode, wParam, lParam);
}
public void Dispose()
{
if (_hook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_hook);
_hook = IntPtr.Zero;
GC.SuppressFinalize(this);
}
}
public static class KeyboardSimulator
{
public static void KeyDown(Keys key) => Send(KEYBDINPUT.Create(key, 0));
public static void KeyUp(Keys key) => Send(KEYBDINPUT.Create(key, KEYEVENTF_KEYUP));
public static void KeyPress(Keys key) { KeyDown(key); KeyUp(key); }
private static void Send(INPUT input)
{
if (NativeMethods.SendInput(1, new[] { input }, Marshal.SizeOf()) == 0)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
public partial class OnScreenKeyboard : Form
{
private readonly GlobalKeyboardHook _hook = new GlobalKeyboardHook();
public OnScreenKeyboard()
{
InitializeComponent();
// 1. 建立時即設定不啟用
Load += (s, e) => SetNoActivate(true);
// 2. 滑鼠點擊標題列才暫時允許焦點 (以便拖曳)
var mouseHook = new GlobalMouseHook(); // 類似鍵盤鉤子實作 WH_MOUSE_LL
mouseHook.MouseEvent += (btn, pt) =>
{
if (btn == MouseButtons.Left)
{
var captionRect = new Rectangle(Location, new Size(Width, SystemInformation.CaptionHeight));
bool onCaption = captionRect.Contains(pt);
SetNoActivate(!onCaption); // 標題列可焦點,其他區域不可
if (onCaption) NativeMethods.SendMessage(Handle, NativeMethods.WM_MOUSEACTIVATE, Handle, (IntPtr)NativeMethods.MA_NOACTIVATE);
}
};
}
private void SetNoActivate(bool enable)
{
var exStyle = NativeMethods.GetWindowLong(Handle, NativeMethods.GWL_EXSTYLE);
if (enable) exStyle = new IntPtr(exStyle.ToInt64() | NativeMethods.WS_EX_NOACTIVATE);
else exStyle = new IntPtr(exStyle.ToInt64() & ~NativeMethods.WS_EX_NOACTIVATE);
NativeMethods.SetWindowLong(Handle, NativeMethods.GWL_EXSTYLE, exStyle);
}
// 按鈕點擊事件範例
private void BtnKey_Click(object sender, EventArgs e)
{
var btn = (Button)sender;
var key = (Keys)btn.Tag; // Tag 存放 Keys 值
if (IsModifier(key)) // Shift/Ctrl/Alt 鎖定切換
{
if (btn.BackColor == SystemColors.Control)
{
KeyboardSimulator.KeyDown(key);
btn.BackColor = SystemColors.ControlDark;
}
else
{
KeyboardSimulator.KeyUp(key);
btn.BackColor = SystemColors.Control;
}
}
else
{
KeyboardSimulator.KeyPress(key);
}
}
private bool IsModifier(Keys k) => k is Keys.ShiftKey or Keys.ControlKey or Keys.Menu; // Alt = Menu
protected override void Dispose(bool disposing)
{
if (disposing) { _hook.Dispose(); /* mouseHook.Dispose(); */ }
base.Dispose(disposing);
}
}
Keys.A = 0x41 Keys.B = 0x42 ... Keys.Z = 0x5A
Keys.D0 = 0x30 ... Keys.D9 = 0x39
Keys.NumPad0 = 0x60 ... Keys.NumPad9 = 0x69
Keys.F1 = 0x70 ... Keys.F12 = 0x7B
Keys.Left = 0x25 Keys.Up = 0x26 Keys.Right = 0x27 Keys.Down = 0x28
Keys.ShiftKey = 0x10 Keys.ControlKey = 0x11 Keys.Menu = 0x12 (Alt)
Keys.LWin = 0x5B Keys.RWin = 0x5C Keys.Apps = 0x5D
| 議題 | 對策 |
|---|---|
| 鉤子效能影響 | 低層鉤子在每次鍵盤事件都會回呼 managed 代碼,**務必極簡化回呼邏輯**,避免 UI 執行緒卡頓。 |
| 64 位元相容 | P/Invoke 簽名使用 IntPtr、uint、SetLastError=true,確保 x64 正常運作。 |
| UAC / 高權限視窗 | 低權限進程**無法**向高權限 (管理員) 視窗注入鍵盤輸入,需將螢幕鍵盤同樣以管理員身分執行。 |
| 多語言/IME | 模擬字元輸入建議用 KEYEVENTF_UNICODE + SendInput 送 Unicode 碼點,而非虛擬鍵碼。 |
| 現代替代 | Windows 10+ 內建 觸控鍵盤 (TabTip.exe),可透過 InputPane API 或啟動 `osk.exe` 替代自製方案。 |