tmytのらくがき

個人の日記レベルです

シーリングライトを力押しでGoogle Assistantに対応させる

シーリングライトPanasonicのHH-XCB1283Aというモデルを使っているのですが、このモデルはリモコンが赤外線ではなくBluetoothになっているのでスマートフォンから操作できるのでとても便利です。なんですが、Bluetoothゆえいわゆる学習リモコンは全滅だしもちろんGoogle Assistantにも対応していないのでしかたなく力押しでGoogle Assistantに対応させました。

構成

今日は天気もよく、室温が高くて寝ているのか起きているのかわからない境目を漂っているときに、今回の構成を思いつきました。

f:id:tmyt:20191227192942p:plain

いろいろ起きた結果、USBで接続されたAndroidに対してadb shell input touchscreen tap x yでリクエストして、あらかじめ起動しておいたPanasonicの純正アプリを操作するだけ、簡単!天才!

アプリのどこを押すか決める

シーリングライトとの通信を取り持ってくれるAndroidをどうやって操作するか結構悩んだ*1結果、ADB経由でアプリを操作すれば解決やーんとなりました。 このアプリはPlay Storeで★1.9ととても微妙な評価なのですが、見た目はこんな感じです。

どうみてもiPhoneのスクショですが、Androidも見た目は全く同じ見た目です。これは1~6に電灯の状態*2をプリセットできる仕組みで、1がよく使うやつ、6が消灯、2~5が時々使うものです。とりあえず点灯と消灯ができればいいので、1と6を押すようにします。

ADB経由で任意の地点を操作するには、inputコマンドを利用すれば簡単です。たとえば、X:100, Y:100 地点をタップするには

$ adb shell input touchscreen tap 100 100

とか呼べばOKです。では今回どこを押せばいいのかというと、今回のADBを受けてくれるNextbit Robinでは次の通りでした。

状態 X Y
ON 267 439
OFF 804 1162

この値は後で使います。

Google AssistantからのWebHookを受けるサーバを書く

次に、WebHookを受け付けるサーバを書きます。ハンドリングは、actions-to-googleっていうパッケージを使えばいいらしいので使いました。OAuth2しないといけない風だったので仕方なく空実装を作りました。ADBを中継するサーバ(後で出てきます)とはSocket.IOを使っています。

const express = require('express');
const bodyParser = require('body-parser');
const util = require('util');
const { smarthome } = require('actions-on-google');

const expressApp = express();
const app = smarthome();
const server = require('http').createServer(expressApp);
const io = require('socket.io')(server);

app.onExecute((body, headers) => {
  io.sockets.emit('execute', body);
  return {
    requestId: body.requestId,
    payload: {
      commands: [{
        ids: [ '1' ],
        status: 'SUCCESS',
        state: { on: true, online: true },
      }],
    },
  };
});
app.onQuery((body, headers) => ({
  requestId: body.requestId,
  payload: {
    devices: {
      '1': { on: true, online: true },
    },
  },
}));
app.onSync((body, headers) => ({
  requestId: body.requestId,
  payload: {
    agentUserId: '1',
    devices: [{
      id: '1',
      type: 'action.devices.types.LIGHT',
      traits: [ 'action.devices.traits.OnOff' ],
      name: {
        defaultNames: ['シーリング ライト'],
        name: 'シーリング ライト',
        nicknames: ['シーリング ライト'],
      },
      willReportState: false,
    }],
  },
}));

const assistantApp = express().use(bodyParser.json());
const oauthApp = express().use(bodyParser.urlencoded({ extended: true }));

assistantApp.post('/fulfillment', app);

oauthApp.get('/authorize', (req, res) => {
  console.log('authorize');
  const uri = `${req.query.redirect_uri}?state=${req.query.state}&code=12345678`;
  res.redirect(uri);
});
oauthApp.post('/token', (req, res) => {
  console.log('token');
  res.send({
    token_type: 'Bearer',
    access_token: '12345678' + Date.now(),
    refresh_token: '12345678',
    expires_in: 86400 * 90,
  });
});

expressApp.use('/assistant', assistantApp);
expressApp.use('/oauth', oauthApp);

io.on('connection', () => { });
server.listen(process.env.PORT);

OAuth2がまったく中身のない実装になっている、うえに、リクエストはトークンの検証していないなどとても雑です。これ、エンドポイントをたたかれると普通に動いてしまうので、皆さんはちゃんと認可を実装しましょうね

