ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity嵌入Winform:SetParent与-parentHWND实践

Unity嵌入Winform:SetParent与-parentHWND实践 简介面向需要在 Winform 桌面界面中嵌入 Unity 独立程序的开发者资源包围绕 Unity 构建产物与 Winform 窗体的融合实现展开适合有一定 C# 基础、希望将三维能力接入现有桌面系统的中高级开发者。典型场景包括三维预览、可视化工具、游戏编辑器外壳集成。包内共 181 个文件压缩后 18.32MB主体为 112 个动态库、C# 源码与工程文件、可执行程序、配置文件以及 Unity 构建生成的资源、关卡数据和全局管理文件。目录中已区分打包输出目录与 Winform 容器项目便于对照工程理解进程启动、子窗口句柄挂接、消息交互和跨线程处理等关键实现。已有 170 人学习下载。借助其中代码、配置与构建产物可快速搭建自己的嵌入原型并对嵌入过程中常见的跨线程调用、句柄挂接失败等问题形成排查思路。1. 为什么非要用“Unity exe嵌入到 Winform”这不仅是套壳是两种 UI 体系的缝合做过数字孪生或者工业上位机的人应该都见过这种尴尬C# 团队把 Winform 的界面搭得整整齐齐报表、曲线、设备状态全在一个窗体里结果 3D 场景只能在旁边单独开一个 Unity 窗口。用户要来回切换窗口一多任务栏就乱成一团项目验收时甲方一句“能不能把那个 3D 场景放进去”就能让你加两周班。所谓“Unity exe 嵌入到 Winform”就是把 Unity Build 出来的独立 exe 进程通过 Windows 窗口句柄机制塞进 Winform 某个 Panel 的区域内渲染。这套做法适合两类人一类是上位机项目里需要 3D 展示但不想重写渲染引擎的 C# 开发者另一类是 Unity 团队做好的演示程序需要被现有 Winform 系统“收编”的集成工程师。它解决的不只是“画面嵌进去”还有焦点、消息传递和退出时机这些窗口级交互问题。下面我把这个方案的原理、代码和坑一次讲完。2. 嵌入方案选型-parentHWND 还是 SetParent决定你少踩一半坑2.1 两种方案的底层差异从进程、句柄到消息循环Windows 上的窗口本质是个 HWND窗口句柄每个窗口都有自己独立的消息循环。Winform 窗体是一个进程里的窗口Unity exe 是另一个进程里的窗口。要把两个窗口“合体”底层思路只有一条把 Unity 窗口的父窗口改成 Winform 的某个容器控件让它成为 Winform 的一个子窗口。这里最关键的两个 Win32 API 是 SetParent 和 MoveWindow它们决定了子窗口的位置和大小跟随父窗口。SetParent 的调用很简单但行为有几个反直觉的点。第一跨进程设置父子关系后Unity 窗口的消息循环依然独立它不会收到 Winform 的 WM_CTLCOLOR 之类的自绘消息也不受 Enabled 属性影响。第二子窗口默认会继承父窗口的可见性父窗体最小化时 Unity 窗口跟着隐藏但父窗体有模态对话框时Unity 窗口不一定自动置底容易出现“模态框在 Unity 上面”的层级错乱。第三SetParent 之后Unity 窗口的边框和标题栏还在只是被“囚禁”在 Panel 里要好看还得用 GWL_STYLE 去掉标题栏或者让 Unity 在启动时自己关掉边框。另一种方案是 Unity 官方提供的 -parentHWND 启动参数。Unity 的 Windows Standalone 播放器支持在命令行传入父窗口句柄启动后直接把渲染画面附着到指定窗口相当于 Unity 引擎在初始化阶段就完成了“寄宿”。这个方案不需要你在 Winform 里手动调 SetParent窗口层级由 Unity 自己管理焦点行为也更接近原生嵌入。但要注意-parentHWND 只对 Unity 5.x 之后的 64 位播放器支持良好老版本或者某些自研引擎不一定认。选型时我一般这样判断如果你的 Winform 是主程序Unity 是偶尔启动的可选模块用 -parentHWND 最省心如果你的 Unity exe 还得保留独立运行的场景比如调试时想单独跑就别 hack 得太死SetParent 反而灵活。另外如果你需要动态切换宿主 Panel比如多个标签页共用一个 Unity 实例SetParent 可以随时改父而 -parentHWND 启动后就不太好变。2.2 为什么优先选 Unity 官方的 -parentHWND团队里有 Unity 背景的同事应该都知道Unity 的 Standalone 播放器启动时会读命令行参数其中 -parentHWND 是官方文档明确支持的。用法是在 Process.Start 时把参数拼到 exe 后面-parentHWND 123456。这个 123456 是 Winform 某个 Panel 控件的句柄值也就是 panel1.Handle.ToInt32()。用它的第一个好处是省掉了自己写 SetParent 的时序问题。你用 SetParent 时必须等 Unity 的主窗口创建出来再操作而 Unity 启动有 C 引擎初始化、mono 运行时加载、场景资源加载这段耗时从几百毫秒到几秒不等你得轮询 FindWindow 或 WaitForInputIdle很容易踩“窗口还没创建”或者“创建的不是主窗口”的坑。用 -parentHWNDUnity 引擎内部会在消息循环建立后主动把自己的窗口 attach 过来你不用管中间态。第二个好处是焦点和输入处理更顺。嵌入后Unity 窗口接收鼠标键盘事件是靠 Win32 的焦点机制手动 SetParent 的窗口在单击进入时经常需要额外的 SetFocus 调用否则你先点一下 Unity 区域第一次点击只“激活”了窗口第二次点击才真正触发场景里的按钮。而 -parentHWND 模式下Unity 会把自己注册成父容器的子窗口焦点切换更接近原生控件虽然偶尔也有需要点击两次的问题但频率低很多。第三个好处和尺寸同步有关。Unity 拿到父窗口句柄后会监听父窗口的 WM_SIZE 消息自动调整渲染分辨率。如果你用 SetParent必须自己在 Winform 的 Resize 事件里调 MoveWindow而且 Unity 内部渲染分辨率不一定跟着变容易留黑边或者拉伸变形。所以做项目我会把 -parentHWND 作为默认方案SetParent 只留给极老版本的兼容。2.3 什么时候才需要自己写 SetParent 备胎逻辑不是所有环境都有现成的 -parentHWND。比如你拿到的是第三方用 Unity 打的 exe没留命令行参数解析的余地或者 exe 是用 IL2CPP 但没开“允许传入命令行参数”的选项有时构建配置里忽略了你只能手动找窗口句柄。还有一种典型场景Unity 启动时有启动画面Splash Screen它会单独创建一个无边框窗口而真正的游戏窗口要等 splash 结束才出现。用 SetParent 时你把 splash 窗口塞进去结果 splash 消失后你的 Panel 里就空了。手动 SetParent 的备胎流程一般是Start 进程 → 循环等待 MainWindowHandle 非零 → 如果 handle 对应的是 splash 窗口再等第二个窗口出现或者干脆关掉 splash用 Unity 的 -nolog 或禁用 Splash 配置→ 拿到真正的目标窗口句柄 → SetParent(handle, panel.Handle) → MoveWindow 铺满 panel。写这段逻辑时最要注意超时。Unity 首次启动可能要 5 秒甚至更久如果一个 while 循环里直接 Thread.Sleep(100) 去轮询Winform 的 UI 线程会卡死。正确做法是把等待逻辑放到 Task 里回调里再切换回 UI 线程操作句柄。下面第 3 章给的就是一份完整可用的最小实现。3. 最小实现把 Unity 打包好的 exe 塞进 Winform 的 Panel3.1 启动 Unity 子进程并等待主窗口句柄先准备一个 Winform 窗体上面放一个 panel 控件取名叫 unityContainerDock Fill。然后写一个 UnityRunner 类负责启动、等待和销毁进程。这里最核心的一点是启动参数要拼对尤其是 -parentHWND 的值必须是 panel.Handle不是窗体的 Handle也不是控件的 Handle 属性转字符串前的值。我用 C# 给你一份能直接跑的代码注释标清楚了关键点。using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows.Forms; public class UnityRunner { [DllImport(user32.dll, SetLastError true)] private static extern bool SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [DllImport(user32.dll, SetLastError true)] private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint); private Process _process; private IntPtr _targetHandle IntPtr.Zero; // 启动 Unity 并嵌入到指定 panel public async Task StartAsync(Control parentControl, string unityExePath, string[] extraArgs) { // 关键把控件句柄转成 32 位整数作为 -parentHWND 的值 int parentHwnd parentControl.Handle.ToInt32(); var psi new ProcessStartInfo { FileName unityExePath, UseShellExecute false, CreateNoWindow false, // true 会隐藏掉 Unity 的控制台输出但有时也把窗口隐藏了 Arguments $-parentHWND {parentHwnd} }; // 如果有额外参数比如 -screen-width -screen-height拼进去 if (extraArgs ! null extraArgs.Length 0) psi.Arguments string.Join( , extraArgs); _process Process.Start(psi); // 等待主窗口出现超时 15 秒 await Task.Run(() { var watch Stopwatch.StartNew(); while (watch.ElapsedMilliseconds 15000) { _process.Refresh(); if (_process.MainWindowHandle ! IntPtr.Zero) { _targetHandle _process.MainWindowHandle; break; } System.Threading.Thread.Sleep(100); } }); if (_targetHandle IntPtr.Zero) throw new TimeoutException(Unity 主窗口在 15 秒内没有出现请检查 exe 是否正确启动); } }逻辑说明这里先取 panel 的 Handle这个句柄在窗体创建后一定有效。Arguments 直接拼 -parentHWND让 Unity 自己认领父窗口。等待窗口用 Process.MainWindowHandle这个属性返回的是进程主线程创建的顶层窗口句柄在 Unity 初始化完成前是零。注意不能用 Process.WaitForInputIdle 代替因为 Unity 的消息循环可能一直在处理渲染消息WaitForInputIdle 会一直等到永远。15 秒超时是血泪经验Unity 冷启动加载大场景时超过 10 秒是常有的事但超过 15 秒基本就是参数传错或者 exe 崩了。参数说明-parentHWND 后面的数字必须是十进制整数不能带 0x 前缀。CreateNoWindow 设为 false 是为了在调试时能看到 Unity 的日志输出窗口生产环境你可以设为 true但那样 Unity 崩溃时你看不到任何提示。如果设为 true 后发现画面没进来先确认窗口句柄是否取到了而不是怀疑这个参数。3.2 用 -parentHWND 传窗体的句柄给 Unity上面代码里-parentHWND 传的是 panel 的 Handle。为什么要传 panel 而不是窗体本身因为 Winform 窗体有边框、标题栏和非客户区Unity 窗口直接嵌入窗体的话位置会包括标题栏区域造成偏移。传 panel 后Unity 窗口的坐标原点和 panel 的客户区左上角对齐Dock 布局也自然响应。但这里有个坑Unity 的 -parentHWND 在启动时只读取一次如果你在启动后修改了 panel 的 Handle比如 panel 被重建Unity 不会自动跟随。所以设计上应该让 panel 在整个生命周期里保持固定不要用 TableLayoutPanel 或者需要动态创建销毁的容器。我一般建议放一个专用的 Panel名字起成 unityHost属性里设 DoubleBuffered true 减少闪烁。还有一种情况你的 Unity 版本比较老-parentHWND 不支持或者 exe 是别人打好的没有传参你只能等窗口出现后再 SetParent。此时改一下逻辑把 -parentHWND 去掉等 MainWindowHandle 出来后调用 SetParent。// 等待窗口出现后手动 SetParent IntPtr unityHwnd _process.MainWindowHandle; SetParent(unityHwnd, parentControl.Handle); MoveWindow(unityHwnd, 0, 0, parentControl.Width, parentControl.Height, true);这段逻辑说明SetParent 把 Unity 窗口变成 panel 的子窗口MoveWindow 把它铺满 panel 客户区。窗口出现到 SetParent 之间有一小段时间Unity 的画面会先闪一下在任务栏里注意把 panel 的 BackgroundImage 设成和 Unity 背景色接近视觉上会好一点。参数说明MoveWindow 第五个参数 repaint 传 true告诉系统立即重绘x 和 y 传 0因为 panel 的客户区原点就是 (0,0)。如果你 panel 里有别的控件比如一个工具栏要预留空间就传对应偏移。3.3 嵌入后的尺寸同步与自适应布局嵌入后最经常被问到的问题Winform 窗口大小变了Unity 画面怎么跟着变-parentHWND 模式下Unity 内部会监听父窗口的 WM_SIZE自动调整渲染尺寸。但实际测试中发现如果父窗口是 Winform 的 panel窗体 Resize 时 panel 的尺寸变化会被 Winform 处理成先改 panel 尺寸再重绘Unity 收到的 WM_SIZE 可能晚一拍于是你看到 Unity 画面有短暂拉伸然后缓过来。这种顿挫感在窗口拖拽时尤其明显。解决方法是手动“帮” Unity 同步。常见做法是重写窗体的 OnResize在 base.OnResize 之后调用 MoveWindow 强制更新子窗口位置protected override void OnResize(EventArgs e) { base.OnResize(e); if (_runner ! null _runner.NativeWindowHandle ! IntPtr.Zero) { MoveWindow( _runner.NativeWindowHandle, 0, 0, unityHost.Width, unityHost.Height, true); } }说明MoveWindow 强制让 Unity 窗口位置大小和 panel 一致即使 Unity 自己的消息循环还没处理 WM_SIZE。这样能消除大部分延迟。但注意MoveWindow 不可以调得过勤在窗体拖动过程中会触发大量 Resize 事件如果每次都同步CPU 占用会飙升。我一般加一个节流记录上一次同步的时间50 毫秒内不重复调。还有分辨率参数。Unity 打包时默认的分辨率是在 Player Settings 里设置的嵌入后用 -screen-width 和 -screen-height 可以覆盖。比如你想固定渲染 1920x1080 的输出缩放铺满 panel那么启动参数可以写UnityApp.exe -parentHWND 123456 -screen-width 1920 -screen-height 1080但要注意这种固定分辨率不会等比缩放如果 panel 宽高比和 1920x1080 不一样多出来的部分要么拉伸要么留边。更稳的做法是让 Unity 的 Canvas Scaler 或者摄像机对准 panel 的实际大小实时分辨率模式。这个在 Unity 侧脚本里去拿 Screen.width 和 Screen.height 即可Winform 这边只管把 panel 尺寸传过去就行。4. 交互打通键盘焦点、消息路由与 Unity 侧参数4.1 焦点管理单击进游戏切换窗体后要“点一下”的玄学嵌入后第一个让你抓狂的就是焦点。用户从 Winform 输入框切到 Unity 画面鼠标直接点一个按钮结果第一次点击没反应按钮只是“被聚焦”了要再点一次才触发。原因在于两个进程的窗口各自管理键盘焦点Winform 窗口失焦后Unity 窗口虽然看得见但没有获得 SetFocus。解决办法是在 panel 的 MouseEnter 事件里主动给 Unity 窗口发焦点。[DllImport(user32.dll)] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport(user32.dll)] private static extern bool SetFocus(IntPtr hWnd); private void unityHost_MouseEnter(object sender, EventArgs e) { if (_runner ! null _runner.NativeWindowHandle ! IntPtr.Zero) { SetForegroundWindow(_runner.NativeWindowHandle); SetFocus(_runner.NativeWindowHandle); } }这段逻辑说明鼠标进入 panel 时把 Unity 窗口提到前台并赋键盘焦点。这能解决 90% 的“要点两次”问题。但副作用是用户鼠标从 Winform 控件移入 Unity 区域时Winform 的 TextBox 会立刻失焦如果用户想在 Unity 和文本框之间连续操作比如在文本框输入参数然后去点 Unity 里的确认MouseEnter 会打断输入。所以更稳妥的做法是在 panel.MouseDown 事件里设焦点而不是 MouseEnterprivate void unityHost_MouseDown(object sender, MouseEventArgs e) { SetForegroundWindow(_runner.NativeWindowHandle); }解释鼠标按下时 Unity 已经接收到这次点击但由于没有焦点Unity 可能会把第一次点击当作激活窗口第二次才传给场景。SetForegroundWindow 必须在 MouseDown 里调用让 Unity 在下一步处理鼠标消息前获得焦点。用了这个方案后基本没有“点两次”问题代价是 Winform 里的任何输入框在点击 Unity 时都会失焦这本来就是预期行为。4.2 用自定义消息把 Winform 的数据传给 Unity嵌入后不只是“看”还得“管”。Winform 端要通知 Unity 切换场景、设置物体速度、更新状态栏与进度条最简单的方式是发 Windows 自定义消息。定义消息 ID 用 RegisterWindowMessage 可以避免和别人冲突比如private static readonly int WM_UNITY_COMMAND RegisterWindowMessage(UnityCommand_ Application.ProductName); [DllImport(user32.dll)] private static extern int RegisterWindowMessage(string lpString); [DllImport(user32.dll)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);发送的时候wParam 传命令 ID比如 1 表示更新速度2 表示暂停lParam 可以传字符串指针用 AllocHGlobal 封送。Unity 侧怎么监听呢Unity 的 Standalone 播放器用 Win32 消息循环你需要在 Unity 的脚本里用SetWindowsHookEx或者重写WndProc但 Unity 不开放主窗口的 WndProc。常见做法是用一个后台线程调用PeekMessage把投递到 Unity 主窗口的自定义消息捞出来或者用 Unity 官方提供的UnityPlayer.dll接口。如果你不想这么底层可以用文件监听或命名管道但消息方式的延迟最低适合实时控制。我建议把命令封装成字符串协议lParam 传一个 JSON 字符串的指针Unity 端解析。Winform 端这样发public void SendCommand(string json) { IntPtr ptr Marshal.StringToHGlobalAnsi(json); SendMessage(_runner.NativeWindowHandle, WM_UNITY_COMMAND, new IntPtr(1), ptr); Marshal.FreeHGlobal(ptr); }说明用 GlobalAlloc 分配的指针会在跨进程消息中被读取但发送后立即 Free 有风险——如果消息是异步的 PostMessage Unity 还没读完你就释放了内存。所以这里必须用 SendMessage它是同步的Unity 的消息循环处理完才返回这时释放是安全的。如果你用 PostMessage务必用 GlobalAddAtom 做内存管理这个坑我踩过后面避坑章会写。4.3 Unity 侧的分辨率、Timescale 和包体参数怎么配合嵌入嵌入场景和普通单机运行不一样Unity 侧有些参数要提前调好。第一个是分辨率自适应Unity PlayerSettings 里勾选 Allow Fullscreen嵌入后用户按 AltEnter 有可能会弹出全屏把宿主 Winform 挡住。要禁掉在 Unity 启动时调用Screen.fullScreen false并且把Resolution Dialog关掉。第二个是Time.timeScale。很多上位机项目想用 Unity 做实时渲染但不需要游戏的时间流动比如你想让 3D 场景静止只响应鼠标旋转。此时在 Unity Start 里设Time.timeScale 0f。但注意timeScale0 不会停住 Update 里的渲染只会让Time.deltaTime变成 0所以如果场景里有Physics需要固定更新可能会怪。更优雅的方式是 Winform 控制暂停时发送一个消息触发 Unity 侧调Time.timeScale 0恢复时调回 1这样 Unity 逻辑还活着只是游戏时间冻结。第三个是包体优化。嵌入方案中 Unity exe 的体积会影响启动速度你在 Player Settings 里做如下设置开启 Strip Engine Code、Managed Stripping Level 设为 High、IL2CPP 编译这样包体能从 200MB 降到 100MB 以下。启动速度改善明显因为 -parentHWND 模式下Unity 是在 Winform 进程里被拉起来的整体耗时等于“Unity 初始化 场景加载”包体越小内存占用越低对 Winform 主程序影响越小。还有一个小参数Unity 启动时默认会显示 Splash Screen嵌入后很丑。你可以在构建时关掉或者用启动参数-nolog -nographics仅服务器模式不能用。最靠谱的是在 Unity 编辑器的 Player Settings 里把 Splash Screen 的 Show Unity Splash Screen 取消勾选需要专业版 License没有的话就忍受一秒加载画面或者自己用一个 Winform 遮罩。5. 避坑手册嵌入后白屏、焦点丢失、退出残留的 5 个真实案例5.1 白屏Process.Start 后立刻 SetParent 太早现象Unity 窗口正常出现在任务栏但 panel 区域一片白偶尔能看到 Unity 的 Splash 闪一下就没了。原因Unity 主窗口还没正式初始化渲染你就调了 SetParent父窗口的样式/消息传递导致 Unity 的渲染表面没挂上。解决等待MainWindowHandle非零后再延迟 500 毫秒或等待 Unity 发出第一个 WM_PAINT。我一般加一个条件_process.WaitForInputIdle()然后Thread.Sleep(300)对大多数版本有效。更稳的是循环里检查窗口标题如果 Unity 窗口标题从空变成默认的UnityPlayer再 SetParent。5.2 黑边Unity 分辨率设置和 Panel 尺寸不一致现象Unity 画面嵌入后上下或左右出现黑边Panel 填满了但内容没有占满。原因Unity 渲染分辨率在启动时被 Player Settings 固定比如 800x600MoveWindow 只能拉伸窗口框架不能改变渲染缓冲。解决给 Unity 传-screen-width和-screen-height为 Panel 当前尺寸。可是 Panel 大小是运行时才知道所以你先启动窗口然后用 MoveWindow 设置窗口大小Unity 收到 WM_SIZE 后会自动改分辨率。如果还不行在 Unity 脚本里写一个OnRectTransformDimensionsChange去适配或者用Camera.aspect (float)Screen.width / Screen.height强制。5.3 焦点问题嵌入后键盘输入失效现象鼠标能点 Unity 里的按钮但键盘输入没反应比如 WASD 控制角色没动。原因键盘消息发送到拥有焦点的窗口而 Unity 窗口虽然可视但不是焦点窗口。解决参考 4.1 节那样在 MouseDown 时 SetForegroundWindow同时注意 Winform 窗体自身的 ShowInTaskbar 是否会影响焦点。另一个坑是如果你在 Winform 上使用了 TopMost 属性Unity 窗口可能会被覆盖并且永远无法接收焦点。把嵌入的 panel 所在窗体设为普通层级或者用SetWindowPos把 Unity 窗口 z 序提到 panel 上面。5.4 退出残留Unity 进程没随主窗体关闭现象关闭 Winform 后Unity 进程还留在任务管理器里占用几百兆内存。原因SetParent 建立的父子关系不会在父窗口销毁时自动杀死子进程。解决在主窗体的 FormClosing 事件里调用_process.CloseMainWindow()然后等待 5 秒的退出如果超时直接Kill()。private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (_runner ! null) { _runner.CloseAsync().GetAwaiter().GetResult(); } } public async Task CloseAsync() { if (_process ! null !_process.HasExited) { _process.CloseMainWindow(); if (!_process.WaitForExit(5000)) { _process.Kill(); } _process.Dispose(); } }注意不能因为 CloseMainWindow 没反应就马上 KillUnity 可能在保存配置或回写日志给 5 秒是合理的。另外如果 Unity 窗口被 SetParent 后CloseMainWindow 会向子窗口发送 WM_CLOSE它可能不响应所以先用SetParent(IntPtr.Zero)把子窗口摘下再 CloseMainWindow成功率更高。5.5 卡顿主线程 WaitForIdle 导致的假死现象Winform 窗体启动后整个界面卡住鼠标转圈直到 Unity 完全加载完才恢复。原因有人在 Form_Load 里用了process.WaitForInputIdle()或者在Invoke里同步等待窗口句柄。解决所有等待逻辑必须放到async方法里用await Task.Run轮询避免阻塞 UI 线程。另外Unity 在加载大场景时会占满 CPUWinform 窗口如果 Direct3D 绘制线程和 UI 线程争用也会卡。可以按 4.3 节优化包体或者把 Unity 的 TimeFixedTimestep 调低减少物理运算开销。6. 进阶把嵌入玩成通信 —— 命令总线 隐藏窗口 多实例隔离嵌入做到能跑只是第一步真正好用靠这几个细节。6.1 隐藏 Unity 自带窗口标题栏的两种手法-ParentHWND 嵌入后 Unity 窗口还是带着标题栏盖在面板上方很丑。第一种手法启动后改窗口样式去掉标题栏和边框[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; public static void RemoveBorder(IntPtr hwnd) { int style GetWindowLong(hwnd, GWL_STYLE); style ~WS_CAPTION; style ~WS_THICKFRAME; SetWindowLong(hwnd, GWL_STYLE, style); }说明改样式后必须调用SetWindowPos强制重绘否则边框残留。这个方法只影响显示不影响消息传递。第二种手法是让 Unity 侧自己在 Player Settings 里选择 Windowed 并去掉 Resizable然后用-parentHWND时 Unity 不会显示边框这种方式更干净但要改构建配置。6.2 双向通信Winform 按钮控制 Unity 场景里的物体速度用自定义消息包 JSONWinform 端发指令Unity 端要有一个持续监听消息的后台线程。Unity 的 C# 脚本中可以用DllImport(user32.dll)调用RegisterWindowMessage和PeekMessage在Update里拉取消息队列。伪代码// Unity 侧 [DllImport(user32.dll)] static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); void Update() { MSG msg; while (PeekMessage(out msg, IntPtr.Zero, 0, 0, 1)) { if (msg.message WM_UNITY_COMMAND) { string json Marshal.PtrToStringAnsi(msg.lParam); HandleCommand(json); } } }注意 PeekMessage 需要传入 Unity 主窗口句柄Unity 里可以通过GetActiveWindow()或硬编码拿。如果用-parentHWND嵌入主窗口句柄就是父窗口传入的那个你在脚本里用Process.GetCurrentProcess().MainWindowHandle取也行。Winform 端发送 JSON 格式如{cmd:SetSpeed,value:2.5}Unity 解析后修改 Rigidbody 的 velocity这就能实现“Winform 界面控制 Unity 物体速度”的典型演示。6.3 同时跑多个 Unity 实例句柄隔离与资源占用有些项目要在 Winform 里放多个 3D 视口比如对比两台设备的数字孪生状态。你不能只启动一个 exe 然后分屏那样的输出窗口永远只有一个。正确做法是启动多个 Unity 进程每个用不同的-parentHWND指向不同的 Panel。但要注意Unity 编辑器构建默认使用相同的 player 身份多个实例会争抢文件锁比如日志文件、偏好设置。启动参数加-logFile指向不同的日志文件并在 Player Settings 里开启 “Allow Fullscreen”? 不要禁用。然后每个实例的-screen-width不同内存开销大是正常的建议只开两个上限。我踩过最深的坑是忘记释放消息里用 GlobalAlloc 分配的内存。同步 SendMessage 还好一旦改成 PostMessage 异步Unity 还没读你就 Free 了直接导致崩溃。后来规定所有跨进程命令统一走同步 SendMessage 或者命名管道不会再用裸指针异步。这套方案上线半年没出过问题希望对你有帮助。本文还有配套的精品资源点击获取
返回列表