Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2012年06月01日] [更新 2015年04月28日] Delphi サンプルプログラム集
679_他のアプリの操作とUI Automation 関係インターフェイス
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro + Firefox 13.0 以降
Windows XP(SP3) 以上でないと使用不可
679_UIAutomation_Test.zip [3,433 KB] 2015年04月26日版 (EXE 同梱)



  • 2012年06月07日
  • オブジェクトの階層のツリー表示と、サンプルの追加
  • ページのタイトルを変更
  • 2012年06月09日
  • Delphi 2007 以前でも利用可能にするために、UIAutomationClient_TLB.pas の文字コードを UTF-8N から SHIFT-JIS に変更した
  • 2012年06月19日
  • イベントハンドラのサンプルと記事を追加
  • 2015年04月26日
  • Firefox のボタンをクリックするサンプルコードを修正
    FireFox のウィンドウハンドルの取得を真面目なコード (?) に変更して、[戻る] [進む] [閉じる] の全ての操作を実行可能にした
  • 2015年04月28日
  • [645_ウィンドウの階層のツリー表示] のツール、ユティリティ関係の記事を本ページに移動




Accessibility ( MSAA ) と UI Automation


IUIAutomation 関係インターフェイスは、IAccessible インターフェイスの後継の API 群です。
このインターフェイスは、Delphi XE2 までの Delphi には実装されていません。Delphi のサンプルコードもネット上で検索する限り、皆無に近い状態です。そこで [IDLコンパイラを使用したタイプライブラリの作成と取り込み] の方法で、インターフェイスのユニットを作成してテストしてみました。

UIAutomationClient.idl から UIAutomationClient_TLB.pas を作成しました。これをパスの通った場所に置いて、uses に追加して使用します。タイプライブラリの取り込みで作成したままのコードではエラーが発生したため、筆者が修正、追加しています。変更内容は、コードの先頭に書いてあります。
コンポーネントパレットに登録して使用することもできますが、コンポーネントのアイコンはありません。登録パレットのデフォルトタブは [ActiveX] です。

IUIAutomation オブジェクト用に、TCUIAutomation というクラスがあります。このクラスを生成してインターフェイスのオブジェクトを作成します。関連するインターフェイスには、

  • (1) 各要素 (エレメント IUIAutomationElement) の GetCurrentPattern メソッドを実行
  • (2) 出力の IInteerface オブジェクトの QueryInterface メソッドで、目的とするオブジェクトのインスタンスを取得

という手順でアクセスできます。

Delphi 2007 以降には IAccessible インターフェイスに相当する OleAcc.pas が実装されていますが、これと UIAutomationClient_TLB.pas と は同時には使用できません。
UIAutomationClient_TLB.pas にも IAccessible インターフェイスが定義されていて、OleAcc.pas のそれとは実装が若干異なります。

UIAutomationClient_TLB.pas には、IAccessible インターフェイスのオブジェクトのプロパティ等の取得や操作用のメソッド類が実装されています。これらを使用することになります。
関係インターフェイスの中には、Windows Viata 以降、あるいは、Windows 7 以降でないと使用できないものがあります。注意してください。



[備考 1]
COM インターフェイスでは、文字列として WideString または PWideString 型を使用します。 Delphi 2009 以降の文字列型である String は、デフォルトで UnicodeString 型です。Delphi 2009 以降の WideString 型は、COM インターフェイスとの互換性のためにあります。


[備考 2]
一般的に、フックも含め、他のアプリケーションの情報取得や操作関係のプログラムは、個人的には難しい部類であると思います。前のページの IAccessible インターフェイスや、このページで扱っている UI Automation 関係インターフェイスのサンプルも難しい部類ではないでしょうか。
ネット上での情報が少ないということも、困難さに拍車をかけているような気もします。


[備考 3]
UI Automation 関係インターフェイスを使用した他のアプリの操作は、対象のアプリケーションのバージョンアップ等で構造が変化した場合、値の取得や操作ができなくなる可能性があります。


[備考 4]
他のアプリの操作では、ともかく最初は対象のウィンドウのハンドルの取得が必要です。ウインドウのハンドルは、操作に有効なウィンドウのハンドルが必要です。本ページの [ブラウザ Firefox... ] のを参考にしてください。FindWindow で取得したウインドウが必ずしも有効なウィンドウであるという保証はありません。





01_起動中のエクセルのウィンドウの位置とサイズを変更


起動中のエクセルを検出し、そのウィンドウハンドルから、UI Automation インターフェイスの機能を使用して、エクセルのウィンドウの位置とサイズを変更します。


図1
ボタンが 1 つだけのテストプログラム

リスト1
エクセルのウィンドウの位置とサイズを変更
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, UIAutomationClient_TLB;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  UI Automationの簡単な使用例
//
//  起動中のエクセルの位置とサイズを変更
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  hTargetWnd   : HWND;
  UIAuto       : TCUIAutomation;
  iElement     : IUIAutomationElement;
  iInter       : IInterface;
  TransPattern : IUIAutomationTransformPattern;
begin
  //起動中のエクセルを検出
  hTargetWnd := FindWindow('XLMAIN', nil);
  if hTargetWnd = 0 then exit;

  //UIAutomation生成
  UIAuto := TCUIAutomation.Create(Self);
  try
    //ウィンドウハンドルからIUIAutomationElement取得
    UIAuto.ElementFromHandle(hTargetWnd, iElement);

    //そのエレメントにフォーカス移動
    iElement.SetFocus;

    //IUIAutomationTransformPatternを取得して、エレメントの位置とサイズを変更
    iElement.GetCurrentPattern(UIA_TransformPatternId, iInter);
    if Assigned(iInter) then begin
      if iInter.QueryInterface(IID_IUIAutomationTransformPattern,
                               TransPattern) = S_OK then begin
        TransPattern.Move   (100, 100);
        TransPattern.Resize (500, 600);
      end;
    end;
  finally
    FreeAndNil(UIAuto);
  end;
end;

end.




02_オブジェクトの階層をツリー表示


UI Automation オブジェクトの階層をツリー表示するサンプルです。テストしやすいように、ウィンドウを選択するダイアログを表示します。このダイアログは、[645_ウィンドウの階層のツリー表示] で使用している、WinSearch.pas をツリー表示部だけに変更したものです。選択ノードのウィンドウを赤線の枠で表示します。

また、オブジェクトのツリーでも、選択中のオブジェクトを赤い枠線で表示します。この枠線の表示には、透明なフォームを使用しています。
[デフォルトアクション実行] と [値のセット] のボタンを追加していますが、対象のオブジェクトに対して、これらの機能が使用できるとは限りません。[値のセット] はオブジェクトの Value プロパティに文字列を送信するものです。ただし、編集可能なテキストであって、且つ、読み出し専用となっていないオブジェクトに限ります。メモ帳等にはセットできます。

ツリーの展開は、IAccessible インターフェイスの場合と比べて時間がかかります。特に階層が深いと待ち時間が長くなります。特定のウィンドウだけに限定した方が無難です。
このサンプルは筆者の失敗でした。ツリー構造を最初に全て取得するようにしてしまいました。そのために時間がかかるようになっています。その代わり、[全て展開] は高速です。機会があれば修正したいと思っています。



図2
設計時画面
図3
実行例
  • ツリーの階層が深いと非常に時間がかかるので注意

リスト2
UIAutomation オブジェクトの階層をツリー表示
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ComCtrls, ActiveX, Menus, plWindowRectFormUnit,
  UIAutomationClient_TLB, Buttons;

type
  TForm1 = class(TForm)
    TreeView1: TTreeView;
    Panel1: TPanel;
    Button2: TButton;
    Splitter1: TSplitter;
    Panel2: TPanel;
    ListView1: TListView;
    Button1: TButton;
    PopupMenu1: TPopupMenu;
    N1: TMenuItem;
    Button3: TButton;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    procedure TreeView1Deletion(Sender: TObject; Node: TTreeNode);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure N1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure SpeedButton1Click(Sender: TObject);
    procedure TreeView1Change(Sender: TObject; Node: TTreeNode);
    procedure SpeedButton2Click(Sender: TObject);
  private
    { Private 宣言 }
    UIAuto     : TCUIAutomation;
    hTargetWnd : HWND;
    RectForm   : TplWindowRectForm;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses WindowTreeUnit, Clipbrd;

