ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Friendly ハンズオン6 GUI操作・・・あれ?

これは「Friendly Advent Calendar 2014 - Qiita」の記事です。

はい、今日からはいよいよ、GUIを操作していきましょう。
なのですが・・・

明日の分も絶対読んでね!

途中で「そっとじ」されると困るので、先に書いておきますが、多分ひきますw。
まあ今日は、途中で読むのやめてもいいんで明日のブログは読んでください。
明日になれば、「良かった、簡単に操作できるじゃん!」ってなってますから。

ハンズオン開始

そうすると、操作対象のGUIを作らないといけないのですが、ちょっと本質的ではないので、私の方で作っておきました。ここからダウンロードしてください。
Ishikawa-Tatsuya/HandsOn6 · GitHub

テストプロジェクトも作っています。こちらを変更しながら学んでいきましょう。
まずはビルドして、GUIを起動してください。
f:id:ishikawa-tatsuya:20141216224058p:plain

ボタンを押すと入力がテキスト形式でメッセージボックスに表示されるというものです。
では、TextBoxに入力します。
OperationTest()の中に実装してくださいね。

_form._textBox.Text = "Codeer";

はい。簡単ですね。いつも使っているTextプロパティーを使うことができました。簡単にテキストボックスの文字を変えることができました。次はDateTimePickerに日付を入力してみましょう。

_form._dateTimePicker.Value = new DateTime(2011, 3, 14);

素晴らしい!あの操作しづらいコントロールも簡単に操作できます。

では、いよいよDataGridViewの文字列を変更しますよー。これは、子ウィンドウのテキストボックスが編集用にポップアップしますね。こういう時は編集作業の一連の処理をアトミックに実行する必要があります。タイミング依存の失敗をなくすためです。その方法は、やりましたね。そうDLLインジェクションです。
ここまでのコードです。

[TestMethod]
public void OperationTest()
{
    _form._textBox.Text = "Codeer";
    _form._dateTimePicker.Value = new DateTime(2011, 3, 14);

    //DLLインジェクション
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);

    //対象プロセスに送り込んだ処理を実行させる
    dynamic thisType = _app.Type(GetType());
    thisType.EditGridName(_form._dataGridView, 0, "ishikawa");
}

//対象プロセス内で実行するコード
static void EditGridName(DataGridView grid, int row, string text)
{
    grid.Focus();
    grid.CurrentCell = grid[0, row];
    grid.BeginEdit(false);
    grid.EditingControl.Text = text;
    grid.EndEdit();
}

では、ブレイクを設定して実行してみます。
f:id:ishikawa-tatsuya:20141216235732p:plain
思った通りに操作できましたね。

あれ?

ん?なんですか?まさか難しかったとか・・・

インテリセンス効かないじゃん

何をいまさら。別プロセスの操作なんだから静的な型つかえないですよ。だからdynamic使って動的に解決しているんです。当然インテリセンスなんて効かないですよー。

ていうか、DataGridViewの操作面倒じゃん

いやいや、普通に自分のプログラムから操作する時も、こう書きますよね?

次も行きますよー。(暴走
ボタンを押した後のメッセージボックス対応です。.Netでもメッセージボックスは実はネイティブでできているんですね。常識ですよねー。ネイティブはUser32.dll、kernel32.dllの公開関数使ったら楽勝で操作できますよー。(待て

[TestMethod]
public void OperationTest()
{
    _form._textBox.Text = "Codeer";
    _form._dateTimePicker.Value = new DateTime(2011, 3, 14);

    //DLLインジェクション
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);

    //対象プロセスに送り込んだ処理を実行させる
    dynamic thisType = _app.Type(GetType());
    thisType.EditGridName(_form._dataGridView, 0, "ishikawa");

    //非同期でボタンを押す
    var async = new Async();
    _form._button.PerformClick(async);

    //メッセージボックスが表示されるのを待つ
    IntPtr msgBox;
    while (true)
    {
        var handles = (IntPtr[])thisType.GetTopLevelWindows();
        if (handles.Length == 1 && handles[0] != (IntPtr)_form.Handle)
        {
            msgBox = handles[0];
            break;
        }
    }

    //メッセージ取得
    var text = thisType.GetMessageText(msgBox);

    //ボタンを押す
    thisType.ClickButton(msgBox);

    //非同期終了待ち
    async.WaitForCompletion();

    //メッセージボックスのテキストをチェック
    Assert.AreEqual("Codeer\r\n2011/03/14\r\nishikawa", (string)text);
}

//対象プロセス内で実行するコード
static void EditGridName(DataGridView grid, int row, string text)
{
    grid.Focus();
    grid.CurrentCell = grid[0, row];
    grid.BeginEdit(false);
    grid.EditingControl.Text = text;
    grid.EndEdit();
}

//以下ネイティブのメッセージボックスを操作するためのコード
[DllImport("user32.dll")]
internal static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

internal delegate int EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam);

