在Winforms-Application

时间:2018-01-15 20:02:29

标签: c# .net wpf winforms unity3d

我目前正在为编辑器开发一个简单的原型。编辑器将使用WinForms(或WPF,如果可能)来提供主用户界面,并且还将嵌入Unity 2017独立应用程序以可视化数据并提供其他控制元素(例如缩放,旋转,滚动......)。 p>

感谢下面这篇不错的帖子,让一个嵌入式Unity应用程序在WinForms应用程序中运行非常简单。

https://forum.unity.com/threads/unity-3d-within-windows-application-enviroment.236213/

此外,还有一个简单的示例应用程序,您可以在此处访问:

Example.zip

不幸的是,我找到的示例或任何帖子都没有回答一个非常基本的问题:如何从嵌入式Unity应用程序中的WinForms应用程序传递数据(或调用方法)(反之亦然) ?

您的WinForms应用程序是否可以在Unity应用程序中简单地调用MonoBehaviour脚本或静态方法?如果是这样,怎么样?如果没有,那么什么是好的解决方法? Unity-to-WinForms通信如何才能起作用呢?

更新

使用Programmer(link)提到的重复页面来实现一个解决方案,该解决方案使用命名管道在WinForms和Unity应用程序之间进行通信。

两个应用程序都使用BackgroundWorkers,WinForms应用程序充当服务器(因为它在客户端启动之前首先启动并需要一个活动的连接侦听器),而嵌入式Unity应用程序充当客户端。

不幸的是,Unity-application抛出NotImplementedException,声明" Mono"不支持ACL。创建NamedPipeClientStream时(使用Unity 2017.3和Net 2.0(不是Net 2.0子集)测试)。 the post mentioned above中的一些评论已经报道了这个例外,但如果它已经解决,则不清楚。建议的解决方案"确保服务器在客户端尝试连接之前启动并运行。和#34;以管理模式启动它"已经尝试过,但到目前为止失败了。

解决方案:

经过一些更多的测试后,很明显,Mono"不支持" ACL。异常导致在创建NamedPipeClientStream实例时使用TokenImpersonationLevel参数。将其更改为TokenImpersonationLevel.None解决了这个问题。

这里是WinForms应用程序使用的代码,它充当命名管道服务器。确保在Unity应用程序客户端尝试连接之前执行此脚本!此外,请确保在启动服务器之前已构建并发布Unity应用程序。将Unity应用程序的Unity可执行文件放在WinForms-applications文件夹中,并将其命名为#34; Child.exe"。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Diagnostics;
using System.IO.Pipes;

