カスタムウィンドウクラス中の UI 要素をセレクト可能にする一手法~Microsoft Active Accessibility の実装例

microsoft-active-accessibility

プロローグ

むかし、むかし、お爺さんは当時入手可能な部品では思い描いたGUIが実現できないと分かると、果敢にも自分で独自のウィンドウクラスを実装し、その中でいくつものUI要素をホストしました。アプリケーションに特化してカスタマイズされたそのウィンドウクラスは少ないメモリでもサクサク動いたため大変重宝されたとさ、めでたし、めでたし。

時を戻そう

2020年。UiPath Robotでそのレガシーなアプリケーションを自動化しようとすると、カスタムウィンドウクラスがホストする複数のUI要素について、セレクターが取得できないと分かります。コンピュータービジョンや画像認識でUI要素をセレクトしたって構わないじゃないか、と状況を否定しない考えもあるかもしれませんが、安定的な自動化を目指すのであれば、やはりネイティブなセレクターを取得したいところです。

どうすればよいでしょうか?

今回の試み

今回はMicrosoft Active Accessibilityを使って自力でネイティブなセレクターを取得可能にしようと試みます。実際の現場でそのようなソリューションを採用することは極めて稀かもしれません。それでも、具体的な実装を例示することで、セレクターに関する理解がより一層進めば幸いと思います。少しの間、お付き合いください。なお、本記事を読み進めるにあたっては、Win32(アンマネージド)のウィンドウプログラムに関する知識とComponent Object Model(COM)に関する知識を前提としています。予めご了承ください。

Microsoft Active Accessibility とは

さて、Microsoft Active Accessibility(MSAA)は、Windows 95の時代に登場したとても由緒あるフレームワークです。もともとは身体にハンディキャップを持つユーザーや高齢のユーザーのコンピューター利用を支援するためのAPIとして登場したようです。いまでもその用途はあるかと思いますが、UiPath の視点からするとUIの自動化のためのAPIという位置付けと捉えたほうが良いでしょう。詳細については、MicrosoftのMSAAに関するウェブサイトを参照ください。

https://docs.microsoft.com/en-us/windows/win32/winauto/microsoft-active-accessibility

Microsoft はUI自動化についてMSAA の後継にあたる新しいフレームワークであるMicrosoft UI Automation(UIA)を出しました。詳細については、Microsoftの UIAに関するウェブサイトを参照ください。

https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32

UIA は MSAA を改善したもので、MSAA よりシンプルな実装になるようですが、MSAA ベースのアプリケーションがまだまだ数多く存在するため、すぐに MSAA が無くなってしまうことはないように思えます。

一方でUiPath Robotでは、UIAをサポートしますが、MSAAも活用してUI要素の認識を行っています。MSAAは古いと言っても、Microsoft の標準コントロール(UI部品)は既定でMSAAに対応しているため、日常的に目にするUiPathのセレクターはMSAA由来のものも少なくないでしょう。セレクター中のタグで言うと、ctrlなどがMSAA由来です。ですから、MSAAに対応すると、アプリケーションのUI要素はUiPath Robotで認識可能となります。

MSAA 実装の要点

では、MSAA実装について要点を解説します。

カスタムウィンドウクラスがホストするUI要素についてMSAAに対応させる場合、UI要素それぞれについて、IAccessibleインターフェースのCOMオブジェクトを実装する必要があります。また関連して、子要素の列挙のために呼び出されるIEnumVARIANTインターフェースも実装する必要があります。そこで、UiPath Robot で UI 要素として認識可能とする観点からの実装の必要性をまとめると次のようになります。

  • IAccessible インターフェース 一部実装の必要あり。その他、空実装可。

  • IDispatch インターフェース 空実装可。

  • IEnumVARIANTインターフェース 実装の必要あり。

  • IUnknown インターフェース 実装の必要あり。

COM の標準的なインターフェースの継承関係がIAccessible およびIEnumVARIANTのインターフェースにもあります。IAccessible インターフェースは、IDispatch インターフェースを継承しています。IDispatch インターフェースと IEnumVARIANT インターフェースは、IUnknown インターフェースを継承しています。

実装すべき IAccessible インターフェースのメソッド

以下にUiPath Robot で UI 要素として認識可能とする観点から実装すべきIAccessibleインターフェースのメソッドを列挙します。

  • get_accParent 親 UI 要素の IAccessible オブジェクトを返します。

  • get_accChildCount 子 UI 要素の個数を返します。

  • get_accChild ID 指定された子 UI 要素の IAccessible オブジェクトを返します。

  • get_accName UI 要素の名前を返します。この値は、UiPath のセレクターにおける ctrl 要素の name 属性値として取り扱われることになります。UiPath Robot で一意に UI 要素を特定させるため、UI 要素の名前を一意になるように設定しましょう。

  • get_accValue UI 要素の値を返します。この値は、UiPath のセレクターにおける ctrl 要素のtext属性値として取り扱われることになります。UIAutomation パッケージの「値を取得」アクティビティが実行されると、本メソッドが呼び出されます。したがって、UI 要素がラベルやボタンの場合、表示しているラベルのテキストを返すようにします。また、テキスト入力フィールドの場合、編集中のテキストを返すようにすれば良いでしょう。

  • get_accRole UI 要素のロールを返します。この値は、UiPath のセレクターにおける ctrl 要素の role 属性値として取り扱われることになります。UI 要素がラベルであれば

