COM, ATL

Active Accessibility - IAccessible 인터페이스 이용 접근

디버그정 2008. 9. 10. 20:17

1. Active Accessibility?


이번 강좌는 아마도 델파이 사용자들에게는
좀 생소한 항목이 될것 같네요. 왜냐면 볼랜드에서 이 주제와 관련한
dll을 델파이에서 사용할 수 있도록 유닛화 해놓지 않았기 때문입니다.
이 Active Accessiblity는 OleAcc.DLL 이란 라이브러리에서 지원이 되며,
Win98 이상의 OS에서만 사용이 가능합니다. MSDN을 살펴보면 이 기능이
Win95 에서는 지원이 안되는 관계로 라이브러리내의 API를 이용할때는
동적 로딩을 사용하라고 충고하고 있습니다. 정적 로딩을 사용할 경우
Win95에서는 Active Accessiblity 관련 기능만 못쓰는게 아니라 프로그램이
아예 실행도 되지 못할테니까 말이죠. 이 유닛 파일은 VC의 헤더 화일과
델파이의 임포트 결과를 참고해서 제가 만들었는데, 이 강좌에서는 그냥
정적 로딩으로 진행합니다.



컴퓨터 상에 동작중인 다른 프로그램을
제어하는 일은 재미있지만 쉬운일이 아니죠. 대상 프로그램이 OLE를
지원해 준다면 (IE 또는 MediaPlayer 처럼) 그나마 수월하게 제어가
가능하겠지만, OLE를 지원하지는 않는 일반 어플리케이션일 경우는 어떻게들
할까요? 보통은 메세지를 이용하죠. 원하는 컨트롤에 메세지를 날리는
방식인데요, 이처럼 내 프로그램이 아닌 다른 영역에 접근 하는것을
Accessiblity 라고 합니다.


오해를 하실까봐 미리 말씀드리지만
이 기능을 이용한다 하더라도 외부의 컨트롤을 직접 제어하는 것은 불가능
합니다. 다만 해당 컨트롤의 핸들을 알아오거나 하는 것이 고작입니다.


2. 무엇을 할 수 있나?


윈도우 표준 컨트롤들은 대부분(모두가
아닌 대부분입니다.) IAccessible 이라는 인터페이스를 지원합니다.
정확히 말한다면 지원하는 게 아니라 저 인터페이스에 바인딩 되어 있다고
하는게 맞겠네요. 저 인터페이스는 자체에 내장된 인터페이스가 아니라
필요에따라 외부에서 생성됩니다. IAccessible을 통하여 어떤 창을 뒤져볼때
알아 낼수 있는 정보는 창위에 어떤 컨트롤들이 있는지 따위의 정보와
컨트롤 속의 자식 컨트롤 등을 뒤져 볼 수 있고, 핸들을 가진 컨트롤이라면
그 핸들을 알아 낼 수 있는 정도 입니다. 그외 컨트롤의 조작은 이를
통해 알아낸 핸들을 통해 메세지를 날리는 방법밖에 없습니다. IAccessible
인터페이스는 멤버함수들의 이름만으로도 기능을 짐작할 수 있을 정도로
쉬운 인터페이스입니다.


3. 예제


먼저 IAccessible에 좀 더 친숙해
지기 위해 간단한 예제를 보죠.


procedure TForm1.Button1Click(Sender: TObject);
var
  H : HWND;
  ACC : IAccessible;
  ENM : IEnumVariant;
  I,K : Integer;
  Fetched : DWORD;
  Child,Role,State : OleVariant;
  Buf, RoleText, StateText: String;
  StateInt : DWORD;
  cBuf : Array[0..256] of Char;
begin
  Memo1.Clear;

  H := FindWindow(nil, '제목 없음 - 메모장');
  if H=0 then Exit;

  if FAILED(AccessibleObjectFromWindow(H, OBJID_WINDOW, IAccessible, ACC)) or
     FAILED(ACC.QueryInterface(IEnumVariant, ENM))
     then
    Exit;


  ENM.Reset;

  if ACC.accChildCount>0 then for I:=0 to ACC.accChildCount-1 do begin
     Child := NULL;
     if Assigned(ENM) then ENM.Next(1, Child, Fetched);
     if Child = NULL then begin
        TVarData(Child).VType := varInteger;
        TVarData(Child).VInteger := I;
     end;

     Role  := ACC.accRole[Child];
     GetRoleText(TVarData(Role).vInteger, cBuf, 256);
     RoleText := cBuf;
     State := ACC.accState[Child];
     StateInt := TVarData(State).vInteger;
     StateText := '';
     K := 1;

     while K<>0 do begin
       if (K and StateInt)<>0 then begin
         GetStateText(K, cBuf, 256);
         StateText := StateText + cBuf + ',';
       end;

       K := K Shl 1;
     end;

     Buf := Format('%2d : %s'#13#10 +
          '   - Desc  : %s'#13#10 +
          '
  - Role  : %s'#13#10 +
          '   - State : %s'#13#10,
          [I+1, ACC.accName[Child],
           ACC.accDescription[Child],
           RoleText,
           StateText
          ]);

     Memo1.Lines.Add(Buf);
  end;
