UI Automation を使ってアプリの自動化を体験!

PowerBuilderと自動化

こんにちは、サポート部の明石です。

8 月も本日が最終日。学生時代、夏休み期間中はさまざまなことを体験でき「ずっと夏休みだったら良いのに」と思いながらも徐々に現実に戻され、最終日には宿題の追い込みに必死で「自動的に宿題が終わっていたら」なんて夢みたいなことを考えながら机にかじりついていました (特に読書感想文が苦手でした)。

さて PowerBuilder 2019 R3 日本語版がリリースされ早いものでちょうど 2 ヶ月となりますが、皆さん、PowerBuilder 2019 R3 利用されていますか?

リリース前から、当ブログでも先出しでアプリケーションの UI 変更.NET Assembly の取り込み、そして PowerClient によるアプリ配布の簡略化といった機能の紹介、また前回はエイタ先輩のブログで組み込みの圧縮/展開機能と紹介してきているので、PowerBuilder 2019 R3 でもさまざまな新機能が搭載されているのは、すでにご存じのところかもしれません。

そして今回も畳みかけるかのように、PowerBuilder 2019 R3 で新たにサポートされた Microsoft UI Automation について紹介したいと思います。


Microsoft UI Automation への対応

PowerBuilder 2019 R3 は、製品として Microsoft UI Automation に正式サポートするバージョンとなります。そもそもお恥ずかしい話ですが … Microsoft UI Automation についても、以前から PowerBuilder で対応していたとされる Microsoft Active Accessibility (MSAA) についても、その存在を最近知りました。

Microsoft 公式によると Microsoft UI Automation とは

Microsoft UI Automation は、Microsoft Windows の新しいアクセシビリティフレームワークであり、Windows Presentation Foundation (WPF) をサポートするすべてのオペレーティングシステムで利用できます。

UI Automation は、デスクトップ上のほとんどのユーザーインターフェイス (UI) 要素へプログラムによるアクセスを提供し、スクリーンリーダーなどの支援技術製品が UI に関する情報をエンドユーザーに提供したり、標準入力以外の方法で UI を操作できるようにします。UI Automation は、自動化されたテストスクリプトが UI と対話することもできます。

といった内容の記載になっていました。纏めると、この機能を使うことで「キーボードやマウスによる入力以外で UI 操作を自動化できる」的なことのようです (この一文は不要ですかね 笑) 。

で、Microsoft UI Automation や Microsoft Active Accessibility といった機能を PowerBuilder アプリケーションで利用する場合には、コントロールの [プロパティ] – [その他] タブの “アクセシブル名” (AccessibleName) プロパティと “アクセシブルの詳細” (AccessibleDescription) プロパティを設定します。この 2 つのプロパティについては、Microsoft Active Accessibility サポートのために、過去バージョンからすでに用意されているプロパティとなります。

※ Microsoft Active Accessibility の場合、 “アクセシブル ロール” プロパティも利用します。

開発を行っていた時代、「これらのプロパティをどうやって活用できるのか?」と疑問に思っていましたが、今回のように UI 操作を自動化するときに力を発揮するプロパティだったんですね。長年の謎がようやく紐解けました。

さて、この自動化の機能を利用することによるメリットとして

  • PowerBuilder 開発者としては …
    • テストの一部を自動化することができる
  • PowerBuilder アプリユーザーとしては …
    • アプリの操作など一部を自動化することができる

この機能、うまく活用できればいろいろと運用面などで便利になるかもしれませんね。


PowerBuilder アプリの準備

はい、物は試し。とりあえず試してみる、明石スタイルではじめていきたいと思います。

今回用意する PowerBuilder アプリと自動化の流れとしては、

「データウィンドウ内のボタンクリックで行挿入」👉「シングルラインエディットにテキスト入力」👉「ボタンクリックでデータウィンドウに値をセット」👉「セットされた値を更新」👉「更新されたデータを一覧に表示」

という流れが自動的に実装されるかを検証したいと思います。それではサンプルを作っていきましょう。

まずはデータウィンドウ。データを更新して表示することを想定しているので、データベースに接続しデータソースを “SQL Select” として作成しています。各コントロールの内容は以下の通り。

データウィンドウ名 コントロール 名前 備考
d_uiautomation カラム col_1 テキストコントロールのテキスト “入力 1”
カラム col_2 テキストコントロールのテキスト “入力 2”
コマンドボタン b_row テキスト “行追加” 、アクセシブル名 “add_row”
d_list カラム col_1 テキストコントロールのテキスト “入力 1”
カラム col_2 テキストコントロールのテキスト “入力 2”