ADBを呼び出すサーバを実装する

あとはこのサーバに接続して、ExecuteをSocket.IO経由で受け取り、接続されたAndroidに対してADBでリクエストをするサーバが必要です。これはsocket.io-clientパッケージを使えば一瞬で書けます。

const socket = require('socket.io-client')('https://ないしょだよ!!!!.example.com');
const { exec } = require('child_process');
const util = require('util');

const on = '267 439';
const off = '804 1162';

socket.on('connect', () => {
  console.log('connect');
});
socket.on('execute', data => {
  const input = data.inputs[0];
  for(const execution of input.payload.commands[0].execution){
    if(execution.command === 'action.devices.commands.OnOff'){
      const arg = execution.params.on ? on : off;
      exec(`adb.exe shell input touchscreen tap ${arg}`);
    }
  }
  console.log(util.inspect(data, true, null));
});

child_process.execでADBを呼び出しています。このプロセスは、Windows上のWSLで動くNodeJSから、Windows側のPATH環境変数に含まれているadb.exeを呼び出しているので".exe"まで含んでいます。Linuxなら不要です。

おわり

あとは、Google Assistantから呼んでもらえるようにいろいろを設定すればできあがり。

console.actions.google.com に新しいアクションを作って、WebHookサーバのURLを設定し、ADBを呼び出すサーバを実行し、Socket.IOの接続が確立できたのを確認したのちにGoogle Assistantに呼びかけます。

ねぇGoogle 電気つけて!!!!!!

きっとシーリングがつくはず…

*1:オートメーション的ななにかを使う?とかいろいろ

*2:明るさ、色など

G8X ThinQのワイドモードを操作するアプリをTaskerに対応しました

G8X ThinQのワイドモードを無理やり有効にする例のボタンですが、Taskerに対応してくれないか?と要望をもらったので、Taskerに対応しました。

Taskerは特定のアプリが起動したときをはじめいろいろなタイミングで定型アクションを実行するツールなんですが、外部アプリをプラグインとして実行できるらしく、プラグインとして対応しました。

play.google.com

Taskerのアクションを選択する際に、プラグインを選ぶと"G8X WideMode"*1があるのでそこからこのアプリを呼び出せます。

Taskerに提供している機能は、ワイドモードを「ONにする」、「OFFにする」、「トグルする」の3種類です。プラグインの設定で動作を切り替えることができます。 例えば、「Kindleを起動したときにワイドモードをONにする」というような感じで使うことができます。便利ですね。

Tasker連携はバージョン1.1.0から利用可能です。アップデートしてご利用ください。

play.google.com

*1:ワイドモードを切り替えっていうキャプションにしたはず…

G8X ThinQ向け、ワイドモードボタンを増やすアプリを作ってみました

TL;DR

  • 右側アクティビティを強制的にワイドモードにするボタンをクイック設定パネルに出せるアプリです
  • 運が悪いと描画が崩壊する可能性があります
  • ダウンロードはここから。 Playストアからどうぞ

G8X ThinQのワイドモード不便ですよね

ごく一部というか実質Chromeしかワイドモードが使えなくて不便すぎるので、簡単なアプリを用意しました。 インストールするとクイック設定パネルに強制的にワイドモードに切り替えるボタンを追加できるようになります。

2019/12/13時点でPlayストアに未公開なので、不明なソースからのインストールを許可しないといけないのでリスクを理解している人向けです。

Playストアに公開しましした🎉

使い方

クイック設定パネルの設定を開きます

f:id:tmyt:20191213224747p:plain:w300

Wide Mode というアイコンが増えているので任意の場所に追加して、保存します。

f:id:tmyt:20191213225000p:plain:w300

ワイドモードにしたいアプリを右画面で起動して、クイック設定パネルの Wide Mode を押します。

f:id:tmyt:20191213224852p:plain:w300

ワイドモードになりました。

f:id:tmyt:20191213224900p:plain:w300

注意点

  • 内部APIを直接呼び出しているので時々描画が崩壊するかもしれません。
  • 最悪デバイスを再起動すればすべて元に戻ります。
  • 実績として、右側画面の画面分割と組み合わせたりしていじってたら描画が崩壊したことが1回、その後Chromeを正規手順でワイドモードにしたり戻したりしていたらSystemServerが再起動したことが1回あります。

