将Unity3D应用程序嵌入WPF中*无需*占用整个窗口

时间:2019-01-23 21:25:53

标签: c# wpf unity3d

我正在尝试修改此问题中发布的代码: https://stackoverflow.com/a/44059700

允许我将Unity3D应用嵌入WPF应用中。

这是我稍作修改的版本:

namespace WPFWithUnity
{
public partial class Page1 : Page
{
    [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);

    Frame p = MainWindow.Instance.floatingFrame;

    bool initialized = false;

    public Page1()
    {
        InitializeComponent();

        MainWindow.Instance.MainWindowClosing += Application_Exit;

        System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
        dispatcherTimer.Tick += attemptInit;
        dispatcherTimer.Interval = new TimeSpan(0, 0, 1);
        dispatcherTimer.Start();

    }

    void attemptInit(object sender, EventArgs e) {

        if (initialized)
            return;

        HwndSource source = (HwndSource)HwndSource.FromVisual(p);

        Console.WriteLine("attempting to get handle...");

        if (source == null) {
            Console.WriteLine("Failed to get handle source");
            return;
        }

        IntPtr hWnd = source.Handle;

        try
        {
            process = new Process();
            process.StartInfo.FileName = "Child.exe";
            process.StartInfo.Arguments = "-parentHWND " + hWnd.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(hWnd, WindowEnum, IntPtr.Zero);

            //unityHWNDLabel.Text = "Unity HWND: 0x" + unityHWND.ToString("X8");
            Console.WriteLine("Unity HWND: 0x" + unityHWND.ToString("X8"));

            panel1_Resize(this, EventArgs.Empty);

            initialized = true;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message + ".\nCheck if Container.exe is placed next to UnityGame.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, (int)p.Width, (int)p.Height, true);
        Console.WriteLine("RESIZED UNITY WINDOW TO: " + (int)p.Width + "x" + (int)p.Height);
        ActivateUnityWindow();
    }

    // Close Unity application
    private void Application_Exit(object sender, EventArgs e)
    {
        try
        {
            process.CloseMainWindow();

            Thread.Sleep(1000);
            while (!process.HasExited)
                process.Kill();
        }
        catch (Exception)
        {

        }
    }

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

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

这是XAML的相关部分:

<Frame Name="floatingFrame" Grid.Row="15" Grid.RowSpan="5" Grid.Column="0" Grid.ColumnSpan="2" Width="640" Height="480" Margin="100,0,0,0" Panel.ZIndex="100" Source="Page1.xaml"/>

真的,唯一的区别是我试图在框架内而不是WinForms面板上使用WPF页面(试图避免WinForms)。嵌入式Unity应用程序可以正常启动...除了它占用整个窗口(即您再也看不到任何WPF控件)了。

所以,这个问题: 如何使Unity应用程序仅停留在WPF页面(位于框架内)内?

enter image description here

(此XY问题的Y在于我只是试图在WPF应用程序内部创建3D图形显示的东西。)

在此先感谢您的帮助。

2 个答案:

答案 0 :(得分:2)

在WPF中使用WindowsFormsHostHwndHost控件。 hwnd在主机控件的Handle属性中。因此,您可以更改此行以将Unity放入主机控件中。

process.StartInfo.Arguments = "-parentHWND " + hwndHost.Handle.ToInt32() + " " + Environment.CommandLine;

并删除获取浮动框架hwnd的代码

HwndSource source = (HwndSource)HwndSource.FromVisual(p);

答案 1 :(得分:2)

上述解决方案的问题在于,关注 Unity-exe 似乎是不可能的。所以是的,我能够在某个 WPF 应用程序的某个用户控件的某个单元格上加载 exe,但无法单击 Unity 框架中的任何内容。

我花了一整天的时间寻找解决方案,现在可以提出一个解决方案,在我看来,它更清晰,也解决了重点问题。

我描述了您可以按步骤执行的操作:

  1. Documentation of Unity : 这里解释了如何在 Winforms 控件中嵌入 Unity-exe。甚至还有一个 .zip 文件“EmbeddedWindow.zip”,您可以在其中下载示例代码。将基本文件从容器 enter image description here 中复制出来。

Form1.cs 包含与 this question 完全相同的代码。

public partial class Form1: Form
    {
        [DllImport("User32.dll")]
        private 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")]
        private 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();

            TopLevel = false;

            try
            {
                process = new Process();
                process.StartInfo.FileName = "[INSERT_FILE_NAME_OF_YOUR_EXE].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. 别忘了看看“Form1.cs”,有可能你要更新的exe不是“Child.exe”,如果是另外一个,直接编辑代码中的字符串即可。 process.StartInfo.FileName = "[INSERT_FILE_NAME_OF_YOUR_EXE].exe"; 还要检查“SelectablePanel.cs”,其中 Selectable 设置为 true 是必不可少的。此 SelectablePanel 用于 Form1.Designer.cs .
<块引用>

SplitContainer 的左侧 panel1 是 SelectablePanel 而不是 Panel !

    class SelectablePanel : Panel
    {
        public SelectablePanel()
        {
            this.SetStyle(ControlStyles.Selectable, true);
            this.TabStop = true;
        }
    }

另请注意,在 Winforms-control 的构造函数中,我需要设置 Toplevel = false; 。这在 Unity 的示例中没有提到,但是当您将其嵌入 WPF 应用程序时需要避免异常。

  1. 转到您的 WPF 应用程序并创建一个包含您的 Winforms-Control 的用户控件。制作一个类似于 this link 中所做的控件。在此示例中,您有一个名为 Grid_To_Embed_Winforms_Control_In 的网格和一小段代码隐藏,如底层代码。
public partial class WPF_User_Control: UserControl 

{ 
    public bool Already_Loaded = false;

    public WPF_User_Control()
    {
        InitializeComponent();
    }

    private void On_Load(object sender, RoutedEventArgs e)
    {
        if (!Already_Loaded)
        {
            // Create the interop host control.
            var host =
                new WindowsFormsHost();

            // Embed the Winforms Control
            host.Child = new Embed_Unity_Exe_Winforms_Control();

            // Add the interop host control to the Grid
            // control's collection of child controls.
            Grid_To_Embed_Winforms_Control_In.Children.Add(host);
            Already_Loaded = true;
        }
    }
}  

  1. 不要忘记在顶部添加 using System.Windows.Forms.Integration;。请注意,我添加了 boolean Already Loaded 以确保当视口发生变化(您想查看 WPF 应用程序的另一个页面)时,该过程不会再次启动。在我的完整解决方案中,我使用了 Microsoft 依赖注入,并且此控件位于作为单例添加的视图模型中。这样,我只启动一次过程。

就是这样,这对我有用。

有什么比我在谷歌搜索时看到的其他解决方案更好:

  • 调整大小效果更好并且“自动”完成,我不需要自己调用调整大小方法(Form1.cs 中的方法除外)。
  • 我还可以控制和专注于 Unity。

PS:如果你想让它看起来更好看,更“嵌入”,你可以用 Winform-control 做以下事情:

  • 选择 splitcontainer1 并转到属性
  • 将边框样式设置为“无”
  • 将 Panel2Collapsed 设置为“True”