{$R *.dfm}

type
  //オブジェクトの情報用のレコード型
  //各ノードのDataプロパティに代入しておく
  TplUIAutoInfoRec = record
    Element : IUIAutomationElement;
    AutoId  : WideString;
  end;
  PplUIAutoInfoRec = ^TplUIAutoInfoRec;


//=============================================================================
//  フォーム生成時の処理
//  ListViewのColumns[1]のAutoSizeはTrueにしてある
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  UIAuto := TCUIAutomation.Create(Self);

  ListView1.PopupMenu := nil;
  TreeView1.PopupMenu := nil;
end;

//=============================================================================
//  フォームを閉じる時の処理
//=============================================================================
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  FreeAndNil(UIAuto);

  if Assigned(RectForm) then begin
    RectForm.Close;
    FreeAndNil(RectForm);
  end;
end;

//=============================================================================
//  [ウィンドウ選択]ボタン
//  ウィンドウのリスト表示用のフォームをモーダル表示して選択
//
//  hTargetWnd : 対象のウィンドウのハンドル
//  は、モーダル表示したフォームから取得する
//  TVariantArgの使用にはusesにActiveXが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);

  //---------------------------------------------------------------------------
  //  関数内関数
  //  (1) TreeViewのノードに表示するテキストを作成し、ノードを追加
  //      テキストは、ControlTypeプロパティとNameプロパティの値 
  //  (2) ノードのDataプロパティにオブジェクトの情報を、レコード型で代入
  //  戻り値は、追加ノードのインスタンス
  //---------------------------------------------------------------------------
  function AddChildNode(iElement: IUIAutomationElement; ATree: TTreeView;
    ANode: TTreeNode): TTreeNode;
  var
    StrType     : WideString;
    StrName     : WideString;
    StrNode     : WideString;
    StrAutoId   : WideString;
    pUIAutoInfo : PplUIAutoInfoRec;
  begin
    iElement.Get_CurrentLocalizedControlType(StrType);
    iElement.Get_CurrentName(StrName);
    StrNode := '[' + StrType + '] ' + ' "' + StrName + '"';

    Result := ATree.Items.AddChild(ANode, StrNode);

    //要素の値をTreeViewのDataプロパティに代入(レコード型)
    if Result <> nil then begin
      iElement.Get_CurrentAutomationId(StrAutoId);
      New(pUIAutoInfo);
      pUIAutoInfo.Element := iElement;
      pUIAutoInfo.AutoId  := StrAutoId;
      Result.Data         := pUIAutoInfo;
    end;
  end;

  //---------------------------------------------------------------------------
  //  関数内関数
  //  pParent階層下の子オブジェクトを再帰処理で列挙し、TreeViewのノードに追加
  //---------------------------------------------------------------------------
  procedure EnumAccTreeItems(pParent: IUIAutomationElement; Scope: TreeScope;
    pCondition: IUIAutomationCondition; ATree: TTreeView; ANode: TTreeNode);
  var
    found    : IUIAutomationElementArray;
    ALen     : Integer;
    i        : Integer;
    iElement : IUIAutomationElement;
    AddNode  : TTreeNode;
  begin
    if pParent = nil then exit;

    pParent.FindAll(Scope, pCondition, found);
    found.Get_Length(ALen);

    //i=0は自分自身なので除外
    for i := 1 to ALen - 1 do begin
      found.GetElement(i, iElement);
      AddNode := AddChildNode(iElement, ATree, ANode);
      EnumAccTreeItems(iElement, Scope, pCondition, ATree, AddNode);
      Application.ProcessMessages;
      Sleep(1);
    end;
  end;
  //---------------------------------------------------------------------------


var
  AForm       : TWindowTreeForm;
  Ret         : Integer;
  RootElement : IUIAutomationElement;
  RootNode    : TTreeNode;
  Scope       : TreeScope;
  varProp     : OleVariant;
  pCondition  : IUIAutomationCondition;
begin
  if Assigned(RectForm) then begin
    RectForm.Close;
    FreeAndNil(RectForm);
  end;
  ListView1.PopupMenu := nil;

  //ウィンドウの選択ダイアログを表示
  Ret := mrOk;
  if Sender <> nil then begin
    AForm := TWindowTreeForm.Create(Self);
    try
      Ret := AForm.ShowModal;
      if Ret = mrOk then begin
        Self.Update;
        hTargetWnd := AForm.hTargetWnd;
      end;
    finally
      FreeAndNil(AForm);
    end;
  end;

  //ウィンドウの選択ダイアログのモーダル結果がmrOkだったら列挙開始
  if Ret = mrOk then begin
    Screen.Cursor := crHourGlass;

    TreeView1.Items.Clear;
    ListView1.Items.Clear;

    //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
    if Succeeded(UIAuto.ElementFromHandle(hTargetWnd, RootElement)) then begin

      //検索条件設定
      //pConditionに検索条件のポインタが格納される
      TVariantArg(varProp).vt    := VT_BOOL;
      TVariantArg(varProp).vbool := True;
      UIAuto.CreatePropertyCondition(UIA_IsControlElementPropertyId,
                                     varProp,
                                     pCondition);
      Scope := TreeScope_Element or TreeScope_Children;

      //TreeViewのルートノード生成
      RootNode := AddChildNode(RootElement, TreeView1, nil);

      TreeView1.Items.BeginUpdate;
      //検索、列挙実行
      EnumAccTreeItems(RootElement, Scope, pCondition, TreeView1, RootNode);
      TreeView1.Items.EndUpdate;
      TreeView1.TopItem.Expand(False);
    end;
    Screen.Cursor := crDefault;
  end;
end;

//=============================================================================
//  [ツリー更新]ボタン
//  オブジェクトの再取得
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  if TreeView1.Items.Count > 1 then begin
    Button1Click(nil);
  end;
end;

//=============================================================================
//  [全て展開]ボタン
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  if Assigned(RectForm) then begin
    RectForm.Close;
    FreeAndNil(RectForm);
  end;

  if TreeView1.Items.Count = 0 then exit;

  TreeView1.Items.BeginUpdate;
  TreeView1.FullExpand;
  TreeView1.TopItem := TreeView1.Items[0];
  TreeView1.Items.EndUpdate;
end;

//=============================================================================
//  [デフォルトアクション実行]ボタン
//  オブジェクトのデフォルト動作を実行
//  全てのオブジェクトに対して有効なわけではない
//
//  IUIAutomationInvokePatternインターフェイスのInvokeでは機能しないコントロー
//  ルがあったので、IUIAutomationLegacyIAccessiblePatternインターフェイスで
//  レガシー(?)なIAccessibleのDoDefaultActionを実行
//  これでも動作しないコントロールがある(Windowsのバージョンとアプリによる)
//=============================================================================
procedure TForm1.SpeedButton1Click(Sender: TObject);
var
  pUIAutoInfo : PplUIAutoInfoRec;
  iElement    : IUIAutomationElement;
  iInter      : IInterface;
  AccPattern  : IUIAutomationLegacyIAccessiblePattern;
begin
  if TreeView1.Selected = nil then exit;

  //NodeのDataプロパティからオブジェクトのデータを取出す
  //このデータはレコード型で格納されている
  pUIAutoInfo := TreeView1.Selected.Data;
  iElement    := pUIAutoInfo.Element;

  if Assigned(iElement) then begin
    iElement.GetCurrentPattern(UIA_LegacyIAccessiblePatternId, iInter);
    if Assigned(iInter) then begin
      if iInter.QueryInterface(IID_IUIAutomationLegacyIAccessiblePattern,
                               AccPattern) = S_OK then begin
        //IUIAutomationLegacyIAccessiblePatternのDoDefaultAction実行
        AccPattern.DoDefaultAction;
      end;
    end;
  end;