ROLESYSTEMSTATICTEXT を返し、プッシュボタンであれば ROLESYSTEMPUSHBUTTON を返せば良いでしょう。ロールの値はまだほかにもいろいろとあります。UI 要素の役割に合わせて適切なロールを選びましょう。

  • getaccState UI 要素の状態を返します。この値は、UiPath のセレクターにおける ctrl 要素の aastate 属性値として取り扱われることになります。入力フォーカスを持てる UI 要素であれば、STATESYSTEMFOCUSABLE を返し、実際に入力フォーカスを持った場合は、STATESYSTEM_FOCUSED もビット和演算すれば良いでしょう。

  • get_accFocus 入力フォーカスを持つ UI 要素を返します。

  • get_accSelection 選択状態にある UI 要素を返します。

  • accLocation ID 指定された UI 要素の矩形領域の位置情報(スクリーン座標)を返します。

  • accHitTest 指定されたスクリーン座標に位置する UI 要素の IAccessible オブジェクトを返します。

  • accDoDefaultAction ID 指定された UI 要素の既定のアクションを実行します。UIAutomation パッケージの「クリック」アクティビティで入力メソッドにシミュレートが選択されている場合に実行されると、本メソッドが呼び出されます。UI 要素がボタン等であれば、アクティベート(クリック)相当の動作を行うようにします。

  • put_accValue ID 指定された UI 要素の値を指定の値に書き換えます。UIAutomation パッケージの「文字を入力」アクティビティで入力メソッドにシミュレートが選択されている場合や「値を設定」アクティビティが実行されると、本メソッドが呼び出されます。UI 要素がテキスト入力フィールド等であれば、編集対象のテキストを指定の値に設定するようにします。

UI要素の親子関係については、ウィンドウの親子関係に相当すると考えて良いでしょう。実際にウィンドウを持たず、親ウィンドウに描画する形のUI要素を今回対象としているため、物理的なウィンドウの親子関係というよりは、論理的なウィンドウの親子関係と言うべきでしょうか。

IAccessibleインターフェースでは、VARIANT 構造体の整数型(VTI4)を使ったID番号によりUI要素を特定する形になります。ゼロは自分自身を表す特別な値(CHILDIDSELF)であるため、ゼロ以外の任意の値を兄弟UI要素(共通の親を持つ子UI要素)で一意に静的に割り当てれば良いと思います。

実装すべき IEnumVARIANT インターフェースのメソッド

IEnumVARIANT インターフェースに固有のメソッドについてはすべて実装します。

  • Next 子 UI 要素の ID 番号を VARIANT 構造体の配列に格納して返します。

  •  Skip 指定の個数の子 UI 要素をスキップします。

  •  Reset 次に列挙する子 UI 要素を最初に戻します。

  •  Clone この IEnumVARIANT インターフェースのコピーを作成します。

実装すべき IUnknown インターフェースのメソッド

IUnknown インターフェースのメソッドについてはすべて実装します。

  • QueryInterface IAccessible、IDispatch、IEnumVARIANT の各インターフェースポインターの問い合わせに対応します。

  •  AddRef 参照カウンターに1加算します。

  •  Release 参照カウンターから1減算します。ゼロの時点でオブジェクトを解放します。

実際にサンプルプログラムの挙動を観察してみると、IAccessible オブジェクトが完全に解放されるタイミング(参照カウンターがゼロのタイミング)は、関連するウィンドウが DestroyWindow された後、遅れてやってきます。あるいは、やってこない場合もあります。IAccessible オブジェクトに関連するオブジェクトの解放タイミングを考える上ではその点に留意する必要があるかもしれません。

実装を省略できるメソッド

UiPath Robot で UI 要素として認識可能とする観点からは、次に挙げるIDispatchインターフェースに固有のメソッド:

  • GetTypeInfoCount

  • GetTypeInfo

  • GetIDsOfNames

  • Invoke

と次に挙げるIAccessibleに固有のメソッド:

  • get_accDescription

  • get_accHelp

  • get_accHelpTopic

  • get_accKeyboardShortcut

  • get_accDefaultAction

  • accSelect

  • accNavigate

  • put_accName

については、E_NOTIMPLを返すだけで中身が空の実装で構いません。

例:

HRESULT STDMETHODCALLTYPE AccessibleObject::get_accHelp(VARIANT varChild, BSTR* pszHelp) { return E_NOTIMPL; }

外界との接点