LG G8X ThinQを契約なしで買ってきました

今日Softbankから発売になったLGのG8X ThinQを契約なしで買ってみました。屏風だしね。

Softbankの直営店に行って、端末だけ購入したいと伝えたらすぐに受付してくれました。とてもスムーズに購入できたので次回からも利用したいですね。

詳しいスペックその他は詳しいページにお譲りするとして、ぶっちゃけ55400円はめちゃくちゃ安いと思いますよ、FeliCaだし、Qiだし、SD855だし、RAM 6GBだし、防水だし、追加画面あるし、etc。

写真その他

しっぽ、絶対なくすと思うんです。間違いなく。磁石でついてるんですけど裏表あって微妙に不便だし、絶対なくすと思うんです(2回目)。

指紋認証もついてて、ディスプレイ内蔵なんですよ。かっこいいですね。若干認識制度があやしいか?っていう気もするんですけどだんだん慣れてきました。指の腹がぷにって押しつぶされるくらいに押さえつけてちょっと待たないとだめっぽいです。慣れればどうってことないです。慣れれば。

SIMロック解除コードが見つからないとか紆余曲折あったものの、無事SIMロックが解除できました。いつも通りとりあえずT-Mobile USAのSIMでローミングする様子を眺める。

あ~~~~~~びょうぶ~~~~~~~~~~~~~ たまんないですね。

挙動の話

さて、この屏風。先行記事では、ワイドモードが一部のアプリしかできないとか書いてるじゃぁありませんか。たしかに、手元で確認したのはChromeだけです。Kindleすら広がらないのはいかがなものか。電書読ませろ。

そして、屏風芸人としてはまずは屏風にしないといけないな。ということで屏風にしてみたんですが、屏風にならないんですこれが。先行記事にあるように、ごくごく一部のアプリでしかワイドモードにできないそうです。 でも、どうしてもまずは屏風にしないといけない気がしたので頑張ったものがこちら。

ImageViewが2画面にわたって表示されていますね。期待した挙動です。完璧です。これを実現するにはこんなコードを書きます。

val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val setWideScreenMode = activityManager.javaClass.getDeclaredMethod("setWideScreenMode", Boolean::class.java)
setWideScreenMode.invoke(activityManager, true)

簡単ですねー。ただ、このコードを実行するのはお勧めしません。なぜなら、ロック画面の動作と、指紋認証の動作がおかしくなります。ロック/ロック解除を数回繰り返すと治ったりしますがお勧めしません。

なんでワイドモードになったりならなかったりするか

それは、ワイドモード対応アプリがハードコードされているからです!

えー…うそやん…えー…まじかよ…って15分ぐらい一人で騒いでいました。まじです。そのリストがこちらです。

  • com.android.chrome
  • com.lge.retailmode
  • com.lge.gallery.lduwidget
  • com.nhn.android.search
  • com.naver.whale
  • com.google.android.googlequicksearchbox
  • com.lge.epmobile

これ以外はワイドモードになりません。残念でした。じゃぁこれらのパッケージ名にしてあげればワイドモードが選べるのか?っていうと、選べました。

f:id:tmyt:20191207035355p:plain

つまりパッケージ名しか見てないということです。残念です。

おわり

画面が大きくて、SoCもはやくて、メモリもまぁまぁ多くて、全体的にポイント高いんですが、屏風という点ではDocomoの屏風に負けてしまうSoftbankの屏風でした。

Uno PlatformでDarkテーマを使うにはPre-releaseにする

Uno CalculatorがDark modeをサポートしたよ!っていうアップデートが配信されていたのでソースを読みながら同じようにしてもDarkにならんのやが…って1日考えてみた結果、Pre-releaseにしたら解決しました。という話です。

ThemeDictionariesが動かない

例えばこんな感じで書くと2019年12月2日現在のリリース版のUno Platformだと動きません。

<Application
    x:Class="UnoApp2.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.ThemeDictionaries>
        <ResourceDictionary x:Key="Default">
          <SolidColorBrush x:Key="ThemeBrush" Color="DarkBlue" />
          <SolidColorBrush x:Key="BackgroundBrush" Color="Gray" />
        </ResourceDictionary>
        <ResourceDictionary x:Key="Light">
          <SolidColorBrush x:Key="ThemeBrush" Color="Red" />
          <SolidColorBrush x:Key="BackgroundBrush" Color="#ccccff" />
        </ResourceDictionary>
      </ResourceDictionary.ThemeDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>

