ARTICLE DETAIL

资讯详情

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

Unity嵌入Winform:窗口句柄、SetParent与WM_COPYDATA双向通信实战

Unity嵌入Winform:窗口句柄、SetParent与WM_COPYDATA双向通信实战 简介一份面向有C#基础、正在研究Unity与Winform集成的开发者的技术实践资源核心解决如何将Unity构建的可执行程序嵌入到原生Windows窗体中运行并实现交互这一难题。资源以Container工程为主线包含完整的C#源码、Visual Studio解决方案、Unity打包后的可执行文件及相关运行库读者可直接运行Container.exe查看实际效果也可阅读源码学习如何启动Unity进程、利用Windows API将外部窗口设置为Winform子控件、处理跨线程调用等关键细节其中EmbeddedWindow模块的封装思路还能帮助理解窗口句柄迁移与消息循环对接。包内共181个文件以dll依赖库、exe可执行文件、cs源码、config配置文件为主另有少量场景资源、缓存与工程元数据整体约18.32MB目录结构贴近实际开发布局便于按模块检索配置文件与工程文件可直接复用能减少环境配置成本。目前已有170人学习下载适合需要参考可运行示例来快速落地嵌入方案、排查集成问题的中高级Windows桌面开发者。1. 把 Unity exe 嵌进 Winform这不是“启动一个进程”那么简单把 Unity exe 嵌入 Winform是那种看起来有现成方案、实际动手全是细节的需求。你要的不是弹出一个独立的 Unity 窗口而是让 Unity 的渲染区域变成 Winform 的一个子控件——和 Button、Panel 一样跟着宿主窗体走接收消息、同步尺寸、生命周期一起释放。适合的场景很具体数字孪生、BIM 预览、带 3D 展示的调度工具或者想把多个版本的 Unity 客户端统一收进一个 C# 壳子里。难点不在 Unity 端而在 Winform 端怎么用 Windows API 接管一个外部进程的窗口。这篇文章就把我从“弹窗”到“嵌入”踩过的坑全部拆开从打包参数到消息交互给到可以直接复现的代码。2. 嵌入的原理与前置窗口句柄、WS_CHILD 与 Unity 打包参数2.1 嵌入的实质是“改窗口的父关系”不是把窗口画上去Windows 里每个可见窗口都是一个内核对象有句柄 HWND 和一套父子层级关系。独立运行的 Unity exe它的窗口父句柄是桌面Desktop所以它是一个顶层窗口有自己的标题栏、边框甚至可以在任务栏里单独出现。而“嵌入”这个动作本质上不是把 Unity 的画面截图贴到 Winform 上而是用 Win32 API 把这个顶层窗口的父布局改成目标 Panel 的句柄让它变成宿主窗体的一个子窗口。改了父关系之后Unity 窗口会获得子窗口的行为跟着宿主窗体移动、最小化时一起隐藏、Z 序被限制在宿主内部。这一步的核心是三个 APISetParent 负责改父关系SetWindowLong 负责改窗口样式必须去掉 WS_POPUP、WS_CAPTION、WS_THICKFRAME加上 WS_CHILDSetWindowPos 负责把窗口摆到正确的位置和尺寸。三件套缺一不可尤其 SetParent 之后如果不调 SetWindowPos 触发刷新嵌入后大概率白屏或窗口消失。有个很常见的误解是“直接在 Winform 上放一个 panel把 exe 拖进去就行”。实际上没有任何现成的控件能拖一个 exe 进去所有嵌入行为都必须走 P/Invoke这也是为什么很多新手在第一步就卡住。你最终要做的是让 Winform 的某个 Panel 充当 Unity 窗口的“画布容器”Unity 窗口的坐标原点永远相对这个 PanelWinform 主窗体的任何布局变化都要同步通知给 Unity 窗口。2.2 Unity 侧打包参数产品名、窗口模式与输出路径嵌入能不能成功有一半其实取决于 Unity 打包参数配得对不对。很多人直接在 Unity 默认设置下打包结果出来的 exe 是带标题栏的独立窗口嵌入后还要用代码去摘掉边框既费劲又容易闪屏。建议在 Build Settings 之前先把 Player Settings 按下面的参数表过一次。打包前需要确认的参数如下参数项推荐值说明Product NameChild摘要里指定的产品名实际是窗口标题后面 FindWindow 和调试都会用到Target PlatformWindows只打包 Windows 独立版别选 WebGL 或 AndroidDefault Screen Width / Height1280 x 720与 Winform 面板比例保持一致避免嵌入后拉伸变形Fullscreen ModeWindowed必须是窗口化独占全屏模式下无法 SetParentResizable Window关闭用户不能自己改大小尺寸完全由宿主面板控制Run In Background勾选宿主窗体失焦时 Unity 仍继续渲染否则会出现“切到别的控件 Unity 就暂停”的鬼畜现象Splash Screen关闭启动闪屏会导致句柄短暂指向 Splash 窗口嵌入时机不好判断输出路径建议直接指定为 Container\bin\Debug和 Winform 宿主程序放在同一个目录。这里要特别提醒Unity 的 data 文件夹Managed、Resources 这些是相对于 exe 路径寻找的所以 Winform 用 Process 启动时WorkingDirectory 必须设置成 exe 所在目录不然 Unity 会报“Failed to load player data”直接闪退。别问我为什么会把这个坑放在第一位因为我第一次做的时候就是漏了 WorkingDirectory整个窗口闪了一下就没了。2.3 Winform 侧项目结构Container 主程序与面板占位Winform 侧的项目结构比较简单但建议在一开始就把层次理清楚。主项目叫 Container.csproj摘要里也提到了这个文件名里面放一个主窗体 MainForm主窗体上放一个专门用来承载 Unity 的 Panel可以用 Dock 填充或者手动布局。我一般会把这个 Panel 的 BackColor 设成黑色这样 Unity 还没起来的时候面板看起来是一块黑屏视觉上不突兀。启动流程建议放在 Form_Load 事件里按顺序执行先启动 Unity 进程再等待句柄可用最后才执行 SetParent。不要放在构造函数里因为构造函数执行时窗体还没显示Panel 的句柄可能还没创建SetParent 会失败。另外建议加一个“回滚逻辑”——如果嵌入失败不要卡死用户直接把 Unity 窗口留在桌面上并提示一句“嵌入失败已切换为独立窗口模式”。目录结构大致是这样Container/ ├── bin/ │ └── Debug/ │ ├── Container.exe │ ├── Child.exe │ └── Child_Data/ │ ├── Managed/ │ ├── Resources/ │ └── ... ├── MainForm.cs ├── Container.csproj └── NativeMethods.csNativeMethods.cs 专门放 P/Invoke 声明这是嵌入项目的核心文件所有 Win32 API 的 DllImport 集中在这里方便后续维护。这个文件里会用到 user32.dll 的 SetParent、SetWindowLong、SetWindowPos、SendMessage以及 kernel32 的 GetCurrentProcessId 之类的方法后面三章会逐个给出。3. 用 Process 启动 Unity 并接管窗口可直接抄的嵌入实现3.1 启动进程并等待句柄WaitForInputIdle 的边界启动 Unity exe 本身不难难点在于时机。很多人第一步就用 Process.Start 然后立刻拿 MainWindowHandle结果拿回来是 0或者拿到的是启动闪屏的窗口句柄。原因是 Unity 启动需要初始化渲染环境和加载场景主窗口并不是瞬间创建的而 MainWindowHandle 属性只有当进程存在“可见主窗口”时才会返回有效值。我一般不用 WaitForInputIdle因为这个方法只代表“进程已经进入消息循环”并不保证窗口创建完成而且对 Unity 这种自绘渲染的应用它经常提前返回 true。更稳妥的做法是轮询 MainWindowHandle同时判断窗口可见。下面这段是经过多次验证的启动代码public static IntPtr StartUnityAndWait(string exePath, int timeoutMs 15000) { ProcessStartInfo psi new ProcessStartInfo { FileName exePath, WorkingDirectory Path.GetDirectoryName(exePath), UseShellExecute false }; Process proc Process.Start(psi); if (proc null) return IntPtr.Zero; DateTime deadline DateTime.Now.AddMilliseconds(timeoutMs); IntPtr hwnd IntPtr.Zero; while (DateTime.Now deadline) { proc.Refresh(); hwnd proc.MainWindowHandle; if (hwnd ! IntPtr.Zero IsWindowVisible(hwnd)) break; Thread.Sleep(100); } _childProcess proc; return hwnd; } [DllImport(user32.dll)] private static extern bool IsWindowVisible(IntPtr hWnd);逻辑说明这段代码会每 100ms 刷新一次进程状态直到拿到非零且可见的窗口句柄或者超时。注意 WorkingDirectory 是必须项Unity 的 data 目录依赖它。参数里 timeoutMs 我默认给 15 秒普通场景 5 秒内就能拿到句柄如果超过 15 秒基本就是打包路径有问题或者杀毒软件拦截了。注意Process.Refresh 必须调用否则 MainWindowHandle 是第一次读取的缓存值。另外如果 Unity 开了 Splash 屏这里拿到的可能是闪屏窗口需要在后面的嵌入代码里校验窗口标题是否为“Child”。这个细节在第 5 章排查部分会有更完整的说明。3.2 用 SetParent 修改父窗口用 SetWindowLong 去掉标题栏拿到句柄之后嵌入的核心操作就开始了。第一步是 SetParent把 Unity 窗口的父句柄设为 Panel 的句柄第二步是 SetWindowLong 修改窗口样式第三步是 SetWindowPos 重设位置和尺寸。三步必须按顺序执行中间不能有 UI 线程卡顿。先看代码public static bool EmbedIntoPanel(IntPtr unityHwnd, Control hostPanel) { IntPtr panelHwnd hostPanel.Handle; // 1. 设置父窗体 IntPtr result SetParent(unityHwnd, panelHwnd); if (result IntPtr.Zero) return false; // 2. 修改窗口样式去掉弹出式、标题栏、边框、最大化按钮 long style GetWindowLong(unityHwnd, GWL_STYLE); style ~WS_POPUP; style ~WS_CAPTION; style ~WS_THICKFRAME; style ~WS_MAXIMIZEBOX; style ~WS_MINIMIZEBOX; style | WS_CHILD; SetWindowLong(unityHwnd, GWL_STYLE, style); // 3. 触发布局刷新 SetWindowPos(unityHwnd, IntPtr.Zero, 0, 0, hostPanel.Width, hostPanel.Height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); return true; } private const int GWL_STYLE -16; private const long WS_POPUP 0x80000000L; private const long WS_CAPTION 0x00C00000L; private const long WS_THICKFRAME 0x00040000L; private const long WS_MAXIMIZEBOX 0x00010000L; private const long WS_MINIMIZEBOX 0x00020000L; private const long WS_CHILD 0x40000000L; private const uint SWP_FRAMECHANGED 0x0020; private const uint SWP_SHOWWINDOW 0x0040;逻辑说明SetParent 返回旧父窗口句柄如果返回 0 表示失败最可能的原因是 unityHwnd 不是顶层窗口或者父句柄无效。GetWindowLong 是 32 位版本的 API现在 64 位系统上建议用 GetWindowLongPtr但 C# 里 32/64 位通用写法是用 GetWindowLongPtr 并从 user32 按平台加载这里为了示例清晰用了 GetWindowLong实际工程里你换成 GetWindowLongPtr 即可。样式修改的 trick 是先读现有样式再用位运算清掉不想要的位最后或上 WS_CHILD。SWP_FRAMECHANGED 这个标志非常关键它告诉系统“窗口样式已经变了请重新计算非客户区”。很多嵌入失败表现为“窗口还在但还带着标题栏”就是因为改了样式没传这个标志。SWP_SHOWWINDOW 则是强制把窗口显示出来因为 Unity 窗口被 SetParent 之后有时候会自己隐藏。3.3 尺寸同步面板 Resize 时用 SetWindowPos 跟随嵌入完成后Unity 窗口不会自动跟随 Panel 的大小变化。你需要监听宿主 Panel 的 Resize 事件在事件里重新调用 SetWindowPos。否则用户拉大主窗体后Unity 窗口还停留在原始尺寸两边对不上非常难看。private void panelUnity_Resize(object sender, EventArgs e) { if (_unityHwnd IntPtr.Zero) return; SetWindowPos(_unityHwnd, IntPtr.Zero, 0, 0, panelUnity.ClientSize.Width, panelUnity.ClientSize.Height, SWP_NOZORDER | SWP_NOACTIVATE); }逻辑说明这里传入的是 ClientSize 而不是 Size因为 ClientSize 是去掉边框和标题栏后的实际客户区尺寸Unity 窗口作为子窗口应该填满这个区域。SWP_NOZORDER 表示不改变 Z 序SWP_NOACTIVATE 防止窗口被激活导致焦点跳动。每次 Resize 都调一次频率不会很高性能没有压力。参数陷阱如果 Winform 开了自动缩放AutoScaleMode.FontPanel 的 Width/Height 和 ClientSize 在 DPI 缩放下可能不是真实像素值。遇到嵌入式窗口错位时优先检查这一条必要时在 app.config 里声明 PerMonitorV2 DPI 感知后面第 5 章会展开讲。4. 与 Unity 双向通信WM_COPYDATA 消息与 Unity 侧回调4.1 消息协议设计命令 ID 负载数据的约定嵌入只是让窗口“住”进了 Winform真正的业务场景往往需要双向通信点击 Winform 上的按钮控制 Unity 里的模型旋转、切换场景或者 Unity 把加载进度回调给 Winform 显示。嵌入场景下最实用的通信方案是 WM_COPYDATA它是 Windows 自带的消息机制不需要引入网络库或命名管道传递的是进程间可共享的内存块格式简单且可靠。先设计协议。我一般用冒号分隔的文本协议结构是“命令 ID 参数”比如cmdLoadScene|paramLevel2 cmdRotate|paramx:30,y:0,z:45 cmdGetProgress|param这样设计的好处是 Unity 侧解析非常轻量不用引入 JSON 解析库字符串 Split 一下就能用。如果业务复杂到需要传对象再考虑把 param 换成 JSON 字符串但消息头一定保持简单。协议字符集统一用 UTF-16。Windows 的 WM_COPYDATA 传递的是内存指针发送端用什么编码接收端就必须用什么编码这里最容易翻车第 5 章会讲。4.2 Winform 侧发送SendMessage 与 COPYDATASTRUCT发送的核心是构造 COPYDATASTRUCT 结构体然后把字符串写入非托管内存再 SendMessage 给 Unity 窗口句柄。直接看代码[StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public IntPtr dwData; public int cbData; public IntPtr lpData; } public static void SendMessageToUnity(IntPtr unityHwnd, string message) { if (unityHwnd IntPtr.Zero) return; byte[] bytes Encoding.Unicode.GetBytes(message); IntPtr ptr Marshal.AllocHGlobal(bytes.Length); try { Marshal.Copy(bytes, 0, ptr, bytes.Length); COPYDATASTRUCT cds new COPYDATASTRUCT { dwData (IntPtr)0x1001, cbData bytes.Length, lpData ptr }; SendMessage(unityHwnd, WM_COPYDATA, IntPtr.Zero, ref cds); } finally { Marshal.FreeHGlobal(ptr); } } private const int WM_COPYDATA 0x004A; private const long WS_POPUP 0x80000000L;逻辑说明Encoding.Unicode 得到的是 UTF-16LE 字节数组这里必须用 2 字节编码因为 1 字节的 ASCII 在 Unity 侧解析中文会全变问号。dwData 可以当命令 ID 用接收端先判断它再决定怎么解析 lpData。SendMessage 是同步阻塞的它会等 Unity 侧处理完 WM_COPYDATA 才返回所以不要在主线程高频循环里调用否则 Winform 界面会卡。参数说明cbData 是字节长度不是字符长度必须传 bytes.Length。lpData 指向的内存块在 SendMessage 返回之前必须保持有效所以先用 AllocHGlobal 分配再在 finally 里释放。如果你用 PostMessage 异步发送内存释放时机就复杂了这个从简方案里不推荐。4.3 Unity 侧接收不用动窗口过程轮询消息队列Unity 侧接收 WM_COPYDATA 有一个更简单的思路Unity 窗口的消息队列是操作系统级的但 Unity 的窗口过程被引擎接管了我们没法直接替换 C# 侧的 WndProc。常见的做法有两个一是写一个 C 原生插件通过 SetWindowLongPtr 替换窗口过程能做但工程量上去了二是在 Unity 的 Update 循环里用 PeekMessage 轮询消息队列把 WM_COPYDATA 从队列里取出来。我用的就是第二种功能够用代码量小。using System; using System.Runtime.InteropServices; using UnityEngine; public class CopyDataReceiver : MonoBehaviour { [DllImport(user32.dll)] private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); [DllImport(user32.dll)] private static extern IntPtr GetForegroundWindow(); private const int WM_COPYDATA 0x004A; private const uint PM_REMOVE 0x0001; [StructLayout(LayoutKind.Sequential)] private struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public int ptX; public int ptY; } [StructLayout(LayoutKind.Sequential)] private struct COPYDATASTRUCT { public IntPtr dwData; public int cbData; public IntPtr lpData; } void Update() { MSG msg; while (PeekMessage(out msg, IntPtr.Zero, WM_COPYDATA, WM_COPYDATA, PM_REMOVE)) { if (msg.message WM_COPYDATA) { COPYDATASTRUCT cds (COPYDATASTRUCT)Marshal.PtrToStructure( msg.lParam, typeof(COPYDATASTRUCT)); byte[] buffer new byte[cds.cbData]; Marshal.Copy(cds.lpData, buffer, 0, cds.cbData); string text System.Text.Encoding.Unicode.GetString(buffer); HandleMessage(text); } } } void HandleMessage(string message) { string[] parts message.Split(|); if (parts.Length 2) return; string cmd parts[0].Replace(cmd, ); string param parts[1].Replace(param, ); Debug.Log($[CopyDataReceiver] cmd{cmd}, param{param}); if (cmd LoadScene) UnityEngine.SceneManagement.SceneManager.LoadScene(param); else if (cmd Rotate) transform.Rotate(ParseAngles(param)); } Vector3 ParseAngles(string param) { string[] axes param.Split(,); float x float.Parse(axes[0].Split(:)[1]); float y float.Parse(axes[1].Split(:)[1]); float z float.Parse(axes[2].Split(:)[1]); return new Vector3(x, y, z); } }逻辑说明PeekMessage 的作用是从消息队列里取一条消息。这里把消息范围限定为 WM_COPYDATA过滤器最小值和最大值都填 0x004A这样只会取出 COPYDATA 消息。PM_REMOVE 表示取出后从队列移除否则下次还会读到同一条。拿到 msg.lParam 后再用 Marshal.PtrToStructure 解析成 COPYDATASTRUCT最后把 lpData 拷贝进托管字节数组。整个过程不阻塞渲染线程唯一要注意的是 Update 里不要有太重的工作解析字符串和加载场景可以拆到协程或异步任务里。参数说明msg.hwnd 可以判断消息是否发给 Unity 主窗口但用 IntPtr.Zero 收所有窗口的消息时要过滤掉宿主进程的无关窗口消息。在嵌入场景里Unity 窗口是整个进程唯一的顶层窗口所以不会混入其他窗口的 COPYDATA。这个脚本挂在任意场景中的 GameObject 上就行不需要额外设置。5. 避坑与排查嵌入后最容易翻车的六个细节5.1 窗口句柄拿不到与嵌入失效现象Process.Start 之后循环轮询 MainWindowHandle 一直返回 0或者返回了一个句柄但 SetParent 失败。原因最常见的是 Unity 启动闪屏。Unity 打包默认开启 Splash Screen程序启动时先创建一个闪屏窗口闪屏消失后再创建主窗口。如果你轮询到的是闪屏句柄而闪屏即将关闭嵌入自然失败。另一类原因是 Unity 程序在启动时弹出了错误对话框比如找不到 data 目录窗口存在但内容错误。解决打包时关闭 Splash Screen轮询时不要只看 MainWindowHandle 非零还要校验窗口可见性以及窗口标题是否为配置的 Product Name。用 FindWindow 按标题找“Child”是更保险的方式[DllImport(user32.dll, CharSet CharSet.Unicode)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); IntPtr hwnd FindWindow(null, Child);5.2 嵌入后白屏或无响应现象SetParent 成功后面板区域一片白或黑Unity 画面没渲染出来鼠标移动上去也没有反应。原因风格修改后没有触发 WM_FRAMECHANGED或者 Unity 窗口仍处于隐藏状态。另一个概率较高的原因是 Unity 的 Graphics API 选了 Vulkan在 SetParent 重设父窗口后渲染线程不重绘。嵌入场景下用 DX11 兼容性最好。解决确认 SetWindowPos 传了 SWP_FRAMECHANGED | SWP_SHOWWINDOWUnity Player Settings 里把 Graphics API 改为 Direct3D 11去掉 Vulkan如果还白屏调用一次 InvalidateRect 强制 Unity 窗口重绘。5.3 鼠标坐标偏移与点击错位现象Unity 窗口嵌入后在面板上点击按钮Unity 里的响应位置偏左上角或右下角拖动框选很不跟手。原因DPI 缩放不一致。Winform 默认是系统 DPI 感知Unity 默认是 PerMonitorV2两个进程的坐标映射基准不同导致 Unity 收到的鼠标消息坐标被放大或缩小。解决给 Winform 的 app.config 加上 DPI 感知声明application windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness /windowsSettings /application同时在嵌入前确认 SetWindowPos 传的尺寸是物理像素。如果 Winform 有 AutoScaleMode建议在嵌入函数里用 panelUnity.Width * (deviceDpi / 96.0) 做一次换算。5.4 WM_COPYDATA 中文乱码与消息丢失现象从 Winform 发送中文参数Unity 收到的字符串乱码或者连续快速发送多条消息时偶尔会丢。原因乱码是编码不一致发送端用了 Encoding.DefaultGBKUnity 端用了 Encoding.Unicode 解码。消息丢失是 SendMessage 同步阻塞导致发送端积压Unity 侧 PeekMessage 来不及处理。解决发送端强制用 Encoding.Unicode 编码接收端用相同编码解码。高频消息不要用 SendMessage改成 PostMessage 并把待发送数据放进一个并发队列Unity 侧每帧只处理一条。一定要记住 COPYDATA 的内存指针在消息返回后无效PostMessage 场景下需要自己管理内存生命周期。5.5 AltTab 切走焦点后 Unity 键盘失灵现象嵌入后单击 Unity 窗口首次键盘输入没反应需要再点一次或者按 Tab 焦点跳到了 Winform 的其他控件上。原因Unity 窗口现在是子窗口不参与 Winform 的 Tab 焦点循环Windows 不会自动把键盘焦点给它。单击时消息先到达 PanelUnity 没收到激活事件。解决在 Panel 的 MouseDown 事件里手动调用 SetFocus把键盘焦点强制给 Unity 窗口private void panelUnity_MouseDown(object sender, MouseEventArgs e) { if (_unityHwnd ! IntPtr.Zero) SetFocus(_unityHwnd); } [DllImport(user32.dll)] private static extern IntPtr SetFocus(IntPtr hWnd);5.6 杀毒软件拦截与进程残留现象Container.exe 启动后被杀毒软件提示“正在尝试修改其他进程”或者程序退出后任务管理器里还能看到 Child.exe。原因SetParent 修改其他进程窗口关系恰好落在一些杀软行为检测的敏感区容易被当作注入行为。进程残留则是退出时只是关闭了主窗体没有主动回收 Unity 子进程。解决正式发布做代码签名开发阶段在杀软里加白名单文件夹退出逻辑里先发 Quit 命令再等待 3 秒超时就 Kill最后再关主窗体。不要用 Environment.Exit 直接结束进程那会让子进程变孤儿。6. 进阶启动参数、退出回收与验证技巧6.1 用启动参数控制加载场景嵌入工程做多了之后你会发现手动在 Unity 编辑器里切换场景很麻烦。更工程化的做法是让 Winform 通过命令行参数告诉 Unity 要加载哪个场景。启动时拼上“-sceneStage1”Unity 侧在 Awake 里用 Environment.GetCommandLineArgs 解析然后异步加载目标场景。这样同一份 exe 可以被 Winform 当作多场景资源复用不用为每个场景单独打包一份。注意解析参数要兼容引号包裹的路径和空格。6.2 退出流程先通知、再等待、再兜底嵌入程序的退出比普通 Winform 要多两步。第一步是向 Unity 窗口发送退出命令第二步是等待进程自然退出第三步才是 Winform 关闭自身的窗体。我习惯把这段写在 FormClosing 事件里private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (_childProcess ! null !_childProcess.HasExited) { SendMessageToUnity(_unityHwnd, cmdQuit|param); if (!_childProcess.WaitForExit(3000)) { try { _childProcess.Kill(); _childProcess.WaitForExit(); } catch { // 进程已结束或权限不足无需额外处理 } } } }这套流程能避免大多数进程残留。WaitForExit(3000) 是给 Unity 做资源释放的宽限时间通常 1 秒内就会退。如果 3 秒还没退说明 Unity 卡在某个流程上再 Kill 兜底不至于等用户手动结束任务。6.3 验证方式从打开到回归的完整检查验证嵌入效果时我习惯按“启动→嵌入→交互→退出”四个环节各留一个检查点。启动环节看 Container 目录下的 Child.exe 和 Child_Data 是否完整嵌入环节看 Unity 窗口是否填满面板且无边框交互环节发一条 LoadScene 消息看日志退出环节看任务管理器里没有残留进程。原文效果视频地址可以参考作者博客里的演示效果https://blog.csdn.net/qq_26318597/article/details/145630246从视觉表现上对照自己的实现是否符合预期。如果想快速反馈可以在 Unity 场景里挂一个脚本把收到的每一条消息写到 Application.persistentDataPath 下的日志文件里Winform 侧只要读这个文件就能确认消息是否到达。从那以后我做嵌入项目都强制把“最小验证工程”放在第一步——先跑通启动和退出再碰 SetParent 和消息交互这样可以快速定位问题出在哪个进程。希望帮到你。本文还有配套的精品资源点击获取
返回列表