実装したIAccessibleオブジェクトに関して、UI要素をホストするウィンドウとウィンドウ外部とのやりとりは、ウィンドウプロシージャでWMGETOBJETメッセージに応答する形で行うことになります。ウィンドウプロシージャに渡されたLPARAMがOBJID_CLINETの場合に、LresultFromObject関数に IAccessible インターフェースのインターフェース ID とウィンドウプロシージャに渡された WPARAM とIAccessible インターフェースを実装したオブジェクトへのポインターを IAccessible インターフェースのポインターにキャストして渡して呼び出し、その戻り値をウィンドウプロシージャの戻り値として返せば良いのです。

例:

case WM_GETOBJECT: if (lParam == OBJID_CLIENT) { return LresultFromObject(IID_IAccessible, wParam, (IAccessible*)(実装オブジェクトへのポインター)); }

IAccessible インターフェースを実装したオブジェクトへのポインターを IAccessible インターフェースにキャストする際は、正しいキャストを行ってください。上の例では、C言語スタイルのキャストをしていますので、コンパイラーが正しいアドレスを計算してくれると思います。実装したオブジェクトの継承するクラスやインターフェースがこのソースコード上明確な場合はstaticcast演算子を使ったり、このソースコード上では不明確な場合は実行時にチェックが行われるdynamiccast演算子を使ったりするのも良いでしょう。

そのほか

以上でUiPath RobotからUI要素を認識可能するIAccessibleインターフェースの実装の要点の説明は終わりです。実際に動くWin32 のサンプルプログラム をgithubにて公開しておりますので、コードに関する詳しいところはこちらを参考にしてください。

https://github.com/UiPathJapan/RespondingWmGetObject

サンプルプログラム

前出の github にて公開しているサンプルプログラムの簡単な説明をします。

このサンプルプログラムでは、トップレベルのウィンドウの子ウィンドウ(パネルと呼んでいます)として1個作成し、そのウィンドウで UI 要素に見たてた矩形のテキストラベルを 5 個ホストしています。

Microsoft Visual Studio でサンプルプログラムをビルドすると次のようなアプリケーションを起動できます。

microsoft-active-accessibility1

ウィンドウフレームの「AA有効」とはもちろんActive Accessibility有効中の意味です。(他の意味はないことにさせてください:-)ファイルメニューから「AA無効」を選択すると、MSAAが効かない状態(アプリケーションで WM_GETOBJECT メッセージに応答せず、DefWindowProc 関数を呼び出す状態)になります。

UI Explorerを起動して「要素を選択」してみましょう。

microsoft-active-accessibility2

クライアントエリアの5個の矩形は、それぞれ個々にウィンドウを持たず、親ウィンドウに直接描画しています。いま、真ん中のUI要素「BENI」にマウスをホバーしていますが、意図したとおりにUI Explorerに認識されているように見えます。

認識されたUI要素「BENI」から生成されるセレクターは次のようになります。

図:UI Explorerから抜粋

microsoft-active-accessibility3

上図にて、セレクターのwnd要素はWin32ウィンドウオブジェクトに由来します。

  • app 属性値はトップレベルの要素に付く特別な属性値であり、アプリの実行ファイル名です。Win32ウィンドウオブジェクトに由来するわけではありません。

  • cls属性値はWin32ウィンドウオブジェクトのウィンドウクラス名です。

  • title属性値はWM_GETTEXTメッセージが返す値です。

また、ctrl要素がMSAAに由来します。

  • name属性値はIAccessible::get_accNameメソッドが返す値です。

  • role属性値はIAccessible::get_accRoleメソッドが返す値です。

  • aastate属性値はIAccessible::get_accStateメソッドが返す値です。

  • text属性値はIAccessible::get_accValueメソッドが返す値です。

なお、今回のサンプルプログラムで「AA無効」とすると、矩形のラベルが認識されなくなり、ctrl 要素が取得できなくなります。そのようなところからも MSAA が UiPath Robot の UI 要素認識に効き目があることが確認できるはずです。

ちなみに、タグ名が uia となる要素は UIA に由来します。UiExplorer のメニューバーでUI フレームワークのメニューで切り替えると取得可能になります。ただし、今回のサンプルプログラムでは UIA に対応していないので、uia 要素はお目にかかれません。悪しからず。

エピローグ

以上、MSAAの実装の要点とサンプルプログラムの簡単な説明を行いました。どうですか? UiPath Robot で UI 要素を認識させるだけだったら、案外簡単だなと思われた方、いらっしゃるのではないでしょうか。機会があったら是非MSAAの実装にチャレンジしてみてください。

免責事項

本ブログの技術情報とサンプルプログラムは、UiPathデベロッパーへの参考情報として提供するものであり、無保証です。内容の正確性の追求やサンプルプログラムの動作確認等を事前に行っておりますが、Microsoft社製品に関する情報については、同社提供の情報を確認の上、各位の責任の下でご利用ください。

Avatar Placeholder Big
Hideaki Narita

Senior Product Management Specialist, UiPath