tmytのらくがき

個人の日記レベルです

WPFの子ウィンドウとしてUnityを実行してみる

UnityってWPFの中に表示したりできひんよね?って聞かれたからドキュメント読んだらできるって書いてたからやってみたらできました。という話。

TL;DR

  • -parentHWND 0x**** で子ウィンドウにできる
  • HwndHostでUnityのプロセスを子ウィンドウとして起動するだけ

Unityを子ウィンドウで起動する

Windows向けにビルドしたUnityはコマンドライン引数で -parentHWND を付けて起動するとUnity Playerは子ウィンドウとして起動します。その時の画面幅などはSTARTUPINFOで指定されたものを使用します。と、Unityのドキュメントに書いてあります。

Windows スタンドアロンアプリケーションを別のアプリケーションに埋め込みます。これを使用する場合は、親アプリケーションのウィンドウハンドル (HWND) を Windows スタンドアロンアプリケーションに渡す必要があります。

これ、日本語だと特に書いてないんですけど、0xXXXXの形で16進数として渡すと成功します。 追記: 0xなしで10進でもOKでした

簡単にC#から起動するにはこんな感じ。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        StartUnity();
    }

    void StartUnity()
    {
        var apppath = @"C:\doko\soko\unity-app.exe";
        var cmdline = $"-parentHWND {new WindowInteropHelper(this).Handle}";
        Process.Start(apppath, cmdline);
    }
}

実行すると、UnityがWPFの中で動いているような感じになりました。が、リサイズしてもウィンドウサイズが追従しないのでいまいちです。

f:id:tmyt:20190926065827p:plain

HwndHostをそれとなく実装していく

HwndHostを実装すれば子ウィンドウのサイズをWPFが面倒見てくれるようになります。Unityを子ウィンドウとして起動してくれるHwndHostを実装したクラスを書いてリサイズをいい感じに面倒見てもらいます。

class UnityHost : HwndHost
{
    private Process _childProcess;
    private HandleRef _childHandleRef;

    public string AppPath { get; set; }
    public HandleRef Child => _childHandleRef;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        var cmdline = $"-parentHWND {hwndParent.Handle}";
        _childProcess = Process.Start(AppPath, cmdline);
        while (true)
        {
            var hwndChild = User32.FindWindowEx(hwndParent.Handle, IntPtr.Zero, null, null);
            if (hwndChild != IntPtr.Zero)
            {
                return (_childHandleRef = new HandleRef(this, hwndChild));
            }
            Thread.Sleep(100);
        }
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        _childProcess.Dispose();
    }
}

static class User32
{
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string pClassName, string pWindowName);
}

急にコードが増えた気がしますが気のせいです。BuildWindowCoreの中でUnityを起動して、できたウィンドウのハンドルを返してるだけです。 このままだと、起動するパスがどこか明示されていないのでXAMLから指定します。

<Window x:Class="WpfApp4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp4"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:UnityHost AppPath="C:\doko\soko\unity-app.exe" />
    </Grid>
</Window>

これでリサイズできるようになりました。

起動時にWPFのクライアントエリア全体にUnityが出ないようにする

ここまでのコードだと、XAMLでUnityHostをGridにいれてサイズを変えたりしていても、Unityを起動したときにクライアントエリア全体にUnityが一瞬表示されます。

f:id:tmyt:20190926070602p:plain

これを回避するにはUnityのドキュメントに書いてあったようにSTARTUPINFOを指定すればいいんですが、残念ながらそれはP/Invokeです。ということでP/Invoke使っていろいろ実装したものがこちらです。

gist.github.com

P/Invokeのせいでやたらと長くなった…やな感じ… 実際の処理はBuildWindowCoreを参照してください。STASRTUPINFO構造体を埋めて、CreateProcessを呼び出しているだけです。

おしまい

  • Unityプレイヤーを-parentHWNDコマンドライン引数を使って、WPFに埋め込んでみました。
  • UnityプレイヤーをHwndHostを使ってWPFの子ウィンドウとして管理しました