この記事は『CRESCO Advent Calendar 2022』 5日目の記事です。

 

こんにちは。クロステック事業部第3クロステックセンターのさつです。
エンジニアブログには2020年のアドベントカレンダーに投稿して以来長いことご無沙汰しておりましたが、今年のアドベントカレンダーには2年ぶりにエントリーすることができました。

前回2020年の記事ではPowerShellの紹介文的な記事を書いたのですが(私自身もうほとんどどんな内容だったか忘れてしまいましたが)、結局技術的にあまり面白い内容を書けなかったのが心残りでした。そこで今回は、技術的にも実際に手を動かして楽しめるものをと思い、私が普段使用しているあるソフトをテーマにしてみることにしました。それが本記事のタイトルにもある「スクリーンリーダー」です。

スクリーンリーダーの概要と本記事の目的

それでは、まずスクリーンリーダーとはどのようなものかというところから説明していきたいと思います。スクリーンリーダーとは、PCやスマートフォンなどの画面を見ることができないまたは非常に困難な人のために、画面に表示されている情報を合成音声で読み上げたり、点字で出力したり(点字ディスプレイと呼ばれる装置を使用)するソフトウェアのことです。
私は生まれつき目が全く見えないため、PCの画面も見ることができません。そこで、私がPCを使用するときには業務でもプライベートでもスクリーンリーダーは欠かすことのできない存在なのです。

さてそんなスクリーンリーダーですが、グラフィカルな画面の情報をユーザーに対して音声や点字などの言語情報(言葉)として伝えなければなりません。そのためには、画面に表示されている個々のGUI部品(以下、本記事ではコントロールと表記)の情報を取得するための技術が必要になります。本記事では、Windows環境に限定して「スクリーンリーダーがどのようにしてGUI画面から情報を取ってくるのか」ということについて、その一部を読者の皆さんにご紹介できればと思います。スクリーンリーダーを使用することのない健常者の皆さんにも、きっと興味を持っていただけるものと思います。

本記事の構成ですが、まずスクリーンリーダーの基本動作について説明した後、GUI画面からの情報取得に利用される技術の1つであるUI オートメーションの概要について説明します。最後に、UI オートメーションを使用して「スクリーンリーダーもどき」を作ってみます。といっても100行未満のコードで気軽に作れるものですので、肩の力を抜いて読み進めていただければ幸いです。なお、本記事ではC#とPowerShellを取り扱います。これらの基本文法や.NET Frameworkの基礎知識があることを想定して書いておりますのでご了承ください。

スクリーンリーダーの基本動作

スクリーンリーダーにもさまざまな製品があります。しかしどの製品もその目的は前述の通り、画面の情報を合成音声などで読み上げることです。それでは、スクリーンリーダーがどのような情報をどのように読み上げるのか、使用した経験のない方には想像がつくでしょうか。
例えば、下の画像のような画面があったとします。これはWindows10のプロキシサーバ設定の画面です。様々なコントロールが並んでいますね。スクリーンリーダーは、これらのコントロールの内容をやみくもに読み上げているのではありません。それは、現在表示されている画面の中でもユーザーが本当に必要としている情報は限られているからです。

一般に、スクリーンリーダーは「現在キーボードフォーカスのあるコントロールを読み上げる」というのを基本動作としています。tabキーなどを押してフォーカス位置が変化していくたびにフォーカス位置の内容を読み上げるのです。今回の例の画像では「プロキシ サーバーを使う」というトグルボタンにフォーカスがあるので、スクリーンリーダーもこの項目を読み上げることになります。もちろん、フォーカス位置のコントロールを読み上げるだけでは不十分なことも多いため、スクリーンリーダーには様々な情報(ステータスバー、タイトルバーなど)を読み上げるためのコマンドが用意されていたり、フォーカスを動かさないで画面内のコントロールを探索できる機能が用意されていたりします。しかし、フォーカス位置の情報を読み上げるという基本動作は、スクリーンリーダーの種類にかかわらずおおむね共通であると言えます。

 

