在 Unity 中开发 Windows 桌面宠物或悬浮窗工具时,开发者通常会面临一个经典的“深水区”冲突:既想让游戏窗口背景完全透明,又想在 Windows 任务栏中隐藏该程序的运行图标。传统的做法是修改窗口扩展样式为 WS_EX_TOOLWINDOW,但这往往会破坏 Unity 与 Windows 桌面窗口管理器(DWM)的渲染链,导致透明通道失效,画面变成黑底。

示例工程

要完美同时实现这两个需求,我们需要将“透明渲染”与“任务栏隐藏”这两个操作彻底剥离。

核心实现原理

解决方案的核心在于:保留标准的无边框透明窗口样式以确保 DWM 渲染正常,并直接调用 Windows Shell 的 COM 接口强制删除任务栏页签。

  1. 绕过启动黑屏(Splash Screen)限制:
    免费版 Unity 强制显示启动 Logo,如果在 Logo 播放完毕前应用透明化 API,画面切换时透明度会被重置。利用协程配合 UnityEngine.Rendering.SplashScreen.isFinished,我们可以精准挂起代码,在 Logo 播放完毕且渲染管线稳定后再执行 API 调用。

  2. 设置无边框与置顶透明:
    利用 GetActiveWindow 获取当前窗口句柄,并通过 DwmExtendFrameIntoClientArea 将透明帧扩展到整个客户区。同时,使用 SetWindowLong 为窗口附加 WS_EX_LAYERED 属性,并结合 SetLayeredWindowAttributes 激活透明度渲染。利用 SetWindowPos 与 HWND_TOPMOST 参数确保窗口始终置顶。

  3. COM 接口实现“图标消失术”:
    引入 GUID 为 56FDF342-FD6D-11d0-958A-006097C9A090 的 ITaskbarList COM 接口。在初始化 HrInit() 后,直接调用 DeleteTab(hWnd),从任务栏中强行抹除当前窗口的图标。

完整实现代码

以下是完整的 TransparentWindow.cs 脚本,您可以直接将其挂载到场景中永远不会被销毁的对象上:

// ***********************************************************************************
// FileName: TransparentWindow.cs
// Description: 实现窗口背景透明并利用 COM 接口隐藏任务栏图标
// *************************************************************************************

using System;
using System.Collections;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;

// ReSharper disable CheckNamespace
namespace OnyxCore.UnityCore.Runtime
{
    public class TransparentWindow : MonoBehaviour
    {
        [DllImport("User32.dll")]
        private static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);

        [DllImport("User32.dll")]
        private static extern IntPtr GetActiveWindow();

        [DllImport("User32.dll")]
        private static extern IntPtr SetWindowLong(IntPtr hWnd, int index, int value);

        [DllImport("User32.dll")]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int uFlags);

        [DllImport("User32.dll")]
        private static extern int SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags);

        // --- COM 接口:用于直接操作任务栏 (重点在这里) ---
        [ComImport]
        [Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
        public class TaskbarList
        {
        }

        [ComImport]
        [Guid("56FDF342-FD6D-11d0-958A-006097C9A090")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface ITaskbarList
        {
            void HrInit();
            void AddTab(IntPtr hwnd);
            void DeleteTab(IntPtr hwnd);
            void ActivateTab(IntPtr hwnd);
            void SetActiveAlt(IntPtr hwnd);
        }

        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 int WS_EX_TRANSPARENT = 0x00000020;
        const int WS_EX_LAYERED = 0x00080000;

        static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);

        const uint LWA_COLORKEY = 0x00000001;

        // Start is called before the first frame update
        private void Start()
        {
            if (!Application.isEditor)
            {
                Application.runInBackground = true;
                // 依然需要等待 Logo 播放完毕
                StartCoroutine(WaitSplashScreenAndApply());
            }
        }

        IEnumerator WaitSplashScreenAndApply()
        {
            // 1. 等待 Unity 启动动画结束
            while (!SplashScreen.isFinished)
            {
                yield return null;
            }

            // 2. 额外缓冲 0.5 秒,确保渲染管线稳定
            yield return new WaitForSeconds(0.5f);

            // 3. 执行透明化和隐藏任务栏图标
            ApplyTransparentAndHideTaskbar();
        }

        void ApplyTransparentAndHideTaskbar()
        {
            var hWnd = GetActiveWindow();
            var margins = new MARGINS { cxLeftWidth = -1 };
            DwmExtendFrameIntoClientArea(hWnd, ref margins);

            SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED);
            SetLayeredWindowAttributes(hWnd, 0, 0, LWA_COLORKEY);

            SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, 0);

            try
            {
                ITaskbarList taskbar = (ITaskbarList)new TaskbarList();
                taskbar.HrInit();
                taskbar.DeleteTab(hWnd); // 任务栏图标消失术
            }
            catch (Exception e)
            {
                Debug.LogError("隐藏任务栏图标失败: " + e.Message);
            }
        }
    }
}

必要的 Unity 引擎设置

要让上述代码完美生效,您必须配合以下 Unity 渲染设置:

  • 摄像机 (Main Camera):将 Clear Flags 设置为 Solid Color,并将 Background 颜色的 Alpha (A) 值设为 0(完全透明)。
  • Player Settings:
    • Fullscreen Mode 必须设为 Fullscreen Window。
    • 务必取消勾选 Use DXGI Flip Model Swapchain for D3D11,否则 DWM 透明层将直接失效。