ちなみに “d_uiautomation” はフリーフォーム、 “d_list” はグリッドで作成しました。また “d_uiautomation” はデータベースへ更新も行うので更新特性の指定を設定しています。

次にウィンドウ。ウィンドウ名は “w_uiautomation” としています。

コントロール 名前 備考
スタティックテキスト st_1 テキスト “入力欄 :”
シングルラインエディット sle_1 アクセシブル名 “text”
データウィンドウ dw_1 データオブジェクト “d_uiautomation”
データウィンドウ dw_2 データオブジェクト “d_list” 、垂直スクロールバー “☑”
コマンドボタン cb_ent テキスト “入力” 、アクセシブル名 “cb_entry”
コマンドボタン cb_upd テキスト “更新” 、アクセシブル名 “cb_update”
コマンドボタン cb_ret テキスト “取得” 、アクセシブル名 “cb_ret”

レイアウトは、自由に配置してください🎵

続きまして各スクリプトを。以下は、すべて “w_uiautomation” ウィンドウに対するスクリプトになります。

[データウィンドウ (dw_1) の constructor] イベント

settransobject( sqlca )

[データウィンドウ (dw_1) の buttonclicked] イベント

Insertrow(0)

[コマンドボタン (cb_ent) の clicked] イベント

string ls_max_no

// 連番MAX値取得
SELECT max(col_1)
  INTO :ls_max_no
  FROM sample ;
  
if isnull(ls_max_no) = true or ls_max_no = "" then ls_max_no =  "0" 
// ボタンクリック時にデータをセット
dw_1.setitem(dw_1.rowcount(), 1, string(long(ls_max_no) + 1))
dw_1.setitem(dw_1.rowcount(), 2, sle_1.text)

[コマンドボタン (cb_upd) の clicked] イベント

// 入力確定
dw_1.accepttext()

// 更新処理
dw_1.update()

commit ;

[コマンドボタン (cb_ret) の clicked] イベント

// データ取得
dw_2.settransobject( sqlca )
dw_2.retrieve( )

PowerBuilder アプリの準備は以上です。もちろん、アプリケーションオブジェクトの Open イベント内にデータベース接続ウィンドウ Open 処理もお忘れなく。

加えて今回は、作成したアプリケーションを起動して自動化の確認をするので、事前に exe を作成しておきます。


自動実行のサンプル作成

次に、自動化するサンプルを作成していきましょう。サンプルは、Visual Studio で C# を利用して作成します (今回は、.NET Framework を利用するため SnapDevelop は利用できませんでした、残念!)。

Visual Studio 起動後、[新しいプロジェクトの作成] – [コンソール アプリ (.NET Framework)] を選択し [作成] ボタンをクリックしてプロジェクトを作成します。プロジェクト名や、場所、ソリューション名は任意の値を設定します。

新規プロジェクト
プロジェクトの作成

プロジェクト作成後、プロジェクトを選択した状態で右クリックし [追加] – [参照]、もしくは “参照” を選択した状態で右クリックから [参照の追加] を選択して、「参照マネージャー」を開きアセンブリに以下の参照を追加します。

  • UIAutomationClient
  • UIAutomationTypes
UIAutomationClient
UIAutomationTypes

最後にプログラム作成です。ウェブで UI Automation などで検索すると、偉大なる C# パイセン様たちが作成しているさまざまなプログラムが見つかるかと思います。私のプログラムは、以下のようになりました😳

※ 処理間隔を表現できるように、処理の合間に「Thread.Sleep(200);」を記述しています。

[Program.cs] ファイル内

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;

namespace ConsoleApp1
{
    class Program
    {
        private static AutomationElement mainForm;

        static void Main(string[] args)
        {
            // テストアプリを開始します
            Process process = Process.Start("uiautomation.exe");
            Thread.Sleep(1000);

            // 起動したWindowのハンドルを取得
            mainForm = AutomationElement.FromHandle(process.MainWindowHandle);

            // 操作対象のコントロールのパターンを取得
            // 行追加ボタンの要素
            var dwbtnClick = FindElementsByName(mainForm, "add_row").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 入力欄の要素
            var slTxtInput = FindElementsByName(mainForm, "text").First()
                .GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            // 入力ボタンの要素
            var btnEntry = FindElementsByName(mainForm, "cb_entry").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 更新ボタンの要素
            var btnUpdate = FindElementsByName(mainForm, "cb_update").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 取得ボタンの要素
            var btnRet = FindElementsByName(mainForm, "cb_ret").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;

            // 行追加ボタンをクリックでデータウィンドウに行を追加
            dwbtnClick.Invoke();
            Thread.Sleep(200);

            // シングルラインエディットに文字列をセット
            slTxtInput.SetValue("PB 2019 R3");
            Thread.Sleep(200);

            // 入力ボタンをクリックして、データウィンドウにデータをセット
            btnEntry.Invoke();
            Thread.Sleep(200);

            // 更新ボタンをクリックして、データを更新
            btnUpdate.Invoke();
            Thread.Sleep(200);

            // 更新確認
            btnRet.Invoke();
        }

