pyAA

pythonmsaa を使う。nvdajp のための技術調査。

pyAA の私家版ビルド

資料

チュートリアル(繋がりにくい)

API

download

    • pyAA 2.2 は Python 2.4 でないとインストールできないと言われる。NVDA の開発に関わっているので Python 2.6 を使いたい。
    • 念のため Python 2.4 + pyAA-2.2.win32-py2.4.exe も試したが、後述する2バイト文字の不具合は同じく存在する。

CVS から落とす?

    • おとしてみた。python setup.py install がエラーになる。
    • def swig_sources(self, sources, extension=None): でエラーが出なくなる
    • swig で怒られる。どうやら swig の win32 版を入れて、msvc を入れる必要がある
    • "msc v.1500 32 bit" とはどうやら "visual studio 2008" のことらしいので 2008 を入れる。
    • SWIG の swigwin-1.3.40.zip も使用。PATHを通しておく。

pyAA CVS HEAD ためしてみる

チュートリアルとずいぶん仕様がちがう。デバッグメッセージがバリバリ表示される。

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

インストールやりなおす: Python 2.6 + pyAA (CVS simpler)

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 + pyAA (CVS simpler)

少し違うが許容範囲。

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)

pyAA でIMEのGUIオブジェクトを探す

上記のサンプルを 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

修正版 : EVENT_OBJECT_HIDE も拾ってみる

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 []

MS-IME 2002 の場合

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

Microsoft Office IME 2007 の場合

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

pyAAでMS-IME2002の候補文字列を取得する

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)

Python 2.6 + pyAA (CVS simpler) + 2バイト文字問題修正

ここまで試して、ちゃんと動かすためにもう一つハードルが見つかった。

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,漢字

MS-IME 2002 で次候補選択のイベントも拾う

http://msdn.microsoft.com/ja-jp/library/ms971336.aspx の「IME による Active Accessibility サポートを使用して候補文字列にアクセスする」によると

  • 1. 重要な WinEvent OBJECT_SHOW および OBJECT_SELECTION をフックします。
  • 2. OBJECT_SHOW イベントを受け取ったら、 STATE_SYSTEM_SELECTED 状態の子オブジェクト IAccessible を検索して、 現在の候補文字列を検出します。
  • 3. OBJECT_SELECTION イベントを受け取ったら、 選択されている候補文字列の IAccessible を取得します。

となっているので、素直にやってみる。

以下、改良版。

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 中野です 一覧項目 選択されています。
pyaa.txt · 最終更新: 2010/02/16 00:00 (外部編集)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0