end;

 

먼저 "메모장"을 실행시키고
버튼을 눌러보면 결과가 나오는데, 설명을 하자면 이렇습니다.


1. FindWindow로 메모장의 핸들을 얻습니다.
2. 얻어진 핸들로부터 IAccessible 인터페이스를 뽑아냅니다.
3. 얻어진 IAccessible을 뒤져봅니다.
   이때 원래는 재귀호출을 사용해 자식의 자식까지 뒤져봐야 하겠지만 여기서는 간단히
   바로 아래 자식만 찾아봅니다.(자식 자식 하니깐 꼭 욕하는것 같죠? ^^;)


결과부터 확인하면 이해가 더 쉬울것 같네요.


 1 : 시스템메뉴
   - Desc  : 창을 조작하는 명령이 포함되어 있습니다.
   - Role  : 메뉴 모음
   - State :

2 :
   - Desc  : 창 이름을 표시하고 조작할 수 있는 컨트롤을 포함합니다.
   - Role  : 제목 표시줄
   - State : 포커스 가능,

3 : 응용 프로그램 메뉴
   - Desc  : 현재 보기나 문서를 조작하는 명령이 포함되어 있습니다.
   - Role  : 메뉴 모음
   - State :

4 : 제목 없음 - 메모장
   - Desc  :
   - Role  : 클라이언트
   - State :

5 : 세로 스크롤 막대
   - Desc  : 수직 보기 영역을 바꿉니다.
   - Role  : 스크롤 막대
   - State : 보이지 않음,

6 : 가로 스크롤 막대
   - Desc  : 수평 보기 영역을 바꿉니다.
   - Role  : 스크롤 막대
   - State : 보이지 않음,

7 : 크기 조절 상자
   - Desc  : 창 너비와 높이를 조정할 수 있습니다.
   - Role  : 크기 조절
   - State : 보이지 않음,

 

이런식으로 뒤져볼수가 있는 것입니다.

여기에 사용된 관련 API를 잠깐 짚고
넘어가죠.


function WindowFromAccessibleObject(Acc : IAccessible; var H : HWND) : HRESULT; stdcall;

function AccessibleObjectFromWindow(H: HWND; dwID : DWORD; const riid : TIID;
                                    out vObject) : HRESULT; stdcall;

AccessibleObjectFromWindow 함수는 핸들로부터 IAccessible 인터페이스로
뽑아내는 기능을 합니다. WindowFromAccessibleObject 함수는 그 반대 겠죠.
여기서 dwID에는 OBJID_WINDOW,OBJID_SYSMENU,OBJID_TITLEBAR,OBJID_CLIENT
등을 넣으면 되는데 델파이에서 ctrl+Click 해 보시면 더 나옵니다.
이름을 보고 판단해서 원하는걸 넣으면 됩니다.
 





인터페이스들 중에 IEnum... 으로 시작하는
인터페이스들이 자주 나오는데, 이들은 모두 열거형 데이터를
엑세스하기 위한 인터페이스입니다. 사용방식은 모두 비슷한데,
여기서는 IEnumUnknown으로 잠깐 설명하고 넘어가죠.


  IEnumUnknown = interface(IUnknown)
    ['{00000100-0000-0000-C000-000000000046}']
    function Next(celt: Longint; out elt; pceltFetched: PLongint): HResult; stdcall;
    function Skip(celt: Longint): HResult; stdcall;
    function Reset: HResult; stdcall;
    function Clone(out enm: IEnumUnknown): HResult; stdcall;
  end;


Next 메소드는 다음 데이터를 가져오라는
명령입니다. celt는 몇개를 가져오라는 건데 보통은 1개씩
가져오는게 편하겠죠. elt는 담길 공간이구요, pceltFetched는
실제로 몇개가 담겨왔다는 갯수가 기록되어 돌아옵니다.
필요없다면 nil을 넣으면 됩니다. 볼랜드에서 파스칼로
컨버젼 하는 과정에서 일관성이 없는게, 어떤곳은 여기처럼
포인터형을 그대로 두고 어떤곳은 var 타입으로 번역해
놓았더라구요.
Skip 메소드는 몇개를 건너뛰라는 명령이고,
Reset은 현재 포인트를 처음으로 돌리는 명령입니다. Clone은
말 그대로 자기를 복제하는 거겠죠.


