You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    }
}

问题根源与修复方案

核心问题

  1. 直接覆盖窗口样式:原有代码中SetWindowLong直接赋值固定样式,会丢失系统默认的窗口扩展属性,导致样式修改不生效。
  2. 无强制刷新逻辑:修改窗口样式后未触发系统重绘,新样式无法立即生效。
  3. 重复无效调用: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();
        }
    }
}

关键修改说明

  1. 保留原有窗口样式:新增GetWindowLong调用,先获取当前窗口的扩展样式,再通过位操作添加/移除WS_EX_TRANSPARENT,避免丢失系统默认属性。
  2. 减少无效调用:在Update中先判断当前状态是否符合预期,仅当需要切换时才调用SetClickthrough。
  3. 强制窗口刷新:修改样式后调用SetWindowPos并传入SWP_FRAMECHANGED标志,强制系统重绘窗口,确保样式立即生效。
  4. 明确透明方式:新增颜色转uint方法,方便配置颜色键透明,同时保留整体透明度的配置选项,确保透明效果与点击穿透逻辑不冲突。

内容的提问来源于stack exchange,提问作者Lie Codex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 11:22:08