#概要
Unityから他のアプリを操作する方法で色々悩みつつ実装した話です。
何故そんな事を的な背景や、何をしたくてこうしたかは下のツイートをご覧ください。
ARマーカーで認識したオブジェクトの表示/非表示でボイチェン/ボイロを切り替えます。 pic.twitter.com/Iu0VUtcj4E
— 琴葉 葵(偽) (@Hv2RMjHzDyqXVIr) 2017年10月3日
#環境
Windows 10
Unity 2017.1.1f1
#手法選定
他アプリの操作には色々な方法があります。
私が知る範囲だと以下のとおりです。
・System.Windows.Forms.SendKeys
・SendMessage
・System.Windows.Automation
##System.Windows.Forms.SendKeys
メリット:とっつきやすい
デメリット:操作対象の値を取得できない、本来の操作の邪魔
##SendMessage
メリット:本来の操作の邪魔にならない
デメリット:要dll参照、wpfアプリを操作しずらい(できない?)
##System.Windows.Automation
メリット:各種コントロール用の操作機能がある
デメリット:Unityからは普通に使えない、一部操作できないコントロールがある?、本来の操作の邪魔になる?
##結局どれ?
今回は本来の操作の邪魔になるのは避けたいのでSendMessageを選択します。
#サンプルコード
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class AppCtrlWrap
{
protected const uint BM_CLICK = 0x00F5;
protected const uint WM_SETTEXT = 0x000C;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
protected static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
protected static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, uint wParam, string lParam);
protected class Info
{
public string name;
public IntPtr handle;
}
protected List<Info> list;
protected List<Info> GetAllHandle(string title)
{
IntPtr app = FindWindowEx(IntPtr.Zero, IntPtr.Zero, null, title);
var handleList = new List<Info>();
GetAllHandle(app, ref handleList);
return handleList;
}
void GetAllHandle(IntPtr parent, ref List<Info> handleList)
{
foreach (Info info in GetChildHandle(parent))
{
handleList.Add(new Info() { name = info.name, handle = info.handle });
GetAllHandle(info.handle, ref handleList);
}
}
List<Info> GetChildHandle(IntPtr parent)
{
var handleList = new List<Info>();
var child = IntPtr.Zero;
while (true)
{
IntPtr handle = FindWindowEx(parent, child, null, null);
if (handle == IntPtr.Zero) break;
int length = GetWindowTextLength(handle);
StringBuilder sb = new StringBuilder(length + 1);
GetWindowText(handle, sb, sb.Capacity);
handleList.Add(new Info() { name = sb.ToString(), handle = handle });
child = handle;
}
return handleList;
}
protected IntPtr GetHandle(string name)
{
foreach (Info i in list)
{
if (i.name == name)
{
return i.handle;
}
}
return IntPtr.Zero;
}
}
public class VoiceroidCtrl : AppCtrlWrap
{
const int edit = 9;
public VoiceroidCtrl()
{
list = GetAllHandle("VOICEROID+ 琴葉葵");
}
public void Send(string text)
{
SendMessage(list[edit].handle, WM_SETTEXT, 0, text);
}
public void Play()
{
SendMessage(GetHandle(" 再生"), BM_CLICK, 0, 0);
}
}
#説明
GetAllHandleでアプリを探し、再帰でGetChildHandleを呼び出し、アプリ以下のウィンドウハンドラを取得します。一旦取得した後はListの順番かGetHandleで名前を指定して探したウィンドウハンドラにSendMessageで各種操作のメッセージを送ります。全て名前で済めばいいですが、名前が付いていないコントロールもあるのでこうなっています。であれば全部順番にしたい所ですが、コントロールによっては順番が変わったりするので困りものです。
#備考
WPFアプリの場合、アプリ自体のウィンドウハンドラはありますが、コントロールのウィンドウハンドラがありません。よって、SendMessageが使えません。解決策があるかもしれませんが、私は諦めました。
実験的にWPFアプリ向けにUIAutomationを使ってみました。Unityからは使えない?ので、かなり強引ですが、コンソールアプリを作って画面を隠すように実行すれば対象の操作自体はできました。ただし、この方法はこの方法で一部のコントロールが操作できないため、やむなくSendMessageを使うなど歪な事になったりします。
##UIAutomationはUnityで使えない?
ちゃんと設定できれば使えるのかもしれませんが、私はダメでした。
UiAutomationCore.dllがNativeなのでどうしたら良いか分からない。というのが理由です。
UiAutomationCore.dllを直接参照して必要な機能を使えば良いのかもしれませんが、内部の関数を調べると直接使うのは非推奨らしく、茨の道感があります。
https://msdn.microsoft.com/en-us/library/windows/desktop/ee684032(v=vs.85).aspx
#参考
http://tech.sanwasystem.com/entry/2015/11/25/171004
http://dobon.net/vb/dotnet/process/getprocessesbywindowtitle.html
https://msdn.microsoft.com/ja-jp/library/ms747327(v=vs.110).aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/ee671216(v=vs.85).aspx
https://msdn.microsoft.com/ja-jp/library/windows/desktop/dd318521(v=vs.85).aspx