since 2010-03-01
nvdajp pyaa msaa python_windows windows 等に関する調査。
since 2013-05-22
情報収集したのでメモしておく。
http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms629013%28v=vs.85%29.aspx
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE
http://msdn.microsoft.com/ja-jp/library/windows/desktop/aa380396%28v=vs.85%29.aspx
TF_SENTENCEMODE_NONE 0x0000 No information for sentence. TF_SENTENCEMODE_PLAURALCLAUSE 0x0001 The IME uses plural clause information to carry out conversion processing. TF_SENTENCEMODE_SINGLECONVERT 0x0002 The IME carries out conversion processing in single-character mode. TF_SENTENCEMODE_AUTOMATIC 0x0004 The IME carries out conversion processing in automatic mode. TF_SENTENCEMODE_PHRASEPREDICT 0x0008 The IME uses phrase information to predict the next character. TF_SENTENCEMODE_CONVERSATION 0x0010 The IME uses conversation mode. This is useful for chat applications. This is equivalent with IME_SMODE values for IMM32
http://blogs.msdn.com/b/tsfaware/archive/2007/05/30/what-is-a-keyboard.aspx
Windows Input Method の歴史 http://d.hatena.ne.jp/NyaRuRu/20070309/p1
IME (Input Method Editor) と Text Services Framework のアクセシビリティ
TSF (コードネーム Cicero)
IMM32 : TSFよりも古い時代のAPIで、Vista からは TSF に本格移行(??)
C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include にあるもの msctf.h msctf.idl
調査中
from comtypes.client import GetModule GetModule("shdocvw.dll") GetModule(["{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}", 1, 1])
のようなことができるらしいが、msctf.dll を GetModule するのはうまくいかなかったので、気持ちを切り替える。
nvda の generate.py が typelibs の中身から comInterfaces の中身を作っている。
http://www.python.jp/doc/contrib/ctypes/sum_sample_jp.html
によれば
midl コンパイラを midl sum.idl /tlb sum.tlb として動作させると, 型ライブラリ sum.tlb が生成されます.
となっている。midl は Visual Studio 2008 に入っている。
が、midl の実行で躓く。msctf.idl を SDK のディレクトリからコピーしてきて midl msctf.idl /tlb mctf.tlb を実行したが何も出力されない。
COMプログラミング : IDL 超入門「タイプライブラリは IDL をバイナリトークンにしたもの」
generate.py をコピーして改変する。
#a test code of TSF (based on NVDA code) by nishimotz # #generate.py #A part of NonVisual Desktop Access (NVDA) #Copyright (C) 2006-2007 NVDA Contributors <http://www.nvda-project.org/> #This file is covered by the GNU General Public License. #See the file COPYING for more details. import comtypes.client comtypes.client.gen_dir='.\\comInterfaces' import comtypes import sys sys.modules['comtypes.gen']=comtypes.gen=__import__("comInterfaces",globals(),locals(),[]) import os import sys from glob import glob COM_INTERFACES = ( ("sum", comtypes.client.GetModule, "sum.tlb"), ) def main(): print "COM interfaces:" for desc, func, interface in COM_INTERFACES: print "%s:" % desc, try: func(interface) print "done." except: print "not found." print if __name__ == "__main__": main() from comInterfaces.SumLib import _1F2B08DF_E59C_4F10_98D9_16078342F74C_0_1_0 o = _1F2B08DF_E59C_4F10_98D9_16078342F74C_0_1_0.ITfThreadMgr() print "ITfThreadMgr:" print dir(o) p = _1F2B08DF_E59C_4F10_98D9_16078342F74C_0_1_0.ITfThreadMgrEventSink() print "ITfThreadMgrEventSink:" print dir(p)
comInterfaces ディレクトリを作り、その中に init.py という名前の空ファイルを作る。 (nvdaのソースの真似)
ここにある sum.idl を改変する。
/* http://www.python.jp/doc/contrib/ctypes/sum_sample_jp.html */ /* A TypeLibrary, compiled to sum.tlb */ import "oaidl.idl"; import "comcat.idl"; import "textstor.idl"; import "ctfutb.idl"; [ uuid(1F2B08DF-E59C-4f10-98D9-16078342F74C), // generated by nishimotz version(1.0), helpstring("Sum 1.0 Type Library") ] library SumLib { importlib("stdole2.tlb"); [ object, uuid(aa80e801-2021-11d2-93e0-0060b067b86e), pointer_default(unique) ] interface ITfThreadMgr; [ object, uuid(aa80e80e-2021-11d2-93e0-0060b067b86e), pointer_default(unique) ] interface ITfThreadMgrEventSink; };
どうやら上記の4つのincludeでインタフェースの定義は終わっていて、library 宣言だけやればよいらしい。
名前は sum のままだが guidgen を実行してタイプライブラリの uuid は作り直した。 Interface の uuid は msctf.idl からコピーした。
Visual Studio 2008 のコマンドプロンプトで下記を実行:
>midl sum.idl Microsoft (R) 32b/64b MIDL Compiler Version 7.00.0500 Copyright (c) Microsoft Corporation 1991-2006. All rights reserved. Processing .\sum.idl sum.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\oaidl.idl oaidl.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\objidl.idl objidl.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\unknwn.idl unknwn.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\wtypes.idl wtypes.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\basetsd.h basetsd.h Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\guiddef.h guiddef.h Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\comcat.idl comcat.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\textstor.idl textstor.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\ctfutb.idl ctfutb.idl Processing .\msctf.idl msctf.idl Processing C:\Program Files\Microsoft SDKs\Windows\v6.0A\include\oaidl.acf oaidl.acf >python generate.py COM interfaces: sum: # Generating comtypes.gen._1F2B08DF_E59C_4F10_98D9_16078342F74C_0_1_0 # Generating comtypes.gen._00020430_0000_0000_C000_000000000046_0_2_0 # Generating comtypes.gen.stdole # Generating comtypes.gen.SumLib done. ITfThreadMgr: ['Activate', 'AddRef', 'AssociateFocus', 'CreateDocumentMgr', 'Deactivate', 'Enu mDocumentMgrs', 'EnumFunctionProviders', 'GetFocus', 'GetFunctionProvider', 'Get GlobalCompartment', 'IsThreadFocus', 'QueryInterface', 'Release', 'SetFocus', '_ AddRef', '_ITfThreadMgr__com_Activate', '_ITfThreadMgr__com_AssociateFocus', '_I TfThreadMgr__com_CreateDocumentMgr', '_ITfThreadMgr__com_Deactivate', '_ITfThrea dMgr__com_EnumDocumentMgrs', '_ITfThreadMgr__com_EnumFunctionProviders', '_ITfTh readMgr__com_GetFocus', '_ITfThreadMgr__com_GetFunctionProvider', '_ITfThreadMgr __com_GetGlobalCompartment', '_ITfThreadMgr__com_IsThreadFocus', '_ITfThreadMgr_ _com_SetFocus', '_IUnknown__com_AddRef', '_IUnknown__com_QueryInterface', '_IUnk nown__com_Release', '_QueryInterface', '_Release', '__class__', '__delattr__', ' __dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__map_case__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduc e_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_case_insensitive_', '_idlflags_', '_iid_', '_methods_'] ITfThreadMgrEventSink: ['AddRef', 'OnInitDocumentMgr', 'OnPopContext', 'OnPushContext', 'OnSetFocus', ' OnUninitDocumentMgr', 'QueryInterface', 'Release', '_AddRef', '_ITfThreadMgrEven tSink__com_OnInitDocumentMgr', '_ITfThreadMgrEventSink__com_OnPopContext', '_ITf ThreadMgrEventSink__com_OnPushContext', '_ITfThreadMgrEventSink__com_OnSetFocus' , '_ITfThreadMgrEventSink__com_OnUninitDocumentMgr', '_IUnknown__com_AddRef', '_ IUnknown__com_QueryInterface', '_IUnknown__com_Release', '_QueryInterface', '_Re lease', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__geta ttribute__', '__hash__', '__init__', '__map_case__', '__metaclass__', '__module_ _', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__size of__', '__str__', '__subclasshook__', '__weakref__', '_case_insensitive_', '_idl flags_', '_iid_', '_methods_']
メソッドの名前を拾えているということは、うまくいくかも。。。と思ったのだが。。。
ITfThreadMgrEventSink をどうやって登録すればいいのか分からない。
方針を変えて、ReadComp の tsf.{cpp,h} を眺めてみることにしたい。
CReadCompTSF::_AppendCompositionText() に文字列を取り出す処理がある。
ようするに ITfRange::GetText() メソッドがゴールだ。
ソリューション readcomp は2つのプロジェクト rcomplib (Win32 DLL) と readcomp (Win32 EXE) から構成される。
VC++2008 でビルドするときに static 変数名 = 値 という箇所がエラーになったので static int に修正。
実行するためには、両プロジェクトの Release ディレクトリに作られる rcomplib.dll と readcomp.exe を同じディレクトリにコピー。
ReadComp プログラムを実行し、別途 notepad.exe を実行すると、IMM と TSF の情報が画面右下に表示される。
MS-IME 2002 の場合は IMM と TSF の両方で表示される。ATOK 2009 の場合は IMM しか表示されない。
readcomp.cpp の WinMain() の中身:
InitInstance() は CreateWindow(), ShowWindow(), UpdateWindow() を呼び出す。いずれも Win32 API である。
InitApp() はやはり readcomp.cpp の中にあり、InitReadComp() をコールする。だがこの BOOL WINAPI InitReadComp() は rcomplib.cpp の中で実装されており、EXE から DLL の呼び出しになっている。
readcomp.cpp の先頭には #include "..\inc\rcomplib.h" という記述があり、この rcomplib.h に
BOOL WINAPI InitReadComp(); BOOL WINAPI UninitReadComp();
が宣言されている。
また readcomp プロジェクトのプロパティによると、リンカの引数に "..\dll\release\rcomplib.lib" がある。
BOOL WINAPI InitReadComp() の重要な仕事は SetWindowsHookEx(WH_GETMESSAGE, …) を呼ぶこと。
http://msdn.microsoft.com/ja-jp/library/cc430103.aspx
HHOOK SetWindowsHookEx( int idHook, // フックタイプ HOOKPROC lpfn, // フックプロシージャ HINSTANCE hMod, // アプリケーションインスタンスのハンドル DWORD dwThreadId // スレッドの識別子 );
WH_GETMESSAGE については 「メッセージキューへポストされたメッセージを監視する 1 個のフックプロシージャをインストールします。詳細については、GetMsgProc フックプロシージャの説明を参照してください。」 とされている。
http://msdn.microsoft.com/ja-jp/library/cc429822.aspx
LRESULT CALLBACK GetMsgProc( int code, // フックコード WPARAM wParam, // 削除オプション LPARAM lParam // メッセージ );
rcomplib.cpp で SysGetMsgProc() というフックプロシージャが定義される。
細かいところを飛ばすと、さらに UINT _SysGetMsgProc(WPARAM wParam, LPARAM lParam) が呼ばれる。
CReadCompTSF::Init(); IncrementDllRefCount(); switch (uMsg) { case WM_IME_STARTCOMPOSITION: case WM_IME_ENDCOMPOSITION: case WM_IME_COMPOSITION: case WM_KEYDOWN: case WM_KEYUP: UpdateIMMCompositionString(); break; default: break; }
ということで、やっと CReadCompTSF が出てきたが、けっきょくフックは IME がらみのキーイベントを引っかけている。
UpdateIMMCompositionString() の中身を見る。
ここでは GetFocus() して ImmGetContext() して ImmGetCompositionStringW() している。
Imm系のAPIは時代遅れという話だったが、ATOK2009 は Imm 系にしか対応していない。
いずれにせよ TSF 系の処理は絡んでいない。TSF を使うだけならウィンドウメッセージのフックは不要? と一瞬思ったが、大事なことが CReadCompTSF::Init() にあるはずなので後でじっくり見る。
TLS というクラスが登場する。tls.h で static メソッドを実装している。 TlsAlloc() や TlsGetValue() などが呼ばれている。
以下のメンバ変数が TLS クラスにある。
CReadCompTSF *_pReadCompTSF; CCompositionPopup *_pCompositionPopup;
http://msdn.microsoft.com/ja-jp/library/cc429388.aspx
TLS とはスレッドローカル記憶域のこと。関連APIは
ヘッダ : winbase.h で宣言されています。 インポートライブラリ : kernel32.lib とリンクします。
とのこと。
tls.cpp には BOOL TLS::InternalDestroyTLS() メソッドの定義がある。
TlsGetValue() の返り値を TLS* ptls に cast している。 そして ptls→_pReadCompTSF を Release() したり ptls→_pCompositionPopup を delete したり。。 最後に LocalFree(ptls) を実行。
TLSの解説(英語) http://support.microsoft.com/?scid=kb;en-us;94804&x=12&y=12
スレッドとTLSの参考資料 http://www7a.biglobe.ne.jp/~tsuneoka/win32tech/19.html
「スレッドはプロセスの仮想アドレス空間とグローバル変数を共有します. しかし,場合によっては各スレッドにローカルな静的記憶域があった方がよいことも あります」
「TLS(スレッドローカル記憶域)を利用して, プロセスの任意のスレッドがスレッドごとに異なる値を格納・取得できるようにする ことができます」
「(2) TLSインデックスを使わなければならないスレッドは,動的記憶域を割り当てて から,Win32 API TlsSetValueで,その記憶域を指すポインタとインデックスを 関連付ける」
「(3) スレッドは,記憶域をアクセスするとき,TLSインデックスを指定して Win32 API TlsGetValue を呼び出してポインタを取得する」
CCompositionPopup クラスは Imm 系APIで得た情報をウィンドウに表示するクラスと思われる。 IME側のスレッドからフックされて呼ばれるので readcomp 側のスレッドとの記憶域の分離が必要なのだろう。。
なお、最近の VC++ では __declspec(thread) という記述で TLS を簡単に扱えるらしい。
見落としていたことがあったので _SysGetMsgProc(WPARAM wParam, LPARAM lParam) に戻る。
_SysGetMsgProc(WPARAM wParam, LPARAM lParam) で先に呼ばれていた CReadCompTSF::Init() を確認。VC++2008の右クリックメニューで追っていく。
tsf.h では static BOOL Init(); として宣言されている。
tsf.cpp に BOOL CReadCompTSF::Init() がある。
やはり最初に TLS::GetTLS() を呼んでいた。
返り値の _pReadCompTSF がもし NULL であれば ptls→_pReadCompTSF = new CReadCompTSF; を実行して初期化。
その直後にやっと出てくるのがテキストサービスのCOMインタフェースだ。TSFスレッドマネージャのオブジェクトを作成している。 生成されたオブジェクトのポインタを ptim で受け取る。
ITfThreadMgr* ptim; HRESULT hr; hr = CoCreateInstance(CLSID_TF_ThreadMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, (void**)&ptim);
http://yokohama.cool.ne.jp/chokuto/urawaza/api/CoCreateInstance.html
CoCreateInstance() により「指定されたCLSIDに関連付けられたクラスの1つの未初期化オブジェクトを作成」する。
うまくいったら _InitThreadMgrSink() が呼び出される。
ptls->_pReadCompTSF->_InitThreadMgrSink();
中身は後で見る。
続き:こんどはドキュメントマネージャだ。スレッドマネージャのGetFocus()メソッドで取り出すらしい。
ITfDocumentMgr *pDocMgr; if (SUCCEEDED(ptim->GetFocus(&pDocMgr))) { if (pDocMgr) { ptls->_pReadCompTSF->_InitTextEditSink(pDocMgr); pDocMgr->Release(); } }
成功したら _InitTextEditSink() を実行。
_InitThreadMgrSink() と _InitTextEditSink() の中身が興味深い。
BOOL CReadCompTSF::_InitThreadMgrSink() を見る。
まず ThreadMgr に対して QueryInterface をかけて、ITfSource インタフェースを取得。
ITfSource *pSource; BOOL fRet; if (_pThreadMgr->QueryInterface(IID_ITfSource, (void **)&pSource) != S_OK) return FALSE;
MSDN の ms628941 によれば、下記に対して ITfSource の QueryInterface をかけることができ、それぞれ機能が違うと書かれている。
ITfThreadMgr ITfContext ITfCompartment ITfInputProcessorProfiles ITfLangBarItem
ここでは pSource は ITfThreadMgr の ITfSource であることを確認。
続きのコードは、pSource に対して AdviseSink を実行し、ITfThreadMgrEventSink として this を登録している。
fRet = FALSE; if (pSource->AdviseSink(IID_ITfThreadMgrEventSink, (ITfThreadMgrEventSink *)this, &_dwThreadMgrEventSinkCookie) != S_OK) { // make sure we don't try to Unadvise _dwThreadMgrEventSinkCookie later _dwThreadMgrEventSinkCookie = TF_INVALID_COOKIE; goto Exit; } fRet = TRUE; Exit: pSource->Release(); return fRet;
念のため tsf.h を確認すると確かに CReadCompTSF は ITfThreadMgrEventSink を実装している:
class CReadCompTSF : public ITfThreadMgrEventSink, public ITfTextEditSink
ms628945 によると最後の引数は
であり、コールバックを解除するときに必要な情報の格納場所。
pSource は AdviceSink だけのために使われる。終わったら IUnknown のメソッド Release() を呼んでいる。
TF_INVALID_COOKIE は SDK の msctf.h で定義されている。
まとめると、スレッドマネージャの Source インタフェースに ITfThreadMgrEventSink として CReadCompTSF のインスタンスを登録。
ms628973 で確認。これで CReadCompTSF の以下のメンバ関数が呼ばれるようになるわけだ。
STDAPI CReadCompTSF::OnSetFocus(ITfDocumentMgr *pDocMgrFocus, ITfDocumentMgr *pDocMgrPrevFocus)
を見ておく。
if (GetSharedMemory()→fReadCompRunning) の場合に下記を実行。
_InitTextEditSink(pDocMgrFocus);
けっきょく CReadCompTSF の初期化時に加えて ITfThreadMgrEventSink が OnSetFocus を受け取ったときにも、後述の _InitTextEditSink が呼ばれる。
ついでにGetSharedMemory()について見ると globals.h で以下の記述。
extern CReadCompSharedMem g_SharedMemory; inline SHAREDMEM *GetSharedMemory() { return g_SharedMemory.GetPtr(); } inline void FlushSharedMemory() { g_SharedMemory.Flush(0); }
このクラスは CReadCompFileMapping がスーパークラスであり filemap.h で実装されている。 OpenFileMapping という API が使われている。 cc430187 に説明があるが。。。
http://wisdom.sakura.ne.jp/system/winapi/win32/win150.html
によると「プロセス間データ共有」とのこと。
ここでは TEXT("ReadCompSharedMem") という名前の共有メモリを作成して、 下記の構造体のポインタを GetSharedMemory() が返す、と理解しておく。
typedef struct { HHOOK hSysGetMsgHook; BOOL fReadCompRunning; } SHAREDMEM;
BOOL CReadCompTSF::_InitTextEditSink(ITfDocumentMgr *pDocMgr) を見る。
引数は ITfThreadMgr の GetFocus() で得たオブジェクトである。
最初に
// clear out any previous sink first
につづく行がある。2回目以降に呼ばれるときの処理。
_dwTextEditSinkCookie != TF_INVALID_COOKIE の場合に _pTextEditSinkContext を使って ITfSource に UnadviseSink をかける。
引数 pDocMgr == NULL のときは、Unadvice だけ行って終了。 caller just wanted to clear the previous sink
1回目から必ず実行されるのはその下。ThreadMgrEventSink と同様だ。
pDocMgr→GetTop(&_pTextEditSinkContext) で NULL が返ったら抜ける:
最後に pSource→AdviseSink する:
if (_pTextEditSinkContext->QueryInterface(IID_ITfSource, (void **)&pSource) == S_OK) { if (pSource->AdviseSink(IID_ITfTextEditSink, (ITfTextEditSink *)this, &_dwTextEditSinkCookie) == S_OK) { fRet = TRUE; } else { _dwTextEditSinkCookie = TF_INVALID_COOKIE; } pSource->Release(); }
ITfTextEditSink は ms628962 に情報。
メソッドは1つだけだ。
STDAPI CReadCompTSF::OnEndEdit(ITfContext *pContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord) をみる。
FlushSharedMemory(); if (GetSharedMemory()->fReadCompRunning) _CheckComposition(pContext, ecReadOnly);
BOOL CReadCompTSF::_CheckComposition(ITfContext *pContext, TfEditCookie ecReadOnly) をさらに見る。
_ClearCompositionText(); hr = pContext->QueryInterface(IID_ITfContextComposition, (void **)&pContextComposition);
ここで ITfContext に QueryInterface して ITfContextComposition を取り出す。
ITfContextComposition の EnumCompositions で EnumCompositionView を取り出す。
EnumCompositionView の Next で ITfCompositionView を取り出す。
ITfCompositionView の GetRange で ITfRange を取り出す。
_AppendCompositionText() に ITfRange を渡す。
void CReadCompTSF::_AppendCompositionText(ITfRange *pRange, TfEditCookie ecReadOnly) も見てみる。
pRange->GetText(ecReadOnly, TF_TF_MOVESTART, wstr, ulcch, &ulcch);
これでやっと入力文字列が取り出せたことになる。
OnEndEdit() で受け取った ecReadOnly が最後まで使われていることになる。
ITfRange は ms628908 に情報。
BOOL CReadCompTSF::_CheckComposition(ITfContext *pContext, TfEditCookie ecReadOnly)
の最後に void CCompositionPopup::SetTSFCompositionString(WCHAR *psz) の呼び出しが出てくる。
ptls->_pCompositionPopup->SetTSFCompositionString(_pszCompositionText);
中では _pszTSFComposition にコピーするだけだ。
表示は
void CCompositionPopup::OnPaint(HWND hwnd, HDC hdc)
にて
TextOutW(hdc, 50, 1, _pszTSFComposition, lstrlenW(_pszTSFComposition));
によって実行される。
OnPaint() の呼び出しは
LRESULT CALLBACK CCompositionPopup::_WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
の中の case WM_PAINT: の処理である。Win32 のイベント処理がクラス化されている。