では、スクリーンリーダーは各コントロールが持つ情報のうち、どのような情報を読み上げているのでしょうか。先ほどのプロキシサーバの設定画面(「プロキシ サーバーを使う」にフォーカスがある状態)でtabキーを押していくと、私が使用しているスクリーンリーダーでは以下のように読み上げられます。

プロキシ サーバーを使う トグルボタン 押されています
アドレス エディット 192.168.1.1
ポート エディット 1234
次のエントリで始まるアドレス以外にプロキシ サーバーを使います。エントリを区切るにはセミコロン (;) を使います。 エディット hogehoge.co.jp
ローカル (イントラネット) のアドレスにはプロキシ サーバーを使わない チェックボックス チェックなし
保存 ボタン

上記の例から分かるように、スクリーンリーダーが読み上げる内容には概ね以下の3つの情報が含まれていると言えます。

  • コントロールの名前(ラベル)
  • コントロールの種類(「トグルボタン」や「エディット」など)
  • コントロールの現在の値(存在する場合。エディットフィールドに入力されたテキストやチェックボックスにおける「チェックなし」など)

ここまでに書いてきたことをまとめると、スクリーンリーダーの基本の動作とは、現在フォーカスのあるコントロールの名前・種類・値を読み上げることだということになります。それでは、なぜスクリーンリーダーは現在フォーカスのあるコントロールがわかるのでしょうか。コントロールの名前や種類や値がどうしてわかるのでしょうか。そのカギを握るのが次節で説明する「UI オートメーション」なのです。

UI オートメーションの概要

UI オートメーションとは、GUI画面の個々のコントロールの情報を取得したり、エディットフィールドに文字を入力する/ボタンを押すなどの操作をプログラムから実行できるようにするための、Windowsに組み込まれた仕組みのことです。スクリーンリーダーは、このUI オートメーションを(他の様々な技術と組み合わせて)画面内の情報の取得に使用しています。類似の技術として、Microsoft Active Accessibility(MSAA)、IAccessible2、Java Access Bridgeなどがあり、スクリーンリーダーではこれらの技術も併用されています。UI オートメーション以外については詳細は割愛しますが、興味があればぜひWebで検索してみてください。

それではUI オートメーションに話を戻します。UI オートメーションにはプロバイダーAPIとクライアントAPIがあり、何らかのGUI画面を表示するアプリケーション(プロバイダー)はプロバイダーAPIを介して画面内のコントロールに関する情報を公開します。一方スクリーンリーダーなどのソフトウェア(クライアント)はクライアントAPIを介してプロバイダーが公開した情報にアクセスするという仕組みになっています。ただし、Windowsの標準コントロールやアクセシビリティに配慮されたフレームワークを使用して作られたコントロールに関しては、OSやフレームワークが自動的にプロバイダーとしての役目を果たしてくれるので、アプリの作成者が自分でプロバイダーAPIを使うケースは(完全に独自描画のコントロールを用いるのでなければ)ほぼありません。

UI オートメーションでは、すべてのコントロールはAutomationElementクラスのインスタンスであり、デスクトップをルートとするAutomationElementオブジェクトのツリー構造として表現されます。ちょうど、HTMLやXMLのDOM(ドキュメントオブジェクトモデル。documentオブジェクトをルートとして要素がツリー構造になっている)と同じようなイメージです。下の画像は、UI オートメーションのツリー構造を表示できるプログラムを使用して前出のプロキシサーバの画面がUI オートメーション的にどう見えるのかを表示したスクリーンショットです。

さらにUI オートメーションにはイベントという概念があり、コントロールの表示内容が変わったりフォーカス位置が変わったりしたときには、イベントを発生させてプログラムに通知する仕組みになっています。スクリーンリーダーがtabキーが押されるたびに新しいフォーカス位置の情報を読み上げることができるのは、この仕組みのおかげなのです。

 