        // 指定したName属性に一致するAutomationElementを返す
        private static IEnumerable<AutomationElement> FindElementsByName(AutomationElement rootElement, string name)
        {
            return rootElement.FindAll(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.NameProperty, name))
                .Cast<AutomationElement>();
        }
    }
}

以上で自動化するソースも完了です (ソースコードレビューはご遠慮ください🙇🙇‍♀)。

こちらも事前にビルドしておきます。


自動化アプリの実装

それでは、実際に動作させてみましょう。事前に作成した PowerBuilder アプリを先ほどビルドした Visual Studio のコンソールアプリと同じフォルダに格納します。

そしてコンソールアプリを実行すると …

SnapDevelop 実行時画面

がーん、エラーになってしまいました。シーケンスに要素が含まれていないとな。

Visual Studio に戻ってデバックしてみると …どうやら [cb_entry] というボタンの部分でエラーが発生しているようです。 “アクセシブル名” から要素が取得できていないようですね。

SnapDevelop 実行時画面

それでは、 “アクセシブル名”で参照するのではなく [入力]、[更新]、[取得] といった “テキスト” の値で参照するように変更にして再チャレンジです。

[Program.cs] ファイル内 (最終形)

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;

namespace ConsoleApp1
{
    class Program
    {
        private static AutomationElement mainForm;

        static void Main(string[] args)
        {
            // テストアプリを開始します
            Process process = Process.Start("uiautomation.exe");
            Thread.Sleep(1000);

            // 起動したWindowのハンドルを取得
            mainForm = AutomationElement.FromHandle(process.MainWindowHandle);

            // 操作対象のコントロールのパターンを取得
            // 行追加ボタンの要素
            var dwbtnClick = FindElementsByName(mainForm, "add_row").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 入力欄の要素
            var slTxtInput = FindElementsByName(mainForm, "text").First()
                .GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            // 入力ボタンの要素
            var btnEntry = FindElementsByName(mainForm, "入力").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 更新ボタンの要素
            var btnUpdate = FindElementsByName(mainForm, "更新").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            // 取得ボタンの要素
            var btnRet = FindElementsByName(mainForm, "取得").First()
                .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;

            // 行追加ボタンをクリックでデータウィンドウに行を追加
            dwbtnClick.Invoke();
            Thread.Sleep(200);

            // シングルラインエディットに文字列をセット
            slTxtInput.SetValue("PB 2019 R3");
            Thread.Sleep(200);

            // 入力ボタンをクリックして、データウィンドウにデータをセット
            btnEntry.Invoke();
            Thread.Sleep(200);

            // 更新ボタンをクリックして、データを更新
            btnUpdate.Invoke();
            Thread.Sleep(200);

            // 更新確認
            btnRet.Invoke();
        }

        // 指定したName属性に一致するAutomationElementを返す
        private static IEnumerable<AutomationElement> FindElementsByName(AutomationElement rootElement, string name)
        {
            return rootElement.FindAll(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.NameProperty, name))
                .Cast<AutomationElement>();
        }
    }
}

改めて実行してみます、結果は … 以下やで!

SnapDevelop 実行時画面

PowerBuilder アプリが自動的に動作しているのが確認できたかと思います。

ただ、なぜボタンの “アクセシブル名” から要素が取得できなかったのかは原因不明のため、何かわかりましたらこのブログに追記させていただきます。


最後に

今回は、自分で作成した自動化の処理にて確認をしてみましたが、Microsoft UI Automation をサポートする (QTP といった) 任意のツールを使用して UI テストを自動化することも可能なようです。また Zeenyx 社の AscentialTest を利用したテストの事例などもあるようですよ。

これらのツールや RPA ツールと連携することで、夏休みの宿題は難しいかもしれませんが PowerBuilder で作成されたアプリに関して日々の業務を一部でも自動化することができれば、今後のアプリ利用フローも大きく変化していくかもしれませんね。

「これも時代か」と思う今日このごろ。

以上、明石でした。

テクニカルブログ 一覧を見る
PowerBuilder マイグレーション
PowerBuilder 2019 R3 日本語版リリース 紹介動画