end;

//=============================================================================
//  [値のセット]ボタン
//  オブジェクトに対してIUIAutomationValuePatternインターフェイスのSetValue
//  メソッドでを実行
//  オブジェクトに値をセットする。編集可能なオブジェクトが対象
//  全てのオブジェクトに対して有効なわけではない
//
//  InputQueryで1行だけとしているが、実際にはTMemo等への複数行のセットも可能
//=============================================================================
procedure TForm1.SpeedButton2Click(Sender: TObject);
var
  pUIAutoInfo : PplUIAutoInfoRec;
  iElement    : IUIAutomationElement;
  iInter      : IInterface;
  ValPattern  : IUIAutomationValuePattern;
  StrText     : WideString;
  StrInput    : String;
begin
  if TreeView1.Selected = nil then exit;

  //NodeのDataプロパティからオブジェクトのデータを取出す
  //このデータはレコード型で格納されている
  pUIAutoInfo := TreeView1.Selected.Data;
  iElement    := pUIAutoInfo.Element;

  iElement.GetCurrentPattern(UIA_ValuePatternId, iInter);
  if Assigned(iInter) then begin
    if iInter.QueryInterface(IID_IUIAutomationValuePattern,
                             ValPattern) = S_OK then begin
      ValPattern.Get_CurrentValue(StrText);
      StrInput := StrText;
      if InputQuery('値のセット', '', StrInput) then begin
        ValPattern.SetValue(StrInput);
      end;
    end;
  end;
end;

//=============================================================================
//  TreeView1のOnChangeイベント処理
//  TreeView1の選択ノードが変更になったら、プロパティ等を取得してListViewに表示
// TVariantArgの使用にはusesにActiveXが必要
//=============================================================================
procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode);

  //---------------------------------------------------------------------------
  //  ListViewにItemを追加するメソッド
  //---------------------------------------------------------------------------
  procedure ListViewItemAdd(LV: TListView; ACaption, SubItem: String);
  var
    LVItem : TListItem;
  begin
    LVItem               := LV.Items.Add;
    LVItem.Caption       := ACaption;
    LVItem.SubItems.Text := SubItem;
  end;
  //---------------------------------------------------------------------------

var
  ShowFlag    : Cardinal;
  pUIAutoInfo : PplUIAutoInfoRec;
  iElement    : IUIAutomationElement;
  AutoId      : WideString;
  StrText     : WideString;
  pIntValue   : Pointer;
  ItemRect    : TRect;
  iInter      : IInterface;
  ValPattern  : IUIAutomationValuePattern;
  varChild    : OleVariant;
  varState    : OleVariant;
  TmpInt      : Integer;
  Len         : Cardinal;
  i           : Integer;
  StateID     : Integer;
  TmpStr      : String;
  LineWidth   : Integer;
  LineColor   : TColor;
begin
  //オブジェクトの周りに表示する矩形用のフォームを生成
  if not Assigned(RectForm) then begin
    RectForm := TplWindowRectForm.Create(Self);
    RectForm.Show;
    TreeView1.SetFocus;
  end else begin
    RectForm.Canvas.FillRect(RectForm.ClientRect);
  end;
  ShowFlag := SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE or SWP_NOOWNERZORDER;
  SetWindowPos(RectForm.Handle, HWND_TOPMOST, 0, 0, 0, 0, ShowFlag);


  //NodeのDataプロパティからオブジェクトのデータを取出す
  //このデータはレコード型で格納されている
  pUIAutoInfo := Node.Data;
  iElement    := pUIAutoInfo.Element;
  AutoId      := pUIAutoInfo.AutoId;

  ListView1.Items.Clear;
  ListView1.Items.BeginUpdate;

  //AutomationIdの値を追加
  //アプリ内で必ずしも一意の値ではないが、固有の値
  ListViewItemAdd(ListView1, 'AutomationId', AutoID);


  //ウィンドウのクラス名を取得して追加
  iElement.Get_CurrentClassName(StrText);
  ListViewItemAdd(ListView1, 'ClassName', StrText);


  //ウィンドウハンドルを取得して追加
  StrText := '';
  iElement.Get_CurrentNativeWindowHandle(pIntValue);
  if HWND(pIntValue) <> 0 then begin
    StrText := Format('$%.8x (%d)', [HWND(pIntValue), HWND(pIntValue)]);
  end;
  ListViewItemAdd(ListView1, 'NativeWindowHandle', StrText);


  //ControlTypeの値を取得して追加(数値)
  iElement.Get_CurrentControlType(TmpInt);
  ListViewItemAdd(ListView1, 'ControlTypeId', IntToStr(TmpInt));


  //ControlTypeの値を取得して追加(テキスト)
  iElement.Get_CurrentLocalizedControlType(StrText);
  ListViewItemAdd(ListView1, 'ControlType', StrText);


  //ContentNameの値を取得して追加
  iElement.Get_CurrentName(StrText);
  ListViewItemAdd(ListView1, 'ContentName', StrText);


  //Valueの値(テキスト等)を取得して追加
  StrText := '';
  iElement.GetCurrentPattern(UIA_ValuePatternId, iInter);
  if Assigned(iInter) then begin
    if iInter.QueryInterface(IID_IUIAutomationValuePattern,
                             ValPattern) = S_OK then begin
      ValPattern.Get_CurrentValue(StrText);
    end;
  end;
  ListViewItemAdd(ListView1, 'ValuePattern.Value', StrText);


  //HelpTextの値を取得して追加
  iElement.Get_CurrentHelpText(StrText);
  ListViewItemAdd(ListView1, 'HelpText', StrText);


  //AcceleratorKeyの値を取得して追加
  iElement.Get_CurrentAcceleratorKey(StrText);
  ListViewItemAdd(ListView1, 'AcceleratorKey', StrText);


  //AccessKeyの値を取得して追加
  iElement.Get_CurrentAccessKey(StrText);
  ListViewItemAdd(ListView1, 'AccessKey', StrText);


  //BoundingRectangleの値を取得して追加
  iElement.Get_CurrentBoundingRectangle(tagRect(ItemRect));
  StrText := '( ' + IntToStr(ItemRect.Left)   + ', ' +
                    IntTostr(ItemRect.Top)    + ', ' +
                    IntTostr(ItemRect.Right)  + ', ' +
                    IntToStr(ItemRect.Bottom) + ' )';
  ListViewItemAdd(ListView1, 'BoundingRectangle', StrText);


  //IAccessibleオブジェクトの子オブジェクトのIDを取得して追加
  StrText := '';
  iElement.GetCurrentPropertyValue(UIA_LegacyIAccessibleChildIdPropertyId,
                                   varChild);
  if TVariantArg(varChild).vt = VT_I4 then begin
    StrText := IntToStr(TVariantArg(varChild).lVal);
  end;
  ListViewItemAdd(ListView1, 'IAccessible ChildID', StrText);


  //IAccessibleオブジェクトのStateプロパティの値を取得し、文字列に変換して追加
  //Stateは複数の場合がある
  //Stateプロパティの値が2の倍数で増加していくことを利用してループ処理
  StrText := '';
  iElement.GetCurrentPropertyValue(UIA_LegacyIAccessibleStatePropertyId, varState);
  if TVariantArg(varState).vt = VT_I4 then begin
    TmpInt  := TVariantArg(varState).lVal;
    StateID := 1;
    for i := 1 to 29 do begin
      if (TmpInt and StateID) <> 0 then begin
        Len := GetStateTextW(StateID, nil, 0);
        SetLength(TmpStr, Len);
        GetStateTextW(StateID, PWideChar(TmpStr), Len);
        StrText := StrText + Trim(TmpStr) + '。' ;
      end;
      StateID := StateID * 2;
    end;
  end;
  ListViewItemAdd(ListView1, 'IAccessible State', StrText);


  //IAccessibleオブジェクトのDescriptionプロパティの値を取得して追加
  StrText := '';
  iElement.GetCurrentPropertyValue(UIA_LegacyIAccessibleDescriptionPropertyId,
                                   varState);
  if TVariantArg(varState).vt = VT_BSTR then begin
    StrText := TVariantArg(varState).bstrVal;
  end;
  ListViewItemAdd(ListView1, 'IAccessible Description', StrText);


  //IAccessibleオブジェクトのDefaultActionプロパティの値を取得して追加
  StrText := '';
  iElement.GetCurrentPropertyValue(UIA_LegacyIAccessibleDefaultActionPropertyId,
                                   varState);
  if TVariantArg(varState).vt = VT_BSTR then begin
    StrText := TVariantArg(varState).bstrVal;
  end;
  ListViewItemAdd(ListView1, 'IAccessible DefaultAction', StrText);

  ListView1.Items.EndUpdate;


  //オブジェクトを囲む枠線を描画
  LineWidth := 4;
  LineColor := clRed;
  with RectForm.Canvas do begin
    Pen.Color := LineColor;
    Pen.Width := LineWidth;

    MoveTo (ItemRect.Left,  ItemRect.Top);
    LineTo (ItemRect.Right, ItemRect.Top);
    LineTo (ItemRect.Right, ItemRect.Bottom);
    LineTo (ItemRect.Left,  ItemRect.Bottom);
    LineTo (ItemRect.Left,  ItemRect.Top);
  end;
  Sleep(1);