ここまでUI オートメーションの概要を簡単に説明してきました。抽象的で分かりにくい概念が多いので初めての方には少し難しかったかもしれませんが、本記事の残りを読むだけなら抽象概念についてはそんなに理解しておく必要はないのでご安心ください。なお、UI オートメーションの詳細についてはMicrosoftの公式ドキュメントを参照してください。
それでは、次節でいよいよスクリーンリーダーもどきを作っていきたいと思います。

スクリーンリーダーもどきを作る

今回の目標と用意するもの

今回は、先述のスクリーンリーダーの基本動作に倣って「キーボードフォーカスが変化したらフォーカス位置のコントロールの名前・種類・存在する場合は値を出力する」というプログラムを作ってみようと思います。
しかし、本物のスクリーンリーダーのように音声読み上げさせるなどの処理を入れるとコード量も多くなってしまうので、今回はただコンソールに出力するだけとします。実装もとりあえず動くレベルとしているため、いろいろとお行儀の悪いコードになっていますがご容赦ください。

作るにあたって用意するものはWindows10以降が動作するPCだけです。UI オートメーションは.NETから利用するマネージドAPIとCOM(Component Object Model)から利用するアンマネージドAPIがありますが、今回はWindows PowerShellとマネージドAPIを使ってコーディングしていきます。
なお、本記事に記載のコードは以下の環境にて動作確認を行っています。

  • Windows10 21H2
  • Windows PowerShell 5.1

アーキテクチャ

