Unity透明应用基于Windows API的点击穿透禁用问题排查
问题:Unity透明窗口点击穿透禁用功能失效
我正在开发一款透明版MS Paint,需求是点击绘画按钮时,保持窗口透明的同时禁用点击穿透,无法与窗口外交互。目前功能基本实现,但点击穿透的禁用功能无法正常工作。
已尝试修改SetLayeredWindowAttributes、调整SetWindowLong的扩展样式等方法,但均未解决问题。
现有代码
using System; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class TransparentWindow : MonoBehaviour { [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); [DllImport("user32.dll", SetLastError = true)] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] static extern int SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); private struct MARGINS { public int cxLeftWidth; public int cxRightWidth; public int cyTopHeight; public int cyBottomHeight; } [DllImport("Dwmapi.dll")] private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins); const int GWL_EXSTYLE = -20; const uint WS_EX_LAYERED = 0x00080000; const uint WS_EX_TRANSPARENT = 0x00000020; static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); const uint LWA_COLORKEY = 0x00000001; const uint LWA_ALPHA = 0x00000002; private IntPtr hWnd; private bool isClickThroughEnabled = true; private void Start() { #if !UNITY_EDITOR hWnd = GetActiveWindow(); MARGINS margins = new MARGINS { cxLeftWidth = -1 }; DwmExtendFrameIntoClientArea(hWnd, ref margins); SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT); //SetLayeredWindowAttributes(hWnd, 0, 0, LWA_COLORKEY); // This makes the window transparent but still clickable SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, 0); #endif Application.runInBackground = true; } private void Update() { Debug.Log(isClickThroughEnabled); if (!isClickThroughEnabled) { // Prevent click-through when not explicitly enabled SetClickthrough(false); return; } // Check if the pointer is over a UI element bool isOverUI = UnityEngine.EventSystems.EventSystem.current != null && UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(); // Check if the pointer is over a 2D collider bool isOverCollider = Physics2D.OverlapPoint(Camera.main.ScreenToWorldPoint(Input.mousePosition)) != null; // Set click-through only if not interacting with UI or colliders SetClickthrough(!(isOverUI || isOverCollider)); } private void SetClickthrough(bool clickthrough) { if (clickthrough) { SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT); } else { SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED); } } public void EnableClickThrough() { isClickThroughEnabled = true; } public void DisableClickThrough() { isClickThroughEnabled = false; SetClickthrough(false); // Ensure click-through is disabled immediately } public void PaintMode() { Image targetButton = GameObject.Find("Brush").GetComponent<Image>(); if (isClickThroughEnabled) { targetButton.color = new Color(73f / 255f, 255f / 255f, 92f / 255f, 1f); DisableClickThrough(); } else { targetButton.color = new Color(1f, 1f, 1f, 1f); EnableClickThrough(); } } }
问题根源与修复方案
核心问题
- 直接覆盖窗口样式:原有代码中
SetWindowLong直接赋值固定样式,会丢失系统默认的窗口扩展属性,导致样式修改不生效。 - 无强制刷新逻辑:修改窗口样式后未触发系统重绘,新样式无法立即生效。
- 重复无效调用:
Update中每帧都调用SetClickthrough,造成系统资源浪费且可能导致状态冲突。
修复后的代码
using System; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class TransparentWindow : MonoBehaviour { [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); [DllImport("user32.dll", SetLastError = true)] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] static extern int SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); [DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); private struct MARGINS { public int cxLeftWidth; public int cxRightWidth; public int cyTopHeight; public int cyBottomHeight; } [DllImport("Dwmapi.dll")] private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins); const int GWL_EXSTYLE = -20; const uint WS_EX_LAYERED = 0x00080000; const uint WS_EX_TRANSPARENT = 0x00000020; static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); const uint LWA_COLORKEY = 0x00000001; const uint LWA_ALPHA = 0x00000002; const uint SWP_FRAMECHANGED = 0x0040; const uint SWP_NOMOVE = 0x0001; const uint SWP_NOSIZE = 0x0002; private IntPtr hWnd; private bool isClickThroughEnabled = true; private uint currentExStyle; private void Start() { #if !UNITY_EDITOR hWnd = GetActiveWindow(); MARGINS margins = new MARGINS { cxLeftWidth = -1 }; DwmExtendFrameIntoClientArea(hWnd, ref margins); // 获取当前窗口样式,保留原有属性 currentExStyle = (uint)GetWindowLong(hWnd, GWL_EXSTYLE); currentExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT; SetWindowLong(hWnd, GWL_EXSTYLE, currentExStyle); // 保留透明效果,根据需求选择颜色键或整体透明度 // SetLayeredWindowAttributes(hWnd, ColorToUint(Color.black), 0, LWA_COLORKEY); // SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA); SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); #endif Application.runInBackground = true; } private void Update() { Debug.Log(isClickThroughEnabled); if (!isClickThroughEnabled) { // 仅当状态不符时修改,避免重复调用 if ((currentExStyle & WS_EX_TRANSPARENT) != 0) { SetClickthrough(false); } return; } bool isOverUI = UnityEngine.EventSystems.EventSystem.current != null && UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(); bool isOverCollider = Physics2D.OverlapPoint(Camera.main.ScreenToWorldPoint(Input.mousePosition)) != null; bool shouldEnableClickThrough = !(isOverUI || isOverCollider); // 状态不一致时才切换 if (shouldEnableClickThrough != ((currentExStyle & WS_EX_TRANSPARENT) != 0)) { SetClickthrough(shouldEnableClickThrough); } } private void SetClickthrough(bool clickthrough) { if (clickthrough) { currentExStyle |= WS_EX_TRANSPARENT; } else { currentExStyle &= ~WS_EX_TRANSPARENT; } SetWindowLong(hWnd, GWL_EXSTYLE, currentExStyle); // 强制窗口刷新,确保样式生效 SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); } private uint ColorToUint(Color color) { return ((uint)(color.r * 255) << 16) | ((uint)(color.g * 255) << 8) | (uint)(color.b * 255); } public void EnableClickThrough() { isClickThroughEnabled = true; } public void DisableClickThrough() { isClickThroughEnabled = false; SetClickthrough(false); } public void PaintMode() { Image targetButton = GameObject.Find("Brush").GetComponent<Image>(); if (isClickThroughEnabled) { targetButton.color = new Color(73f / 255f, 255f / 255f, 92f / 255f, 1f); DisableClickThrough(); } else { targetButton.color = new Color(1f, 1f, 1f, 1f); EnableClickThrough(); } } }
关键修改说明
- 保留原有窗口样式:新增
GetWindowLong调用,先获取当前窗口的扩展样式,再通过位操作添加/移除WS_EX_TRANSPARENT,避免丢失系统默认属性。 - 减少无效调用:在
Update中先判断当前状态是否符合预期,仅当需要切换时才调用SetClickthrough。 - 强制窗口刷新:修改样式后调用
SetWindowPos并传入SWP_FRAMECHANGED标志,强制系统重绘窗口,确保样式立即生效。 - 明确透明方式:新增颜色转uint方法,方便配置颜色键透明,同时保留整体透明度的配置选项,确保透明效果与点击穿透逻辑不冲突。
内容的提问来源于stack exchange,提问作者Lie Codex
相关产品推荐
相关产品推荐

