Help us understand the problem. What is going on with this article?

Unityから他アプリを操作する方法

More than 1 year has passed since last update.

概要

Unityから他のアプリを操作する方法で色々悩みつつ実装した話です。
何故そんな事を的な背景や、何をしたくてこうしたかは下のツイートをご覧ください。


環境

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした