在开发桌面应用时,我们经常会遇到一些特殊需求。比如你想做一个桌面悬浮时钟、系统监控面板或者游戏辅助界面,这时候传统的窗口边框就显得多余了。更理想的情况是:窗口完全透明,只显示你需要的内容,而且鼠标点击还能穿透到后面的窗口。
我在开发一个桌面歌词显示工具时就遇到了这个问题。传统方案要么有难看的边框,要么会挡住后面的窗口操作。后来发现通过调用Windows API可以完美解决,这就是我们今天要讲的技术。
在开始写代码之前,有几个关键设置必须完成。我刚开始做这个功能时,就因为漏掉了一个设置调试了半天。
首先打开Player Settings:
然后设置主摄像机:
csharp复制Camera.main.backgroundColor = new Color(0, 0, 0, 0);
这个设置让摄像机的背景完全透明。记得检查场景中不能有其他会遮挡的UI元素。
我们要用到的几个关键API都来自user32.dll和Dwmapi.dll。简单来说:
在C#中调用这些原生API需要使用P/Invoke(平台调用)技术。这就像是在C#和原生代码之间架了一座桥。
每个Windows窗口都有一个唯一的句柄(HWND),就像身份证号一样。我们需要先获取当前Unity窗口的句柄:
csharp复制[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
private IntPtr hWnd;
void Start() {
hWnd = GetActiveWindow();
// 其他初始化代码...
}
这个hWnd后面所有API调用都会用到。注意在Unity编辑器中运行时获取的句柄可能不准确,所以建议在实际构建的exe中测试。
接下来是最关键的部分 - 修改窗口样式。我们需要组合使用几个标志:
csharp复制const int GWL_EXSTYLE = -20;
const uint WS_EX_LAYERED = 0x00080000;
const uint WS_EX_TRANSPARENT = 0x00000020;
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
void MakeWindowTransparent() {
// 设置分层窗口样式
SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT);
}
这里WS_EX_LAYERED允许窗口有透明度,WS_EX_TRANSPARENT让鼠标事件穿透。我刚开始漏掉了WS_EX_LAYERED,结果透明度怎么都设置不成功。
有了分层窗口样式后,就可以设置具体的透明度了:
csharp复制[DllImport("user32.dll")]
static extern int SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags);
const uint LWA_COLORKEY = 0x00000001;
const uint LWA_ALPHA = 0x00000002;
void SetTransparency() {
// 完全透明(alpha=0)
SetLayeredWindowAttributes(hWnd, 0, 0, LWA_ALPHA);
}
这里有个坑要注意:LWA_COLORKEY和LWA_ALPHA的区别。前者是基于颜色键的透明(指定某种颜色透明),后者是基于alpha通道的透明。我们这里用后者更合适。
为了让窗口始终显示在最前面,并且去掉边框:
csharp复制[DllImport("user32.dll", SetLastError = true)]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
void SetWindowTopMost() {
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, 0);
}
这个函数还能用来调整窗口大小和位置。参数中的0表示保持当前值不变。
如果你想要Windows 7那种Aero玻璃效果,可以用DwmExtendFrameIntoClientArea:
csharp复制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);
void ExtendFrame() {
MARGINS margins = new MARGINS { cxLeftWidth = -1 };
DwmExtendFrameIntoClientArea(hWnd, ref margins);
}
设置边距为-1表示扩展到整个窗口。这个效果在现代Windows版本上可能不太明显,但知道这个技巧还是很有用的。
把上面所有功能整合起来:
csharp复制using UnityEngine;
using System.Runtime.InteropServices;
public class TransparentWindow : MonoBehaviour {
[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")] static extern int SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags);
[DllImport("user32.dll", SetLastError = true)] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("Dwmapi.dll")] private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);
private struct MARGINS { public int cxLeftWidth; public int cxRightWidth; public int cyTopHeight; public int cyBottomHeight; }
const int GWL_EXSTYLE = -20;
const uint WS_EX_LAYERED = 0x00080000;
const uint WS_EX_TRANSPARENT = 0x00000020;
const uint LWA_ALPHA = 0x00000002;
static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private IntPtr hWnd;
void Start() {
#if !UNITY_EDITOR
hWnd = GetActiveWindow();
// 设置窗口样式
SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT);
// 设置透明度
SetLayeredWindowAttributes(hWnd, 0, 0, LWA_ALPHA);
// 窗口置顶
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, 0);
// 扩展边框
MARGINS margins = new MARGINS { cxLeftWidth = -1 };
DwmExtendFrameIntoClientArea(hWnd, ref margins);
#endif
Application.runInBackground = true;
}
}
在实际项目中,我遇到过几个典型问题:
这种技术可以用于很多有趣的场景:
我在一个音乐可视化项目中就用到了这个技术,让频谱分析器像"浮"在桌面上一样,用户还能正常操作其他窗口。效果非常酷炫,而且实现起来比想象中简单。
记住,调试这类功能时要有耐心。Windows窗口管理有很多细节,不同系统版本可能表现略有不同。建议先在简单的测试项目中验证功能,再集成到主项目中。