Excel VBAでUIAutomationを用いて画面の要素を調査_ver.1.0

# 概要
* VBAでUIAutomationを用いて、UI要素を調べるためのソースコードです。以下の動画で使われているものです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/J27Sw5v3f0M?si=fJcEjudAJ0S3y8x9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


※2023/12/09に以下の動画を新しくアップロードしましたので、合わせてご確認ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/Zd1ukhIJnTs?si=ov-zVqHweYuBS6Cv" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 動作前の設定
* 必ずVBEの参照設定でUIAutomationClientの参照を有効化してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618291/e7f2f0d1-a9b3-87b6-c912-acfaa46cdf92.png)

# 実行環境
以下の環境で動作確認をしました。
* Windows10(64bit)でのExcel 2016
* Windows11でのExcel 2021

# 注意点
* プログラムの実行については、すべて自己責任で行ってください。実行により発生した、いかなる直接的または間接的被害について、作者はその責任を負いません。

# コードの簡単な解説
プログラムの大まかな流れは以下のとおりです。
1. ウインドウ名からウインドウハンドルを取得
2. ウインドウ内のすべての要素を取得
3. それぞれの要素の名前やコントロールタイプをデバッグする

# 実行方法

実行時は「CheckAllElementsWithUIAutomation」サブプロシージャ内の「Call GetAllElements(　,　)」の引数を指定して、当該プロシージャを実行します。

* 第一引数（必須）は、ウィンドウ名（名前の一部でも可能）を指定してください。同じウィンドウ名のものが二つ以上開かれていないことを確認してください。

* 第二引数（省略可能）は、要素名を入力してください。第二引数を指定すると、当該要素名を含む要素が出現した際に、プロシージャの実行が止まります。

# 実行例
* コードに記載されている「Call GetAllElements("Challenge", "電話番号")」は、記載例です。
* これを実行すると、「Challenge」という名前がつく最初のウインドウを取得し、「電話番号」という要素名のものが出現したらデバッグが止まります。
 ※「Challenge」という名前がつくウインドウが複数ないことを確認したうえで、実行します。

# 各識別子について
* ControlTypeIdは以下のサイトからよく使うものを抜粋しました。

https://learn.microsoft.com/ja-jp/windows/win32/winauto/uiauto-controltype-ids

* ControlPatternは以下のサイトからよく使うものを抜粋しました。

https://learn.microsoft.com/ja-jp/windows/win32/winauto/uiauto-controlpattern-ids