作成するスクリーンリーダーもどきは、以下の2つのコンポーネントで構成されます。

  • フォーカス位置の変化イベントに応答するイベントハンドラを含むヘルパークラス(C#で実装)
  • フォーカス位置のコントロールの情報をコンソールに出力するメインスクリプト(PowerShellで実装)

動作フローの概要としては以下のようになります。

  1. tabキーなどでフォーカスを移動させると、UI オートメーションにより、フォーカス位置が変更されたことを示すイベント(AutomationFocusChangedEvent)が発生する。
  2. ヘルパークラス内のイベントハンドラが呼び出され、新しくフォーカスされたコントロールのAutomationElementオブジェクトをヘルパークラス内のキューに追加する。
  3. メインスクリプトはキューの内容を取り出し、コントロールの情報をコンソールに出力する。
  4. フォーカス位置が変化するたびに上記の動作が繰り返される。

ちなみに、どうしてイベントハンドラをC#で実装しているかというと、PowerShellスクリプトではどうしてもうまく動作するイベントハンドラを実装することができなかったためです。PowerShellの制約によるものなのか私のコードに問題があったのかはまだわかりませんが、とりあえず今回はこの方式で行きたいと思います。

ヘルパークラスの実装

それではいよいよ実装です。まずはヘルパークラスを作ります。以下のコードをテキストエディタに張り付け、適当なフォルダに「uia_test_helper.cs」という名前で保存してください。

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Windows.Automation;
namespace UIATest {
public class UIATestHelper {
// 新しくフォーカスされたコントロールのAutomationElementオブジェクトを入れるキュー
public BlockingCollection<Object> EventSourcesQueue {get;set;}
//フォーカス変更イベントに登録するイベントハンドラ
public AutomationFocusChangedEventHandler Handler {get;private set;}
public UIATestHelper() {
this.EventSourcesQueue=new BlockingCollection<Object>();
this.Handler=new AutomationFocusChangedEventHandler(OnFocusChanged);
}
//フォーカス変更イベント発生時に走る処理の本体
private void OnFocusChanged(Object src,AutomationFocusChangedEventArgs e) {
//イベントハンドラに渡される引数srcに新しくフォーカスされたAutomationElementが入るので、これをキューに入れる
AutomationElement sourceElement;
try {
sourceElement = src as AutomationElement;
}
catch (ElementNotAvailableException) {
return;
}
this.EventSourcesQueue.TryAdd(sourceElement);
}
~UIATestHelper() {
this.EventSourcesQueue.Dispose();
}
}
}

コードの内容はコード内のコメントの通りです。UIATestHelperクラスをインスタンス化すると、そのインスタンスのHandlerプロパティにイベントハンドラとして動作するデリゲートが作成されるので、メインスクリプトではこれをAutomationFocusChangedEventに登録することになります。また、フォーカス位置が変化するたびにEventSourcesQueueプロパティのキューに新たにフォーカスされたコントロールの情報が入ります。

メインスクリプトの実装

次に、メインスクリプトの作成を行います。以下のコードをテキストエディタに張り付け、「uia_test.ps1」という名前で保存してください。保存場所は「uia_test_helper.cs」と同じフォルダである必要があります。

using namespace System.Windows.Automation
using namespace UIATest
cd $PSScriptRoot
# UI オートメーションのアセンブリとヘルパークラスの読み込み
add-type -AssemblyName UIAutomationClient
add-type -AssemblyName UIAutomationTypes
add-type -AssemblyName UIAutomationClientsideProviders
add-type -ReferencedAssemblies UIAutomationClient,UIAutomationTypes,UIAutomationClientsideProviders -path uia_test_helper.cs
# UI オートメーションでwin32コントロールなどの古いコントロールにアクセスできるようにする
while ($true) {
try {
[ClientSettings]::RegisterClientsideProviderAssembly([UIAutomationClientsideProviders.UIAutomationClientSideProviders].Assembly.GetName())
break
}
catch {
continue
}
}
$hlp=new-object UIATestHelper
# フォーカス変更イベントにイベントハンドラーを登録
[Automation]::AddAutomationFocusChangedEventHandler($hlp.Handler)
$focusedObject=$null
$output=""
try {
# キューに追加されたAutomationElementオブジェクトの内容をコンソールに出力するループ
while ($true) {
sleep -milliseconds 50
# キューに何も入っていなければ以後の処理は実行しない
if (-not $hlp.EventSourcesQueue.TryTake([ref]$focusedObject)) {continue}
# コントロールの名前の取得
$output=$focusedObject.Current.Name+"`t"
# コントロールの種類の取得
$output+=$focusedObject.Current.LocalizedControlType
# 値が存在するなら
if ($focusedObject.GetCurrentPropertyValue([AutomationElement]::IsValuePatternAvailableProperty)) {
# コントロールの値の取得
$output+="`t"+$focusedObject.GetCurrentPropertyValue([ValuePattern]::ValueProperty)
}
echo $output
}
}
# 終了処理
finally {
[Automation]::RemoveAutomationFocusChangedEventHandler($hlp.Handler)
}

こちらも処理内容についてはコード内のコメントを参照していただければと思いますが、コントロールの値の取得について1点補足します。

UI オートメーションには「コントロールパターン」という概念があります。コントロールパターンを簡単に説明すると、そのコントロールからどのような情報を取得できるか、またどのような操作を実行できるのかを定義するものと言えます。
例えば、ボタンやハイパーリンクなどのコントロールはクリックしたりenterキーを押すなどして「実行」することができるという性質を持ちます。このような「実行」できる性質を持つコントロールでは「InvokePattern」というコントロールパターンが利用できるようになっており、これを通じて「ボタンを押す」とか「リンクをクリックする」などの操作ができるようになっています。
また、チェックボックスなどのオン/オフを切り替える形式のコントロールでは「TogglePattern」が利用できるようになっていて、これを通じて現在のチェック状態の取得や変更といった操作をできるようになっています。
さて、上記のメインスクリプトのコードでは「ValuePattern」というコントロールパターンを使用しています。これは何らかの値を持つコントロールで利用できるコントロールパターンであり、このコントロールパターンを介して値の取得や変更ができるのです。コントロールによって値を持つ者と持たないものがあるので、値を持っているかどうかを「ValuePatternが利用できるか」を問い合わせることで確かめます(38行目)。そして利用できることが分かると、ValuePatternが利用可能な場合に参照できるプロパティである「Value」というプロパティから値を取得する(40行目)という動作となっています。

動作確認

以上で「スクリーンリーダーもどき」の実装は完了しました。早速動作確認してみましょう。以下のような手順で行います(例によってプロキシ設定の画面で動作確認します)。

  1. Windows PowerShellを起動します。
  2. カレントディレクトリをスクリーンリーダーもどきを保存したフォルダに変更します。
  3. 「Set-ExecutionPolicy bypass -scope process -force」と入力して実行し、スクリプトファイルの実行を許可します。
  4. PowerShell画面はそのままにしてWindowsの設定アプリを起動し、「ネットワークとインターネット」→「プロキシ」の順に進み、プロキシ設定画面を表示させます。
  5. PowerShellの画面に戻って「.\uia_test」と入力してenterを押します。何も出力されませんがこれで正常です。
  6. プロキシ設定画面に戻り、tabキー/shift+tabキーで何度かキーボードフォーカスを変更してみてからPowerShell画面に戻り、コンソールの表示内容を確認します。
  7. 動作確認が終わったらctrl+cでスクリプトを停止します。

上記手順を実行すると私の環境では以下のように出力されます。環境の違いにより出力内容は異なる場合があります。

PS C:\work\advent> .\uia_test
ウィンドウ
設定 一覧項目
設定を自動的に検出する toggle switch
セットアップ スクリプトを使う toggle switch
プロキシ サーバーを使う toggle switch
アドレス edit 192.168.1.1
ポート edit 1234
次のエントリで始まるアドレス以外にプロキシ サーバーを使います。エントリを区切るにはセミコロン (;) を使います。 edit hogehoge.co.jp
ローカル (イントラネット) のアドレスにはプロキシ サーバーを使わない check box
保存 button
ウィンドウ
Windows PowerShell 一覧項目
Text Area ドキュメント
Windows PowerShell ウィンドウ
PS C:\work\advent>

どうでしょうか。ほんの100行未満のコードで、フォーカスの変化に応じてコントロールの情報を出力する処理が実装できていることがおわかりになるかと思います。実際のスクリーンリーダーも、このようにイベントに応答しながらコントロールの情報を収集してユーザーに対してフィードバックするということをやっているのです。
一方、スクリーンリーダーもどきではトグルボタンやチェックボックスのオン/オフの情報を取得できていません。この情報はValuePatternではなくTogglePatternというコントロールパターンによって取得するものなのです。他にも、リストボックスなら項目が選択されているかいないか、ツリービューなら折りたたまれているか展開されているかなど、状況によってユーザーが必要とする情報もその取得方法も変わってきます。このあたりを「うまいことやってくれる」のがスクリーンリーダーもどきと本物の大きな違いではないでしょうか。

最後に

ここまで、「スクリーンリーダーがどのようにGUIの画面から情報を取得しているか」というテーマで書いてきました。実際のスクリーンリーダーは画面から情報を抜き出してくるだけが仕事ではなく、目の不自由な人が画面を見なくてもデバイスを使いこなせるように様々な工夫がなされています。これを機会に、スクリーンリーダーについて興味を持っていただければ幸いです。また、アプリ開発者の方はそのアプリがスクリーンリーダーに読んでもらえるか(UI オートメーション的にそのアプリの画面がどう見えるか)をぜひ考えてみてください。
その他、UI オートメーションについて学んでおけば手製の自動化ツールを作ることができたり、RPA製品などでも利用されているケースがあるため、そのような製品の動作を理解する助けにもなるのではないかと思います。

本記事を通じて1人でも多くの方にスクリーンリーダーやUI オートメーションのような技術、ひいては情報アクセシビリティ一般について興味を持っていただけることを願います。最後までお読みいただきましてありがとうございました。