现在主要是要处理一个问题:Unity打包程序嵌入WinForm/WPF中后无法输入中文问题处理;顺便整理一下Unity嵌入WinForm的操作。
公司原来的项目是WinForm的,要进入3D展示功能。考虑了WPF和Unity,结果选择了Unity,WPF开发的话,本身类似直接用底层API开发,和Unity游戏引擎开发比复杂多了,Unity学习和开发成本更低。
这样就有个问题,如何在WinForm中使用Unity开发的程序。
一开始(两年前了,还是使用4.6版本的时候)是用WebPlayer打包的,需要在电脑上安装WebPlayer播放器,WinForm中引入WebPlayer的控件,加载Unity打包生成的WebPlayer文件。WebPlayer第一次运行要下载文件的,后面就不需要了...具体碰到的一些问题以前还有记录下来,那时是第一次用博客记录问题的(UnityWebPlayer使用1,2,3),主要是要整合好多人的资料加上一些实验才能解决问题。
后来(5.0版本开始吧),Unity对WebPlayer要渐渐不支持了,而且随着WebPlayer播放器的升级,要联网的问题处理不了了,改成Standalone打包,嵌入exe程序到WinForm中的方式,用Windows的API。这里就是整理嵌入中碰到的一些问题。
Unity版本5.3.8
打包设置
嵌入方式1:用Windows的API
1.启动exe
基本代码
ProcessStartInfo info = new ProcessStartInfo(Path);
process = Process.Start(info);
2.获取窗口句柄
process.MainWindowHandle是窗口的句柄,但是上面程序启动时,process.MainWindowHandle是0。要等窗口创建出来后才有,中间有个时间差,可以用计时器也可以用Application.Idle事件
Process[] ps=Process.GetProcessesByName(process.ProcessName);
if (ps.Length > 0)
{
process = ps[0];
}
IntPtr unityHWND = process.MainWindowHandle;
3.嵌入窗口
IntPtr parentHandle = unityPanel1.Handle;
IntPtr unityHWND = process.MainWindowHandle;
User32.SetParent(unityHWND, parentHandle);
User32是一个帮助类,内部封装了对user32.dll的调用。
4.调整大小
Control parentControl = unityPanel1;
IntPtr unityHWND = process.MainWindowHandle;
User32.MoveWindow(unityHWND, 0, 0, parentControl.Width, parentControl.Height, true);
5.设置无边框
Control parentControl = unityPanel1;
IntPtr unityHWND = process.MainWindowHandle;
User32.SetWindowLong(new HandleRef(parentControl, unityHWND), User32.GWL_STYLE, User32.WS_VISIBLE);//无边框
到这里就完成了嵌入工作。
这其实是一个通用的过程,任何exe程序都能嵌入进来
6.关闭窗口,在主程序退出后要把Unity程序也退出,不然会在后台继续运行,没有界面的状态下。
process.Kill();
相关代码整理成一个独立的工具类
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using WinAPIs.Dlls;
namespace UnityContainer
{
public class WindowEmbedTool
{
public string Path { get; private set; }
public Control ParentControl { get; private set; }
public IntPtr ParentWnd { get; private set; }
public IntPtr WndHandle { get; private set; }
public Process WndProcess { get; private set; }
public WindowEmbedTool(string path,Control parentControl)
{
Path = path;
ParentControl = parentControl;
if (parentControl != null)
{
ParentWnd = parentControl.Handle;
parentControl.Resize += ParentControl_Resize;
}
Application.ApplicationExit += Application_ApplicationExit;
}
private void ParentControl_Resize(object sender, EventArgs e)
{
SetBounds();
}
public void Start()
{
ProcessStartInfo info = new ProcessStartInfo(Path);
//info.UseShellExecute = true;//默认就是true
info.WindowStyle = ProcessWindowStyle.Minimized;//默认最小化到任务栏,不弹出界面。
//info.CreateNoWindow = true;//没影响,可能是因为窗口是后面才创建的
info.Arguments = "-popupwindow";//Unity的命令行参数
WndProcess = Process.Start(info);
Application.Idle += Application_Idle;
}
private void Application_Idle(object sender, EventArgs e)
{
if (WndProcess.MainWindowHandle == IntPtr.Zero)
{
Process[] ps = Process.GetProcessesByName(WndProcess.ProcessName);
foreach (Process p in ps)
{
if (p.MainWindowHandle != IntPtr.Zero)//可能之前的exe忘记关闭了
{
WndProcess = p;
break;
}
}
}
if (WndProcess.MainWindowHandle != IntPtr.Zero)
{
Application.Idle -= Application_Idle;
WndHandle = WndProcess.MainWindowHandle;
EmbedWnd();
SetBounds();//设置位置大小
//SetNoBorder();//设置无边框
}
}
public void EmbedWnd()
{
User32.SetParent(WndHandle, ParentWnd);
}
public void SetNoBorder()
{
User32.SetWindowLong(new HandleRef(ParentControl, WndHandle), User32.GWL_STYLE, User32.WS_VISIBLE);//无边框
}
private void Application_ApplicationExit(object sender, EventArgs e)
{
Stop();
}
public void Stop()
{
if (WndProcess.HasExited) return;
if (WndProcess != null)
{
WndProcess.Kill();
}
}
public void SetBounds()
{
SetBounds(ParentControl.Width, ParentControl.Height);
}
public void SetBounds(int width,int height)
{
if (WndHandle == IntPtr.Zero) return;
if (width <= 0 || height <= 0) return;
User32.MoveWindow(WndHandle, 0, 0, width, height, false);
ActivateWindow();//激活
}
public void ActivateWindow()
{
if (WndHandle == IntPtr.Zero) return;
User32.SendMessage(WndHandle, User32.WM_ACTIVATE, User32.WA_ACTIVE, IntPtr.Zero);
}
public void DeactivateWindow()
{
if (WndHandle == IntPtr.Zero) return;
User32.SendMessage(WndHandle, User32.WM_ACTIVATE, User32.WA_INACTIVE, IntPtr.Zero);
}
}
}
使用代码
private WindowEmbedTool embedTool;
private void button11_Click(object sender, EventArgs e)
{
if (embedTool != null)
{
embedTool.Stop();
}
embedTool = new WindowEmbedTool(Path, unityPanel1);
embedTool.WaitTime = int.Parse(textBox1.Text);
embedTool.Start();
}
结果:
嵌入后Unity内的内容大小没有调整好。
手动一步一步点击时可以的。
时间差至少是能解决掉的。
添加了个WaitTime。
private void Application_Idle(object sender, EventArgs e)
{
if (WndProcess.MainWindowHandle == IntPtr.Zero)
{
Process[] ps = Process.GetProcessesByName(WndProcess.ProcessName);
foreach (Process p in ps)
{
if (p.MainWindowHandle != IntPtr.Zero)//可能之前的exe忘记关闭了
{
WndProcess = p;
break;
}
}
}
if (WndProcess.MainWindowHandle != IntPtr.Zero)
{
Application.Idle -= Application_Idle;
WndHandle = WndProcess.MainWindowHandle;
EmbedWnd();
SetBounds();//设置位置大小
//SetNoBorder();//设置无边框
}
}
改成:
private void Application_Idle(object sender, EventArgs e)
{
if (WndProcess.MainWindowHandle == IntPtr.Zero)
{
Process[] ps = Process.GetProcessesByName(WndProcess.ProcessName);
foreach (Process p in ps)
{
if (p.MainWindowHandle != IntPtr.Zero) //可能之前的exe忘记关闭了
{
WndProcess = p;
break;
}
}
}
if (WndProcess.MainWindowHandle != IntPtr.Zero)
{
Application.Idle -= Application_Idle;
WndHandle = WndProcess.MainWindowHandle;
EmbedWnd();
DoActionDelay(SetBounds, WaitTime);
}
}
private static void DoActionDelay(Action action, int ms)
{
try
{
Thread.Sleep(ms);
Thread thread = new Thread(() =>
{
action();
});
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
这种方法有不确定性。
200ms以上基本是能正常显示;10-150ms有时能正常显示,有时不行;0ms肯定是不行的。
但是后来0ms有时也是可以的...
不用多线程的方式也不等待(原来的代码)是一定显示异常的。
换台电脑不知道怎样?
有没有某种事件或者属性判断Unity内内容是否显示正常呢?
作为父窗口的类型是一个Panel控件。
嵌入方式2:用Unity的启动命令行参数
关键是-parentHWND参数,用于设置父窗体,另外有官方例子EmbeddedWindow.zip。
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
namespace Container
{
public partial class Form1 : Form
{
[DllImport("User32.dll")]
static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
internal delegate int WindowEnumProc(IntPtr hwnd, IntPtr lparam);
[DllImport("user32.dll")]
internal static extern bool EnumChildWindows(IntPtr hwnd, WindowEnumProc func, IntPtr lParam);
[DllImport("user32.dll")]
static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
private Process process;
private IntPtr unityHWND = IntPtr.Zero;
private const int WM_ACTIVATE = 0x0006;
private readonly IntPtr WA_ACTIVE = new IntPtr(1);
private readonly IntPtr WA_INACTIVE = new IntPtr(0);
public Form1()
{
InitializeComponent();
try
{
process = new Process();
process.StartInfo.FileName = "Child.exe";
process.StartInfo.Arguments = "-parentHWND " + panel1.Handle.ToInt32() + " " + Environment.CommandLine;
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForInputIdle();
// Doesn't work for some reason ?!
//unityHWND = process.MainWindowHandle;
EnumChildWindows(panel1.Handle, WindowEnum, IntPtr.Zero);
unityHWNDLabel.Text = "Unity HWND: 0x" + unityHWND.ToString("X8");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ".\nCheck if Container.exe is placed next to Child.exe.");
}
}
private void ActivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
}
private void DeactivateUnityWindow()
{
SendMessage(unityHWND, WM_ACTIVATE, WA_INACTIVE, IntPtr.Zero);
}
private int WindowEnum(IntPtr hwnd, IntPtr lparam)
{
unityHWND = hwnd;
ActivateUnityWindow();
return 0;
}
private void panel1_Resize(object sender, EventArgs e)
{
MoveWindow(unityHWND, 0, 0, panel1.Width, panel1.Height, true);
ActivateUnityWindow();
}
// Close Unity application
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
try
{
process.CloseMainWindow();
Thread.Sleep(1000);
while (process.HasExited == false)
process.Kill();
}
catch (Exception)
{
}
}
private void Form1_Activated(object sender, EventArgs e)
{
ActivateUnityWindow();
}
private void Form1_Deactivate(object sender, EventArgs e)
{
DeactivateUnityWindow();
}
}
}
有几点区别:
1.不用api嵌入窗口了,用-parentHWND传递参数,中间也不用找窗口句柄了。
2.有ActivateUnityWindow、DeactivateUnityWindow、EnumChildWindows等函数的使用,另外在对一些Form事件的处理。
优点:嵌入过程简单。
缺点:
1. 这个官方用例加载后的Unity就是不能输入中文的。
2. 焦点切换要添加额外代码。
EnumChildWindows时:
1、Unity内的输入框不会响应键盘事件,虽然输入法界面会出来,但是实际上感觉是外部控件的输入法。
2、不会响应鼠标中键滚轮滚动。
3、无法接受焦点。界面上添加其他控件,点击Unity区域,焦点不会切换。
4、鼠标左右键点击、按住旋转正常。
EnumChildWindows使用时:
1.一开始Unity内的输入框可以输入,切换到外面WinForm的输入框口,在移动进去就不能输入了。
这个问题在项目中我是通过定时器定时判断鼠标位置,通过调用ActivateUnityWindow()函数来激活Unity解决的。
2.无法输入中文。
3.可以响应鼠标中键滚轮滚动。
4.鼠标点击无法切换焦点,要用代码做额外处理。
5.鼠标左右键点击、按住旋转正常
EnumChildWindows可以枚举一个父窗口的所有子窗口:
BOOL EnumChildWindows(
HWND , // handle to parent window // 父窗口句柄
WNDENUMPROC , // callback function // 回调函数的地址
LPARAM // application-defined value // 你自已定义的参数
);
就这么简单,让我们再定义一个回调函数,像下面这样:
BOOL CALLBACK EnumChildProc(
HWND , // handle to child window
LPARAM // application-defined value
);
注意:这个回调函数要么是类的静态函数,要么就是一个全局的函数。
--------------------------------
在调用EnumChildWindows 这个函数时,直到调用到最个一个子窗口被枚举或回调函数返回一个false,否则将一直枚举下去。
代码中的回调函数是返回0的,0是false,枚举到此结束。
不过改成return 1;也没发现有什么影响。
private int WindowEnum(IntPtr hwnd, IntPtr lparam)
{
unityHWND = hwnd;
ActivateUnityWindow();
return 0;
}
按我的理解是让系统知道panel1有了一个新的子窗口,这样键盘消息才能发送进去,同时通过这里获取到unity窗口的句柄。
在该例子中Form1构造函数中的嵌入相关代码用我前面写的WindowEmbedTool替换掉
WindowEmbedTool embedTool = new WindowEmbedTool(@"D:\SoftwareProjects\TrunkClient2017\Bin\Debug\U3D\U3DJF.exe", panel1);
embedTool.Start();
结果中文输入、焦点切换都没问题...
其实我一开始是用自己写的用API函数嵌入的,后面发现官方提供了例子,并且我的API代码有点问题(1.打开Unity程序时会闪 一下,2.在不同电脑、操作系统上有时候会出问题),于是就高高兴兴的用官方例子修改代码...
焦点问题解决了,中文输入的问题当初没时间就放着了,因为项目中可以绕过该问题(在外面WinForm中输入就好了)。
发现写在注释中的以前的总结
///
/// 启动unity打包的exe程序
/// 这里嵌套exe有三种方式,本质是一样的。
/// 1.用AppContainer嵌套exe,内部是用win api。
/// 关键是-popupwindow参数。这个是最早的,在没找到-popupwindow参数前,很难搞。
/// 这个的问题是exe从打开到嵌入界面中间有点时间差,会有个弹出窗口闪一下。原本是通过修改TopMost隐藏一下,但是在其他电脑上可能发生TopMost没有修改回来的情况。
/// 2.用U3DContainerForm,这个是unity官方例子的代码。
/// 这里用到了-parentHWND参数,处理崩溃问题时发现的,之前若是就知道就好了。能节省很多时间。
/// 这个的问题是右上角机柜信息界面没有绑定在右上角。
/// 3.将官方例子的代码里相关代码和当前类结合,实现嵌套的效果。
/// 这里关键是要把当前的U3DPanel改成继承Form,不然右键旋转功能不行。
///
public void StartU3DExe()
AppContainer其实就是我以前整理的嵌入类,那个比较通用,不用管是不是unity的程序。
先不管他,用新写的WindowEmbedTool替换掉里面的嵌入代码。
结果,有问题,无法嵌入进去!
我现在的项目是WPF,里面有个区域时WinForm,然后在WinForm控件中嵌入Unity。
结果发现Application.Idle事件不会进去,Application是System.Windows.Forms里的类,可能在WPF的环境下该事件不会触发,其他事件也可能这样。
修改,用定时器,通过线程实现。
public void Start()
{
ProcessStartInfo info = new ProcessStartInfo(Path);
//info.UseShellExecute = true;//默认就是true
info.WindowStyle = ProcessWindowStyle.Minimized;//默认最小化到任务栏,不弹出界面。
//info.CreateNoWindow = true;//没影响,可能是因为窗口是后面才创建的
info.Arguments = "-popupwindow";//这个还是需要的
WndProcess = Process.Start(info);
StartIdleEvent();
}
private void StartIdleEvent()
{
Thread idleThread = new Thread(EmbedWndWhenIdleThread);
idleThread.Start();
}
private void EmbedWndWhenIdleThread()
{
while (true)
{
if (EmbedWndWhenIdle())
{
break;
}
else
{
Thread.Sleep(100);
}
}
}
public bool IsEmbeded { get; set; }
private bool EmbedWndWhenIdle()
{
GetMainWindowHandle();
if (WndProcess.MainWindowHandle != IntPtr.Zero)
{
WndHandle = WndProcess.MainWindowHandle;
IsEmbeded = true;
EmbedWnd();
DoActionDelay(SetBounds, WaitTime);
return true;
}
else
{
return false;
}
}
这样就可以了。
还有个细节问题,刚打开时调整大小会导致界面闪一下。
这个倒也没什么影响,后续再找时间调整。
接下来整理代码,把多余的代码都去掉好了,就一种嵌入方式。