namespace Container
{
    public partial class MainForm : 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);

        /// <summary>
        /// A Delegate for the Update Log Method.
        /// </summary>
        /// <param name="text">The Text to log.</param>
        private delegate void UpdateLogCallback(string text);

        /// <summary>
        /// The Unity Application Process.
        /// </summary>
        private Process process;

        /// <summary>
        /// The Unity Application Window Handle.
        /// </summary>
        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);

        /// <summary>
        /// The Background Worker, which will send and receive Data.
        /// </summary>
        private BackgroundWorker backgroundWorker;

        /// <summary>
        /// A Named Pipe Stream, acting as the Server for Communication between this Application and the Unity Application.
        /// </summary>
        private NamedPipeServerStream namedPipeServerStream;



        public MainForm()
        {
            InitializeComponent();

            try
            {
                //Create Server Instance
                namedPipeServerStream = new NamedPipeServerStream("NamedPipeExample", PipeDirection.InOut, 1);

                //Start Background Worker
                backgroundWorker = new BackgroundWorker();
                backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
                backgroundWorker.WorkerReportsProgress = true;

                backgroundWorker.RunWorkerAsync();

                //Start embedded Unity Application
                process = new Process();
                process.StartInfo.FileName = Application.StartupPath + "\\Child.exe";
                process.StartInfo.Arguments = "-parentHWND " + splitContainer.Panel1.Handle.ToInt32() + " " + Environment.CommandLine;
                process.StartInfo.UseShellExecute = true;
                process.StartInfo.CreateNoWindow = true;

                process.Start();
                process.WaitForInputIdle();

                //Embed Unity Application into this Application
                EnumChildWindows(splitContainer.Panel1.Handle, WindowEnum, IntPtr.Zero);

                //Wait for a Client to connect
                namedPipeServerStream.WaitForConnection();
            }
            catch (Exception ex)
            {
                throw ex;
            }

        }

        /// <summary>
        /// Activates the Unity Window.
        /// </summary>
        private void ActivateUnityWindow()
        {
            SendMessage(unityHWND, WM_ACTIVATE, WA_ACTIVE, IntPtr.Zero);
        }

        /// <summary>
        /// Deactivates the Unity Window.
        /// </summary>
        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, splitContainer.Panel1.Width, splitContainer.Panel1.Height, true);
            ActivateUnityWindow();
        }

        /// <summary>
        /// Called, when this Application is closed. Tries to close the Unity Application and the Named Pipe as well.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            try
            {
                //Close Connection
                namedPipeServerStream.Close();

                //Kill the Unity Application
                process.CloseMainWindow();

                Thread.Sleep(1000);

                while (process.HasExited == false)
                {
                    process.Kill();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private void MainForm_Activated(object sender, EventArgs e)
        {
            ActivateUnityWindow();
        }

        private void MainForm_Deactivate(object sender, EventArgs e)
        {
            DeactivateUnityWindow();
        }

        /// <summary>
        /// A simple Background Worker, which sends Data to the Client via a Named Pipe and receives a Response afterwards.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            //Init
            UpdateLogCallback updateLog = new UpdateLogCallback(UpdateLog);
            string dataFromClient = null;

            try
            {
                //Don't pass until a Connection has been established
                while (namedPipeServerStream.IsConnected == false)
                {
                    Thread.Sleep(100);
                }

                //Created stream for reading and writing
                StreamString serverStream = new StreamString(namedPipeServerStream);

                //Send to Client and receive Response (pause using Thread.Sleep for demonstration Purposes)
                Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("A Message from Server.") + " Bytes." });
                Thread.Sleep(1000);
                dataFromClient = serverStream.ReadString();
                Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });

                Thread.Sleep(1000);

                Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("A small Message from Server.") + " Bytes." });
                Thread.Sleep(1000);
                dataFromClient = serverStream.ReadString();
                Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });

                Thread.Sleep(1000);

                Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("Another Message from Server.") + " Bytes." });
                Thread.Sleep(1000);
                dataFromClient = serverStream.ReadString();
                Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });

                Thread.Sleep(1000);

                Invoke(updateLog, new object[] { "Send Data to Client: " + serverStream.WriteString("The final Message from Server.") + " Bytes." });
                Thread.Sleep(1000);
                dataFromClient = serverStream.ReadString();
                Invoke(updateLog, new object[] { "Received Data from Server: " + dataFromClient });
            }
            catch(Exception ex)
            {
                //Handle usual Communication Exceptions here - just logging here for demonstration and debugging Purposes
                Invoke(updateLog, new object[] { ex.Message });
            }
        }

        /// <summary>
        /// A simple Method, which writes Text into a Console / Log. Will be invoked by the Background Worker, since WinForms are not Thread-safe and will crash, if accessed directly by a non-main-Thread.
        /// </summary>
        /// <param name="text">The Text to log.</param>
        private void UpdateLog(string text)
        {
            lock (richTextBox_Console)
            {
                Console.WriteLine(text);
                richTextBox_Console.AppendText(Environment.NewLine + text);
            }
        }
    }
}

将此代码附加到Unity应用程序中的GameObject。另外,请确保将带有TextMeshProUGUI组件的GameObject(TextMeshPro-Asset,您将在资源商店中找到它)引用到&text;&#39; -member,因此应用程序不会崩溃,你可以看到一些调试信息。 另外(如上所述)确保您构建并释放您的Unity应用程序,并将其命名为&#34; Child.exe&#34;并将其放在与WinForms-application相同的文件夹中。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using System.IO.Pipes;
using System.Security.Principal;
using Assets;
using System.ComponentModel;
using TMPro;



/// <summary>
/// A simple Example Project, which demonstrates Communication between WinForms-Applications and embedded Unity Engine Applications via Named Pipes.
/// 
/// This Code (Unity) is considered as the Client, which will receive Data from the WinForms-Server and send a Response in Return.
/// </summary>
public class SendAndReceive : MonoBehaviour
{
    /// <summary>
    /// A GameObject with an attached Text-Component, which serves as a simple Console.
    /// </summary>
    public GameObject textObject;

    /// <summary>
    /// The Background Worker, which will send and receive Data.
    /// </summary>
    private BackgroundWorker backgroundWorker;

    /// <summary>
    /// A Buffer needed to temporarely save Text, which will be shown in the Console.
    /// </summary>
    private string textBuffer = "";



    /// <summary>
    /// Use this for initialization.
    /// </summary>
    void Start ()
    {
        //Init the Background Worker to send and receive Data
        this.backgroundWorker = new BackgroundWorker();
        this.backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
        this.backgroundWorker.WorkerReportsProgress = true;
        this.backgroundWorker.RunWorkerAsync();
    }

