python で msaa を使う。nvdajp のための技術調査。
pyAA の私家版ビルド
チュートリアル(繋がりにくい)
API
download
CVS から落とす?
チュートリアルとずいぶん仕様がちがう。デバッグメッセージがバリバリ表示される。
C:\work\python\pyaa-cvs\pyAA>python Python 2.6.4 (r264:75708, Oct 26 2009, 08:23:19) [MSC v.1500 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import pyAA >>> import win32gui >>> hwnd = win32gui.GetDesktopWindow() >>> objid = pyAA.OBJID_WINDOW >>> ao = pyAA.AccessibleObjectFromWindow(hwnd, objid) IAccessible(247620) 0->Package >>> print ao.Name <bound method AccessibleObject.Name of <pyAA.AccessibleObject instance at 0x00AFDF80>> >>> >>> ao.ChildCount() 7
simpler ブランチがチュートリアルに近そうに見える。
>cvs -z3 -d:pserver:anonymous@uncassist.cvs.sourceforge.net:/cvsroot/uncassist co -r simpler -P -d pyAA-simpler pyAA
pyAAc_raw.i をいじる。
10: // #include "winable.h" 191: // %constant int CCHILDREN_FRAME = CCHILDREN_FRAME;
python setup.py build python setup.py install
少し違うが許容範囲。
Python 2.6.4 (r264:75708, Oct 26 2009, 08:23:19) [MSC v.1500 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import pyAA >>> import win32gui >>> h = win32gui.GetDesktopWindow() >>> o = pyAA.Constants.OBJID_WINDOW >>> ao = pyAA.AccessibleObjectFromWindow(h, o) >>> print ao.Name デスク >>> for c in ao.Children: ... print c.Name ... シス None アプリケ デスク 垂 水 None
文字が最後まで取れていない問題。後述する pyAA の修正で解決。。
以下、動いたコード。
import os, time import pyAA import pythoncom class LocationWatcher(pyAA.WindowWatcher): def Start(self): # create a deferred self.result = pyAA.Deferred() # register a hook to watch for window movement self.AddWinEventHook(callback=self.OnLocationEvent, event=pyAA.Constants.EVENT_OBJECT_LOCATIONCHANGE, obj_id=pyAA.Constants.OBJID_WINDOW) # return the deferred return self.result def OnLocationEvent(self, event): # create a new deferred r = pyAA.Deferred() # return the event and the deferred self.result.Callback(event, r) # store the new deferred self.result = r def Output(event, deferred): # print the object type and its location print event.ObjectName, event.AccessibleObject.Location # register this function again for the next event callback deferred.AddCallback(Output) if __name__ == '__main__': # watch for instances of windows explorer ww = LocationWatcher() r = ww.Start() r.AddCallback(Output) # a message pump is needed to receive events while 1: pythoncom.PumpWaitingMessages() time.sleep(0.05)
コマンドプロンプトで python test.py を実行して、 別のウィンドウをドラッグすると、下記のようなイベントが表示される。
OBJID_WINDOW (495, 18, 655, 848) OBJID_WINDOW (495, 18, 655, 848) OBJID_WINDOW (495, 18, 655, 848) OBJID_WINDOW (466, 18, 655, 848)
上記のサンプルを OBJECT_SHOW に変更すればここに書いてある 「候補選択 UI ウィンドウが開かれると、 ウィンドウから OBJECT_SHOW WinEvent が通知されます」 ができるんじゃない? と思って試す。
import os, time import pyAA import pythoncom class MyEventWatcher(pyAA.WindowWatcher): def Start(self): self.result = pyAA.Deferred() self.AddWinEventHook(callback=self.OnShowEvent, event=pyAA.Constants.EVENT_OBJECT_SHOW, obj_id=pyAA.Constants.OBJID_WINDOW) return self.result def OnShowEvent(self, event): r = pyAA.Deferred() self.result.Callback(event, r) self.result = r def Output(event, deferred): if event.AccessibleObject != None: print event.ObjectName, event.AccessibleObject.GetClassName() deferred.AddCallback(Output) if __name__ == '__main__': ww = MyEventWatcher() r = ww.Start() r.AddCallback(Output) while 1: pythoncom.PumpWaitingMessages() time.sleep(0.05)
if event.AccessibleObject != None のところは、たまに GetClassName() でエラーが出るのを回避するため。
実は MS-IME を消してしまっていた。まあ ATOK でやってみよう。
C:\work\python\pyaa-work>python test.py OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW ATOK22PaletteTemplate OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW ATOK22CompStr OBJID_WINDOW ATOK22ToolTip OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW ATOK22Cand OBJID_WINDOW ATOK22Scroll OBJID_WINDOW ATOK22PaletteTemplate OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW ATOK22PaletteTemplate OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW Static OBJID_WINDOW Static
でたー。じゃあ Google IME でも。
C:\work\python\pyaa-work>python test.py OBJID_WINDOW Static OBJID_WINDOW MSCTFIME Composition OBJID_WINDOW GoogleJapaneseInputCandidateWindow OBJID_WINDOW Static
import time import pyAA import pythoncom import win32gui class MyEventWatcher(pyAA.WindowWatcher): def Start(self): self.result = pyAA.Deferred() self.AddWinEventHook(callback=self.OnEvent, event=[pyAA.Constants.EVENT_OBJECT_SHOW, \ pyAA.Constants.EVENT_OBJECT_HIDE ], obj_id=pyAA.Constants.OBJID_WINDOW) return self.result def OnEvent(self, event): r = pyAA.Deferred() self.result.Callback(event, r) self.result = r def PrintInfo(eventname, classname, obj): role = obj.GetRoleText() state = obj.GetState() sel = obj.GetSelection() wh = obj.GetWindow() t = win32gui.GetWindowText(wh) print "%s %s %s %s %s %s" % (eventname, classname, role, state, sel, t) def Output(event, deferred): eventname = event.GetEventName() obj = event.AccessibleObject if eventname == "EVENT_OBJECT_SHOW": classname = obj.GetClassName() else: classname = "" if obj != None: PrintInfo(eventname, classname, obj) if classname[0:4] == "ATOK" or \ classname == "MSCTFIME" or \ classname[0:19] == "GoogleJapaneseInput": for c in event.AccessibleObject.GetChildren(): if c != None: PrintInfo("(children)", c.GetClassName(), c) deferred.AddCallback(Output) if __name__ == '__main__': ww = MyEventWatcher() r = ww.Start() r.AddCallback(Output) while 1: pythoncom.PumpWaitingMessages() time.sleep(0.05)
「Google日本語入力」で試す。EVENT_OBJECT_HIDEも表示するようにした。
GetSelection は試してみたが何も出てこない。
GetWindowText を使ってみたが変換候補は取れないらしい。 エディタのウインドウタイトルは取れるのだが。 WINDOWではなくCLIENTオブジェクトをフックする必要があるのだろうか。
EVENT_OBJECT_SHOW GoogleJapaneseInputCandidateWindow ウィンドウ 0 [] (children) GoogleJapaneseInputCandidateWindow メニュー バー 32768 [] (children) GoogleJapaneseInputCandidateWindow タイトル バー 32768 [] (children) GoogleJapaneseInputCandidateWindow メニュー バー 32768 [] (children) GoogleJapaneseInputCandidateWindow クライアント 0 [] (children) GoogleJapaneseInputCandidateWindow スクロール バー 32768 [] (children) GoogleJapaneseInputCandidateWindow スクロール バー 32768 [] (children) GoogleJapaneseInputCandidateWindow グリップ 32769 [] EVENT_OBJECT_HIDE ウィンドウ 32768 [] EVENT_OBJECT_SHOW GoogleJapaneseInputCandidateWindow ウィンドウ 0 [] (children) GoogleJapaneseInputCandidateWindow メニュー バー 32768 [] (children) GoogleJapaneseInputCandidateWindow タイトル バー 32768 [] (children) GoogleJapaneseInputCandidateWindow メニュー バー 32768 [] (children) GoogleJapaneseInputCandidateWindow クライアント 0 [] (children) GoogleJapaneseInputCandidateWindow スクロール バー 32768 [] (children) GoogleJapaneseInputCandidateWindow スクロール バー 32768 [] (children) GoogleJapaneseInputCandidateWindow グリップ 32769 [] EVENT_OBJECT_HIDE ウィンドウ 32768 [] EVENT_OBJECT_SHOW #32768 ウィンドウ 0 []
EVENT_OBJECT_SHOW,MSCTFIME Composition,ウィンドウ,利用できません。+フォーカスでき ます。,,None,None event from object OBJID_WINDOW EVENT_OBJECT_SHOW,CiceroUIWndFrame,ウィンドウ,利用できません。+フォーカスできます。 ,CiceroUIWndFrame,None,None event from object OBJID_CLIENT EVENT_OBJECT_SHOW,MSCandUIWindow_Candidate,クライアント,既定値,,None,None event from object OBJID_WINDOW EVENT_OBJECT_SHOW,MSCandUIWindow_Candidate,ウィンドウ,利用できません。+フォーカス できます。,,None,None event from object OBJID_WINDOW
C:\work\python\pyaa-work>python test.py event from object OBJID_WINDOW EVENT_OBJECT_SHOW,MSCTFIME Composition,ウィンドウ,利用できません。+フォーカスでき ます。,,None,None event from object OBJID_WINDOW event from object OBJID_WINDOW EVENT_OBJECT_SHOW,MSCTFIME Composition,ウィンドウ,利用できません。+フォーカスでき ます。,,None,None event from object OBJID_WINDOW EVENT_OBJECT_HIDE,,ウィンドウ,利用できません。+フォーカスできます。,,None,None event from object OBJID_WINDOW EVENT_OBJECT_HIDE,,ウィンドウ,利用できません。+フォーカスできます。,,None,None event from object OBJID_WINDOW EVENT_OBJECT_HIDE,,ウィンドウ,利用できません。+フォーカスできます。,,None,None event from object OBJID_WINDOW EVENT_OBJECT_HIDE,,ウィンドウ,利用できません。+フォーカスできます。,,None,None event from object OBJID_CLIENT EVENT_OBJECT_SHOW,mscandui40.candidate,クライアント,利用できません。+フォーカスで きます。,,None,None event from object OBJID_CLIENT EVENT_OBJECT_SHOW,mscandui40.comment,クライアント,利用できません。+フォーカスでき ます。,,None,None event from object OBJID_WINDOW
MSDN 文書番号: 418798
1. イベント フックをインストールします。 2. EVENT_OBJECT_SHOW イベントまたは EVENT_OBJECT_SELECTION イベントが通知 されたら、 候補 UI (クラス名 MSCandUIWindow_Candidate のウィンドウ) か ら通知されたものか確認します。 3. イベントを手がかりに IAccessible オブジェクトを取得します。 4. 取得した IAccessible オブジェクトの単純要素を調べ、 Role = ROLE_SYSTEM_LISTITEM であり、 かつ State & STATE_SYSTEM_SELECTED フラグが 0 以外のものを見つけます。 5. 該当する単純要素の Value プロパティから、現在選択されている候補文字列を 取得します。 IAccessible オブジェクトのすべての単純要素を検査するには注意が必要です。 現在の IME 2002 の実装では、IAccessible::get_accChildCount メソッドは実際に存在する 単純要素の数より 1 つ少ない値を返します。よって、このメソッドで取得した値を使う場合には、 取得した値 + 1 が実際の数であると仮定する必要があります。 この影響により、 AccessibleChildren 関数も同様に実際に表示されているよりも 1 つ少ない 単純要素しか返さないので、すべての単純要素を検査するには、Child ID = 1 から Child ID = IAccessible::get_accChildCount + 1 までの単純要素のプロパティを取得していく 必要があります。 IME 2002 では accLocation、accNavigate、accSelect、accDefaultAction、 accDoDefaultAction、get_accSelection などによる候補 UI 要素の操作および取得は サポートされていないので、候補文字列の取得は上に示した方法で行ってください。
と書かれているので、それに従ってやってみる。
だいたいやれたと思うコード:
# encoding: shift_jis import time import pyAA import pythoncom import win32gui class MyEventWatcher(pyAA.Watcher): def Start(self): self.result = pyAA.Deferred() self.AddWinEventHook(callback=self.OnCandEvent, event=[pyAA.Constants.EVENT_OBJECT_SHOW, pyAA.Constants.EVENT_OBJECT_SELECTION]) return self.result def OnCandEvent(self, event): r = pyAA.Deferred() self.result.Callback(event, r) self.result = r def Output(event, deferred): o = event.AccessibleObject if o != None: if o.GetClassName() == "MSCandUIWindow_Candidate": for c in o.GetChildren(): try: r = c.GetRole() s = c.GetState() if (r == pyAA.Constants.ROLE_SYSTEM_LISTITEM) and (s != pyAA.Constants.STATE_SYSTEM_SELECTED): v = str.decode(c.GetValue(), 'shift_jis') print "0x%x,0x%x,%s" % (r, s, v) except: pass deferred.AddCallback(Output) if __name__ == '__main__': ww = MyEventWatcher() r = ww.Start() r.AddCallback(Output) while 1: pythoncom.PumpWaitingMessages() time.sleep(0.05)
ここまで試して、ちゃんと動かすためにもう一つハードルが見つかった。
pyAA が2バイト文字の長さを正しく処理できていなかった。
下記のパッチを当てて、pyAA をインストールし直す。
Index: pyAAc_raw.i =================================================================== RCS file: /cvsroot/uncassist/pyAA/Attic/pyAAc_raw.i,v retrieving revision 1.1.2.9 diff -r1.1.2.9 pyAAc_raw.i 10c10 < #include "winable.h" --- > // #include "winable.h" 191c191 < %constant int CCHILDREN_FRAME = CCHILDREN_FRAME; --- > // %constant int CCHILDREN_FRAME = CCHILDREN_FRAME; 323c323 < PyObject* res = Unicode2PythonString(bname, SysStringLen(bname)); --- > PyObject* res = Unicode2PythonString(bname, SysStringByteLen(bname)); 385c385 < PyObject* res = Unicode2PythonString(bname, SysStringLen(bname)); --- > PyObject* res = Unicode2PythonString(bname, SysStringByteLen(bname)); 415c415 < PyObject* res = Unicode2PythonString(bname, SysStringLen(bname)); --- > PyObject* res = Unicode2PythonString(bname, SysStringByteLen(bname)); 445c445 < PyObject* res = Unicode2PythonString(bname, SysStringLen(bname)); --- > PyObject* res = Unicode2PythonString(bname, SysStringByteLen(bname)); 470c470 < s = Unicode2PythonString(res.bstrVal, SysStringLen(res.bstrVal)); --- > s = Unicode2PythonString(res.bstrVal, SysStringByteLen(res.bstrVal)); 541c541 < PyObject* res = Unicode2PythonString(bname, SysStringLen(bname)); --- > PyObject* res = Unicode2PythonString(bname, SysStringByteLen(bname)); 860c860 < result = Unicode2PythonString(bname, SysStringLen(bname)); --- > result = Unicode2PythonString(bname, SysStringByteLen(bname));
これで(MSDNに書かれている「最後の1要素を取れない不具合」はあるかも知れないが)動いた。。
C:\work\python\pyaa-work>python msime2002test.py 0x22,0x200000,莞爾 0x22,0x200000,貫之 0x22,0x200000,完次 0x22,0x200000,幹事 0x22,0x200000,感じ 0x22,0x200000,かんじ 0x22,0x200000,漢字 0x22,0x200000,莞爾 0x22,0x200000,貫之 0x22,0x200000,完次 0x22,0x200000,幹事 0x22,0x200000,感じ 0x22,0x200000,かんじ 0x22,0x200000,漢字
http://msdn.microsoft.com/ja-jp/library/ms971336.aspx の「IME による Active Accessibility サポートを使用して候補文字列にアクセスする」によると
となっているので、素直にやってみる。
以下、改良版。
is_ime_classname() にいろいろ書いているが MS-IME 2002 のみ対応。
# encoding: shift_jis import time import pyAA import pythoncom import win32gui class MyEventWatcher(pyAA.Watcher): def Start(self): self.result = pyAA.Deferred() self.AddWinEventHook(callback=self.OnCandEvent, event=[pyAA.Constants.EVENT_OBJECT_SHOW, pyAA.Constants.EVENT_OBJECT_SELECTION, pyAA.Constants.EVENT_OBJECT_CREATE, pyAA.Constants.EVENT_OBJECT_LOCATIONCHANGE]) return self.result def OnCandEvent(self, event): r = pyAA.Deferred() self.result.Callback(event, r) self.result = r def is_ime_classname(classname): if classname.startswith("ATOK") or \ classname.startswith("MSIME") or \ classname.startswith("MSCTFIME") or \ classname.lower().startswith("mscandui") or \ classname.startswith("CiceroUIWndFrame") or \ classname.startswith("GoogleJapaneseInput"): return True return False def Output(event, deferred): o = event.AccessibleObject try: classname = o.GetClassName() except: classname = '' if is_ime_classname(classname): try: objname = event.GetObjectName() eventname = event.GetEventName() print "%s,%s,%s" % (classname, objname, eventname) if eventname == "EVENT_OBJECT_SELECTION": w = o.GetWindow() v = str.decode(o.GetValue(), 'shift_jis') rt = str.decode(o.GetRoleText(), 'shift_jis') st = str.decode(o.GetStateText(), 'shift_jis') print " %x %s %s %s" % (w, v, rt, st) except: pass for c in o.GetChildren(): try: r = c.GetRole() if (r == pyAA.Constants.ROLE_SYSTEM_LISTITEM): w = c.GetWindow() v = str.decode(c.GetValue(), 'shift_jis') rt = str.decode(c.GetRoleText(), 'shift_jis') st = str.decode(c.GetStateText(), 'shift_jis') print " %x %s %s %s" % (w, v, rt, st) else: print " %x %s %s %s" % (w, '*', rt, st) except: pass deferred.AddCallback(Output) if __name__ == '__main__': ww = MyEventWatcher() r = ww.Start() r.AddCallback(Output) while 1: pythoncom.PumpWaitingMessages() time.sleep(0.05)
実行例。候補ウィンドウの表示(MSCandUIWindow_Candidate,OBJID_CLIENT,EVENT_OBJECT_SHOW)と、 スペースバーで次候補を選んだときのOBJID_CLIENT,EVENT_OBJECT_SELECTIONを拾っている。
Role や State もちゃんとdecodeすれば表示できる。
候補ウィンドウのアイテムは確かに1つ足りないことがあるが、SELECTION はちゃんと動いているので、 問題ないように思える。
MSCTFIME Composition,OBJID_WINDOW,EVENT_OBJECT_SHOW MSCTFIME Composition,OBJID_WINDOW,EVENT_OBJECT_SHOW CiceroUIWndFrame,OBJID_WINDOW,EVENT_OBJECT_SHOW MSCandUIWindow_Candidate,OBJID_CLIENT,EVENT_OBJECT_SHOW 1d0890 中のです 一覧項目 選択できます。 1d0890 仲のです 一覧項目 選択されています。 1d0890 中野です 一覧項目 選択できます。 1d0890 なかのです 一覧項目 選択できます。 1d0890 ナカノです 一覧項目 選択できます。 1d0890 仲野です 一覧項目 選択できます。 1d0890 仲之です 一覧項目 選択できます。 1d0890 那珂のです 一覧項目 選択できます。 MSCandUIWindow_Candidate,OBJID_CLIENT,EVENT_OBJECT_FOCUS 1d0890 中のです 一覧項目 選択できます。 1d0890 仲のです 一覧項目 選択されています。 1d0890 中野です 一覧項目 選択できます。 1d0890 なかのです 一覧項目 選択できます。 1d0890 ナカノです 一覧項目 選択できます。 1d0890 仲野です 一覧項目 選択できます。 1d0890 仲之です 一覧項目 選択できます。 1d0890 那珂のです 一覧項目 選択できます。 MSCandUIWindow_Candidate,OBJID_WINDOW,EVENT_OBJECT_SHOW MSCandUIWindow_Candidate,OBJID_CLIENT,EVENT_OBJECT_SELECTION 1d0890 中野です 一覧項目 選択されています。