end;

//=============================================================================
//  TreeView1のOnDeletionイベント処理
//  TreeViewのDataプロパティに代入したデータの解放処理
//=============================================================================
procedure TForm1.TreeView1Deletion(Sender: TObject; Node: TTreeNode);
begin
  if Assigned(Node.Data) then begin
    Dispose(PplUIAutoInfoRec(Node.Data));
    Node.Data := nil;
  end;
end;

//=============================================================================
//  ListView1のOnSelectItemイベント処理
//  Itemを選択したら、ポップアップメニューを使用可能にする
//=============================================================================
procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  if Selected then begin
    ListView1.PopupMenu := PopupMenu1;
  end else begin
    ListView1.PopupMenu := nil;
  end;
end;

//=============================================================================
//  ListView1のボップアップメニューの[コピー]
//  PopupMenu1のAutoHotkeysとAutoLineReductionプロパティはmaManual
//=============================================================================
procedure TForm1.N1Click(Sender: TObject);
var
  AIndex  : Integer;
  StrText : String;
begin
  AIndex  := ListView1.ItemIndex;
  StrText := ListView1.Items[AIndex].Caption + Char(VK_TAB) +
             ListView1.Items[AIndex].SubItems[0];
  Clipboard.AsText := StrText;
end;

end.




03_ブラウザ Firefox の [ 戻る ] [ 進む ] ボタンをクリック


Firefox ブラウザの [戻る] と [進む] ボタンを、Delphi から操作するサンプルです。
まず、Firefox のハンドルを取得します。これらのボタンオブジェクトの name プロパティは、[戻る] と [進む] となっています。Firefox のウィンドウハンドルの階層下のオブジェクトから、これらの name プロパティを持つオブジェクトを探し出しています。

[閉じる] ボタンのテストコードがありますが、Windows XP(SP3) + Firefox 12.0 では動作しましたが、Windows 7 U64(SP1) + Firefox 13.0 では動作しませんでした。
FireFox はウィンドウの階層構造が、バージョンによって変更されています。そこで、2015年04月26日の更新で、FireFox のウィンドウハンドルを、EnumWindows のコールバック関数内で検出する方法に変更しました。この変更で、FireFox 37.0.2 では [戻る] [進む] [閉じる] が全て動作するようになっています。この変更がない場合、FireFox 37.0.2 では全てのボタンが機能しません。



図4
実行中の画面

リスト3
Firefox ブラウザの [戻る] と [進む] ボタンを、Delphi 側から操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, UIAutomationClient_TLB;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    UIAuto : TCUIAutomation;
    procedure SearchElementInvokeByName(RootElement: IUIAutomationElement;
      StrText: WideString);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses ActiveX;

{$R *.dfm}

//対象のウィンドウの情報を格納するレコード型の定義
type
  TTargetWndInfo = record
    hWindow   : HWND;
    ClassName : String;
  end;
  PTargetWndInfo = ^TTargetWndInfo;


