1. 项目概述:为什么需要无边框透明窗口?
在Unity的常规认知里,它是个强大的游戏引擎,做出来的东西要么是全屏游戏,要么是带标准标题栏和边框的桌面应用。但如果你想过把Unity变成一个“桌面魔法师”,让它做出像Rainmeter那样的桌面小部件、像Wallpaper Engine那样的动态壁纸伴侣,或者一个可以满屏幕溜达的虚拟宠物,那么第一步,也是最关键的一步,就是打破那个“游戏窗口”的固有形态。
无边框透明窗口,就是这个魔法的起点。它意味着你的Unity应用将失去所有操作系统赋予的标准窗口装饰——没有标题栏,没有最小化/最大化/关闭按钮,没有边框。同时,窗口背景可以是完全透明的,或者带有Alpha通道的半透明效果。这样一来,你的应用就能像一张“贴纸”一样,静静地“悬浮”在桌面上,与其他窗口和平共处,甚至作为背景的一部分。
我最初想实现这个功能,是为了做一个桌面时钟和系统监控悬浮窗。市面上很多工具要么功能单一,要么定制性差,而用Unity来做,我可以完全控制UI的每一个像素,用Shader做出炫酷的视觉效果,还能用C#轻松接入系统信息。这个需求直接把我引向了Windows平台下的原生窗口API。对于宠物应用来说,这个技术更是核心:一个可爱的小家伙能在你工作、看网页时,毫无违和感地在桌面角落散步、睡觉,这种沉浸感和趣味性是传统窗口应用无法提供的。
2. 核心原理与方案选型:绕过Unity的窗口管理器
Unity本身提供了一个Screen类和PlayerSettings里的窗口设置,但这些设置非常基础,通常只能在全屏、最大化窗口和普通窗口之间切换,无法实现真正的无边框和点击穿透。要实现高级的窗口控制,我们必须直接与操作系统对话,也就是调用平台的原生API。
2.1 Windows API:User32.dll 是我们的钥匙
在Windows平台上,这一切的核心是user32.dll这个动态链接库。它提供了管理窗口的所有底层函数。我们需要用到几个关键的函数:
- FindWindow: 根据类名和窗口标题找到我们Unity应用的窗口句柄(HWND)。句柄就像是窗口的身份证号,所有后续操作都需要它。
- SetWindowLong: 这个函数功能强大,我们可以用它来修改窗口的扩展样式(GWL_EXSTYLE)和普通样式(GWL_STYLE)。无边框和透明效果就靠修改这些样式位来实现。
- SetLayeredWindowAttributes: 这是实现透明和颜色键控(让某种颜色变透明)的关键函数。
- DwmExtendFrameIntoClientArea(可选): 如果要做类似Windows Aero的毛玻璃效果,会用到这个来自
dwmapi.dll的函数。
我们的整体思路是:在Unity应用启动后,获取到它的主窗口句柄,然后调用这些API,移除窗口的边框样式(WS_BORDER, WS_CAPTION等),并添加分层窗口样式(WS_EX_LAYERED)来支持透明度。
2.2 方案对比:纯代码 vs. 插件
在具体实施上,主要有两种路径:
- 纯C# P/Invoke方案:直接在Unity的C#脚本中,使用
[DllImport(“user32.dll”)]来声明和调用上述Windows API。这是最轻量、依赖最少的方式,代码完全在自己掌控中。 - 使用第三方插件:市面上有一些封装好的Unity插件,如
StandaloneWindow、BorderlessWindow等。它们将原生调用封装成了更友好的Unity组件和接口。
我为什么最终选择了纯代码方案?插件虽然开箱即用,但往往灵活性不足,遇到特定问题(比如与某些UI系统的兼容性、多显示器适配)时调试黑盒很深。而自己通过P/Invoke实现,虽然前期需要查阅不少文档,但每一步都清晰可见,能够针对自己的需求做精细调整。例如,我可以精确控制点击穿透的区域,或者实现仅对鼠标移入事件有响应但视觉上完全透明的窗口,这对于做非矩形的悬浮工具至关重要。
注意:直接操作窗口API是平台相关的。本文重点讲解Windows平台的实现。如果你需要发布到macOS或Linux,需要分别研究Cocoa或X11的相关接口,这通常是完全不同的另一套代码。
3. 实战:从零打造无边框透明窗口
接下来,我们一步步用代码实现。我会创建一个名为BorderlessWindow.cs的脚本。
3.1 第一步:声明所需的Windows API函数
首先,我们需要在C#脚本中声明将要使用的原生函数、常量和结构体。
using System; using System.Runtime.InteropServices; using UnityEngine; public class BorderlessWindow : MonoBehaviour { // 引入user32.dll [DllImport("user32.dll")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong); [DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); [DllImport("user32.dll")] private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw); [DllImport("user32.dll", SetLastError = true)] static extern int GetWindowRect(IntPtr hwnd, out RECT lpRect); // 窗口样式索引 private const int GWL_STYLE = -16; private const int GWL_EXSTYLE = -20; // 窗口样式常量 private const uint WS_POPUP = 0x80000000; private const uint WS_VISIBLE = 0x10000000; private const uint WS_EX_LAYERED = 0x00080000; private const uint WS_EX_TRANSPARENT = 0x00000020; private const uint WS_EX_TOOLWINDOW = 0x00000080; // 不在任务栏显示 private const uint WS_EX_TOPMOST = 0x00000008; // 窗口置顶 // SetWindowPos 使用的常量 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_FRAMECHANGED = 0x0020; // SetLayeredWindowAttributes 使用的常量 private const uint LWA_COLORKEY = 0x00000001; private const uint LWA_ALPHA = 0x00000002; // 矩形结构体,用于获取窗口位置 [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } private IntPtr _windowHandle; }代码解析:
DllImport属性告诉C#编译器,这个函数实现在指定的DLL里。IntPtr类型用来存储窗口句柄(HWND),它是一个指向内存地址的指针。- 我们声明了最核心的几个函数:
FindWindow,Get/SetWindowLong,SetLayeredWindowAttributes。 - 定义了大量的常量,如
WS_POPUP(弹出式窗口,无边框基础)、WS_EX_LAYERED(支持分层,实现透明的关键)、WS_EX_TOPMOST(置顶)。 RECT结构体用于在C#和原生代码之间传递窗口的坐标信息。
3.2 第二步:在Unity中获取并修改窗口
我们需要在Unity启动后的合适时机执行窗口修改。Start()或Awake()方法是一个好选择。
void Start() { // 1. 获取当前Unity进程的窗口句柄 // 窗口标题默认为产品名,可以在PlayerSettings中修改 string windowTitle = Application.productName; _windowHandle = FindWindow(null, windowTitle); if (_windowHandle == IntPtr.Zero) { Debug.LogError("无法找到游戏窗口句柄!"); return; } // 2. 先获取当前的扩展样式和普通样式 int extendedStyle = GetWindowLong(_windowHandle, GWL_EXSTYLE); int style = GetWindowLong(_windowHandle, GWL_STYLE); // 3. 设置新的样式:移除边框,添加分层、置顶等特性 // 普通样式:设置为弹出式(WS_POPUP)并保留可见性(WS_VISIBLE),这移除了标题栏和边框 uint newStyle = WS_POPUP | WS_VISIBLE; SetWindowLong(_windowHandle, GWL_STYLE, newStyle); // 扩展样式:添加分层、置顶、工具窗口属性 uint newExStyle = (uint)(extendedStyle | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW); // 如果你希望窗口可以被点击穿透,可以添加 WS_EX_TRANSPARENT // newExStyle |= WS_EX_TRANSPARENT; SetWindowLong(_windowHandle, GWL_EXSTYLE, newExStyle); // 4. 设置窗口为完全透明(Alpha=255为不透明,0为完全透明) // 这里先设置为不透明,后续通过Unity Camera的Background alpha控制内容透明 SetLayeredWindowAttributes(_windowHandle, 0, 255, LWA_ALPHA); // 5. 应用样式更改,并强制窗口重绘 SetWindowPos(_windowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); Debug.Log("无边框窗口设置完成。"); }关键点解释:
FindWindow(null, Application.productName):第一个参数为类名,传null表示匹配所有类;第二个参数是窗口标题,默认是你在PlayerSettings里设置的Product Name。确保你的应用运行时标题是这个。WS_POPUP样式是成为无边框窗口的关键。WS_VISIBLE确保窗口保持可见。WS_EX_LAYERED是启用窗口透明度功能的前提,必须设置。WS_EX_TOPMOST让窗口始终在最前端,这对于桌面悬浮工具是刚需。WS_EX_TOOLWINDOW是一个很有用的样式,它会让窗口不在任务栏显示,也不会在Alt+Tab列表中出现,让应用更像一个“后台服务”或“小部件”。WS_EX_TRANSPARENT需要谨慎使用。它会让窗口对鼠标点击完全“穿透”,即点击会直接落到它后面的窗口上。这对于完全作为背景展示的应用(如动态壁纸)是好的,但对于需要交互的宠物或工具,则不能添加此样式。SetWindowPos函数末尾的SWP_FRAMECHANGED标志至关重要,它通知系统窗口框架已改变,必须调用才能让样式更改生效。
3.3 第三步:在Unity中实现视觉透明
仅仅设置了窗口支持分层(Layered)还不够,我们需要告诉Unity渲染一个透明的背景。
设置相机背景:在你的主相机(Main Camera)上,将
Clear Flags设置为Solid Color,然后将Background的Alpha值(A)设置为0。这样相机就会渲染一个完全透明的背景。处理UI和物体的透明:确保你的UI Canvas的渲染模式是
Screen Space - Camera或World Space,并且其Graphic Raycaster等组件工作正常。对于2D/3D物体,使用支持透明通道的Shader(如Sprites/Default,Standard材质开启透明度混合等)。发布设置:在
File -> Build Settings -> Player Settings...中,转到Resolution and Presentation选项卡。确保Fullscreen Mode设置为Windowed(窗口化)。你还可以在这里设置窗口的默认Width和Height。
至此,一个基本的无边框透明窗口就完成了。运行构建出的.exe文件,你会看到一个没有标题栏、背景透明、悬浮在最顶层的窗口。
4. 进阶技巧与功能拓展
基础功能实现后,我们可以让它变得更强大、更实用。
4.1 实现鼠标拖拽移动窗口
由于移除了标题栏,我们失去了拖拽移动窗口的能力。需要自己用代码实现。
using UnityEngine.EventSystems; public class BorderlessWindow : MonoBehaviour, IDragHandler { // ... 之前的API声明和Start代码 ... [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ReleaseCapture(); [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); private const uint WM_SYSCOMMAND = 0x0112; private const uint SC_MOVE = 0xF010; private const uint HTCAPTION = 0x0002; // 在需要拖拽的UI元素上添加Event Trigger,调用此方法 public void OnDrag(PointerEventData eventData) { // 释放鼠标捕获 ReleaseCapture(); // 向窗口发送“开始拖拽移动”的系统命令 SendMessage(_windowHandle, WM_SYSCOMMAND, (IntPtr)(SC_MOVE | HTCAPTION), IntPtr.Zero); } }将这个脚本挂载到你想作为“拖拽柄”的UI元素(比如一个顶部的Panel或一个图标)上,并为该UI元素添加Event Trigger组件,将OnDrag事件关联到BorderlessWindow.OnDrag方法。这样,拖拽这个UI元素就能移动整个窗口了。
4.2 实现不规则窗口(非矩形)
有时我们想要一个圆形、圆角矩形或者任意形状的窗口(比如一个心形的宠物)。这需要用到SetWindowRgn函数来设置窗口区域。
[DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nWidthEllipse, int nHeightEllipse); [DllImport("gdi32.dll")] private static extern IntPtr CreateEllipticRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect); void MakeRoundWindow(int cornerRadius) { if (_windowHandle == IntPtr.Zero) return; RECT rect; GetWindowRect(_windowHandle, out rect); int width = rect.Right - rect.Left; int height = rect.Bottom - rect.Top; // 创建一个圆角矩形区域 IntPtr hRgn = CreateRoundRectRgn(0, 0, width, height, cornerRadius, cornerRadius); // 或者创建一个椭圆形区域 // IntPtr hRgn = CreateEllipticRgn(0, 0, width, height); // 将窗口区域设置为新创建的区域 SetWindowRgn(_windowHandle, hRgn, true); }重要提示:创建不规则窗口后,窗口区域外的部分将不响应任何鼠标事件(包括点击穿透)。你需要仔细设计你的UI布局,确保所有交互元素都在该区域内。对于复杂形状(如小动物轮廓),通常的实践是:窗口本身仍为矩形(或稍大的形状),但通过将大部分背景设置为透明,并仅在特定“精灵”部分接收鼠标事件,来模拟不规则形状。这涉及到更精细的Graphic Raycaster或自定义物理碰撞体来处理点击。
4.3 窗口置顶管理
我们之前用WS_EX_TOPMOST实现了强制置顶。但有时用户可能想暂时把它隐藏。我们可以通过动态修改样式来实现“切换置顶”功能。
private bool _isTopMost = true; public void ToggleTopMost() { _isTopMost = !_isTopMost; int extendedStyle = GetWindowLong(_windowHandle, GWL_EXSTYLE); if (_isTopMost) { extendedStyle |= (int)WS_EX_TOPMOST; SetWindowPos(_windowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); } else { extendedStyle &= ~(int)WS_EX_TOPMOST; SetWindowPos(_windowHandle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); // HWND_NOTOPMOST } SetWindowLong(_windowHandle, GWL_EXSTYLE, (uint)extendedStyle); }4.4 与桌面交互:获取屏幕信息与多显示器支持
对于桌面工具,获取准确的屏幕尺寸和位置很重要。
[DllImport("user32.dll")] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); public List<RECT> GetAllMonitorRects() { List<RECT> monitorRects = new List<RECT>(); MonitorEnumProc callback = (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT rect, IntPtr data) => { monitorRects.Add(rect); return true; }; EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero); return monitorRects; }你可以利用这些信息,将你的悬浮窗口智能地定位到某个显示器的角落,或者实现窗口在多个显示器间“跳跃”的趣味效果。
5. 打造桌面宠物应用的核心架构
有了无边框透明窗口这个基础,我们来构思一个桌面宠物应用的核心系统。这不仅仅是图形显示,更是一套状态机和行为逻辑。
5.1 宠物状态机设计
一个简单的宠物至少包含以下几种状态:Idle(空闲/待机)、Walking(行走)、Sleeping(睡觉)、Eating(吃东西)、Reacting(对鼠标/事件反应)。使用一个简单的状态机(State Machine)来管理是最清晰的。
public enum PetState { Idle, Walking, Sleeping, Eating, Reacting } public class DesktopPet : MonoBehaviour { public PetState currentState = PetState.Idle; private Animator _animator; private Vector2 _targetPosition; public float moveSpeed = 100f; void Update() { switch (currentState) { case PetState.Idle: // 播放待机动画,有一定概率切换到Walking if (Random.Range(0f, 1f) < 0.001f * Time.deltaTime) { SetState(PetState.Walking); } break; case PetState.Walking: // 向目标点移动 transform.position = Vector2.MoveTowards(transform.position, _targetPosition, moveSpeed * Time.deltaTime); // 更新动画方向 UpdateFacingDirection(_targetPosition); // 到达目标点 if (Vector2.Distance(transform.position, _targetPosition) < 1f) { SetState(PetState.Idle); } break; case PetState.Sleeping: // 播放睡觉动画,计时结束后醒来 break; // ... 其他状态 } } void SetState(PetState newState) { // 退出当前状态逻辑 switch (currentState) { case PetState.Walking: // 停止移动相关逻辑 break; } currentState = newState; // 进入新状态逻辑 switch (newState) { case PetState.Walking: // 在屏幕安全区域内随机一个目标点 _targetPosition = GetRandomPositionOnScreen(); break; case PetState.Sleeping: // 播放睡觉动画,设置一个唤醒计时器 break; } // 触发Animator中的状态切换 _animator.SetInteger("State", (int)newState); } Vector2 GetRandomPositionOnScreen() { // 需要考虑任务栏等区域,最好获取工作区(Work Area)而非屏幕分辨率 // 这里简化处理,使用Screen.width/height float padding = 50f; // 避免宠物走到屏幕边缘外 float x = Random.Range(padding, Screen.width - padding); float y = Random.Range(padding, Screen.height - padding); return Camera.main.ScreenToWorldPoint(new Vector3(x, y, 10)); // 假设相机Z=-10 } }5.2 交互逻辑:鼠标与宠物
宠物需要能对用户的鼠标行为做出反应。
void OnMouseOver() // 或者使用UI EventSystem的IPointerEnterHandler { // 当鼠标悬停在宠物碰撞体/UI上时 if (currentState != PetState.Reacting) { // 可以播放一个开心的动画,或者看向鼠标 SetState(PetState.Reacting); // 设置一个计时器,几秒后返回Idle CancelInvoke(nameof(ReturnToIdle)); Invoke(nameof(ReturnToIdle), 1.5f); } } void OnMouseDown() // 或 IPointerDownHandler { // 鼠标点击宠物 if (currentState == PetState.Walking || currentState == PetState.Idle) { // 播放一个被点击的惊讶动画 SetState(PetState.Reacting); // 也可以在这里触发拖拽宠物的逻辑(需要另一套拖拽代码,与窗口拖拽区分) } } void ReturnToIdle() { if (currentState == PetState.Reacting) { SetState(PetState.Idle); } }5.3 性能优化与后台运行
桌面宠物是长期运行的应用,性能优化至关重要。
- 帧率控制:在
Quality Settings中降低默认帧率,或者使用Application.targetFrameRate = 30;。宠物动画不需要60FPS,30FPS甚至15FPS都足够流畅,能大幅降低CPU和GPU占用。 - 渲染优化:
- 宠物模型/精灵使用尽可能少的顶点和面数。
- 使用
Sprite Atlas打包2D精灵。 - 减少实时光照,使用烘焙光照或自发光材质。
- 如果宠物是2D,使用
Camera的Occlusion Culling(虽然对2D作用有限)或自行管理渲染层级。
- 逻辑更新优化:将一些非实时逻辑(如状态切换的概率检查)放在
Coroutine中,每几秒检查一次,而不是每帧检查。 - 后台行为:当检测到用户全屏运行游戏或应用时(可以通过
Windows API获取前台窗口信息),宠物可以自动进入Sleeping状态或降低活动频率,减少干扰。
6. 常见问题与避坑指南
在实际开发中,我踩过不少坑,这里总结一下最典型的几个问题和解决方案。
6.1 窗口初始化时机问题
问题:在Awake()或Start()中调用窗口修改API有时会失败,因为此时Unity窗口可能尚未被操作系统完全创建和注册。
解决方案:使用IEnumerator延迟执行,或者监听OnApplicationFocus(true)事件。更可靠的方法是使用[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]属性标记一个静态初始化方法。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void OnRuntimeMethodLoad() { GameObject go = new GameObject("BorderlessWindowManager"); go.AddComponent<BorderlessWindow>(); DontDestroyOnLoad(go); }6.2 点击穿透与交互冲突
问题:设置了WS_EX_TRANSPARENT后,整个窗口都无法点击了。但我们可能希望宠物身体可点击,透明背景部分穿透。
解决方案:不要使用WS_EX_TRANSPARENT样式。正确的做法是:
- 保持窗口可点击。
- 在Unity中,将背景的
Canvas Renderer组件的Raycast Target取消勾选。 - 只为需要交互的UI元素(如宠物精灵、控制按钮)开启
Raycast Target。 - 对于复杂的非矩形点击区域,可以使用多个
Rect Transform组合,或者为2D精灵添加Polygon Collider 2D,并配合Physics2D.Raycast进行更精确的点击检测。
6.3 任务栏与Alt+Tab列表中的显示
问题:即使使用了WS_EX_TOOLWINDOW,在某些Windows版本或设置下,应用仍可能出现在Alt+Tab列表中。
解决方案:WS_EX_TOOLWINDOW通常能解决大部分问题。如果还有残留,可以尝试组合使用WS_EX_APPWINDOW样式的反向操作(即移除它),但这需要更精细的样式位操作。一个更彻底的“隐身”方法是创建一个不可见的父窗口,将我们的Unity窗口作为其子窗口。但这会带来额外的复杂度。
6.4 抗锯齿与透明边缘黑边/白边
问题:当窗口透明,且UI元素有Alpha渐变时,边缘可能出现难看的黑边或白边。
解决方案:
- 检查图片导入设置:确保UI Sprite或Texture的
Wrap Mode为Clamp,Filter Mode为Bilinear,并关闭Generate Mip Maps。对于边缘渐变的精灵,Alpha Source应为Input Texture Alpha,Alpha Is Transparency务必勾选。 - 修改Shader:Unity UI默认的Shader对边缘处理可能不完美。可以创建一个简单的UI Shader,在片段着色器中对最终颜色进行
premultiplied alpha处理,这能有效减少黑边。// 一个简化的Premultiplied Alpha混合示例(在Shader中) fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; col.rgb *= col.a; // Premultiply Alpha return col; } - 相机设置:确保相机的
Allow HDR和Allow MSAA设置与你的渲染需求匹配,有时关闭它们反而能减少混合错误。
6.5 打包发布后的路径与权限
问题:在编辑器里运行正常,打包成exe后窗口修改失效。
排查步骤:
- 确认窗口标题:打包后的exe,其窗口标题可能包含版本号等后缀。使用
Spy++(Windows SDK工具)或Process Explorer这类工具,查看运行中exe的实际窗口标题和类名,然后在FindWindow函数中使用准确的信息。 - 管理员权限:某些窗口样式修改可能需要管理员权限。可以尝试右键“以管理员身份运行”你的exe看是否生效。如果必须,可以在项目清单文件(
app.manifest)中请求requireAdministrator权限,但这会降低用户体验。 - 杀毒软件干扰:部分安全软件可能会拦截对窗口样式的修改。将你的exe添加到杀毒软件的白名单中。
实现Unity无边框透明窗口,是打开桌面级应用开发大门的第一把钥匙。它剥离了游戏的“外壳”,让Unity的内容能够以最原生、最自由的方式融入用户的数字桌面环境。从简单的系统监控悬浮球,到复杂的互动桌面宠物,其底层都依赖于这套与操作系统直接对话的技术。