TSF を使う (2) - tsfapp

Windows SDK のインストール

さて,実際に TSF を始めるにあたって,まずは Windows SDK をインストールするところから始めましょう.

TSF のサンプルプログラム

Windows SDK には TSF 関連のサンプルも含まれています.サンプルコードは,標準では以下の場所にインストールされているかと思います.

  • C:\Program Files\Microsoft SDKs\Windows\v6.0\Samples\winui
    • tsfapp
    • TSFcase
    • tsfcompart
    • TSFmark

ビルドに関する注意

これらはサンプルは Platform SDK の頃から収録されており,ビルドは当然 nmake です.とはいえ,デバッグのことを考えると Visual C++ 2005 で開発した方が楽でしょう.Visual C++ 2005 用のプロジェクトファイルを置いておきますのでご活用くださいませ.
http://www.dwahan.net/nyaruru/hatena/winui-tsfproj.zip
Visual C++ 2005 でビルドする場合,Windows SDK のインクルードディレクトリとライブラリディレクトリを追加するのを忘れないでください.TSF 関連のヘッダファイルが Vista のタイミングでアップデートされているので,旧バージョンのファイルを使用されないよう注意する必要があります.
TIP のサンプルである TSFcase と TSFmark はひとまず置いておいて,アプリケーション側の TSF 対応の参考になる tsfapp サンプルを見てみます.
何はともあれまずはビルドしてみましょう.うまくいけば一安心です.tsfapp は TSF 1.0 時代の機能しか使用しないので,実のところ古い SDK でも問題ありません.

tsfapp バグ修正その 1

真の問題は,このサンプルが Vista に付属する MSIME では意図通り動作しないことです.率直に言えばバグがあるのですが,Vista で遊ぶにはまずこのバグを修正しなければなりません.
TextStor.cpp を開いてください.次に CTSFEditWnd::InsertTextAtSelection メソッドの実装部分に移動します.メソッド先頭部分で,次のように NULL チェックを行っています.

//verify pwszText
if(NULL == pwszText)
{
    return E_INVALIDARG;
}

これを,次のように TS_IAS_QUERYONLY フラグのチェックの直後に移動します.

if(dwFlags & TS_IAS_QUERYONLY)
{
    *pacpStart = acpStart;
    *pacpEnd = acpOldEnd;
    return S_OK;
}

//移動してきた
//verify pwszText
if(NULL == pwszText)
{
    return E_INVALIDARG;
}

要するに NULL チェックが早すぎたんですね.この修正で Vista に付属する MSIME や MSIME 2007 でもコンポジション時に入力できるようになりました.

tsfapp バグ修正その 2

もう一点,tsfapp の終了時にヒープ破壊で警告が出ることがありますが,これも一緒に修正しておきましょう.
tsfedit.cpp 485 行目の Release で参照カウントが 0 になると,直後の m_hWnd へのアクセスは解放済みオブジェクトのメンバ変数に書き込んでいることになってしまいます.

case WM_NCDESTROY:
    pThis->Release();

    pThis->m_hWnd = NULL;
    break;

デストラクタ内で m_hWnd は使用されていないようなので,以下のように先に NULL 代入するように変更してみました.恐らくこれで大丈夫だと思いますが,気になる方はアルゴリズムを再度点検してみてください.

// 修正後
case WM_NCDESTROY:
    pThis->m_hWnd = NULL;
    pThis->Release();
    break;

tsfapp バグ修正その 3

(追記) まだバグらしきものがありました.
tsfedit.cpp の _IsLocked メソッドですが,2 つほど問題があります.

BOOL CTSFEditWnd::_IsLocked(DWORD dwLockType) 
{ 
    if(m_dwInternalLockType)
    {
        return TRUE;
    }

    return m_fLocked && (m_dwLockType & dwLockType); 
}

最初の問題は m_dwInternalLockType がコンストラクタで初期化されていないというものです.メモリに何かゴミが入っていれば常に TRUE を返してしまうことになるので,コンストラクタで m_dwInternalLockType を 0 で初期化するコードを追加しておきましょう.

CTSFEditWnd::CTSFEditWnd(HINSTANCE hInstance, HWND hwndParent)
{
    m_hWnd = NULL;
    m_hwndEdit = NULL;
    m_hwndStatus = NULL;
    m_hInst = hInstance;
    m_ObjRefCount = 1;
    m_hwndParent = hwndParent;
    m_pThreadMgr = NULL;
    m_pDocMgr = NULL;
    m_pPrevDocMgr = NULL;
    m_pContext = NULL;
    m_fLocked = FALSE;
    m_dwLockType = 0;
    m_dwInternalLockType = 0;  // 追加
    m_fPendingLockUpgrade = FALSE;
    m_acpStart = 0;
    m_acpEnd = 0;
    m_fInterimChar = FALSE;
    m_ActiveSelEnd = TS_AE_START;
    m_pServices = NULL;
    m_cCompositions = 0;
    m_pCategoryMgr = NULL;
    m_pDisplayAttrMgr = NULL;
    m_fLayoutChanged = FALSE;
    m_fNotify = TRUE;
    m_cchOldLength = 0;

    ZeroMemory(&m_AdviseSink, sizeof(m_AdviseSink));
    ZeroMemory(&m_rgCompositions, sizeof(m_rgCompositions));
}

もう一点は単純な bit check の問題なのですが,dwLockType に複数 bit にまたがった値が指定されることが実際にあるため,以下のように厳密に bit をチェックする必要があるというものです.

// 修正後
BOOL CTSFEditWnd::_IsLocked(DWORD dwLockType) 
{ 
    if(m_dwInternalLockType)
    {
        return TRUE;
    }

    return m_fLocked && ((m_dwLockType & dwLockType) == dwLockType); 
}

これらのバグは「_IsLocked(dwLockType) が FALSE を返した場合はエラー」といった使い方をしている限りは基本的に表面化することは無いようです.いくつかの実験から,Text Store のメソッドがコールバックされる多くのケースで TSF Manager が事前にロック状態の検証を行っていて,適切なロックを持たないことが分かった場合はコールバックを呼ぶ前にエラーを返しているものと考えられます.

まとめ

いくつかバグを修正することになりましたが,TSF 対応アプリケーションを作るには,まずこの tsfapp を参考に,メッセージポンプの修正や,テキストストア (ITextStoreACP) を実装について確認してみると良いでしょう..
Text Services Framework』に日本語資料もあるので,そちらも参考にしながらソースとデバッガでひとつひとつ動作を確認してみることをおすすめします.

tsfapp 動作イメージ