鼠标穿透本质是通过SetWindowLongPtr设置WS_EX_TRANSPARENT样式使窗口不参与消息分发,需配合SetWindowPos生效;WinForms应重写CreateParams,WPF需在SourceInitialized中操作HwndSource.Handle。
Windows API 的 SetWindowLongPtr 是关键入口
鼠标穿透的本质是让窗口不参与 windows 的消息分发链,尤其是 wm_mousemove 和 wm_lbuttondown 这类输入消息。c# 没有内置“穿透”属性,必须调用 setwindowlongptr 修改窗口样式,去掉 ws_ex_layered 或加上 ws_ex_transparent —— 但注意:ws_ex_transparent 才是真正让鼠标穿过的开关,不是透明度控制那个 ws_ex_layered。
常见错误是只设了 WS_EX_LAYERED + UpdateLayeredWindow 做视觉透明,却忘了加 WS_EX_TRANSPARENT,结果看起来透明,点下去还是拦着鼠标。
- 必须先用
GetWindowLongPtr读取原样式,再用按位或(|)添加WS_EX_TRANSPARENT - 修改后要调用
SetWindowPos(参数全传 0)触发重绘和消息路由更新,否则改动不生效 -
WS_EX_TRANSPARENT对子窗口无效,只作用于顶层窗口;若你的窗体是子窗体(比如嵌在另一个 WinForm 内),得对父容器做穿透或换架构
WinForms 中绕不开的 CreateParams 重写
直接在 Form_Load 里调 SetWindowLongPtr 往往失败——因为此时窗口还没完全创建好,句柄可能为 0 或不稳定。可靠做法是在构造函数之后、Show() 之前,重写 CreateParams 属性,把 WS_EX_TRANSPARENT 塞进 ExStyle。
示例片段:
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.ExStyle |= 0x20; // 0x20 就是 WS_EX_TRANSPARENT
return cp;
}
}这个值必须在窗口创建前就定死,不能运行时动态改。如果需要“可切换穿透”,就得销毁重建窗口,或者用更底层的 SetWindowLongPtr 配合 WS_EX_TRANSPARENT 切换(但有闪烁风险)。
- 别用
0x00000020L这种带 L 后缀的写法,.NET Core/.5+ 的CreateParams.ExStyle是int,不是long - 如果窗体启用了双缓冲(
DoubleBuffered = true),穿透仍有效;但若用了自定义绘制(如重写OnPaintBackground清空背景),要确保没意外拦截鼠标事件
WPF 用户别碰 AllowsTransparency 误区
WPF 里设 WindowStyle="None" + AllowsTransparency="True" 看起来能做无边框透明窗,但它默认就带 WS_EX_TRANSPARENT 效果?错。实际上 AllowsTransparency 只影响渲染层,不自动开启鼠标穿透;反而因为 WPF 的 HWND 封装机制,直接调 SetWindowLongPtr 容易失效或崩溃。
正确路径是:先确保 WindowStyle="None",再在 SourceInitialized 事件里获取 HwndSource,用其 Handle 调用 API:
private void Window_SourceInitialized(object sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
SetWindowLongPtr(hwnd, -20, GetWindowLongPtr(hwnd, -20) | 0x20);
SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, 0x0001 | 0x0002);
}这里 -20 是 GWL_EXSTYLE 常量,别写成 GWLP_EXSTYLE(那是 64 位偏移,WinForms 用它,WPF 的 HwndSource.Handle 是 32 位兼容句柄)。
- WPF 窗口若设置了
ResizeMode="NoResize"或SizeToContent,可能干扰SetWindowPos调用,建议先固定Width/Height - 如果穿透后发现按钮等控件点不动,检查是否误把
IsHitTestVisible="False"设到了整个窗体或根 Grid 上——这会阻止所有 WPF 内部命中测试,和 Windows 层穿透是两回事
穿透失效的三个高频原因
不是代码写错,而是环境或组合逻辑卡住。最常被忽略的是这三点:
- 窗体
TopMost = true时,WS_EX_TRANSPARENT仍生效,但若上层还有另一个TopMost窗口(比如任务管理器、录屏软件浮窗),你的窗体实际被挡在中间,鼠标根本到不了它——先关掉其他置顶程序再测 - .NET 6+ 默认启用
HighDpiMode,某些高 DPI 缩放场景下SetWindowLongPtr返回 0 表示失败,需检查Marshal.GetLastWin32Error()是否为 1400(无效窗口句柄),此时应延迟到Shown事件再操作 - 用
Process.Start("notepad.exe")启动外部程序后,试图穿透自己的窗体去点记事本——不行。Windows 不允许穿透窗体去激活/点击另一个进程的顶层窗口,这是安全限制,不是 bug
穿透不是万能的图层开关,它只改变当前窗口在系统消息路由中的“存在感”。一旦涉及跨进程交互、DPI 变化或 UI 自动化工具(如 AutoIt、UIAutomation),行为会更不可控。


