//-----------------------------------------------------------------------------
//  EnumWindowsのコールバック関数
//  可視で他に所有されていないトップレベルのウィンドウだけを検出
//  PTargetWndInfoはウィンドウの情報を格納するレコード型のポインタ
//-----------------------------------------------------------------------------
function EnumVisbleNoOwnedWndProc(hWindow: HWND; lData: PTargetWndInfo): BOOL; stdcall;

  //---------------------------------------------------------------------------
  //  引数のウィンドウがオーナーウィンドウであるかを調査
  //---------------------------------------------------------------------------
  function IsOwnedWindow(hWindow: HWND): Boolean;
  begin
    Result := Windows.GetWindow(hWindow, GW_OWNER) <> 0;
  end;

  //-----------------------------------------------------------------------------
  //  ウィンドウハンドルからウィンドウクラス名を取得
  //-----------------------------------------------------------------------------
  function GetClassNameStr(hWindow: HWND): string;
  var
    Buffer : array[0..MAX_PATH-1] of Char;
    Len    : Integer;
  begin
    FillChar(Buffer, SizeOf(Buffer), #0);
    Len := GetClassName(hWindow, Buffer, Length(Buffer));
    if Len > 0 then Result := Buffer else Result := '';
  end;
  //-----------------------------------------------------------------------------

var
  LClassName : String;
begin
  Result := True;
  if IsOwnedWindow(hWindow) then exit;
  if not Windows.IsWindowVisible(hWindow) then exit;

  //クラス名が同じだったら、そのウィンドウハンドルをコールバックの引数に返す
  LClassName := UpperCase(GetClassNameStr(hWindow));
  if LClassName = UpperCase(lData^.ClassName) then begin
    lData^.hWindow := hWindow;
    Result := False;
  end;
end;

//=============================================================================
//  UIAutomationオブジェクトのインスタンスを生成
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  UIAuto := TCUIAutomation.Create(Self);
end;

//=============================================================================
//  [戻る]ボタンクリック
//  要素の検索にFindAllメソッドを使用した例
//  TVariantArgの使用にはusesにActiveXが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LWndInfo     : TTargetWndInfo;
  LTargetWnd   : HWND;
  LRootElement : IUIAutomationElement;
  LvarProp     : OleVariant;
  LScope       : TreeScope;
  LpCondition  : IUIAutomationCondition;
  LfoundArray  : IUIAutomationElementArray;
  Lfound       : IUIAutomationElement;
  LStrSearch   : WideString;
  LiInter      : IInterface;
  LpInvoke     : IUIAutomationInvokePattern;
begin
  //Firefoxのハンドルを取得
  LWndInfo.ClassName := 'MozillaWindowClass';
  LWndInfo.hWindow   := 0;
  //指定クラス名のウィンドウを検索
  EnumWindows(@EnumVisbleNoOwnedWndProc, LPARAM(@LWndInfo));
  LTargetWnd := LWndInfo.hWindow;
  if LTargetWnd = 0 then exit;

  //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
  if UIAuto.ElementFromHandle(LTargetWnd, LRootElement) = S_OK then begin

    //検索条件設定
    //Nameプロパティの値で検索
    //pConditionに検索条件のポインタが格納される
    LStrSearch := '戻る';
    TVariantArg(LvarProp).vt      := VT_BSTR;
    TVariantArg(LvarProp).bstrVal := PWideChar(LStrSearch);
    UIAuto.CreatePropertyCondition(UIA_NamePropertyId, LvarProp, LpCondition);

    LScope := TreeScope_Element or TreeScope_Children or TreeScope_Subtree;

    //検索実行
    LRootElement.FindAll(LScope, LpCondition, LfoundArray);
    //見つかった最初の要素だけを取り出す
    LfoundArray.GetElement(0, Lfound);
  end;

  //要素のInvoke関係のIUIAutomationInvokePatternインターフェイスを取得
  if Assigned(Lfound) then begin
    Lfound.GetCurrentPattern(UIA_InvokePatternId, LiInter);
    if Assigned(LiInter) then begin
      if LiInter.QueryInterface(IID_IUIAutomationInvokePattern,
                               LpInvoke) = S_OK then begin
        LpInvoke.Invoke;
      end;
    end;
  end;
end;

//=============================================================================
//  [進む]ボタンクリック
//  要素の検索にFindFirstメソッドを使用した例
//
//  FindFirstの方がFindAllよりも検索が早いらしい
//  MSDNの記事によると、Windows Presentation Foundation(WPF)コントロールの場合
//  はFindAllメソッドの方が速いらしい
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LWndInfo     : TTargetWndInfo;
  LTargetWnd   : HWND;
  LRootElement : IUIAutomationElement;
  LStrSearch   : WideString;
begin
  //Firefoxのハンドルを取得
  LWndInfo.ClassName := 'MozillaWindowClass';
  LWndInfo.hWindow   := 0;
  //指定クラス名のウィンドウを検索
  EnumWindows(@EnumVisbleNoOwnedWndProc, LPARAM(@LWndInfo));
  LTargetWnd := LWndInfo.hWindow;
  if LTargetWnd = 0 then exit;

  //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
  if UIAuto.ElementFromHandle(LTargetWnd, LRootElement) = S_OK then begin
    LStrSearch := '進む';
    SearchElementInvokeByName(LRootElement, LStrSearch);
  end;
end;

//=============================================================================
//  [閉じる]ボタンクリック
//  要素の検索にFindFirstメソッドを使用した例
//
//  Windows 7 + Firefox 13.0では動作しない
//
//  FindFirstの方がFindAllよりも検索が早いらしい
//  MSDNの記事によると、Windows Presentation Foundation(WPF)コントロールの場合
//  はFindAllメソッドの方が速いらしい
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LWndInfo     : TTargetWndInfo;
  LTargetWnd   : HWND;
  LRootElement : IUIAutomationElement;
  LStrSearch   : WideString;
begin
  //Firefoxのハンドルを取得
  LWndInfo.ClassName := 'MozillaWindowClass';
  LWndInfo.hWindow   := 0;
  //指定クラス名のウィンドウを検索
  EnumWindows(@EnumVisbleNoOwnedWndProc, LPARAM(@LWndInfo));
  LTargetWnd := LWndInfo.hWindow;
  if LTargetWnd = 0 then exit;

  //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
  if UIAuto.ElementFromHandle(LTargetWnd, LRootElement) = S_OK then begin
    LStrSearch := '閉じる';
    SearchElementInvokeByName(LRootElement, LStrSearch);
  end;
end;

//=============================================================================
//  NameプロパティのオブジェクトをRootElementの要素の階層下から検索
//  検索に成功したら、デフォルトのアクションを実行
//  IUIAutomationInvokePatternインターフェイスのInvokeでは機能しないコントロー
//  ルがあったので、IUIAutomationLegacyIAccessiblePatternインターフェイスで
//  レガシー(?)なIAccessibleのDoDefaultActionを実行
//  これでも動作しないコントロールがある(OSとアプリによる)
//
//  TVariantArgの使用には、usesにActiveXが必要
//=============================================================================
procedure TForm1.SearchElementInvokeByName(RootElement: IUIAutomationElement;
  StrText: WideString);
var
  LvarProp     : OleVariant;
  LScope       : TreeScope;
  LpCondition  : IUIAutomationCondition;
  Lfound       : IUIAutomationElement;
  LiInter      : IInterface;
  LAccPattern  : IUIAutomationLegacyIAccessiblePattern;
begin
  //検索条件設定
  //Nameプロパティの値で検索
  //pConditionに検索条件のポインタが格納される
  TVariantArg(LvarProp).vt      := VT_BSTR;
  TVariantArg(LvarProp).bstrVal := PWideChar(StrText);

  UIAuto.CreatePropertyCondition(UIA_NamePropertyId, LvarProp, LpCondition);
  LScope := TreeScope_Element or TreeScope_Children or TreeScope_Subtree;

  //検索実行
  RootElement.FindFirst(LScope, LpCondition, Lfound);

  //要素のIUIAutomationLegacyIAccessiblePatternインターフェイスを取得
  if Assigned(Lfound) then begin
    Lfound.GetCurrentPattern(UIA_LegacyIAccessiblePatternId, LiInter);
    if Assigned(LiInter) then begin
      if LiInter.QueryInterface(IID_IUIAutomationLegacyIAccessiblePattern,
                                LAccPattern) = S_OK then begin
        //IUIAutomationLegacyIAccessiblePatternのDoDefaultAction実行
        LAccPattern.DoDefaultAction;
      end;
    end;
  end;
end;

end.




04_アクセサリの電卓を操作


Windows 7 の [アクセサリ] の [電卓] を起動して、操作します。実際には、
  • 100 + 350 =

という計算を、Delphi のコードからキーを押して計算します。

オブジェクトの情報取得、あるいは操作には、AutomationId の値を使用する方が確実です。
AutomationId の値は同じアプリ内で一意ということになっていますが、この電卓の場合、Windows XP と Windows 7 とでは同じ数字のボタンでも違っています。そこで、このサンプルでは、どちらでも簡単にコード修正をしてテストできるように、Name プロパティの値でオブジェクトを検索しています。



[備考]
アクセサリの「電卓」は、Windows XP と Windows 7 とでは、全く別のアプリケーションです。
Windows 7 の電卓の各キーも、クラス名が Button のウィンドウコントロールですが、ウィンドウの階層構造が Windows XP の電卓とは違います。



図5
[電卓] の操作

リスト4
Windows 7 のアクセサリの電卓を操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, UIAutomationClient_TLB;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    UIAuto     : TCUIAutomation;
    hTargetWnd : HWND;
    procedure SearchElementInvokeByName(RootElement: IUIAutomationElement;
      StrText: WideString);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses ActiveX, ShellAPI;

{$R *.dfm}

//=============================================================================
//  UIAutomationオブジェクトのインスタンスを生成     ClassName  CalcFrame
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  UIAuto := TCUIAutomation.Create(Self);
end;

//=============================================================================
//  [電卓起動]ボタンクリック
//  ShellExecuteの使用には、usesにShellAPIが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  hTargetWnd := FindWindow(nil, '電卓');
  if hTargetWnd <> 0 then exit;

  ShellExecute(Handle, nil, 'calc.exe', nil, nil, SW_SHOW);
end;

//=============================================================================
//  [電卓操作]ボタンクリック
//  ボタンの名前(Nameプロパティの値)も、数字以外は違っているので注意
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  RootElement : IUIAutomationElement;
  SleepTime   : Cardinal;
begin
  hTargetWnd := FindWindow(nil, '電卓');
  if hTargetWnd = 0 then exit;

  //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
  if UIAuto.ElementFromHandle(hTargetWnd, RootElement) = S_OK then begin

    SleepTime := 800;

    //Windows XPでは CE
    SearchElementInvokeByName(RootElement, '入力のクリア');
    Sleep(SleepTime);

    SearchElementInvokeByName(RootElement, '1');
    Sleep(SleepTime);
    SearchElementInvokeByName(RootElement, '0');
    Sleep(SleepTime);
    SearchElementInvokeByName(RootElement, '0');
    Sleep(SleepTime);

    //Windows XPでは +
    SearchElementInvokeByName(RootElement, '加算');
    Sleep(SleepTime);

    SearchElementInvokeByName(RootElement, '3');
    Sleep(SleepTime);
    SearchElementInvokeByName(RootElement, '5');
    Sleep(SleepTime);
    SearchElementInvokeByName(RootElement, '0');
    Sleep(SleepTime);

    //Windows XPでは =
    SearchElementInvokeByName(RootElement, '等号');
  end;
end;

//=============================================================================
//  NameプロパティのオブジェクトをRootElementの要素の階層下から検索
//  検索に成功したら、IUIAutomationInvokePatternインターフェイスのInvokeを実行
//  TVariantArgの使用には、usesにActiveXが必要
//=============================================================================
procedure TForm1.SearchElementInvokeByName(RootElement: IUIAutomationElement;
  StrText: WideString);
var
  varProp     : OleVariant;
  Scope       : TreeScope;
  pCondition  : IUIAutomationCondition;
  found       : IUIAutomationElement;
  iInter      : IInterface;
  pInvoke     : IUIAutomationInvokePattern;
begin
  //検索条件設定
  //Nameプロパティの値で検索
  //pConditionに検索条件のポインタが格納される
  TVariantArg(varProp).vt      := VT_BSTR;
  TVariantArg(varProp).bstrVal := PWideChar(StrText);

  UIAuto.CreatePropertyCondition(UIA_NamePropertyId, varProp, pCondition);
  Scope := TreeScope_Element or TreeScope_Children or TreeScope_Subtree;

  //検索実行
  RootElement.FindFirst(Scope, pCondition, found);

  //要素のIUIAutomationInvokePatternインターフェイスを取得
  if Assigned(found) then begin
    found.GetCurrentPattern(UIA_InvokePatternId, iInter);
    if Assigned(iInter) then begin
      if iInter.QueryInterface(IID_IUIAutomationInvokePattern,
                               pInvoke) = S_OK then begin
        //IUIAutomationInvokePatternインターフェイスのInvokeメソッドを実行
        pInvoke.Invoke;
      end;
    end;
  end;
end;

end.




05_音量ミキサーのボリュームの値を設定


Windows 7 の [音量ミキサー] の設定ダイアログを起動して、スピーカーのボリュームの値を変更します。値が最大値に設定されている場合は、半分の値に、そうでない場合は、最大値に設定します。


図6
音量ミキサーを起動
図7
ボリュームの値を設定

リスト5
Windows 7 の音量ミキサーのボリュームの値を設定
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, UIAutomationClient_TLB;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    UIAuto     : TCUIAutomation;
    hTargetWnd : HWND;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses ActiveX, ShellAPI;

{$R *.dfm}

//=============================================================================
//  UIAutomationオブジェクトのインスタンスを生成
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  UIAuto := TCUIAutomation.Create(Self);
end;

//=============================================================================
//  [音量ミキサー]のダイアログを閉じる
//=============================================================================
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  SendMessage(hTargetWnd, WM_CLOSE, 0, 0);
end;

//=============================================================================
//  [音量ミキサー起動]ボタンクリック
//  Windows 7 専用
//  ShellExecuteの使用には、usesにShellAPIが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  ShellExecute(Handle, nil, 'SndVol.exe', nil, nil, SW_SHOW);
  Sleep(1000);
  hTargetWnd := FindWindow('#32770', nil);
end;

//=============================================================================
//  [音量設定]ボタンクリック
//  Windows 7 専用
//  TVariantArgの使用には、usesにActiveXが必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  RootElement  : IUIAutomationElement;
  varProp      : OleVariant;
  Scope        : TreeScope;
  pCondition   : IUIAutomationCondition;
  found        : IUIAutomationElement;
  StrSearch    : WideString;
  iInter       : IInterface;
  pRangeVal    : IUIAutomationRangeValuePattern;
  CurrentValue : Double;
  MaxValue     : Double;
begin
  if hTargetWnd = 0 then exit;

  //ウィンドウハンドルからIUIAutomationElementオブジェクトを取得
  if UIAuto.ElementFromHandle(hTargetWnd, RootElement) = S_OK then begin

    //検索条件設定
    //ControlTypeプロパティの値で検索
    //pConditionに検索条件のポインタが格納される
    StrSearch := 'スライダー';
    TVariantArg(varProp).vt      := VT_BSTR;
    TVariantArg(varProp).bstrVal := PWideChar(StrSearch);
    UIAuto.CreatePropertyCondition(UIA_LocalizedControlTypePropertyId,
                                   varProp,
                                   pCondition);

    //TreeScope_Subtreeは含めない
    //含めると、他のスライダーが検索されてしまう
    Scope := TreeScope_Element or TreeScope_Children;

    //検索実行
    RootElement.FindFirst(Scope, pCondition, found);
  end;

  //要素のIUIAutomationRangeValuePatternインターフェイスを取得
  if Assigned(found) then begin
    found.GetCurrentPattern(UIA_RangeValuePatternId, iInter);
    if Assigned(iInter) then begin
      if iInter.QueryInterface(IID_IUIAutomationRangeValuePattern,
                               pRangeVal) = S_OK then begin
        //ボリュームの設定可能な最大値と現在の設定値を取得                       
        pRangeVal.Get_CurrentMaximum (MaxValue);
        pRangeVal.Get_CurrentValue   (CurrentValue);

        //設定可能な最大値と現在の設定値が同じなら半分の値に設定
        //それ以外は最大値に設定                       
        if Round(MaxValue) = Round(CurrentValue) then begin
          pRangeVal.SetValue(MaxValue / 2.0);
        end else begin
          pRangeVal.SetValue(MaxValue);
        end;
      end;
    end;
  end;
end;

end.




06_イベントハンドラ - エクセルの起動と終了を検出


UIAutomation のイベントハンドラを使用して、アプリケーションの起動と終了を検出するサンプルプログラムです。ここでは、イベントの処理を開始してから、最初に起動したエクセルだけを対象としています。
毎回、同じようなサンプルなので、追加として、メニューを開く時のイベントも検出するようにしてみました。電卓のキー操作を検出するというのも考えたのですが、電卓は、Windows XP と 7 ではオブジェクトの構成が違うのでやめました。

UIAutomation のイベント処理は、処理するイベントに応じたインターフェイスを使用します。

  • (1) 検出するイベントに応じたインターフェイスを実装
  • (2) そのインターフェイスのインスタンスを生成
  • (3) 検出するイベントに応じた、イベント登録用関数を実行
    この時、対象のオブジェクトとスコープ (子、孫オブジェクトを含めるのか) を決める
  • (4) イベント検出用インターフェイスの関数内で、検出したイベントを処理
  • (5) 不要になったら、RemoveAllEventHandlers 関数等でイベントの登録を削除
という手順になります。実装したインターフェイスの関数は、自動で呼び出され、コールバック関数のような役割を行います。
イベントの登録解除用の関数である、RemoveAllEventHandlers の実行には少し時間がかかるようです。このため、イベントハンドラ実行中にフォームを閉じると、例外が発生することがあります。


図8
設計時画面
図9
実行例

リスト6
エクセルの起動と終了を検出
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, UIAutomationClient_TLB, ActiveX;

type
  //イベント検出用のクラスの定義
  //HandleAutomationEventは継承元のIUIAutomationEventHandlerの関数
  TplUIEventHandler = class(TInterfacedObject, IUIAutomationEventHandler)
  private
    function HandleAutomationEvent(const sender: IUIAutomationElement;
      eventId: SYSINT): HRESULT; stdcall;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    UIAuto       : TCUIAutomation;
    RootElement : IUIAutomationElement;
    EventHandler : TplUIEventHandler;
    hTargetWnd   : HWND;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム作成時の処理
//  (1) UIAutomationオブジェクトのインスタンスを生成
//  (2) イベントハンドラー用のクラスのインスタンスを生成
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  UIAuto       := TCUIAutomation.Create(Self);
  EventHandler := TplUIEventHandler.Create;
  RootElement  := nil;

  hTargetWnd := 0;

  Button1.Enabled := True;
  Button2.Enabled := False;
end;

//=============================================================================
//  フォームを閉じる時の処理
//=============================================================================
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  UIAuto.RemoveAllEventHandlers;
end;

//=============================================================================
//  [Start EVENT]ボタン
//  検出するイベントを設定
//  システムの状態によっては少し時間がかかることがある
//
//  本サンプルでは、全て、IUIAutomationEventHandlerインタフェイスを継承した
//  TplUIEventHandlerというクラスを実装して、そのメンバーの関数で検出
//  イベントごとにクラスを生成して検出してもよい
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Scope : TreeScope;
begin
  Button1.Enabled := False;
  Screen.Cursor   := crHourGlass;

  //ここではデスクトップ直下のオブジェクトのみを対象とするので
  //GetRootElement関数でデスクトップのオブジェクトを取得
  if RootElement = nil then begin
    if UIAuto.GetRootElement(RootElement) <> S_OK then exit;
  end;

  Scope := TreeScope_Subtree;        //直接の子オブジェクトのみを対象とする

  UIAuto.AddAutomationEventHandler(
    UIA_Window_WindowOpenedEventId,  //ウィンドウが開くイベントを検出
    RootElement,                     //対象のエレメント
    Scope,                           //スコープ
    nil,                             //キャッシュは使用しない
    EventHandler);                   //イベント検出用クラスのインスタンス

  UIAuto.AddAutomationEventHandler(
    UIA_Window_WindowClosedEventId,  //ウィンドウが閉じる時のイベントを検出
    RootElement,                     //対象のエレメント
    Scope,                           //スコープ
    nil,                             //キャッシュは使用しない
    EventHandler);                   //イベント検出用クラスのインスタンス

  UIAuto.AddAutomationEventHandler(
    UIA_MenuOpenedEventId,           //メニューが開く時のイベントを検出
    RootElement,                     //対象のエレメント
    Scope,                           //スコープ
    nil,                             //キャッシュは使用しない
    EventHandler);                   //イベント検出用クラスのインスタンス

  Button1.Enabled := False;
  Button2.Enabled := True;
  Screen.Cursor   := crDefault;
end;

//=============================================================================
//  [Stop EVENT]ボタン
//  イベントの検出を終了
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Button2.Enabled := False;

  UIAuto.RemoveAllEventHandlers;

  RootElement := nil;
  Button1.Enabled := True;
  Button2.Enabled := False;
end;

{ TplUIEventHandler }

//-----------------------------------------------------------------------------
//  ウィンドウのクラス名を取得する関数
//-----------------------------------------------------------------------------
function GetClassNameStr(hWindow: HWND): String;
var
  Buffer : array[0..MAX_PATH-1] of Char;
  Len    : Integer;
begin
  System.FillChar(Buffer, SizeOf(Buffer), #0);
  Len := Windows.GetClassName(hWindow, Buffer, Length(Buffer));
  if Len > 0 then Result := Buffer else Result := '';
end;

//=============================================================================
//  イベントハンドラーのコード
//
//  sender  : イベントを発生した要素
//  eventId : イベント識別子
//
//  UIA_Window_WindowClosedEventId識別子では、ウィンドウが閉じたことが検出され
//  るだけで、ハンドルやプロパティは取得できないことに注意
//  そこで、クラス名が取得できるかどうかで存在を確認している
//=============================================================================
function TplUIEventHandler.HandleAutomationEvent(
  const sender: IUIAutomationElement; eventId: SYSINT): HRESULT;
var
  StrText : WideString;
begin
  //イベント識別子の値で処理を分岐
  case eventId of

    UIA_Window_WindowOpenedEventId:
    begin
      //NativeWindowHandle値を取得して、クラス名で判定
      sender.Get_CurrentNativeWindowHandle(Pointer(Form1.hTargetWnd));
      if GetClassNameStr(Form1.hTargetWnd) = 'XLMAIN' then begin
        Form1.Memo1.Lines.Add('Open : Microsoft Excel');
      end;
    end;

    UIA_Window_WindowClosedEventId:
    begin
      if Form1.hTargetWnd <> 0 then begin
        if GetClassNameStr(Form1.hTargetWnd) = '' then begin
          Form1.Memo1.Lines.Add('Close : Microsoft Excel');
          Form1.hTargetWnd := 0;
        end;
      end;
    end;

    UIA_MenuOpenedEventId:
    begin
      sender.Get_CurrentName(StrText);
      Form1.Memo1.Lines.Add('NemuOpen : ' + StrText);
    end;
  end;

  Result := S_OK;
end;

end.




07_イベントハンドラ用のインターフェイス


イベントハンドラ (イベントハンドラー) 用のインターフェイスには、下表のようなものがあります。使用するインターフェイスは、表2 のイベントハンドラ登録用関数に、どれを使用するかによって異なります。
これらのインターフェイスには、イベント検出用の関数があります。この関数内でイベントを検出して処理することになります。この関数は、実際には実装部がありません。利用者が作成します。
関数の名前、引数の型と数は、インターフェイスによって違います。UIAutomationClient_TLB.pas で確認してください。

表1
イベントハンドラ用のインターフェイス

IUIAutomationEventHandler 一般的な UIAutomation のイベント処理用
IUIAutomationFocusChangedEventHandler キーボードフォーカスの移動イベント処理用
IUIAutomationPropertyChangedEventHandler プロパティの変更イベント処理用
IUIAutomationStructureChangedEventHandler オブジェクトのツリー構造の変更検出用


下のコードは、IUIAutomationEventHandler インターフェイスの宣言部の例です。このコードのように、継承元に TInterfacedObject を含めると、_AddRef、_Release、QueryInterface 等、基本的なメソッドを実装しなくて済むので便利です。

リスト7
UIAutomationClient_TLB.pas 内の IUIAutomationEventHandler インターフェイスの定義
// *********************************************************************//
// インターフェイス: IUIAutomationEventHandler
// フラグ: (256) OleAutomation
// GUID: {146C3C17-F12E-4E22-8C27-F894B9B79C69}
// *********************************************************************//
  IUIAutomationEventHandler = interface(IUnknown)
    ['{146C3C17-F12E-4E22-8C27-F894B9B79C69}']
    function HandleAutomationEvent(const sender: IUIAutomationElement; 
      eventId: SYSINT): HResult; stdcall;
  end;

リスト8
上のインターフェイスを継承した、イベントハンドラ用インターフェイスの実装例
  TplUIEventHandler = class(TInterfacedObject, IUIAutomationEventHandler)
  private
    function HandleAutomationEvent(const sender: IUIAutomationElement;
      eventId: SYSINT): HRESULT; stdcall;
  end;




08_イベントハンドラの登録用関数とイベントの識別子


イベントハンドラ (イベントハンドラー) 登録用の関数類です。
これらの関数の引数の型、数は、関数によって異なります。また、使用するイベントハンドラ検出用のインターフェイスに応じた関数を使用します。例えば、IUIAutomationEventHandler を使用する時は、AddAutomationEventHandler を使用します。

表2
イベントハンドラの登録用関数類

AddAutomationEventHandler 一般的な UIAutomation のイベント処理用
AddFocusChangedEventHandler フォーカスの移動イベント処理用
AddPropertyChangedEventHandler プロパティ変更イベントの処理用
AddPropertyChangedEventHandlerNativeArray プロパティの変更イベント処理用
AddStructureChangedEventHandler オブジェクトのツリー構造の変更検出用


次の表は、イベントハンドラとして、上の表の AddAutomationEventHandler メソッドを使用する場合のイベントの識別子です。その他の関数、イベントハンドラ用のインターフェイスを使用する場合は、UIAutomationClient_TLB.pas 内の、先頭が UIA_ の定数値を参考にしてください。各定数の機能については、定数名から判断するか、以下のリンクの MSDN の記事を参考にします。


表3
AddAutomationEventHandlerメソッドで使用するイベントの識別子

UIA_AsyncContentLoadedEventId 非同期のコンテンツがロードされる時
UIA_AutomationFocusChangedEventId フォーカスが別の要素に移動した時
UIA_AutomationPropertyChangedEventId プロパティの値の変更時
UIA_Drag_DragStartEventId 要素をドラッグ開始した時
UIA_Drag_DragCancelEventId ドロップターゲット上で要素を削除する前に、ドラッグ操作を終了した時
UIA_Drag_DragCompleteEventId ドロップターゲット上の要素をドロップした時
UIA_DropTarget_DragEnterEventId ドロップターゲットの境界内に要素をドラッグした時
UIA_DropTarget_DragLeaveEventId ドロップターゲットの境界から要素をドラッグした時
UIA_DropTarget_DroppedEventId ドロップターゲット上の要素をドロップした時
UIA_HostedFragmentRootsInvalidatedEventId 不明
UIA_InputDiscardedEventId 指定された入力の廃棄または失敗時
UIA_InputReachedOtherElementEventId 指定された入力が、対象の要素以外の要素に達した時
UIA_InputReachedTargetEventId 指定されたマウスやキーボードの入力が対象の要素に達した時
UIA_Invoke_InvokedEventId コントロールが呼び出されるかアクティブになった時
UIA_LayoutInvalidatedEventId コントロール内の子項目のレイアウトの変更時
UIA_LiveRegionChangedEventId ライブリージョン内容の変更
UIA_MenuClosedEventId メニューが閉じた時
UIA_MenuModeEndEventId メニューモードの終了時
UIA_MenuModeStartEventId メニューモードの開始時
UIA_MenuOpenedEventId メニューが開かれた時
UIA_Selection_InvalidatedEventId コンテナ内の選択が大幅に変更されたときに発生
UIA_SelectionItem_ElementAddedToSelectionEventId 項目がコレクションに追加された時
UIA_SelectionItem_ElementRemovedFromSelectionEventId 選択項目の削除時
UIA_SelectionItem_ElementSelectedEventId selectionプロパティの変更時
UIA_StructureChangedEventId オブジェクトのツリー構造変更時のイベント
UIA_SystemAlertEventId システムアラート発行時
UIA_Text_TextChangedEventId テキストが変更される時
UIA_Text_TextSelectionChangedEventId テキストの選択変更時
UIA_ToolTipClosedEventId ツールチップが閉じられた時
UIA_ToolTipOpenedEventId ツールチップが開かれた時
UIA_Window_WindowClosedEventId ウィンドウが閉じられた時
UIA_Window_WindowOpenedEventId ウィンドウが開かれた時




09_ツール類  -  Inspect Objects ( Inspect.exe )


Microsoft Windows SDK 7.1 には、IAccessible, UI Automation 関係のツールが付属しています。
Inspect Objects と Accessble Event Watcher というツールです。

下の 2 つの図は、Inspect Objects です。実行ファイル名は Inspect.exe です。SDK 7.0 にも同名のツールが付属していますが、ウィンドウの階層表示の機能はありません。
このツールは、IAccessible または UI Automation インターフェイスを使用して、ウィンドウハンドルを持たないオブジェクト類を階層表示し、操作も可能ですが、MSDN の [IAccessible インターフェイス] の説明にある通り、全てのコントロールに対して有効というわけではありません。
[スタート] メニューから起動できます。ツリーのオブジェクトを選択、クリックすると、そのオブジェクトのプロパティを右のペインに表示します。

このようなシステム関係の操作に関するツール類は、「管理者として実行」しないと、動作機能が制限されることがあります。



図10
Inspect.exe を起動
  • 当然、管理者として実行する
図11
Inspect.exe の実行時の画面

Windows SDK 8.1 にも同じツールが付属しています。
Windows SDK 8.1 のこのツールは、デフォルトの状態では [スタート] メニューには登録されていません。下図のように、[bin] フォルダ内にあります。機能は Microsoft Windows SDK 7.1 のものと同じようです。


図12
inspect.exe がある場所

Windows XP に Microsoft Windows SDK 7.1 をインストールして実行した場合、次のようなエラーが表示される時は、[Description of the Windows Automation API] のページから、Windows XP 用の更新プログラムをダウンロードしてインストールすると、解決するかも知れません。

The program was unable to query the UI Automation client interfaces. Please confirm that the latest framework is installed.


  • 図13
  • Windows XPでの起動時のエラー表示




10_ツール類  -  Accessble Event Watcher ( AccEvent.exe )


Microsoft Windows SDK 7.1 に付属の Accessble Event Watcher の実行ファイル名は AccEvent.exe です。これも下図のように [スタート] メニューから起動できます。
AccEvent.exe はイベントをフックして、そのオブジェクトを表示するツールです。デフォルトではフォーカス移動したオブジェクトを追跡して表示します。


図14
AccEvent.exe の起動
図15
AccEvent.exe の実行時の画面

Windows SDK 8.1 にも同じツールが付属しています。
Windows SDK 8.1 のこのツールは、デフォルトの状態では [スタート] メニューには登録されていません。下図のように、[bin] フォルダ内にあります。機能は Microsoft Windows SDK 7.1 のものと同じようです。


図16
AccEvent.exe がある場所




11_ツール類  -  VisualUIAVerifyNative.exe


Windows SDK 8.1 には、上記以外にも UI Automation 関係ツールが付属しています。これらも [bin] フォルダ内にあります。下図はその 1 つです。
詳しくは下記のマイクロソフトの記事で確認してください。


図17
VisualUIAVerifyNative.exe の場所
図18
VisualUIAVerifyNative.exe を実行




12_ツール類  -  Accessible Explorer ( AccExplorer32.exe ) 等


[Active Accessibility の UI サービス] の記事に、前述の Inspect Object の他に、テストツールとして [Accessible Explorer] というのがあります。これも Inspect Object 同様、IAccessible インターフェイスを使用して、コントロール類の情報の取得や操作ができるようになっています。実行ファイル名は、AccExplorer32.exe です。

IAccessible, UI Automation インターフェイス関係のツールではありませんが、マイクロソフトでは、SPY++ というツールも提供しています。これは、Visual Studio に付属しているツールです。ただし、無料版の Express Edition には含まれていません。Windows SDK 7.0 には UI SPY というツールが付属していましたが、7.1 には付属していません。
AccExplorer32.exe, SPY++, UI SPY 共に、後述の GitHub からダウンロード可能です。

また、ManagedSpy の紹介記事もありますが、これは、筆者の Windows 7 の 64 ビット版では正常起動しませんでした。32 ビットの Windows では正常に起動しましたが、動作が緩慢です。
記事中では「新しい ManagedSpy ユーティリティ」となっていますが、.NET Framework 2.0 時代に提供されていたものです。.NET Framework の 2.0 より前、あるいは新しいバージョンで作成されたアプリケーションを起動した状態で、この ManageSpy を起動すると、例外が発生して、DLL がメモリ上に残ってしまうことがありました。

この ManageSpy の動作を見ると、.NET Framework による開発では、.NET Framework のバージョンにも注意する必要があると言う、教訓を得ることができます。


[備考 1]
2019年08月06日現在、Accessible Explorer と UI Spy tools の提供は廃止されています。



図19
AccExplorer32.exe の起動画面
図20
ウィンドウを選択表示
  • [Options] から
    [Choose window from list]]
    [Select window with Mouse]
    のどちらかで、対象ウィンドウを選択する




13_ツール類  -  Winspector

IAccessible, UI Automation インターフェイス関係のツールではありませんが、Winspector というツールがあります。
これは SPY++ の拡張版としての機能があり、ウインドウハンドルを持つコントロールの情報の取得やウィンドウメッセージ等の監視用ツールです。
現在、提供元のサイトは存在しませんが、Internet Archive または SOFTPEDIA からダウンロードはできます。

Delphi 7 までは、SPY++ と同じような機能を持つ WinSight というツールが付属していましたが、現在は付属していないようです。



図21
Winspector ダウンロード




14_ツール類の一括ダウンロード  ( SPY++ を含む )


本ページで紹介したツール類の全てではありませんが、以下の GitHub から各種のツール類をまとめてダウンロードできます。現在、マイクロソフトが正式には提供していないツール類もあります。
ファイル名の SPYXX.EXE は SPY++ のことです。


図22
GitHub からダウンロードしたツール類