深入Windows消息循环:手把手教你用Unity拦截WM_SIZING实现自定义窗口控制
深入Windows消息循环:手把手教你用Unity拦截WM_SIZING实现自定义窗口控制
在Unity开发中,窗口管理通常被视为引擎自动处理的"黑箱"部分。但当我们需要实现特殊窗口效果——比如强制保持16:9比例、自定义标题栏或无边框窗口拖动时,就必须深入Windows消息机制的核心层。本文将揭示如何通过拦截WM_SIZING消息,在Unity中实现操作系统级的窗口控制。
1. Windows消息机制基础
1.1 消息循环原理
每个Windows应用程序都运行在一个持续处理消息的循环中。当用户调整窗口大小时,系统会生成WM_SIZING消息(消息代码0x214),其中包含窗口新尺寸的矩形区域信息。通过拦截这个消息,我们可以修改其参数来实现自定义控制。
关键消息类型对照表:
| 消息代码 | 含义 | 典型应用场景 |
|---|---|---|
| 0x214 | WM_SIZING | 窗口大小调整中 |
| 0x216 | WM_MOVING | 窗口移动中 |
| 0x0112 | WM_SYSCOMMAND | 系统菜单命令(如最大化) |
1.2 Unity窗口的特殊性
Unity生成的Windows窗口默认使用UnityWndClass作为窗口类名。要操作这个窗口,我们需要:
- 通过
EnumThreadWindows枚举当前线程的所有窗口 - 用
GetClassName识别Unity窗口 - 获取其窗口句柄(HWND)
[DllImport("user32.dll")] private static extern bool EnumThreadWindows( uint dwThreadId, EnumWindowsProc lpEnumFunc, IntPtr lParam); private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);2. 实现自定义窗口比例控制
2.1 窗口过程(WindowProc)重定向
核心步骤是替换默认的窗口处理函数:
// 定义委托类型 private delegate IntPtr WndProcDelegate( IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); // 替换窗口过程 IntPtr newWndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate); IntPtr oldWndProcPtr = SetWindowLong( unityHWnd, GWLP_WNDPROC, newWndProcPtr);注意:32位和64位系统需要使用不同的API函数
SetWindowLong32/SetWindowLongPtr64
2.2 处理WM_SIZING消息
在自定义窗口过程中,我们需要:
- 解析
lParam指向的RECT结构 - 根据拖拽方向(
wParam)计算新尺寸 - 应用宽高比约束
- 更新
RECT并写回
case WM_SIZING: RECT rc = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT)); // 计算边框尺寸 RECT windowRect = new RECT(); GetWindowRect(hWnd, ref windowRect); RECT clientRect = new RECT(); GetClientRect(hWnd, ref clientRect); int borderWidth = windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left); int borderHeight = windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top); // 应用宽高比计算 rc.Right = rc.Left + Mathf.RoundToInt( (rc.Bottom - rc.Top - borderHeight) * aspect) + borderWidth; Marshal.StructureToPtr(rc, lParam, true); break;3. 高级窗口控制技巧
3.1 全屏模式处理
在全屏切换时需要特殊处理:
void Update() { if (Screen.fullScreen && !wasFullscreenLastFrame) { // 计算带黑边的全屏分辨率 bool needHorizontalBars = aspect < (float)Screen.currentResolution.width / Screen.currentResolution.height; int width = needHorizontalBars ? Mathf.RoundToInt(Screen.currentResolution.height * aspect) : Screen.currentResolution.width; int height = needHorizontalBars ? Screen.currentResolution.height : Mathf.RoundToInt(Screen.currentResolution.width / aspect); Screen.SetResolution(width, height, true); } wasFullscreenLastFrame = Screen.fullScreen; }3.2 最小/最大尺寸限制
在Inspector中暴露参数:
[SerializeField] private int minWidthPixel = 640; [SerializeField] private int maxWidthPixel = 1920;在消息处理中应用限制:
int newWidth = Mathf.Clamp( rc.Right - rc.Left, minWidthPixel, maxWidthPixel); int newHeight = Mathf.Clamp( rc.Bottom - rc.Top, minHeightPixel, maxHeightPixel);4. 实战:无边框窗口拖动
通过扩展消息处理,可以实现无边框窗口的拖动功能:
4.1 拦截非客户区消息
private const int WM_NCHITTEST = 0x0084; private const int HTCLIENT = 1; private const int HTCAPTION = 2; IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == WM_NCHITTEST) { // 将客户端区域点击视为标题栏 IntPtr result = CallWindowProc( oldWndProcPtr, hWnd, msg, wParam, lParam); if (result == (IntPtr)HTCLIENT) return (IntPtr)HTCAPTION; return result; } // ...其他消息处理 }4.2 自定义标题栏实现
结合UI元素可以实现现代化标题栏:
- 创建Canvas覆盖窗口顶部
- 添加关闭/最小化按钮
- 通过
WM_SYSCOMMAND处理按钮点击:
private const int SC_CLOSE = 0xF060; private const int SC_MINIMIZE = 0xF020; if (msg == WM_SYSCOMMAND) { switch (wParam.ToInt32() & 0xFFF0) { case SC_CLOSE: Application.Quit(); break; case SC_MINIMIZE: ShowWindow(hWnd, 2); // SW_MINIMIZE break; } }5. 性能优化与错误处理
5.1 安全的回调清理
在退出时必须恢复原始窗口过程:
private bool ApplicationWantsToQuit() { if (!started) return false; StartCoroutine(DelayedQuit()); return false; } IEnumerator DelayedQuit() { SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr); yield return new WaitForEndOfFrame(); Application.Quit(); }5.2 编辑器兼容性处理
在Unity编辑器中需要特殊处理:
#if !UNITY_EDITOR // 仅在实际构建中挂钩窗口过程 oldWndProcPtr = SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr); #endif实现这种底层控制时,最棘手的部分是处理不同Windows版本间的API差异。在实际项目中,建议添加详细的日志记录,特别是在窗口过程回调中,这能帮助快速定位消息处理问题。