여기에 사용된건 IEnumVariant 인터페이스
인데, 당연히 Variant 타입의 열거형 이겠죠. 자식들을
액세스하는데 필요한 이 Variant 타입인자는 두가지 경우가
있는데, vType=varInteger 인 경우와 vType=varDispatch
인 경우 입니다. 전자의 경우 이걸 매개로 Get_accChild
메소드를 호출하면 자식의 IAccessible 인터페이스를 얻을수
있고, 후자의 경우는 그 자체가 IAccessible 인터페이스
입니다.



이번에는 메모장에 특정 문자열들을
집어 넣는 프로그램을 만들어 보도록 하겠습니다. 이건 WM_SETTEXT
메세지를 이용하면 간단한 문제죠. 하지만 이 메세지를 사용하기 위해서는
메시지를 받을 핸들을 먼저 알아야 합니다.이 핸들을 찾아내는데, IAccessible
이 사용됩니다.
여기서 Role 이라는게 있는데, 이 Role은 해당 컨트롤의
속성을 담고 있습니다. 롤플레잉게임 할때 그 "롤"입니다. 배역,
역할 등의 의미가 있는데(왠 영어공부 --;), ROLE_SYSTEM_TITLEBAR
이면 타이틀바, ROLE_SYSTEM_COMBOBOX 면 콤보박스를 가리키는 겁니다.
메모나 에디터 컨트롤은 ROLE_SYSTEM_TEXT라는 Role을 가지게 됩니다.
그러면 IAccessible과 그자식들을 뒤지면서 저 Role을 찾으면 되겠군요.
에디터 박스가 두개 이상일 경우 등의 복잡한 조건하에서는 몇번째 해당
Role 컨트롤인지를 찾아야 하겠죠. 여기서는 자식의 자식까지 뒤져야하니까
재귀호출이 사용됩니다.



function TForm1.FindChildAccessibleWithRole(ACC : IAccessible; Role : Integer) : IAccessible;
var
  I : Integer;
  V : Variant;
  ChildACC : IAccessible;
  ChildVar : OleVariant;
  ENM : IEnumVariant;
  Fetched : DWORD;
begin
  Result := nil;

  V := ACC.accRole[CHILDID_SELF];

  if TVarData(V).VInteger = Role then begin
     Result := ACC;
     Exit;
  end;

  if FAILED(ACC.QueryInterface(IEnumVariant, ENM)) then Exit;

  ENM.Reset;

  if ACC.accChildCount>0 then for I:=0 to ACC.accChildCount - 1 do begin
     if FAILED(ENM.Next(1, ChildVar, Fetched)) or
        VarIsNULL(ChildVar) then Exit;

     try
       if TVarData(ChildVar).VType=varDispatch then begin
            if FAILED(IDispatch(TVarData(ChildVar).VDispatch).QueryInterface(IAccessible, ChildACC)) then Continue;
            end
         else if FAILED(ACC.accChild[ChildVar].QueryInterface(IAccessible, ChildACC)) or
            (ACC = ChildACC) then Continue;
         Result := FindChildAccessibleWithRole(ChildACC, Role);

         if Assigned(Result) then Exit;
     except
       Exit;
     end;
  end;
end;


procedure TForm1.Button2Click(Sender: TObject);
var
  H, ChildHandle : HWND;
  ChildACC, ACC : IAccessible;
  cBuf : PChar;
begin
  H := FindWindow(nil, '제목 없음 - 메모장');
  if H=0 then Exit;

  if FAILED(AccessibleObjectFromWindow(H, OBJID_WINDOW, IAccessible, ACC))
     then Exit;

  ChildACC := FindChildAccessibleWithRole(ACC, ROLE_SYSTEM_TEXT);
  if not Assigned(ChildACC) then Exit;

  if FAILED(WindowFromAccessibleObject(ChildACC, ChildHandle)) then Exit;

  GetMem(cBuf, Length(Memo1.Lines.Text)+1);
  StrPCopy(cBuf, Memo1.Lines.Text);
  SendMessage(ChildHandle, WM_SETTEXT, 0, LParam(cBuf));
  FreeMem(cBuf);
end;
 


소스만 던져두고 자세한 설명이 없는것 같아 좀 그렇지만, 사실 별로 설명할
것도 없습니다. 크게 어려운 것도 없죠.

MSDN에는 이런 예제도 포함되어 있더군요.
SetWindowEventHook으로 윈도우 이벤트를 감시하면서 원하는 이벤트가
잡히면
AccessibleObjectFromEvent
함수로 IAccessible 인터페이스를 얻어내서 할 일을 하는 예제인데요,
별로 쓸모는 없겠더라구요