表單中有多個 TextBox,需針對 特定 Panel 內的 TextBox 僅保留右鍵「複製」功能,封鎖貼上、剪下、全選等行為,且不影響其他輸入框。
WH_MOUSE_LL (14):全域低層滑鼠鉤子,可在應用程式外部也能攔截,但需注意效能與穩定性。TextBox.ContextMenuStrip 客製化、ShortcutsEnabled=false + 自訂命令綁定、WPF InputBindings。本文示範鉤子做法以展示進階技術。using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class GlobalMouseHook : IDisposable
{
private const int WH_MOUSE_LL = 14;
private const int WM_RBUTTONDOWN = 0x204;
private const int WM_RBUTTONUP = 0x205;
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
private readonly LowLevelMouseProc _proc;
private readonly IntPtr _hookId = IntPtr.Zero;
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hInstance, uint threadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
public event Action RightClickDetected;
public GlobalMouseHook()
{
_proc = HookCallback;
using (var curProcess = Process.GetCurrentProcess())
using (var curModule = curProcess.MainModule)
{
_hookId = SetWindowsHookEx(WH_MOUSE_LL, _proc, GetModuleHandle(curModule.ModuleName), 0);
}
if (_hookId == IntPtr.Zero) throw new System.ComponentModel.Win32Exception();
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && (wParam == (IntPtr)WM_RBUTTONDOWN || wParam == (IntPtr)WM_RBUTTONUP))
{
var info = Marshal.PtrToStructure(lParam);
var args = new MouseEventArgs(MouseButtons.Right, 1, info.pt.x, info.pt.y, 0);
RightClickDetected?.Invoke(args);
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[StructLayout(LayoutKind.Sequential)]
private struct MSLLHOOKSTRUCT { public POINT pt; public int mouseData, flags, time, dwExtraInfo; }
public void Dispose()
{
if (_hookId != IntPtr.Zero) UnhookWindowsHookEx(_hookId);
GC.SuppressFinalize(this);
}
}
public partial class MainForm : Form
{
private readonly GlobalMouseHook _hook;
private readonly ContextMenuStrip _copyOnlyMenu;
public MainForm()
{
InitializeComponent();
// 僅複製的選單
_copyOnlyMenu = new ContextMenuStrip();
_copyOnlyMenu.Items.Add("複製", null, (s, e) =>
{
if (ActiveControl is TextBox tb) Clipboard.SetText(tb.SelectedText);
});
_hook = new GlobalMouseHook();
_hook.RightClickDetected += OnGlobalRightClick;
}
private void OnGlobalRightClick(MouseEventArgs e)
{
// 僅當焦點在 panelSpecial 內的 TextBox 時套用
if (ActiveControl is TextBox tb && tb.Parent == panelSpecial)
{
// 在 UI 執行緒切換選單
BeginInvoke((Action)(() => tb.ContextMenuStrip = _copyOnlyMenu));
}
else
{
BeginInvoke((Action)(() =>
{
if (ActiveControl is TextBox other) other.ContextMenuStrip = null; // 還原預設
}));
}
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_hook.Dispose();
base.OnFormClosed(e);
}
}
| 風險 | 說明 | 緩解 |
|---|---|---|
| 鉤子阻塞 | 回調執行過久會導致系統滑鼠卡頓 | 回調僅做最小判斷,UI 操作用 BeginInvoke 非同步處理 |
| 多視窗衝突 | 同一進程開啟多個視窗共用鉤子,邏輯複雜 | 單視窗應用;多視窗改用 WH_MOUSE 執行緒鉤子或各視窗獨立管理 |
| 權限問題 | 低完整性程序 (如瀏覽器) 無法鉤住高完整性視窗 | 不適用跨完整性層級場景 |
| 除錯困難 | 中斷點命中時鉤子卡住,滑鼠失效 | 使用 Trace/Log 替代中斷點;或暫時停用鉤子除錯 |
// 僅需限制特定 TextBox 功能,無需全域鉤子
textBoxSpecial.ShortcutsEnabled = false; // 關閉 Ctrl+C/V/X/A
textBoxSpecial.ContextMenuStrip = new ContextMenuStrip();
textBoxSpecial.ContextMenuStrip.Items.Add("複製", null, (s, e) =>
Clipboard.SetText(textBoxSpecial.SelectedText));
全域滑鼠鉤子 (WH_MOUSE_LL) 能實現跨控制項、跨視窗的統一攔截邏輯,但複雜度高、風險大。單純「限制特定 TextBox 選單」建議使用 ContextMenuStrip + ShortcutsEnabled 純 Managed 方案,穩定且易維護。僅在需全系統級攔截 (如螢幕錄製、宏錄製、無障礙工具) 時才採用鉤子。