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
してください。
メッセージループ
ITfMessagePump
とITfKeystrokeMgr
を使うこと以外には特に気を付けることはないです。
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を返しているにもかかわらずBeginPaint
とEndPaint
を呼ばなかったりするとFALSEになったりします。)
BOOL focus;
m_thread_mgr->IsThreadFocus(&focus);
ITextStoreACPまたはITextStoreACP2の実装
頑張ってください。
ちなみにITextStoreACPとITextStoreACP2の間に親子関係はないので気を付けてください。(はまりました)
それからテキストストアのSetTextなどでUpdateWindow
している実装がありますがそのためにITfContextOwnerCompositionSink
が用意されているのでそちらを追加で実装したほうが良いと思います。(関数3つの軽いインターフェースですし)
表示属性について
ITfContext
を作った後ITfContext::GetProperty
にGUID_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;
}
}