# コード_ver.1.0
```vb:VBA
Option Explicit

' GetNextWindow関数の宣言
Declare PtrSafe Function GetNextWindow Lib "user32" Alias "GetWindow" _
  (ByVal hwnd As LongPtr, ByVal wFlag As Long) As LongPtr
' GetNextWindow関数は、指定されたウィンドウの次のウィンドウのハンドルを取得する関数です。

' IsWindowVisible関数の宣言
Declare PtrSafe Function IsWindowVisible Lib "user32" _
  (ByVal hwnd As LongPtr) As LongPtr
' IsWindowVisible関数は、指定されたウィンドウが可視状態かどうかを判定する関数です。

' GetWindowText関数の宣言
Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
  (ByVal hwnd As LongPtr, ByVal lpString As String, ByVal cch As Long) As LongPtr
' GetWindowText関数は、指定されたウィンドウのテキストを取得する関数です。

' FindWindow関数の宣言
Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
  (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
' FindWindow関数は、指定されたクラス名やウィンドウ名からウィンドウのハンドルを取得する関数です。

Dim hwnd As LongPtr ' ウィンドウのハンドルを保持する変数

Const GW_HWNDLAST = 1 ' 最後のウィンドウ
Const GW_HWNDNEXT = 2 ' 次のウィンドウ

' getWindowHandle関数（独自関数）
Function getWindowHandle(ByVal PartialWindowName As String) As LongPtr
    Dim strCaption As String * 500 ' ウィンドウのテキストを保持する変数
    hwnd = FindWindow(vbNullString, vbNullString) ' 最初のウィンドウのハンドルを取得
    Dim cnt As Long ' カウンタ変数
    cnt = 0
    Dim cap As String ' ウィンドウのテキストを保持する変数
    Do
        If IsWindowVisible(hwnd) Then ' ウィンドウが可視状態かどうかを判定
            GetWindowText hwnd, strCaption, Len(strCaption) ' ウィンドウのテキストを取得
            cap = Left(strCaption, InStr(strCaption, vbNullChar) - 1) ' ヌル文字までの部分を抽出
            If InStr(cap, PartialWindowName) <> 0 Then ' ウィンドウのテキストに指定の文字列が含まれているかを判定
                getWindowHandle = hwnd ' ウィンドウのハンドルを返す
                Exit Function ' 関数を終了
            End If
        End If
        hwnd = GetNextWindow(hwnd, GW_HWNDNEXT) ' 次のウィンドウのハンドルを取得
        DoEvents ' イベントを処理
        If hwnd = GetNextWindow(hwnd, GW_HWNDLAST) And cnt = 0 Then ' 最後のウィンドウで初回の場合
            cnt = 1
        ElseIf hwnd = GetNextWindow(hwnd, GW_HWNDLAST) And cnt = 1 Then ' 最後のウィンドウで2回目の場合
            Debug.Print "ウィンドウ取得に失敗" ' デバッグウィンドウにメッセージを出力
            Application.Wait [Now()] + (1 / 86400) ' ウィンドウハンドル失敗用の待機            
            hwnd = FindWindow(vbNullString, vbNullString) ' 最初のウィンドウのハンドルを取得し直す
            cnt = 0 ' カウンタをリセット
        End If
    Loop
End Function

' UIAutomationを使用して、指定された部分ウィンドウ名内のUI要素を取得し、デバッグ表示するサブプロシージャ
Sub CheckAllElementsWithUIAutomation()
    ' GetAllElements関数を呼び出し、指定された部分ウィンドウ名と検索ワードを渡す
    Call GetAllElements("Challenge", "電話番号")
End Sub

Function GetAllElements(ByVal PartialWindowName As String, Optional ByVal searchWord As String) As String
    ' UIAutomationのインスタンスを作成
    Dim uiAuto As UIAutomationClient.CUIAutomation  ' UIAutomationのインスタンスを格納する変数
    Set uiAuto = New UIAutomationClient.CUIAutomation  ' 新しいUIAutomationのインスタンスを作成

    ' ウィンドウのハンドルを取得
    Dim hwnd As LongPtr  ' ウィンドウのハンドルを格納する変数
    hwnd = getWindowHandle(PartialWindowName)  ' 指定された部分ウィンドウ名のウィンドウのハンドルを取得

    ' ウィンドウ要素を取得
    Dim elm01 As UIAutomationClient.IUIAutomationElement  ' ウィンドウ要素を格納する変数
    Set elm01 = uiAuto.ElementFromHandle(ByVal hwnd)  ' ウィンドウのハンドルからUIAutomation要素を取得

    ' 条件を作成
    Dim uiCnd As IUIAutomationCondition  ' 条件を格納する変数
    Set uiCnd = uiAuto.CreateTrueCondition  ' 真の条件を作成（CreateTrueConditionは、常に真となる条件を作成します。特定の条件を満たす要素を検索するのではなく、全ての要素を取得する際に利用されます。）

    ' UIAutomationの要素を格納するための配列を宣言し、要素を取得する
    Dim aryElm As UIAutomationClient.IUIAutomationElementArray  ' aryElmはUIAutomationの要素を格納する変数(配列)です
    Set aryElm = elm01.FindAll(TreeScope_Subtree, uiCnd)  ' elm01（ウィンドウ要素）内のサブツリー全体から条件（uiCnd）に一致する要素を取得し、aryElmに格納

    ' デバッグ用にループ内の要素情報を表示
    Dim i As Long

    ' aryElm内の各要素に対して処理を行うループ
    For i = 0 To aryElm.Length - 1
        Application.Wait [Now()] + (0.05 / 86400)     ' 簡単なウェイト（待機）を挿入することで、処理をゆっくり進める
        Debug.Print "i=" & i    ' デバッグ用に現在のインデックスを表示
        Debug.Print "   要素名:" & aryElm.GetElement(i).CurrentName    ' デバッグ用に要素の名前を表示
        Call GetControlTypeByValue(aryElm.GetElement(i).CurrentControlType)    ' 要素のコントロールタイプを表示する関数を呼び出す
        Call GetUIPatternAndDebug(aryElm.GetElement(i))    ' UI パターンの情報を表示する関数を呼び出す

        ' 検索ワードが指定されており、要素の名前が検索ワードを含む場合は停止
        If searchWord <> "" And InStr(aryElm.GetElement(i).CurrentName, searchWord) > 0 Then
            Stop
        End If

        ' 検索ワードが指定されていない場合、50回ごとに停止
        If searchWord = "" And i Mod 50 = 0 And i <> 0 Then
            Stop
        End If
    Next i
End Function


Function GetControlTypeByValue(ByVal num01 As Long) As String
    ' num01の値によってコントロールの種類を返す独自関数

    ' コントロールの種類を定義
    Select Case num01
        Case 50000
            GetControlTypeByValue = "UIA_ButtonControlTypeId" ' ボタンの種類を表します。
        Case 50002
            GetControlTypeByValue = "UIA_CheckBoxControlTypeId" ' チェックボックスの種類を表します。
        Case 50003
            GetControlTypeByValue = "UIA_ComboBoxControlTypeId" ' コンボボックス（ドロップダウンリスト）の種類を表します。
        Case 50004
            GetControlTypeByValue = "UIA_EditControlTypeId" ' テキストボックスの種類を表します。
        Case 50005
            GetControlTypeByValue = "UIA_HyperlinkControlTypeId" ' ハイパーリンク（通常はウェブページへのリンク）の種類を表します。
        Case 50013
            GetControlTypeByValue = "UIA_RadioButtonControlTypeId" ' ラジオボタンの種類を表します。
        Case 50020
            GetControlTypeByValue = "UIA_TextControlTypeId" ' テキストエリアやスタティックテキスト（表示専用のテキスト）の種類を表します。
        Case 50031
            GetControlTypeByValue = "UIA_SplitButtonControlTypeId" ' ボタンとドロップダウンリストを組み合わせたコントロール（スプリットボタン）の種類を表します。
        Case 50006
            GetControlTypeByValue = "UIA_ImageControlTypeId" ' 画像の種類を表します。例えば、アイコンや写真など。
        Case Else
            ' 未知のコントロールタイプの場合
            GetControlTypeByValue = "" ' 未知のコントロールタイプです。
    End Select

    ' 結果をデバッグ出力
    Debug.Print "   要素のコントロールタイプ:" & GetControlTypeByValue  ' 結果をデバッグウィンドウに出力します。
End Function


' UI要素のパターンを取得し、それに基づいてデバッグメッセージを出力する関数
Function GetUIPatternAndDebug(ByVal uiElm As UIAutomationClient.IUIAutomationElement)
    ' uiElm: UIAutomation要素オブジェクト

    ' UI要素を展開／折り畳みするためのパターン
    Dim UIExpandCollapsePattern As IUIAutomationExpandCollapsePattern
    Set UIExpandCollapsePattern = uiElm.GetCurrentPattern(10005)

    ' UI要素を起動するためのパターン
    Dim UIInvokePattern As IUIAutomationInvokePattern
    Set UIInvokePattern = uiElm.GetCurrentPattern(10000)

    ' UI要素を選択するパターン
    Dim UISelectionItemPattern As IUIAutomationSelectionItemPattern
    Set UISelectionItemPattern = uiElm.GetCurrentPattern(10010)

    ' UI要素のオンとオフの状態を切り替えるためのパターン
    Dim UITogglePattern As IUIAutomationTogglePattern
    Set UITogglePattern = uiElm.GetCurrentPattern(10015)

    ' UI要素の値を設定／取得するためのパターン
    Dim UIValuePattern As IUIAutomationValuePattern
    Set UIValuePattern = uiElm.GetCurrentPattern(10002)

    ' 各パターンが取得できているかを確認し、デバッグメッセージを出力
    If Not UIExpandCollapsePattern Is Nothing Then
        Debug.Print "   可能な操作:UIExpandCollapsePattern"
    End If
    If Not UIInvokePattern Is Nothing Then
        Debug.Print "   可能な操作:UIInvokePattern"
    End If
    If Not UISelectionItemPattern Is Nothing Then
        Debug.Print "   可能な操作:UISelectionItemPattern"
    End If
    If Not UITogglePattern Is Nothing Then
        Debug.Print "   可能な操作:UITogglePattern"
    End If
    If Not UIValuePattern Is Nothing Then
        Debug.Print "   可能な操作:UIValuePattern"
    End If
End Function

```

# コードの修正履歴
2023/12/4：ウィンドウの取得に失敗した際の待機命令を追加しました。