[DllImport("user32.dll")]
internal static extern int EnumWindows(EnumWindowsDelegate lpEnumFunc, IntPtr lParam);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindow(IntPtr hWnd);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindowEnabled(IntPtr hWnd);

[DllImport("kernel32.dll")]
internal static extern int GetCurrentThreadId();

[DllImport("user32.dll")]
internal static extern int EnumChildWindows(IntPtr hWndParent, EnumWindowsDelegate lpEnumFunc, IntPtr lParam);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll")]
internal static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
internal static extern IntPtr SetFocus(IntPtr hWnd);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

const int BM_CLICK = 0x00F5;

public static IntPtr[] GetTopLevelWindows()
{
    int processId = Process.GetCurrentProcess().Id;
    int currentThreadId = GetCurrentThreadId();
    IntPtr serverWnd = IntPtr.Zero;
    List<IntPtr> handles = new List<IntPtr>();
    EnumWindowsDelegate callback = delegate(IntPtr hWnd, IntPtr lParam)
    {
        if (!IsWindow(hWnd))
        {
            return 1;
        }
        if (!IsWindowVisible(hWnd))
        {
            return 1;
        }
        if (!IsWindowEnabled(hWnd))
        {
            return 1;
        }
        int windowProcessId = 0;
        int threadId = GetWindowThreadProcessId(hWnd, out windowProcessId);
        if (processId == windowProcessId && currentThreadId == threadId)
        {
            handles.Add(hWnd);
        }
        return 1;
    };
    EnumWindows(callback, IntPtr.Zero);
    GC.KeepAlive(callback);
    return handles.ToArray();
}

static void ClickButton(IntPtr parent)
{
    var button = GetByClassName(parent, "Button");
    SetFocus(button);
    SendMessage(button, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
}

static string GetMessageText(IntPtr parent)
{
    var msg = GetByClassName(parent, "Static");
    StringBuilder buf = new StringBuilder(1024);
    GetWindowText(msg, buf, buf.Capacity);
    return buf.ToString();
}

static IntPtr GetByClassName(IntPtr parent, string className)
{
    IntPtr target = IntPtr.Zero;
    EnumWindowsDelegate callback = delegate(IntPtr hWnd, IntPtr lParam)
    {
        StringBuilder buf = new StringBuilder(1024);
        GetClassName(hWnd, buf, buf.Capacity);
        if (buf.ToString() == className)
        {
            target = hWnd;
            return 0;
        }
        return 1;
    };
    EnumChildWindows(parent, callback, IntPtr.Zero);
    GC.KeepAlive(callback);
    return target;
}

確かに何でもできるけど・・・

はい。Friendlyは対象プロセスでプログラムで書けることなら、ほぼすべて操作側からプロセスの壁を越えて実行することができます。でもそれって、「.Net全部使っていいよ。何でも作れるでしょ?」って言ってるようなものなんですね。通常のGUI操作をする分には、もっときることが少ない方が使いやすいのです。

Friendlyの基本部分は別プロセスのプログラム操作

そうなんです。ここがその他の操作ライブラリと根本的に違うところなんですが、基本部分はGUI操作ライブラリではないんですね。別プロセスのプログラムを操作するライブラリなのです。

頻繁に使う処理はラップしておけばよい

そうです。普通のプログラムと同じ考え方ですね。しかもこの辺は全世界共通で使いますね。だから、FriendlyコミュニティーでGUI操作に関してはラップしたライブラリを提供しております。
Win32、WinForms、WPFとそろっております。これを使ってください。
概略は明日紹介させてもらいます。

でもFriendlyの基本部分は最初に抑えておかないと

じゃあ、最初からGUI操作ライブラリの使い方教えてくれれば良いのにって思いうかもしれませんが、ちょっと違うのです。やっぱり基本を押さえておかないと、上位ライブラリを使うときにも違和感を感じてしまいます。上位ライブラリにもFriendlyの思想が入っているのです。それが強みであるのであえて隠していません。それと、上位ライブラリと同時に一気に覚えると、絶対に混乱します。でも、順番に覚えると非常にシンプルな思想です。基本→上位ライブラリの順番でお願いします。
それから、フィールドとかデータとか取ってくるときには絶対に基本部分つかいますしね。

明日に続く・・・
(絶対に読んでねー)