LoginSignup
3
7

More than 3 years have passed since last update.

Text Services Framework(TSF)メモ

Last updated at Posted at 2018-08-11

TL;DR

UWPを使うならばWindows.UI.Text.Coreを使うことをおすすめします。そちらのほうがずっと簡単です。
しかし何らかの理由で凶悪なText Services Frameworkに向き合う必要があるかもしれません。
そこでメモ的なものを残しておきます。
このメモで扱う範囲はTSFの初期化から文字の属性の扱い方までです。
ここ違うぞ。やここはこうやったほうが...などあったら教えてください。

Ref

https://docs.microsoft.com/en-us/windows/desktop/tsf/using-text-services-framework
一通り目を通すといいと思います。
http://hp.vector.co.jp/authors/VA050396/tech_01.html
https://qiita.com/496_/items/95acf68eaf31fd57f44e
TextStoreの実装の参考になると思います。
https://github.com/Microsoft/Windows-classic-samples/tree/master/Samples/Win7Samples/winui/input/tsf/tsfapps
Microsoftのサンプル。
https://github.com/tignear/sakura-qiita
僕が適当に実装したやつ。
https://github.com/yohei-yoshihara/GLBaseApp/tree/master/nxLib/nx/tsf
https://charatsoft.sakura.ne.jp/develop/toaru2/index.php?did=3

初期化

CoInitializeから始めますがTSFでないので省略します。
アプリケーションで一つ持てばいいリソースとして以下があります。

  • ITfThreadMgr
    • かならず必要
    • CoCreateInstanceで作成。
    • こいつを使っていろいろつくる。
  • ITfCategoryMgr
    • 同じくCoCreateInstanceで作成。
    • 表示属性を扱うときに必要。
  • ITfDisplayAttributeMgr
    • これもCoCreateInstanceで作成。
    • 同じく表示属性を扱うときに必要。
  • ITfMessagePump
    • ITfThreadMgrのQueryInterfaceを呼べば入手できる。
    • TextServiceにメッセージの事前処理とかをやらせる。普通使うGetMessage関数の代わりにこれのGetMessage関数を使う。(お望みならPeekMessageも使える)
  • ITfKeystrokeMgr
    • これもITfThreadMgrのQueryInterfaceを呼べば入手できる。
    • TextServiceにキーを処理させる。(でないと入力できない。)

適切なタイミングでITfThreadMgr::Activate関数を呼び出す必要があります。
(Activateを呼び出したのと同じ回数だけ終了時にはITfThreadMgr::Deactivateを呼び出してください。)

各ドキュメントごとに持つリソースとして以下があります。

  • ITfDocumentMgr

各コンテキストごとに持つリソースとして以下があります。
コンテキストはドキュメントに1:1で対応しなくてもよいですが対応することが多いと思います。

  • ITfContext
  • ITfProperty(GUID_PROP_ATTRIBUTE)
    • 表示属性に関する情報をここから取り出す。

有効化するコンテキストをITfDocumentMgr::Pushを呼び出してスタックに追加してください。
使用しなくなる場合はITfDocumentMgr::Popしてください。

メッセージループ

