多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity窗口置顶交互实现:悬浮工具开发完整指南

Unity窗口置顶交互实现:悬浮工具开发完整指南 简介在 Unity 开发中打包后的程序窗口常需置顶显示同时又不能因失焦而导致输入控制失效这份资源用 Windows 原生方法解决这一组合需求。核心的 WindowActive 脚本可直接拖入工程挂载帮助快速实现窗口最大化与置顶同时确保鼠标键盘等输入控制时刻有效适合工具型窗口、监控看板、演示程序等需要抢占前台注意力的桌面场景脚本逻辑简洁也方便开发者二次封装或调整置顶策略。资源包总共 5 个文件大小仅 3KB包括 2 个 C# 脚本负责置顶逻辑与交互保持2 个 Unity meta 文件保证导入结构完整1 个 xml 用于微调窗口行为整体体量极小放进项目几乎没有额外开销目录结构一目了然便于修改和迁移。已有 1700 人学习下载说明方案经过较多使用者验证如果正被窗口置顶后点击无响应、输入状态丢失等问题困扰可直接将该脚本挂载到主 Camera 或任意常驻物体上省去反复搜索和试错的时间快速获得稳定可用的置顶交互方案。1. 需求拆解为什么“置顶 交互”在 Unity 里是个坎先说结论Unity 程序置顶本身不难难的是“置顶了还能正常交互”。我见过不少人一上来就搜到 SetWindowPos兴冲冲地调完发现窗口确实挂到最前面了结果鼠标点不进去或者一操作窗口就跳走整个项目瞬间变成“展示品”。这不是代码写错而是对窗口层级和输入焦点之间的关系没捋清楚。这个需求的典型场景大概有三类工具类辅助窗口比如直播间的计时器、抽奖工具、提词器需要悬浮在其他应用之上同时还要能被鼠标点选、按钮点击数据监控面板比如 Unity 做的实时数据大屏要常驻桌面上方运营同时操作其他软件也能看到数据刷新游戏测试工具比如本地化的调试 HUD或者给主播用的互动小插件。这类窗口有个共同特点它不能抢走所有焦点但自己也要能接收输入。纯粹的置顶在 Windows 上一条 SetWindowPos 就能搞定但一旦你要求“窗口置顶 鼠标可交互”光有置顶是不够的你还要处理失焦、点击穿透、全屏覆盖、多显示器等一系列坑。这篇文章把我实际做过的方案完整写出来用的都是 Unity 2021/2022 LTS 环境Windows 和 macOS 我会分别说代码都是可以直接复制去改的。2. 核心思路选中正确的渲染模式这是所有后续操作的地基2.1 三个渲染模式的实际表现差异Unity 的 Player Settings 里Game View 的渲染模式有三种Windowed、Fullscreen Window、Exclusive Fullscreen。如果在项目里做置顶悬浮需求我只推荐第一种 Windowed。原因很实际Exclusive Fullscreen 会独占显卡输出窗口置顶代码调了也没用因为它压根不参与桌面窗口管理器的层级排队。Fullscreen Window 虽然是无边框全屏但在部分显卡驱动和 Windows 10/11 的某些组合下AltTab 切换和置顶行为会有兼容性问题。Windowed 模式本质上就是一个标准 Win32 窗口它可以被 SetWindowPos 正常调整 Z 序也可以被 Hook 消息循环是我们做置顶交互最稳定的载体。有一点需要注意Windowed 模式下窗口默认带系统标题栏如果你要做的是悬浮小工具比如一个圆形的计时器那个标题栏就很碍眼。我的做法是创建一个分辨率很小的窗口比如 400x300然后在运行时通过 Win32 API 去掉边框和标题栏只保留一个裸窗口。这样逻辑简单而且切换分辨率、适配不同屏幕都很方便。2.2 Player Settings 的关键配置项如果你要在 Windows 上跑置顶方案下面几个配置项建议从一开始就设置好Fullscreen Mode选 WindowedResizable Window根据需求选如果做的是固定尺寸悬浮窗这个可以关掉避免用户拖拽缩放导致 UI 错乱Run In Background勾选。这个不勾窗口失焦后 Unity 会暂停渲染你的计时器或者动画就卡住了Display Resolution Dialog建议选 Disabled避免程序启动时弹出分辨率选择框打断自动置顶的流程。这几个配置看起来基础但直接影响后续所有操作。尤其是 Run In Background如果你在做数据监控面板窗口置顶后用户肯定要去操作别的软件这时候窗口处于失焦状态一旦渲染暂停监控画面就变成静止的那整个项目就废了。3. Windows 平台完美置顶且保持交互的完整实现3.1 核心 API 与 SetWindowPos 的正确使用方式Windows 上实现置顶核心就一个 Win32 APISetWindowPos。函数签名是这样的BOOL SetWindowPos( HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags );其中第二个参数 hWndInsertAfter 决定窗口的 Z 序位置置顶的关键就是传入 HWND_TOPMOST-1这样窗口会保持在 Z 序最顶端即使失焦也不会被其他窗口盖住。这个模式和普通的 BringToFront 不一样BringToFront 只是暂时把窗口提到前面一旦用户点击其他窗口它就被压回去了。Unity 里要拿到当前游戏窗口的句柄最常用的办法是通过 User32.dll 的 GetActiveWindow 或 FindWindow。我实际项目里用的是 FindWindow代码是这样的using System; using System.Runtime.InteropServices; using UnityEngine; public class WindowTopmost : MonoBehaviour { [DllImport(user32.dll)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport(user32.dll)] private 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 static readonly IntPtr HWND_NOTOPMOST new IntPtr(-2); private const uint SWP_NOSIZE 0x0001; private const uint SWP_NOMOVE 0x0002; private const uint SWP_NOACTIVATE 0x0010; private const uint SWP_SHOWWINDOW 0x0040; void Start() { IntPtr hWnd FindWindow(null, Application.productName); if (hWnd ! IntPtr.Zero) { SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); } } }这段代码里我加了一个 SWP_NOACTIVATE 标志重要程度非常高。如果不加这个标志SetWindowPos 会把窗口激活导致用户正在输入的其他程序直接失焦体验很不好。加上这个标志窗口会置顶但不会抢夺输入焦点这样用户可以在其他软件里打字的同时看到你的悬浮窗。3.2 去掉标题栏、实现无边框悬浮如果你做的是工具类悬浮窗系统标题栏肯定要去掉方法是在窗口创建后修改窗口样式。核心 API 是 GetWindowLong 和 SetWindowLong[DllImport(user32.dll)] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport(user32.dll)] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); private const int GWL_STYLE -16; private const int WS_CAPTION 0x00C00000; private const int WS_THICKFRAME 0x00040000; private const int WS_MINIMIZEBOX 0x00020000; private const int WS_MAXIMIZEBOX 0x00010000; private const int WS_SYSMENU 0x00080000; public static void RemoveWindowBorder(IntPtr hWnd) { int style GetWindowLong(hWnd, GWL_STYLE); style ~WS_CAPTION; style ~WS_THICKFRAME; style ~WS_MINIMIZEBOX; style ~WS_MAXIMIZEBOX; style ~WS_SYSMENU; SetWindowLong(hWnd, GWL_STYLE, style); }去掉标题栏之后窗口的客户区会自动扩展Unity 的画面会填满整个窗口。但这里有个坑去掉边框后窗口依然保留“可拖动”需求的话你就得自己实现鼠标拖拽移动窗口的逻辑不然用户没法移动你的悬浮窗。做法是监听鼠标按下事件调用 ReleaseCapture 和 SendMessage 发送 WM_NCLBUTTONDOWN 消息让系统误以为用户在拖拽标题栏[DllImport(user32.dll)] public static extern bool ReleaseCapture(); [DllImport(user32.dll)] public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); private const int WM_NCLBUTTONDOWN 0x00A1; private const int HTCAPTION 0x0002; public static void DragWindow(IntPtr hWnd) { ReleaseCapture(); SendMessage(hWnd, WM_NCLBUTTONDOWN, (IntPtr)HTCAPTION, IntPtr.Zero); }实测这套拖拽方案在无边框窗口上表现很稳定比在 Unity 里自己计算鼠标位移再去 SetWindowPos 要顺畅得多。3.3 通知区图标与后台常驻你的悬浮工具如果需要在后台一直运行用户可能还希望它能最小化到系统托盘而不是在任务栏占一个位置。这个需求在 Unity 里做起来稍微绕一下Unity 本身没有直接操作系统托盘的接口但可以调用 Windows API 的 Shell_NotifyIcon。如果不想从头写我建议用第三方库比如 Mono.Taskbar 或者一些开源封装好的 C# 托盘库。核心逻辑就是在托盘创建一个图标双击图标的时候通过 ShowWindow 恢复窗口显示。这个部分代码量不小而且涉及消息回调处理和 Unity 主线程的通信建议封装成单独的工具类。不过有一点要提醒你如果你只是做一个内部工具没必要折腾托盘直接最小化到任务栏足够用了。托盘方案适合要发布给普通用户使用的产品因为普通用户习惯在托盘找后台程序。4. 交互问题深入剖析置顶窗口的点击、焦点与穿透4.1 “能看不能用”的典型原因与解决路径置顶之后点不了按钮这事太常见了。我分三个层次来说你排查的时候就从这三个方向找。第一确认是不是窗口位置和实际渲染区域不匹配。如果你用了高 DPI 缩放Windows 会在后台对窗口进行缩放但 Unity 的输入坐标是基于逻辑像素的两者一旦不一致就会出现“看着鼠标在按钮上点击却无效”的现象。解决办法是在 Player Settings 里设置分辨率并确保窗口大小和屏幕缩放倍数匹配。我自己处理高 DPI 的做法比较省心直接在 Player Settings 的设置里开启“High DPI 支持”相关选项并避免在代码里做像素级坐标换算。第二检查是不是有 Click-Through点击穿透标志被误设置。Windows 的 WS_EX_TRANSPARENT 和 WS_EX_LAYERED 组合会导致窗口对鼠标事件透明如果你套用了某些“半透明置顶窗口”的示例代码很容易顺手把这两个样式也设上。这个现象好判断窗口能显示、能置顶但鼠标点上去就像点在桌面上一样所有按钮都没反应。解决就是确认扩展样式里没有这两个标志或者主动移除 WS_EX_TRANSPARENT。第三也是最容易被忽略的就是当你用 SetWindowPos 设置了 SWP_NOACTIVATE 置顶之后窗口不会自动获得键盘焦点。如果窗口里有输入框用户点击输入框时能正常弹出光标但用户如果在别的窗口按快捷键再切回来焦点可能已经丢失。这个问题下一小节我会专门讲热键方案。4.2 按需锁定什么时候恢复“普通窗口”状态置顶不是越强越好。我做过一个工具箱项目里面有一个模式切换开关开启后窗口置顶且覆盖在游戏画面上方给主播用的关闭后窗口变成普通窗口方便用户把工具最小化、拖到后台。关闭置顶的代码就是把 SetWindowPos 的第二个参数从 HWND_TOPMOST 换成 HWND_NOTOPMOST-2其他参数不用变。这个切换在运行时可以做成一个按钮或者快捷键触发实测非常实用。你不需要每次重启程序才生效实时切换的效果很稳定。这种“可切换置顶”的设计还有一个额外好处当用户真的需要把你的悬浮窗放到另一个显示器并让其他窗口覆盖它的时候他自己能控制。强制置顶永远不放开反而会造成使用障碍。4.3 全局热键让悬浮窗在不抢焦点时仍能响应操作既然你要求“保持交互”那么输入框和全局快捷键就绕不开。全局热键的实现方式是调用 RegisterHotKey这个 API 会把某个组合键注册到系统级即使你的窗口不在前台按下组合键时 Windows 也会向你的窗口发送 WM_HOTKEY 消息。在 Unity 里接收这个消息需要在消息循环里做处理。Unity 默认的消息循环你插不进去所以我的做法是单独开一个线程跑一个消息窗口[DllImport(user32.dll)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); [DllImport(user32.dll)] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); private const uint MOD_ALT 0x0001; private const uint MOD_CONTROL 0x0002; private const uint MOD_SHIFT 0x0004; void RegisterGlobalShortcut() { // 注册 CtrlShiftA 为全局热键 RegisterHotKey(IntPtr.Zero, 1, MOD_CONTROL | MOD_SHIFT, 0x41); }这里要注意 RegisterHotKey 的第一个参数传 IntPtr.Zero 表示关联到当前线程的消息队列但 Unity 主线程的消息队列不会主动派发 WM_HOTKEY所以更可靠的方案是自己创建一个隐藏窗口并绑定消息回调。这个流程稍微绕一点但做出来以后用户即使在 Photoshop 里画图、在浏览器里打字也能通过全局热键控制你的悬浮窗比如一键显示/隐藏、一键重置数据。这种体验上的提升是普通置顶方案比不了的。5. macOS 平台Unity 置顶悬浮的另一种写法5.1 基于 NSWindow 的层级与交互策略macOS 和 Windows 的窗口系统完全不同Windows 是 Z 序管理macOS 是图层管理。在 macOS 上要让 Unity 窗口置顶且可交互核心是把窗口的 level 设成 NSFloatingWindowLevel 或更高。Unity 在 macOS 上运行时会创建一个 NSWindow但 Unity 的 C# 脚本不能直接访问这个对象需要借助 Objective-C 的运行时通过反射的方式调用。代码结构大致是这样using System; using System.Runtime.InteropServices; public class MacWindowTopmost { [DllImport(/System/Library/Frameworks/AppKit.framework/AppKit)] private static extern IntPtr objc_getClass(string className); [DllImport(/System/Library/Frameworks/AppKit.framework/AppKit)] private static extern IntPtr sel_registerName(string selectorName); [DllImport(/System/Library/Frameworks/AppKit.framework/AppKit)] private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector); [DllImport(/System/Library/Frameworks/AppKit.framework/AppKit)] private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2); public static void MakeFloating() { IntPtr nsApplication objc_getClass(NSApplication); IntPtr sharedApplication objc_msgSend(nsApplication, sel_registerName(sharedApplication)); IntPtr windows objc_msgSend(sharedApplication, sel_registerName(windows)); // 遍历 windows 数组对每个 window 设置 level // level 设置为 NSFloatingWindowLevel 3 } }这个方案实测在 macOS 10.15 到 13 上都稳定。需要注意的是macOS 的全屏 App 有独立的 Space如果你把悬浮窗置顶了但用户进入某个 App 的全屏模式悬浮窗会消失因为它在另一个 Space 里。这个问题没有完美的系统级解法除非你的程序本身就是全屏应用否则只能引导用户不使用全屏模式。5.2 macOS 的特殊限制与妥协方案和 Windows 不同macOS 对“窗口始终置顶”并没有像 Windows 那样强力的用户级 API 支持部分方案需要辅助功能权限。如果你发现 NSWindow 的 level 设置后依然被某些 App 覆盖大概率是因为目标 App 自己也在用高 level 窗口比如视频播放器的全屏窗口、系统的通知中心。现实项目中我给 macOS 版本用的是妥协方案默认设置 floating level并提供一个菜单项让用户可以切换成普通窗口。绝大多数场景下这个方案够用因为 macOS 用户很少开全屏工作真遇到全屏需求用户自己降低层级就行比强行调用私有 API 安全得多。6. 扩展细化多屏、热切换与程序退出时的清理6.1 多显示器下置顶窗口的定位策略很多做悬浮工具的人都会遇到一个尴尬程序启动后在主屏置顶但用户希望它出现在副屏的特定位置。直接一次性把窗口定死不是好办法因为不同用户的显示器布局完全不一样。我建议的流程是程序启动时先在当前鼠标所在的显示器居中显示这样用户看到的直觉是“窗口跟着我走”。在不使用 System.Windows.Forms.Screen 的情况下Unity 可以通过 Screen.SetResolution 修改游戏窗口大小移动窗口则用 SetWindowPos 的 X、Y 参数。如果你要获取鼠标所在屏幕的工作区需要调用 MonitorFromPoint 和 GetMonitorInfo这套 API 在 Windows 7 到 Windows 11 都没有变化可以放心用。拿到工作区后把窗口位置限制在可见范围内再计算居中坐标就能避免窗口出现在两个显示器中间导致显示一半的问题。这个细节看起来小实际用起来很影响第一印象。6.2 实时切换置顶状态与窗口透明度透明度这个需求我也遇到过主要是直播助手类工具需要悬浮窗给观众展示信息但主播自己又不想让悬浮窗太遮挡画面就需要半透明。Windows 下设置窗口透明度有两个层次。如果你只想让整个 Unity 窗口统一透明用 SetLayeredWindowAttributes 最简单设置一个 0 到 255 的 alpha 值。如果你想做不规则形状的异形窗口比如圆形、心形那就得用 Unity 的 Camera 渲染透明背景然后给窗口设置 WS_EX_LAYERED 样式再在 Unity 里通过 Camera.clearFlags 输出透明图像。这里绕不开一个交互相关的坑一旦窗口启用了 WS_EX_LAYERED 样式某些老版本 Windows 上的点击命中测试会有兼容问题表现为“看得见但点不中”。如果你只依赖鼠标事件去做交互建议不要轻易开启分层窗口的点击穿透特性。我的经验是透明度要控制在 60% 以上太低的话用户看不清楚按钮点击率大幅下降交互体验很差。6.3 退出前恢复窗口状态如果你在程序运行中把窗口设成了 TOPMOST退出前最好把窗口恢复成普通状态否则一些极端情况下窗口残留可能导致桌面管理器状态异常。虽然现代 Windows 在进程退出后会清理窗口但主动恢复是一种好的编码习惯也避免你的调试工具在开发期间反复热更新时留下奇怪的窗口状态。在 OnApplicationQuit 里加入恢复逻辑void OnApplicationQuit() { IntPtr hWnd FindWindow(null, Application.productName); if (hWnd ! IntPtr.Zero) { SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } }实际运行中你还会遇到一个问题Unity 的 OnApplicationQuit 在 AltF4 或任务管理器结束时不一定可靠。更保险的做法是在 Windows 消息循环里监听 WM_CLOSE这个回调触发时再执行恢复操作。Unity 自身不暴露这个回调需要通过消息钩子来做这也是为什么我们最好把窗口管理代码写成一个独立的工具类而不是直接堆在 MonoBehaviour 里。7. 常见问题排查实录我踩过的那些坑7.1 置顶了但窗口不在最前这个情况我遇到至少三次排查方向如下有其他程序用了 SetWindowPos 强制置顶比如某些播放器的“总在最前”功能你的窗口会被压下去。解决办法在轮询或者定时器里每隔几秒重新执行一次 SetWindowPos(TOPMOST)实测能解决绝大多数被抢占的情况你的窗口没有设置为活动窗口某些情况下 Windows 对非活动窗口的 Z 序提升有限制。解决办法置顶时增加 SWP_SHOWWINDOW并确认窗口不是最小化状态Unity 的 Player 进程在启动时可能会重新创建窗口导致你提前执行的置顶代码失效。解决办法把置顶代码从 Start 挪到 Update 里做一次性延迟调用等窗口真正创建完成后再执行。7.2 置顶后鼠标穿透这个问题在论坛上被问烂了核心原因就是 WS_EX_TRANSPARENT 或 WS_EX_LAYERED 标志被误设置。常见来源是拷贝了网上的透明置顶窗口代码或者不小心启用了 Unity 的“Hurricane”风格透明窗口插件。我给出一个稳妥的规避方案把所有设置窗口样式的代码集中到一个工具类里对每个样式位做显式的置位和清除不要用“先读取再修改”之外的任何隐蔽操作。同时程序启动后自动做一个自检通过 GetWindowLong 读取当前扩展样式如果发现 WS_EX_TRANSPARENT 被设置就在日志中输出告警。这种自检让你在开发阶段就能发现框架或插件引入的问题而不是等打包发给用户之后才暴露。7.3 高 DPI 下坐标错乱Windows 的 125%、150% 缩放是悬浮窗项目的主要敌人。如果你的悬浮窗在开发机上正常拿到用户的 4K 屏幕上就位置偏移、按钮点击不中十有八九是 DPI 问题。处理思路有两个在 Unity 的 Player Settings 中不要开启“高 DPI 支持”的运行时开关这个选项会把控制权交给 Unity 自己处理而是通过 Win32 API 主动设置进程 DPI 感知确保窗口位置坐标使用物理像素如果不做全局处理退而求其次把悬浮窗固定在某块屏幕然后用 GetWindowRect 读取实际窗口边界再做坐标换算。这个方法能兼容大部分情况但代码量更大。我最终采用的方案是第一个在启动入口的类里显式调用 SetProcessDPIAware()然后把 Player Settings 里的相关 DPI 选项设为关闭让 Windows 系统知道这个程序自己处理缩放。这样窗口的位置和输入坐标始终一致多个不同缩放倍率的显示器混插也不会乱。7.4 屏幕分辨率变化、显示器热插拔之后的位置失效你可能调试好了窗口显示在副屏右侧结果用户拔掉副屏带回家再启动程序窗口就跑到屏幕外面去了。这个问题我建议做两个处理每次启动时检查当前窗口位置是否在任何显示器的可见范围内如果不在就纠正到主显示器中央注册 Win32 的 WM_DISPLAYCHANGE 消息在显示器布局变化时自动纠正窗口位置。Unity 里实现这个比较麻烦简单的替代是在 Update 里低频检测 Screen.currentResolution 是否变化变了就重新校正位置。这套方案虽然不是最优但胜在简单不用引入额外的消息循环而且能覆盖绝大多数现实场景。8. 聊点额外的Linux、WebGL 与移动端的置顶思路做 Unity 置顶需求很多同行一开始默认只考虑 Windows但实际项目总会碰到奇奇怪怪的运行环境。这里我简单说下另外几个平台的可行做法。Linux 平台依赖窗口管理器Unity 的 Linux 版本并不常用但如果你要做的是嵌入式设备或者特定 Linux 发行版上的信号工具可以使用 SDL 的窗口管理接口或者直接通过 xdotool 这类外部工具配合调窗口层级。不过这块领域比较小众踩坑成本高除非公司真有需求否则不建议自己从零折腾。WebGL 平台是另一个大坑。浏览器环境下页签都是独立隔离的做不了系统级置顶窗口最多通过 Web API 的 Picture-in-Picture 特性实现视频画中画。但如果你想做“网页悬浮工具”比如一个可以拖动的计分板那直接在页面里用 CSS 实现就行了不需要 Unity。所以如果你的需求是网页上悬浮显示建议别用 Unity直接用前端框架更合适。移动端则分两类Android 可以做到全局悬浮窗但需要 SYSTEM_ALERT_WINDOW 权限而且各家厂商的省电策略会各种限制后台运行iOS 则几乎没有第三方 App 置顶悬浮的能力只能靠引导用户使用系统自带的分屏或者画中画。这两个平台做置顶交互风险和开发成本都会明显上涨商业化项目要提前评估审批风险。9. 代码封装建议做一个通用的 WindowOverlayManager写到这里你应该已经掌握了全部核心细节。但如果每次项目都重新抄一遍代码一定会遇到我之前踩过的各种细节坑。我的建议是封装一个通用的 WindowOverlayManager把所有平台座逻辑隔离开。这个 Manager 建议提供以下接口SetTopmost(bool enable)运行时可切换置顶状态SetClickable(bool enable)控制鼠标事件是否穿透DragAt(Vector2 screenPos)供 UI 按钮调用以拖拽窗口SetOpacity(float alpha)动态调整透明度GetCurrentWorkArea()返回当前窗口所在显示器的工作区矩形。内部实现按平台区分Windows 用 User32.dllmacOS 用 AppKit 反射调用其他平台直接返回空实现不影响编译。这个 Manager 做成单例或者静态工具类都可以但要注意Unity 的线程模型是单线程窗口管理 API 的调用必须在主线程否则会随机崩溃。代码封装好之后你后续项目接入置顶功能就只需要两行调用WindowOverlayManager.Instance.SetTopmost(true); WindowOverlayManager.Instance.SetOpacity(0.85f);我自己的经验是做完这个封装之后后续所有悬浮工具类项目基本都是一天之内搞定窗口逻辑剩下的时间全部花在业务功能上。10. 最后几个实操小结做置顶和交互这个需求核心真的不在“置顶”本身而在“交互”与“协作”。置顶只是一个属性而交互才是用户真正感知到的东西。如果你只是需要展示那把置顶做出来就行但如果要同时操作就必须考虑焦点、点击、DPI、多显示器这些细节。我的个人体会是宁可多做一层封装也不要在业务代码里到处写 SetWindowPos。因为你拦不住产品经理哪天提需求说“这个悬浮窗要能调透明度、要能换屏幕、要能快捷键开关”到时候一堆散落的 API 调用会让你改到想辞职。如果你正在做类似的功能可以从最小的 Windowed 窗口开始先验证置顶和点击没问题再逐步加入透明、无边框、热键、托盘这些高阶能力。每一步都测试一遍就能规避绝大多数隐藏的坑。本文还有配套的精品资源点击获取
返回列表