Winium.Cruciatusを使いデスクトップアプリをコマンドとして利用する

# はじめに

デスクトップアプリの機能をコマンドとして利用したい時に、そのアプリがコマンド用の起動引数を提供しているのならいいのですが、そんなものは無いって時に苦肉の策として[2gis/Winium.Cruciatus](https://github.com/2gis/Winium.Cruciatus)を使ってコマンドとして利用出来るようにしてみます。

今回はWindowsの標準アクセサリである「電卓」を、コマンドとして利用してみます。

[作成物はこちら(GitHub)](https://github.com/wordijp/calcmd)

# デモ

コマンドの実行

![calcmd.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/3fa9a0a9-d62f-1d04-c61c-9f933fe2d261.gif)

Pythonからコマンドとして実行

![calcmd_py.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/50f5437a-dfb5-3d98-aa05-3b6f8b5f9e04.gif)

# 2gis/Winium.Cruciatusとは？

.Netに付属しているGUIコンポーネントを操作できるUI Automationライブラリのラッパーライブラリになっています。
UI AutomationはGUIをブログラムから利用する支援の他に、GUIの自動テストに利用される事を目指しており、Winium.Cruciatusを使うとコードの記述量を減らす事が出来ます。

# ソースコード

C#で作成しています。
ソースコードは短いので全文載せておきます。

```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

using System.Windows.Automation;

using Winium.Cruciatus.Core;
using Winium.Cruciatus.Extensions;

namespace winium_test {
	public class DisposeableApplication : IDisposable {
		public DisposeableApplication(string executableFilePath) {
			this.App = new Winium.Cruciatus.Application(executableFilePath);
			this.App.Start();
		}

		public void Dispose() {
			this.App.Close();
		}

		public Winium.Cruciatus.Application App { get; private set; }
	}

	class Program {
		private const string calcPath = @"C:\\Windows\\System32\\calc.exe";

		static void Main(string[] args) {
			initializeLoggingRule();
			checkAlreadyRunning();

			using (var calc = new DisposeableApplication(calcPath)) {
				var win = Winium.Cruciatus.CruciatusFactory.Root;

				// 入力に対応したボタンをクリック
				foreach (var ch in String.Join("", args)) {
					win.FindElementByUid(keyIds[ch].ToString()).Click();
				}

				// 計算結果を表示する
				win.FindElementByUid("121").Click();                      // =
				var result = win.FindElementByUid("150").Properties.Name; //結果のテキスト
				Console.Write(result);
			}
		}

		// 電卓のキーとidの対応表
		private static readonly IDictionary<char, int> keyIds = new Dictionary<char, int>() .{
			{'.', 84}, {'/', 91}, {'*', 92}, {'+', 93}, {'-', 94}, {'=', 121},
			{'0', 130}, {'1', 131}, {'2', 132}, {'3', 133}, {'4', 134},
			{'5', 135}, {'6', 136}, {'7', 137}, {'8', 138}, {'9', 139},
		};

		// ログをコマンド用に設定する
		// Infoは出力しない
		// Warn以上を標準エラー出力に出す
		static void initializeLoggingRule() {
			var fact = Winium.Cruciatus.CruciatusFactory.Logger.Factory;

			var target = new NLog.Targets.ConsoleTarget();
			// 出力先を標準エラー出力へ
			target.Error = true;
			// Warning以上だけ表示
			var rule = new NLog.Config.LoggingRule("*", NLog.LogLevel.Warn, target);

			fact.Configuration.LoggingRules.Clear();
			fact.Configuration.LoggingRules.Add(rule);
			fact.ReconfigExistingLoggers();
		}

		// 対象のアプリが起動してないか？
		static void checkAlreadyRunning() {
			var procname = System.IO.Path.GetFileNameWithoutExtension(calcPath);
			if (System.Diagnostics.Process.GetProcessesByName(procname).Any()) {
				Console.Error.WriteLine("error!, already running calc process");
				Environment.Exit(1);
			}
		}
	}
}
```

起動引数から与えられた計算式に対応したボタンを押して、最後に結果を標準出力へと表示しています。

各ボタンコンポーネントのAutomation IdはInspectを使って調査しています。
開発中はInspectでアプリのコンポーネント情報とのにらみ合いがメインです、普段の開発とは勝手が違ってきます。


## コマンド化にあたり

2gis/Winium.Cruciatusは、初期設定ではコンポーネント操作ログを標準出力へ表示します、このままだとコマンド化しても利用に困るので、操作時の異常系ログのみを標準エラー出力へ表示するように変更しています。
その処理は`initializeLoggingRule`関数で行っています。

また、電卓を多重起動していると、ボタンの操作がおかしくなったため、既に起動済みの時に実行出来ないようにしています。


# 他のライブラリ

## [Microsoft/WinAppDriver(Beta)](https://github.com/Microsoft/WinAppDriver)

Microsoft純正です、しかしまだBeta版なので動作が不安定でした、僕の環境でもコンポーネントの値が取得出来なかったり(バグ？)したので見送り。


## [2gis/Winium.Desktop](https://github.com/2gis/Winium.Desktop)

同じ開発元です、Selenium WebDriverで動作するSelenium派生になっています。
こちらでも問題なかったのですが[^ok]、[Winium.Desktop.Driver](https://github.com/2gis/Winium.Desktop/releases)を別途起動しておく必要があったため、ちょっと面倒かな、って事で保留。

[^ok]: むしろ対応プラットフォームが充実


# 終わりに

コマンド化したら便利ではあるんですが、あくまでもGUI上の操作結果を取得しているに過ぎないため、GUIアプリケーション側がバージョンアップした際にコマンドが上手く動かない場合があります。
また、キーボードやマウス操作をコマンド実行に使用するため、コマンド実行中は何もしないようにしましょう、結果がバグります。
あとは動作が遅いです、これはCruciatusが内部で250msのSleep処理を入れているのも一因なんですが、それを改善したとしても遅いと思います。

なので、コマンド版や、コマンド用の起動引数がある場合はそちらを利用しましょう、今回のような方法はあくまでも苦肉の策です。