これは、前にも話題にしたインラインなResourceDictionaryの動作に制限があるためで、ThemeDictionaries内のResourceDictionaryは解釈されません。

ただ、あくまでもインラインなResourceDictionaryが問題なのであって以前に話題に挙げたようにglobal-level*1な ResourceDictionaryに定義すれば2019年12月2日現時点のリリース版でもThemeDictionariesの中身は解釈されます。 ただ、この場合常にテーマはLightとして処理されます。

ThemeDictionariesは動く

ThemeDictionariesは動きます。ただUno.UIのPre-Release版が必要です。

しつこいようですが2019年12月2日現在のリリース版は2019年7月23日にリリースされた1.45.0というバージョンが最新です。 このバージョンではThemeDictionariesを解釈することはできるのですが、DarkやHighContractを使用することはできません。

Light以外のテーマを使えるようになるには、2019年10月9日にマージされた#1628というプルリクエストを含んだバージョンにする必要があります。 これが一体どのバージョンからなのか…というのは探すのが大変なのであきらめました…

対応してるバージョンの境目を探すのも大変なので、とりあえず今回はUno.UIを2.1.0-dev.52というバージョンにNuGetから更新します。 このバージョンはプレリリース版としてマークされているので、NuGetの画面からプレリリースを含めるというところにチェックを付けると表示されます。

f:id:tmyt:20191202012036p:plain

Uno.UI以外はプロジェクトが生成されたときのデフォルトのままのバージョンを使ってビルドしても正しく動いているようです。

Uno.UIがプレリリース版に更新できたので、次のXAMLを適当な場所に保存します。ここではThemes/Styles.xamlという名前で保存しました。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Default">
      <SolidColorBrush x:Key="ForegroundBrush" Color="DarkBlue" />
      <SolidColorBrush x:Key="BackgroundBrush" Color="Gray" />
    </ResourceDictionary>
    <ResourceDictionary x:Key="Light">
      <SolidColorBrush x:Key="ForegroundBrush" Color="Red" />
      <SolidColorBrush x:Key="BackgroundBrush" Color="#ccccff" />
    </ResourceDictionary>
  </ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

これを使用するMainPage.xamlを次の内容で作成しました。

<Page
    x:Class="UnoApp2.MainPage"
    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"
    mc:Ignorable="d">

  <Grid Background="{ThemeResource BackgroundBrush}">
    <TextBlock Foreground="{ThemeResource ForegroundBrush}" Text="Hello, world !" Margin="20" FontSize="30" />
  </Grid>
</Page>

App.xamlにはUWPとUnoの互換性の都合でMergedDictionariesの設定を書いてあります。

<Application
    x:Class="UnoApp2.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Application.Resources>
      <ResourceDictionary>
          <ResourceDictionary.MergedDictionaries>
              <ResourceDictionary  Source="Themes/Styles.xaml" />
          </ResourceDictionary.MergedDictionaries>
      </ResourceDictionary>
  </Application.Resources>
</Application>

ここまでをビルドして実行すると次の結果になります。期待した通りの結果ですね。

f:id:tmyt:20191202011938p:plain

ただ、Unoのテーマ実装ではまだ動的なテーマ変更に対応していません。おそらく#1766がマージされた日にはいろいろと解決するのでしょう…

github.com

*1:ルート要素がResourceDictionaryとなってるXAMLファイルをこう呼んでいるようです

せっかくなのでUno Platformの話がしたい

Uno Platformというものがある

UWPのコードをベースにして、自力でXAMLを解釈して、Xamarinの上でViewをいい感じに動かすっていうアプローチ。Xamarin.Formsのラッパーではない。

俺たちが期待してるUniversalはこれなんだよ、いいからMSは早く買収するんだ。という気持ちを抑えながら、公式ページで図解されている構造を見てみるとこういう感じ。

https://s3.amazonaws.com/uno-website-assets/wp-content/uploads/2019/08/21141002/diagram.png

あれこれなんか4年前に似たような図を描いたような気がする*1

簡単なコードなら普通に動くし、複雑なコードもUno Platformをターゲットに最初から作ればわりかしちゃんと動く。よくできている。