    /// <summary>
    /// Update is called once per frame.
    /// </summary>
    void Update ()
    {
        //Update the Console for debugging Purposes
        lock (textBuffer)
        {
            if (string.IsNullOrEmpty(textBuffer) == false)
            {
                textObject.GetComponent<TextMeshProUGUI>().text = textObject.GetComponent<TextMeshProUGUI>().text + Environment.NewLine + textBuffer;
                textBuffer = "";
            }
        }
    }

    /// <summary>
    /// A simple Background Worker, which receives Data from the Server via a Named Pipe and sends a Response afterwards.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            //Init
            NamedPipeClientStream client = null;
            string dataFromServer = null;

            //Create Client Instance
            client = new NamedPipeClientStream(".", "NamedPipeExample", PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.None);
            updateTextBuffer("Initialized Client");

            //Connect to Server
            client.Connect();
            updateTextBuffer("Connected to Server");

            //Created stream for Reading and Writing
            StreamString clientStream = new StreamString(client);

            //Read from Server and send Response (flush in between to clear the Buffer and fix some strange Issues I couldn't really explain, sorry)
            dataFromServer = clientStream.ReadString();
            updateTextBuffer("Received Data from Server: " + dataFromServer);
            client.Flush();
            updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Some data from client.") + " Bytes.");

            dataFromServer = clientStream.ReadString();
            updateTextBuffer("Received Data from Server: " + dataFromServer);
            client.Flush();
            updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Some more data from client.") + " Bytes.");

            dataFromServer = clientStream.ReadString();
            updateTextBuffer("Received Data from Server: " + dataFromServer);
            client.Flush();
            updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("A lot of more data from client.") + " Bytes.");

            dataFromServer = clientStream.ReadString();
            updateTextBuffer("Received Data from Server: " + dataFromServer);
            client.Flush();
            updateTextBuffer("Sent Data back to Server: " + clientStream.WriteString("Clients final message.") + " Bytes.");

            //Close client
            client.Close();
            updateTextBuffer("Done");
        }
        catch (Exception ex)
        {
            //Handle usual Communication Exceptions here - just logging here for demonstration and debugging Purposes
            updateTextBuffer(ex.Message + Environment.NewLine + ex.StackTrace.ToString() + Environment.NewLine + "Last Message was: " + textBuffer);
        }
    }

    /// <summary>
    /// A Buffer, which allows the Background Worker to save Texts, which may be written into a Log or Console by the Update-Loop
    /// </summary>
    /// <param name="text">The Text to save.</param>
    private void updateTextBuffer(string text)
    {
        lock (textBuffer)
        {
            if (string.IsNullOrEmpty(textBuffer))
            {
                textBuffer = text;
            }
            else
            {
                textBuffer = textBuffer + Environment.NewLine + text;
            }
        }
    }
}

此外,两个脚本都需要一个额外的类来封装管道流,因此发送和接收文本变得更加容易。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Assets
{
    /// <summary>
    /// Simple Wrapper to write / read Data to / from a Named Pipe Stream.
    /// 
    /// Code based on:
    /// https://stackoverflow.com/questions/43062782/send-message-from-one-program-to-another-in-unity
    /// </summary>
    public class StreamString
    {
        private Stream ioStream;
        private UnicodeEncoding streamEncoding;

        public StreamString(Stream ioStream)
        {
            this.ioStream = ioStream;
            streamEncoding = new UnicodeEncoding();
        }

        public string ReadString()
        {
            int len = 0;

            len = ioStream.ReadByte() * 256;
            len += ioStream.ReadByte();
            byte[] inBuffer = new byte[len];
            ioStream.Read(inBuffer, 0, len);

            return streamEncoding.GetString(inBuffer);
        }

        public int WriteString(string outString)
        {
            byte[] outBuffer = streamEncoding.GetBytes(outString);
            int len = outBuffer.Length;
            if (len > UInt16.MaxValue)
            {
                len = (int)UInt16.MaxValue;
            }
            ioStream.WriteByte((byte)(len / 256));
            ioStream.WriteByte((byte)(len & 255));
            ioStream.Write(outBuffer, 0, len);
            ioStream.Flush();

            return outBuffer.Length + 2;
        }
    }
}

如果你读到这篇文章:谢谢:)我希望它能帮助你成功的开发者之旅!

我原型的最终结果: WinForms-application with embedded Unity Engine, successfully communicating via named pipes

1 个答案:

答案 0 :(得分:1)

如果不出意外,您可以依靠基本文件I / O来进行两者之间的通信。