Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入

    现在主要是要处理一个问题: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

打包设置

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第1张图片

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第2张图片

嵌入方式1:用Windows的API

1.启动exe

基本代码

            ProcessStartInfo info = new ProcessStartInfo(Path);
            process = Process.Start(info);

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第3张图片

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;

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第4张图片

3.嵌入窗口

            IntPtr parentHandle = unityPanel1.Handle;
            IntPtr unityHWND = process.MainWindowHandle;
            User32.SetParent(unityHWND, parentHandle);

User32是一个帮助类,内部封装了对user32.dll的调用。

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第5张图片

4.调整大小

            Control parentControl = unityPanel1;
            IntPtr unityHWND = process.MainWindowHandle;
            User32.MoveWindow(unityHWND, 0, 0, parentControl.Width, parentControl.Height, true);

 Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第6张图片

5.设置无边框

            Control parentControl = unityPanel1;
            IntPtr unityHWND = process.MainWindowHandle;
            User32.SetWindowLong(new HandleRef(parentControl, unityHWND), User32.GWL_STYLE, User32.WS_VISIBLE);//无边框

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第7张图片

到这里就完成了嵌入工作。

这其实是一个通用的过程,任何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打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第8张图片

嵌入后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的启动命令行参数

Unity打包程序嵌入WinForm或者WPF(1) 嵌入、中文输入_第9张图片

关键是-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;
            }
        } 

这样就可以了。

还有个细节问题,刚打开时调整大小会导致界面闪一下。

这个倒也没什么影响,后续再找时间调整。

接下来整理代码,把多余的代码都去掉好了,就一种嵌入方式。

 

Demo项目源代码文件没有了,找不到了,应该说是合并到公司项目中了,那部分现在别人在管,我都好久没动过了。

当初我写的时候应该是把相关代码都贴出来了。

主要是记录一个处理问题的过程,碰到相同问题的可以尝试我的思路、方法吧。

你可能感兴趣的:(Unity)