実際にWindows 10の電卓が移植されていて、Play StoreとかApp Storeからダウンロードできるので、とりあえず「すげーーー」って言うには電卓を入れるのがおすすめ。

Uno Calculator

Uno Calculator

  • nventive
  • 仕事効率化
  • 無料
apps.apple.com play.google.com

ただ、本物のUWPと、UWPに似せたAPIを提供したXamarinだとどうしても違うことがあるので現行のUWPのコードを突っ込めばそのまま動くか?と聞かれれば答えは”ほぼNo”という感じ。 実際にコピペしたら全然動かんかった。

implicit styleが動かんという話

さて、いろいろ動かないことはあるんだけども一番困ったimplicit styleが動かんというトピックを紹介しておきます。 アプリの見た目をそろえるときにこういう書き方をすることがあります。

<Application
    x:Class="UnoApp2.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Application.Resources>
    <Style TargetType="TextBlock">
      <Setter Property="Foreground" Value="Red" />
    </Style>
  </Application.Resources>
</Application>

すべてのTextBlock の文字色が赤になることを期待しています。当然UWPで実行すると赤で表示されます。

f:id:tmyt:20191130044459p:plain

ところが、Uno Platformでビルドすると次のメッセージが得られます。

Error reading response 1>MSBUILD : error : Generation failed: System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.InvalidOperationException: Generation failed for Uno.UI.SourceGenerators.XamlGenerator.XamlCodeGenerator. System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Exception: Processing failed for file ***\UnoApp2.Shared\App.xaml ---> System.Exception: Implicit styles in inline resources are not supported (Application, Line 1:2) 1>MSBUILD : error : 場所 Uno.UI.SourceGenerators.XamlGenerator.XamlFileGenerator.RegisterResources(XamlObjectDefinition topLevelControl) 1>MSBUILD : error : 場所 Uno.UI.SourceGenerators.XamlGenerator.XamlFileGenerator.BuildApplicationInitializerBody(IndentedStringBuilder writer, XamlObjectDefinition topLevelControl) 1>MSBUILD : error : 場所 Uno.UI.SourceGenerators.XamlGenerator.XamlFileGenerator.InnerGenerateFile() 1>MSBUILD : error : 場所 Uno.UI.SourceGenerators.XamlGenerator.XamlFileGenerator.GenerateFile()

なんやかんや書いてますが、要するにx:Key が指定されていない暗黙的なリソースはサポートされてないですよ。ということらしいのです。めっちゃ困る。

これに関連したIssue(というかPR)をGitHubで探すとこれが見つかります。

github.com

ここの、 What is current behavior? に書いてあるのですが

Uno only supports global-level resources, including implicit styles

らしいのです。このglobal-levelがポイントです。

解決する話

先のPRから現状の実装ではglobal-levelならimplicit stylesが動く。と読み取れます。 じゃぁglobal-levelなstyleとはなんぞや。という話があり、ドキュメントなどから雰囲気をつかむと要するにこれです。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Style TargetType="TextBlock">
    <Setter Property="Foreground" Value="Red" />
  </Style>
</ResourceDictionary>

てきとうにXAMLを作って、ResourceDictionaryを書いて、その中ならimplicit styleと呼ばれているものを書きまくってもいいということらしいのです。実際動かすと動きます。

f:id:tmyt:20191130045520p:plain:w220 f:id:tmyt:20191130045903p:plain:w220

そんじゃこれ、UWPで動くんかというと動きません。実行結果はこれです。

f:id:tmyt:20191130044539p:plain

ではこれが、全部の環境で期待値となるXAMLはどうなるか。というと、App.xamlでResourceDictionaryとして読めばいい。

<!-- Themes/ImplicitStyles.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="TextBlock">
        <Setter Property="Foreground" Value="Red" />
    </Style>
</ResourceDictionary>

<!-- App.xaml -->
<Application
    x:Class="UnoApp2.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Application.Resources>
    <ResourceDictionary Source="Themes/ImplicitStyles.xaml" />
  </Application.Resources>
</Application>

これで最初の画像と同じ結果が得られます(見た目変らないので画像省略)。

だんだん奇妙なXAMLになってきましたね。さすがはCross Platform…たまんないですね…

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の子ウィンドウとして管理しました