Unity调用Windows API实现透明桌面挂件:P/Invoke与user32.dll实战

📅 2026/7/12 4:30:06
Unity调用Windows API实现透明桌面挂件:P/Invoke与user32.dll实战
1. 项目概述为什么要在Unity里折腾桌面挂件如果你是一个Unity开发者或者对C#编程有一定了解可能大部分时间都在用它制作游戏、VR/AR应用或者移动端App。但有没有想过Unity其实也能做出一些“不务正业”但非常实用的小玩意儿比如一个常驻在桌面角落的、半透明的、可以实时显示信息比如CPU温度、待办事项、歌词并且还能点击交互的悬浮挂件。这听起来像是传统WinForm或WPF的领域但用Unity来做优势在于其强大的UI系统UGUI、丰富的动画能力和跨平台潜力虽然这里我们聚焦Windows桌面。这个项目的核心挑战在于Unity默认创建的窗口是一个不透明的、带有标准边框的应用程序窗口。要实现一个无边框、背景透明、且鼠标点击能穿透或响应特定区域的“异形窗口”就需要跳出Unity的舒适区去调用Windows操作系统底层的API。这就是user32.dll登场的时候。通过C#的P/Invoke技术我们可以直接与这个存放着大量Windows用户界面核心函数的动态链接库对话从而实现对窗口样式的深度定制。简单来说这篇教程会带你从零开始在Unity中创建一个项目然后通过编写C#脚本调用user32.dll一步步实现一个完全透明、可自定义点击区域的桌面悬浮窗口。无论你是想做个个性化的桌面宠物还是一个高效的工作辅助工具这套方法都能为你打开一扇新的大门。2. 核心原理与Windows API初探2.1 什么是user32.dll我们为什么要调用它user32.dll是Microsoft Windows操作系统中一个至关重要的动态链接库它包含了用于管理Windows用户界面如窗口、消息、控件的大量函数。Unity引擎本身在Windows平台运行时其实也在底层调用了这些API来创建和管理它的主窗口。但是Unity暴露给我们的Screen或Window类功能非常有限仅限于设置分辨率、全屏等基础操作无法实现诸如“窗口置顶”、“移除边框”、“设置透明色”等高级特性。为了突破这个限制我们需要直接与user32.dll对话。在C#中这通过一种叫做“平台调用”Platform Invocation Services 简称P/Invoke的技术来实现。它允许托管代码我们的C#脚本调用非托管动态链接库中的函数。这听起来有点复杂但实际操作起来你只需要用[DllImport]属性声明一个函数指定它来自哪个DLL以及它的函数签名参数和返回值类型剩下的交给.NET运行时去处理。2.2 实现透明可点击窗口的关键API要实现我们的目标主要需要用到以下几个来自user32.dll和其兄弟库gdi32.dll的关键函数SetWindowLong: 这个函数是“窗口样式魔法师”。它可以改变一个窗口的扩展样式GWL_EXSTYLE。我们将用它来给窗口添加两个至关重要的样式WS_EX_LAYERED: 启用分层窗口。这是实现透明度Alpha混合和颜色键控透明指定一种颜色为透明的前提。WS_EX_TRANSPARENT: 这个样式会让窗口变得“对鼠标点击透明”。鼠标事件会穿透你的窗口直接到达它后面的窗口或桌面。这对于制作一个纯粹的、不干扰操作的“信息展示层”非常有用。但注意如果你希望窗口上的某些按钮能被点击就不能对整个窗口使用这个样式需要更精细的控制。SetLayeredWindowAttributes: 一旦窗口启用了分层样式WS_EX_LAYERED就可以用这个函数来设置透明度。它有两种主要模式颜色键控透明Color Keying指定一种RGB颜色比如亮粉色RGB(255, 0, 255)窗口中所有是这个颜色的像素都会变成完全透明。这是早期游戏和软件常用的“抠图”方式。整体Alpha透明度为整个窗口设置一个统一的透明度0-2550完全透明255完全不透明。这能实现类似毛玻璃的整体半透效果。GetWindowLong: 用于获取窗口当前的样式通常和SetWindowLong配合使用采用“先获取再修改后设置”的模式避免覆盖掉其他已有的重要样式。FindWindow/GetActiveWindow: 我们需要知道要操作哪个窗口的句柄IntPtr hWnd。FindWindow可以通过类名和窗口标题查找而GetActiveWindow可以获取当前活动窗口的句柄。在Unity脚本中我们通常会在Start()或Awake()方法里通过GetActiveWindow获取Unity自己创建的主窗口句柄。UpdateLayeredWindow(可选更高级): 这是一个比SetLayeredWindowAttributes功能更强大的函数允许你提供一个位图Bitmap并指定每个像素的Alpha值从而实现每像素透明度。这能做出形状不规则的、边缘有羽化效果的窗口。但实现起来也更复杂需要与GDI (gdiplus.dll) 配合。在本教程中我们将主要使用颜色键控透明和整体Alpha透明度这两种相对简单的方法因为它们足以应对大多数悬浮挂件的需求且与Unity的渲染流程兼容性更好。3. 环境准备与Unity项目设置3.1 创建新项目与基础设置首先打开Unity Hub创建一个新的项目。项目模板选择2D或者3D都可以这不会影响我们的核心功能。给项目起个名字比如“DesktopWidget”。项目创建好后进行以下几项关键设置构建平台设置在菜单栏选择File-Build Settings。在打开的窗口中确保Platform选择了PC, Mac Linux Standalone并且在右侧的Target Platform下拉菜单中选择了Windows。点击Switch Platform按钮。播放器设置Player Settings在Build Settings窗口左下角点击Player Settings...按钮或在项目设置中找到Player。分辨率与呈现Resolution and PresentationFullscreen Mode: 设置为Windowed。我们不需要全屏。WidthHeight: 设置一个合适的初始窗口大小例如 400 x 300。这个大小就是你悬浮挂件的初始尺寸。取消勾选Resizable Window。悬浮挂件通常不需要用户调整大小。其他设置Other SettingsRendering-Color Space: 保持默认的Gamma即可。如果项目需要更真实的色彩可以用Linear但这对于简单的桌面挂件影响不大。Configuration-Scripting Backend: 确保是Mono。虽然IL2CPP性能更好但在调用一些原生插件时Mono的兼容性和调试便利性有时更佳。对于这个项目两者皆可但教程以Mono为准。3.2 构建基础UI我们的挂件需要一个界面来显示内容。在Hierarchy面板右键 -UI-Canvas创建一个画布。选中新建的Canvas在Inspector面板中将Render Mode从Screen Space - Overlay改为Screen Space - Camera。然后将Render Camera拖拽赋值可以创建一个新的正交摄像机专门用于UI渲染。World Space模式也可以但Screen Space - Camera更易于管理和理解。在Canvas下创建一个Panel作为背景再在Panel里添加一些UI元素比如Text显示“Hello Widget”一个Button一个Image显示图标等。随意布局这是你挂件的外观。注意这里有一个非常重要的细节。为了实现颜色键控透明我们需要一个“透明”的背景。在Unity中Canvas默认的背景是“空”的但窗口本身有颜色。我们需要确保最终渲染到窗口的、UI后面的区域是我们指定的透明色。一个常见做法是将主摄像机的Background Type设置为Solid Color并且将该颜色设置为我们将要用于颜色键控的特定颜色例如纯绿色RGB(0, 255, 0)。这样UI之外的所有区域都会被渲染成绿色。4. 编写核心C#脚本调用Windows API接下来是核心部分。在Assets文件夹下创建一个Scripts文件夹然后新建一个C#脚本命名为TransparentWindow.cs。4.1 声明外部函数P/Invoke打开TransparentWindow.cs脚本首先我们需要引入必要的命名空间并声明我们将要使用的Windows API函数。using UnityEngine; using System; using System.Runtime.InteropServices; // 这是P/Invoke的核心命名空间 public class TransparentWindow : MonoBehaviour { // 定义需要用到的Windows常量 private const int GWL_EXSTYLE -20; // 设置窗口扩展样式 private const int WS_EX_LAYERED 0x80000; // 分层窗口 private const int WS_EX_TRANSPARENT 0x20; // 透明窗口鼠标穿透 private const int LWA_COLORKEY 0x1; // 使用颜色键控透明 private const int LWA_ALPHA 0x2; // 使用Alpha值透明 // 导入user32.dll中的函数 [DllImport(user32.dll)] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport(user32.dll)] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport(user32.dll)] private static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags); [DllImport(user32.dll)] private static extern IntPtr GetActiveWindow(); // 获取当前活动窗口句柄 [DllImport(user32.dll, SetLastError true)] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); private static readonly IntPtr HWND_TOPMOST new IntPtr(-1); // 置顶 private const uint SWP_NOSIZE 0x0001; // 保持大小 private const uint SWP_NOMOVE 0x0002; // 保持位置 private const uint SWP_SHOWWINDOW 0x0040; private IntPtr hWnd; // 用于存储我们的窗口句柄 }代码解读DllImport属性告诉编译器这个函数的实现在指定的DLLuser32.dll中。extern关键字表明这是一个外部方法。函数签名参数类型、返回类型必须与DLL中的原始C函数声明尽可能匹配。这里我们使用了IntPtr来代表Windows中的句柄HWND用int或uint代表各种标志和索引。GetActiveWindow()在Unity脚本运行时通常能正确获取到Unity游戏窗口的句柄。4.2 初始化窗口样式现在我们在Start()或Awake()方法中获取窗口句柄并应用样式。void Start() { // 1. 获取当前Unity窗口的句柄 hWnd GetActiveWindow(); if (hWnd IntPtr.Zero) { Debug.LogError(Failed to get window handle!); return; } // 2. 获取当前的扩展样式 int extendedStyle GetWindowLong(hWnd, GWL_EXSTYLE); // 3. 添加分层窗口和透明样式 // 注意WS_EX_TRANSPARENT会让整个窗口鼠标穿透慎用 // extendedStyle extendedStyle | WS_EX_LAYERED | WS_EX_TRANSPARENT; // 我们先只添加分层样式以便设置透明度 extendedStyle extendedStyle | WS_EX_LAYERED; // 4. 将新的样式设置回窗口 SetWindowLong(hWnd, GWL_EXSTYLE, extendedStyle); // 5. 设置窗口置顶可选对于挂件很实用 SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); Debug.Log(Window style set to layered.); }4.3 实现颜色键控透明这是实现“异形”窗口的经典方法。我们将Unity摄像机背景设为某种颜色如亮绿然后告诉Windows将这个颜色视为透明。设置摄像机背景色回到Unity编辑器找到你的UI摄像机或主摄像机。在Inspector面板将Background设置为纯绿色即RGB(0, 255, 0)。在脚本中应用颜色键在Start()方法的最后添加设置颜色键的代码。void Start() { // ... 之前的获取句柄和设置样式的代码 ... // 设置颜色键控透明 // 颜色值需要是BGR格式的而不是RGB。纯绿色(0,255,0)的BGR值也是(0,255,0) uint colorKey 0x00FF00; // 这是绿色的十六进制表示 (BBGGRR格式: 0x00FF00) byte alpha 255; // 整体Alpha值255为不透明这里颜色键透明不依赖它 SetLayeredWindowAttributes(hWnd, colorKey, alpha, LWA_COLORKEY); Debug.Log(Color key transparency applied (Green).); }现在运行游戏。你应该会看到一个窗口其中所有绿色的部分即UI元素之外的摄像机背景都变成了透明直接露出了后面的桌面或其它窗口。而你的UI元素则正常显示。4.4 实现整体Alpha透明度如果你想要的是整个窗口包括UI元素都半透明类似于一个淡淡的遮罩可以使用Alpha混合模式。// 在Start()中替换或注释掉颜色键设置的代码 void Start() { // ... 之前的获取句柄和设置样式的代码 ... // 设置整体Alpha透明度 (例如150/255 ≈ 59% 不透明度) uint colorKey 0; // 颜色键模式不使用设为0 byte alpha 150; // 透明度值0完全透明255完全不透明 SetLayeredWindowAttributes(hWnd, colorKey, alpha, LWA_ALPHA); Debug.Log(Alpha transparency applied.); }重要提示LWA_COLORKEY和LWA_ALPHA是两种不同的模式dwFlags参数一次只能使用其中一个。你不能同时用同一个SetLayeredWindowAttributes调用既设置颜色键又设置整体Alpha。但你可以先设置颜色键实现形状抠图然后通过其他方式如调整UI元素的Canvas Group Alpha来改变UI自身的透明度。4.5 实现可点击与点击穿透的混合控制默认情况下添加了WS_EX_LAYERED样式的窗口其上的所有像素包括透明部分默认都是可以接收鼠标消息的。这有时不是我们想要的。我们可能需要场景A整个窗口作为信息板鼠标完全穿透不影响桌面操作。 - 添加WS_EX_TRANSPARENT样式。场景B窗口背景透明且鼠标穿透但窗口上的某个按钮需要能被点击。 - 这需要更精细的控制WS_EX_TRANSPARENT样式会禁用整个窗口的点击。对于场景B一个更可行的方案是不使用WS_EX_TRANSPARENT而是动态处理鼠标输入。我们可以通过检测鼠标位置是否落在可点击的UI元素如按钮上来决定是否拦截鼠标消息。但这涉及到更底层的Windows消息钩子SetWindowsHookEx实现复杂度陡增。一个在Unity内相对取巧的替代方案是不使用WS_EX_TRANSPARENT让窗口默认可点击。在Unity中创建一个覆盖整个窗口的、完全透明的Image或Panel作为“背景板”并为其添加一个Canvas Group组件将其Alpha设为0同时取消勾选Interactable和Blocks Raycasts。这样这个背景板既不可见也不会阻挡射线检测。你的可点击按钮放在这个背景板之上。由于背景板不阻挡射线鼠标事件会穿过它被按钮捕获。对于你希望鼠标穿透的区域实际上就是“没有放置任何可交互UI元素”的区域。因为窗口本身是可点击的但点击在这些“空白”区域时如果没有UI元素响应事件理论上会由窗口本身处理。为了确保穿透我们需要确保Unity不会在这些区域产生碰撞检测。这通常就是默认情况。然而这种方法在窗口拖动时可能会遇到问题你可能会意外点击到UI后面的窗口标题栏区域。一个更彻底的解决方案是自定义窗口的拖动区域这又需要调用user32.dll的SendMessage来模拟窗口拖动消息WM_NCLBUTTONDOWN在HTCAPTION区域。这超出了入门教程的范围但你可以将其作为一个进阶方向通过P/Invoke调用ReleaseCapture()和SendMessage(hWnd, WM_NCLBUTTONDOWN, HTCAPTION, 0)来允许用户通过点击UI的特定部分比如一个自定义的标题栏来拖动整个窗口。5. 构建、运行与调试技巧5.1 构建可执行文件完成脚本编写并挂载到场景中的某个GameObject例如主摄像机或一个空对象后就可以进行构建了。再次打开File - Build Settings。点击Add Open Scenes将当前场景加入构建列表。点击Build按钮选择一个输出文件夹例如在项目根目录创建一个Builds文件夹。等待构建完成。你会得到一个.exe文件和一个_Data文件夹如果未勾选Create Visual Studio Solution等选项。运行这个.exe文件你应该能看到一个具有透明背景或整体半透效果的窗口。你可以将其固定在桌面任意位置。5.2 调试与常见问题排查在开发过程中你可能会遇到以下问题问题1窗口没有变透明还是默认的灰色背景。可能原因AAPI调用失败窗口样式未成功设置。排查在Start()中每个API调用后检查返回值。SetWindowLong成功时返回之前的样式值失败返回0。SetLayeredWindowAttributes返回bool。可以添加Debug.Log输出这些值。确保脚本在游戏启动早期运行Awake或Start并且挂载的GameObject是激活的。可能原因B颜色键颜色不匹配。排查确保Unity摄像机的Background颜色与你代码中colorKey的值完全一致。注意colorKey是BBGGRR格式。纯绿色(0,255,0)的十六进制是0x00FF00。使用取色工具确认摄像机背景色的RGB值。技巧可以暂时将colorKey设置为一个非常显眼的颜色如亮粉色RGB(255,0,255)即0xFF00FF看看窗口中哪些部分被抠掉了以确认API生效。问题2窗口变得完全透明连UI也看不见了。可能原因错误地使用了WS_EX_TRANSPARENT样式或者Alpha值设为了0。解决检查代码中是否添加了WS_EX_TRANSPARENT。如果不需要全局鼠标穿透请移除它。检查SetLayeredWindowAttributes中bAlpha参数的值。问题3窗口无法置顶或被其他窗口覆盖。可能原因SetWindowPos调用失败或参数错误。排查SetWindowPos返回bool检查其返回值。确保hWnd句柄有效。注意某些系统设置或第三方软件如某些桌面美化工具可能会覆盖或限制窗口的置顶行为。问题4在Unity编辑器中运行正常但打出的exe无效。可能原因构建后窗口句柄获取方式可能略有不同或者构建设置影响了窗口初始化顺序。解决尝试将初始化代码放在Start()中而非Awake()中。或者使用[RuntimeInitializeOnLoadMethod]属性标记一个静态方法确保在游戏加载时尽早执行。进阶可以考虑使用FindWindow函数通过Unity窗口的类名通常是“UnityWndClass”和窗口标题来查找句柄这样更稳定。[DllImport(user32.dll, SetLastError true)] static extern IntPtr FindWindow(string lpClassName, string lpWindowName); // 在Start中可以尝试用这种方式获取句柄 hWnd FindWindow(UnityWndClass, Application.productName); // Application.productName是你的游戏名 if (hWnd IntPtr.Zero) { // 如果失败再回退到GetActiveWindow hWnd GetActiveWindow(); }问题5性能考虑。持续调用SetLayeredWindowAttributes来改变透明度例如实现闪烁效果是可以的但频率不宜过高。使用颜色键透明LWA_COLORKEY在大多数现代电脑上性能开销极小。如果你需要动态改变窗口形状而不仅仅是整体透明度频繁调用UpdateLayeredWindow并上传位图数据可能会成为性能瓶颈需要谨慎优化。6. 进阶思路与扩展应用掌握了基础透明窗口创建后你可以将这个技术应用到更多有趣的方向动态透明度与交互反馈结合UI事件实现鼠标悬停时窗口变清晰移出时变半透明。这只需要在相应事件如EventTrigger的OnPointerEnter和OnPointerExit中调用SetLayeredWindowAttributes修改bAlpha值即可。不规则窗口形状颜色键控透明可以实现简单的矩形抠图。但对于复杂形状如圆形、星形窗口你需要在Unity中将你需要显示的形状渲染到一张纹理上背景为透明色Alpha0。然后你需要使用更高级的UpdateLayeredWindow方法并提供一个包含每像素Alpha通道的位图Bitmap。这需要将Unity中的Texture2D数据通过System.Drawing命名空间转换为GDI位图过程较为复杂涉及大量的数据格式转换和内存管理。与系统交互的桌面小工具系统信息使用System.Diagnostics命名空间下的PerformanceCounter来获取CPU、内存使用率并在UI上实时显示。网络请求使用UnityWebRequest显示天气、新闻摘要。音乐控制通过调用Windows多媒体API或与特定播放器如Spotify的接口进行交互实现桌面歌词秀或迷你控制器。窗口拖拽与缩放如前所述通过拦截鼠标消息并发送WM_NCLBUTTONDOWN等消息可以实现用UI元素来拖动无边框窗口。同样可以监听鼠标在窗口边缘的事件然后调用SetWindowPos来改变窗口大小。多显示器适配使用Screen和System.Windows.Forms.Screen需要引用System.Windows.Forms程序集来获取多显示器信息确保你的挂件能正确地在指定的显示器上启动和定位。实现这些进阶功能意味着你需要更深入地学习Windows API和C#的互操作技术。每一次挑战都会让你对Windows桌面编程和Unity的边界有更深的理解。这个小小的透明窗口项目就像一扇门背后连接着桌面应用开发的广阔世界。