ITfMessagePumpITfKeystrokeMgrを使うこと以外には特に気を付けることはないです。

    ComPtr<ITfKeystrokeMgr> keyMgr;
    ComPtr<ITfMessagePump> msgPump;

    if (FAILED(m_thread_mgr.As(&keyMgr)))
    {
        return 1;
    }
    if (FAILED(m_thread_mgr.As(&msgPump)))
    {
        return 1;
    }
    for (;;)
    {
        MSG msg;
        BOOL fResult;
        BOOL fEaten;
        BOOL focus;
        m_thread_mgr->IsThreadFocus(&focus);
        OutputDebugStringA(focus ? "" : "×");
        try {
            if (FAILED(msgPump->GetMessage(&msg, 0, 0, 0, &fResult)))
            {
                return -1;
            }
            else if (msg.message == WM_KEYDOWN)
            {
                if (keyMgr->TestKeyDown(msg.wParam, msg.lParam, &fEaten) == S_OK && fEaten &&
                    keyMgr->KeyDown(msg.wParam, msg.lParam, &fEaten) == S_OK && fEaten)
                {
                    continue;
                }
            }
            else if (msg.message == WM_KEYUP)
            {
                if (keyMgr->TestKeyUp(msg.wParam, msg.lParam, &fEaten) == S_OK && fEaten &&
                    keyMgr->KeyUp(msg.wParam, msg.lParam, &fEaten) == S_OK && fEaten)
                {
                    continue;
                }
            }

            if (fResult == 0)
            {
                return static_cast<int>(msg.wParam);
            }
            else if (fResult == -1)
            {
                return -1;
            }

        }
        catch(...)
        {
            //17134.191ではMS-IMEはキーを長押しし続けるとTestKeyDownでアクセス違反を引き起こすので握りつぶす。
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

NOTE:
以下のようにどこかで確認するとよいです。もしTextServiceを使って入力しているにもかかわらずFALSEならばどこか間違えています。
(WM_PAINTで0を返しているにもかかわらずBeginPaintEndPaintを呼ばなかったりするとFALSEになったりします。)

BOOL focus;
m_thread_mgr->IsThreadFocus(&focus);

ITextStoreACPまたはITextStoreACP2の実装

頑張ってください。
ちなみにITextStoreACPとITextStoreACP2の間に親子関係はないので気を付けてください。(はまりました)
それからテキストストアのSetTextなどでUpdateWindowしている実装がありますがそのためにITfContextOwnerCompositionSinkが用意されているのでそちらを追加で実装したほうが良いと思います。(関数3つの軽いインターフェースですし)

表示属性について

ITfContextを作った後ITfContext::GetPropertyGUID_PROP_ATTRIBUTEを指定して呼び出します。
描画時に以下のようにRangeを列挙したのちに描画属性を取得しそれに基づいて描画します。
ITfContextOwnerCompositionSinkを実装しているのであれば更新時のみ描画属性を取得し描画時の負荷を減らすことも可能です。

        //draw string
        ComPtr<IEnumTfRanges> enumRanges;
        FailToThrowHR(m_attr_prop->EnumRanges(m_edit_cookie, &enumRanges, NULL));
        ComPtr<ITfRange> range;
        while (enumRanges->Next(1, &range, NULL) == S_OK) {
            VARIANT var;
            try {
                VariantInit(&var);
                if (!(m_attr_prop->GetValue(m_edit_cookie, range.Get(), &var) == S_OK && var.vt == VT_I4)) {
                    continue;
                }
                GUID guid;
                FailToThrowHR(m_category_mgr->GetGUID((TfGuidAtom)var.lVal, &guid));
                ComPtr<ITfDisplayAttributeInfo> dispattrinfo;
                FailToThrowHR(m_attribute_mgr->GetDisplayAttributeInfo(guid, &dispattrinfo, NULL));
                TF_DISPLAYATTRIBUTE attr;
                dispattrinfo->GetAttributeInfo(&attr);
                //attrに属性が入っているので属性に基づいて描画させる
                ComPtr<TsfDWriteDrawerEffect> effect = new TsfDWriteDrawerEffect(
                    convertColor(attr.crBk, t, transparency.Get()).Get(),
                    convertColor(attr.crText, t, black.Get()).Get(),
                    attr.lsStyle == TF_LS_NONE ? std::unique_ptr<TsfDWriteDrawerEffectUnderline>() : std::make_unique<TsfDWriteDrawerEffectUnderline>(convertLineStyle(attr.lsStyle),
                    static_cast<bool>(attr.fBoldLine), 
                    convertColor(attr.crLine, t, black.Get()).Get())
                );
                ComPtr<ITfRangeACP> rangeAcp;
                range.As(&rangeAcp);
                LONG start, length;
                if (FAILED(rangeAcp->GetExtent(&start, &length))) {
                    continue;
                }
                DWRITE_TEXT_RANGE write_range{ static_cast<UINT32>(start), static_cast<UINT32>(length) };
                layout->SetDrawingEffect(effect.Get(), write_range);
                layout->SetUnderline(effect->underline ? TRUE : FALSE, write_range);
                VariantClear(&var);
            }
            catch (...)
            {
                VariantClear(&var);
                throw;
            }
        }
3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7