Window10でTEditorコンポーネントのキャレットが消える問題への対応

解決


risa  2021-05-08 18:20:54  No: 149662

 いつもお世話になっております。表記の件について質問させてください。

1 OS
  Window10 Home 64bit バージョン20H2

2 IME
  Windows標準のMicrosoft IME

3 開発環境(2パターン)
 ・Delphi6 personal + TEditor
 ・Delphi10.3.3 community + TEditor
 ※TEditorは、元々本田勝彦様が開発され、Mae様やDeko様がUnicode対応された
  有名なエディタコンポーネントです。

4 問題の内容
 ・TEditorで、日本語を入力し、変換・確定すると、その直後にキャレットが消える。
 ※入力は継続できるが、キャレットが表示されていないため、円滑な入力が困難となる。
  (3の開発環境、どちらでも同様に起きる。)

 ※現在は、次のページなどを参考に、「以前のバージョンの Microsoft IME を使う」の
  オプションを有効にして対応していますが、Microsoftによると、この対策はあくまで
  も一時的な回避策として、長期間は使わないように、と言っているようです。
  https://k2top.jpn.org/k2soft/bbs2/wforum.cgi?mode=newsort
  https://support.microsoft.com/ja-jp/topic/microsoft-ime-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88-windows-10-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3-20h2-%E3%81%8A%E3%82%88%E3%81%B3-windows-10-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3-2004-%E3%81%A7%E5%95%8F%E9%A1%8C%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B%E5%8F%AF%E8%83%BD%E6%80%A7%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99-63696506-47d2-9997-0b72-41a68e328692

 ※この問題は、他のTEditorを使ったエディタでも同様に発生しています。
  (Terapad、VXEditor、K2Editor等)

5 ご質問事項(2点)
 ①この問題について、標準のIMEのまま、一時的な回避策なしに、Delphi側で解決されて
  いる方がいらっしゃいましたら、どのような対策を取られたのかご教示いただけます
  でしょうか。
  (単純にShowCaretなどのコマンドを実行しても再表示されませんでした。そもそも、
  OSのバグならば、コマンドを送っても無駄なのかもしれませんが…)
 ②この不具合は、いずれ直る見込みなのでしょうか。Microsoftのホームページでは、IME
  に関して不具合があるのを認識しているようですが、このキャレットが消えるという問
  題については上記のページにも記載されていませんでした。


risa  2021-05-10 22:20:55  No: 149663

先程改めて最新版のterapadを試してみましたが、最新版のterapadであれば、変換してもキャレットは表示されるようです。
ですが、JmEditor、MKEditorはやはり駄目でした。


HFUKUSHI  2021-05-11 10:56:08  No: 149664

何らか対策は存在するんですね…とはいえ、新しいIMEでのみ発生する問題なのでIME側で修正されるのを待つしかないと思います。
なおEmbarcaderoのスタンスとしては、

Windows 10(20H2)以降の環境で、VCLフォームアプリケーションを実行した時のIMEの挙動 - Support
http://docwiki.embarcadero.com/Support/ja/Windows_10(20H2)%E4%BB%A5%E9%99%8D%E3%81%AE%E7%92%B0%E5%A2%83%E3%81%A7%E3%80%81VCL%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%9F%E6%99%82%E3%81%AEIME%E3%81%AE%E6%8C%99%E5%8B%95


> エンバカデロでは、今後もIMEに影響する動作に関しては、VCL側で特定のバージョンに合わせて独自の対処コードを埋め込む方針ではなく、基本的にWindows OSおよびIMEの修正によって対処すべきと判断しております。
とあるように、IMEの不具合に個別に対応する考えはなさそうです。

ところでこの問題ってMicrosoftに認識されているんでしょうか?Microsoftのサポート情報の

Microsoft IME を使用している場合、Windows 10 バージョン 20H2 および Windows 10 バージョン 2004 で問題が発生する可能性があります。
https://support.microsoft.com/ja-jp/topic/microsoft-ime-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88-windows-10-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3-20h2-%E3%81%8A%E3%82%88%E3%81%B3-windows-10-%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3-2004-%E3%81%A7%E5%95%8F%E9%A1%8C%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B%E5%8F%AF%E8%83%BD%E6%80%A7%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99-63696506-47d2-9997-0b72-41a68e328692

を見ると、Microsoftとしては新しいIMEの問題はすべて解決済と認識しているように思えます。Windows 10のフィードバックHubで報告が必要かもしれませんね。


au  2021-05-11 11:30:36  No: 149665

ざっくりと確認した感じ、CaretBeginUpdateとCaretEndUpdateの呼び出し回数が一致せずCaretBeginUpdateの方が多く呼び出される為キャレットが表示されない状態になっているようです。
WM_IMENOTIFYのIMN_OPENCANDIDATEが、変換前の文字入力毎に投げられててIMN_CLOSECANDIDATEが飛んできてないっぽいです。

非常に乱暴な方法としては、HEditor.pasのTEditor.WMImeCompositionメソッド内のPutStringToLine(S);の後でFCaretUpdateCountが0になるまでCaretEndUpdateを呼び出すと取りあえずキャレットは表示されるようになりました。
ただ、他に副作用が出る気がするのでどうなんですかね。


risa  2021-05-11 20:49:32  No: 149666

HFUKUSHI 様、au様、書きこみありがとうございます。

HFUKUSHI 様、あまりエンバカデロ様の動きは気にしておりませんでしたが、
エンバカデロ様は当然といえば当然のような考え方をなさっているようですね。
あまり期待はできなさそうです。
私自身、TEditorの熱烈な支持者で、これを使った他のエディタも深くリスペクト
して使用させていただいております。微力ではありますが、知り合いにお願いし、
Windows10のスタートメニューから、フィードバックhubへこの件を「重要問題」
扱いで報告させていただきました。私のソフトの改善のみならず、皆さんもそれらの
ソフトを問題なく使える日がまた来るといいな、と思っています。

au様、私がShowCaretを試行的に入れてみたのは、まさにそのWMImeComposition
メソッド内です。そういえば、Caretは単なる表示非表示の切替ではなくて、隠した
回数分表示コマンドを実行しないと表示されない、といったような仕様になっていた
ことを思い出しました。
ためしに、以下のような行を1行入れてみましたところ、Caretは表示されるように
なりました。

procedure TEditor.WMImeComposition(var Msg: TMessage);
var
  Imc: HIMC;
  L: Integer;
  S: String;
begin
  inherited;
  // WM_IME_CHAR メッセージが発行され、IME 文字列長分の WM_CHAR が
  // Windows の DefWindowsProc によってポストされる。
  if (Msg.LParam and GCS_RESULTSTR <> 0) and not FReadOnly then
  begin
    Imc := ImmGetContext(Handle);
    L := ImmGetCompositionString(Imc, GCS_RESULTSTR, nil, 0);
    SetLength(S, L + 1);
    ImmGetCompositionString(Imc, GCS_RESULTSTR, PChar(S), L + 1);
    ImmReleaseContext(Handle, Imc);
    SetLength(S, L);
    FImeCount := L;
    PutStringToLine(S);
    while FCaretUpdateCount > 0 do CaretEndUpdate; // この1行を追加。CaretEndUpdateのなかでFCaretUpdateCount はDecされる。
  end;
end;

 シンプルに考えてですが、IMEの変換処理の途中で不具合が起きてい
る(IMN_CLOSECANDIDATEが飛んでこず、CaretEndUpdateの回数がこ
なせない)というのであれば、IMEの変換処理が終わる際(このプロシ
ージャを抜ける直前)に、きちんと本来あるべき姿に戻してあげれば
よいはずです。
(=FCaretUpdateCountが0となり、Caretが表示される状態にする)
 そのため、お示しいただいた処理というのは、おそらく正解なので
はないかと考えております。
 引き続き皆さまのお知恵をお借りしたいと思いますが、当方ではこの
処理を入れて、しばらく違和感がないか試験的に使ってみようと思います。

 お二人とも、お知恵をお貸しくださり、ありがとうございました。
OSのバグらしく、直る見込みもないと考えてしまっていたので、絶望感の
中にいたのですが、引き続き自分のソフトを最高の作品に仕上げていこうと
いうモチベーションが保てそうです。

 引き続き、もし何かございましたら、よろしくお願いいたします。


risa  2021-05-11 21:40:18  No: 149667

今試してみましたが、IMEの変換中にEscapeを押すと、キャレットが消えてしまいました。
(エンターキーを押した場合はキャレットはきちんと表示されるのですが)
もう少し悩みそうな感じです…


au  2021-05-11 23:18:42  No: 149668

候補ウィンドウをキャンセルした時はWMImeCompositionのMsg.LParamが0でメッセージが来るっぽいのでその時にも、FCaretUpdateCountが0になるまでCaretEndUpdateを呼べば良いかと思います。
ttps://docs.microsoft.com/ja-jp/windows/win32/intl/wm-ime-composition


risa  2021-05-12 19:45:26  No: 149669

 au様、改めてのご助言、誠にありがとうございます。本当に心強く感じております。

先に書いたコードなのですが、

 if (Msg.LParam and GCS_RESULTSTR <> 0) and not FReadOnly then

という条件文にあるとおり、ReadOnlyを設定していたときにもスルーされてしまう
(キャレットが消えてしまう)という問題があります。ご助言いただいた「変換をキ
ャンセルしたとき」や「ReadOnlyが設定されていたとき」などにも必ず処理が行われ
るよう、追加した一文の位置をIF文の外に記述しなおしました。これが良好に動作する
か、少し時間をとって動作検証をしてみたいと思います。

※まだ試行錯誤のところもございますので、大変恐縮ですが、お気づきの点等ござい
 ましたら、ご助言をお願いできれば幸いです。

procedure TEditor.WMImeComposition(var Msg: TMessage);
var
  Imc: HIMC;
  L: Integer;
  S: String;
begin
  inherited;
  // WM_IME_CHAR メッセージが発行され、IME 文字列長分の WM_CHAR が
  // Windows の DefWindowsProc によってポストされる。
  if (Msg.LParam and GCS_RESULTSTR <> 0) and not FReadOnly then
  begin
    Imc := ImmGetContext(Handle);
    L := ImmGetCompositionString(Imc, GCS_RESULTSTR, nil, 0);
    SetLength(S, L + 1);
    ImmGetCompositionString(Imc, GCS_RESULTSTR, PChar(S), L + 1);
    ImmReleaseContext(Handle, Imc);
    SetLength(S, L);
    FImeCount := L;
    PutStringToLine(S);
  end;
  while FCaretUpdateCount > 0 do CaretEndUpdate; // IF文の外に配置。ここなら変換キャンセル時やReadOnly時にも処理される。
end;


take  2021-05-14 08:14:58  No: 149670

参考までに TEditor以外でもMemo系の拡張でキャレットの消失現象は発生しています。
他にもキャレットの消失と同時に入力フォーカスを失う場合や
ATOKとの組み合わせではMemo系自体がエラーを吐いて落ちたりします。

不思議と日本製のアプリでは発生して、海外製アプリに日本語化パッチを当てたものは発生しないですね


risa  2021-05-15 21:05:44  No: 149671

 途中経過のご報告です。
 まだそれほど長文を何度も打ったわけではありませんが、新しいIMEと古いIMEで、思いつく限り
でのパターンを試してみました。今のところ、あまり違和感のある動作にはなっておりません。
TEditorのコメントなども見ると、やはり上記のような処理で問題ないのかな、という気はいたし
ます。もう少し長文を打ってみたり、可能であればATOKやグーグルIMEなどでも様子を見てみよう
と思います。

・ひらがなのまま確定、スペースは押さずにIMEの提案した文字列を選択入力、スペースを押して変換入力
・入力モード(半角英数⇔ひらがな)を随時切替
・エンターで確定、マウスで確定、マウスでIMEでない余白部分をクリックして確定
・バックスペースキーでキャンセル、Escapeでキャンセル

 take様、書き込みありがとうございます。もし今後、不具合が発生する範囲が広がれば、マイクロ
ソフト様も動いていただける可能性が上がりそうですね。フィードバックhubを拝見いたしますと、
「IMEは事前に万全の試験を行うべきだ」といったような書き込みもあったように記憶しております。


risa  2021-05-16 17:15:42  No: 149672

 いろいろ考え、処理を見直しました。
 IME関連のメッセージは、以下のような順番でやってきます。(一部省略)

  1. WM_IME_NOTIFY ( IMN_OPENCANDIDATE )
  2. WM_IME_COMPOSITION ( GCS_RESULTSTR )
  3. WM_IME_NOTIFY ( IMN_CLOSECANDIDATE )

古いIMEでは、変換開始時に1でCaretBeginUpdate(HideCaret)が1回実行され、
変換終了後に3でCaretEndUpdate(ShowCaret)が1回実行され、キャレットが
問題なく再表示されるという流れになっているようですが、新しいIMEでは1の
メッセージが文字ごと(例えば「あ」を3文字打てば3回)きて、3のメッセージ
が来ないためにキャレットが隠されてしまう様子でした。

そのため、2のメッセージを処理した後、ここの処理の最後にCaretEndUpdateを
処理してやれば、Caretが再表示されることになります。
(この段階でCaretEndUpdateを処理してしまった場合で、もし3のメッセージが
飛んできても、そこで実行されるCaretEndUpdateは、「FCaretUpdateCountが
0なので処理しません」ということで無視され、実害はありません。新旧IME
どちらにも対応できるということになります。)

 上記では、割と簡単に解決しようとしていましたが、今回は、「Windows10
の一時的な不具合を最小限度の回避策で対策する」という観点から、コードを
以下のように見直しました。(変換を確定して文字列が入力されるタイミングと、
IMEの変換がキャンセルされたタイミングの2つのタイミングでだけ、
CaretEndUpdateを呼ぶ仕様としました。)

 今のところはこれでも違和感ある処理にはなっていませんので、自分では今後、
この処理で進めてみようと思います。au様、あなたのご助言で具体的なコードまで
考えることができました。改めて深く御礼申し上げます。このコードをしばらく
使ってみて、最終的にまた結果をご報告いたします。

procedure TEditor.WMImeComposition(var Msg: TMessage);
var
  Imc: HIMC;
  L: Integer;
  S: String;
begin
  inherited;
  if (Msg.LParam and GCS_RESULTSTR <> 0) then begin
    if not FReadOnly then begin // ここの条件文を分けた
      Imc := ImmGetContext(Handle);
      L := ImmGetCompositionString(Imc, GCS_RESULTSTR, nil, 0);
      SetLength(S, L + 1);
      ImmGetCompositionString(Imc, GCS_RESULTSTR, PChar(S), L + 1);
      ImmReleaseContext(Handle, Imc);
      SetLength(S, L);
      FImeCount := L;
      PutStringToLine(S);
    end;
    while FCaretUpdateCount > 0 do CaretEndUpdate; // 文字列が確定(挿入)されたタイミングでCaretを再表示(ここだけはReadOnlyか否かを問わず処理)
  end else if Msg.LParam = 0 then begin
    while FCaretUpdateCount > 0 do CaretEndUpdate; // 変換がキャンセルされたタイミングでCaretを再表示 (EscやBkspなどで)
  end;
end;


risa  2021-05-19 17:28:20  No: 149673

 あれからメッセージの流れを詳細に確認し、以下のような別解も考えましたので、参考までにご報告いたします。
 IMN_CLOSECANDIDATEが飛んでこないなら、必要な時に自分で飛ばしてしまえという発想です。新しいIMEで、
IMN_OpenCandidateが連続、重複して発生しないよう、
 ①既にIMN_OpenCandidateが発生していて、
 ②まだIMN_CloseCandidateが発生していない
という場合には、FImeWindowShowingなる変数により重複判定も行うようにしました。

 ただ、今更なのですが、MSDNでのやり取りも少し見かけたのですが、IMN_CloseCandidateがドキュメントどおり
に飛んでこないというのは、どちらかというとバグに近い現象のように感じました。今回の手法では、
IMN_CloseCandidateが飛んできても来なくても問題なく処理できるようにしたものの、Windowsのバグに合わせて
自分のプログラムを作りこむというのはまた違う話のように思えました。

 まだIMEは新しくなってそれほど期間がたっておらず、修正される可能性もあることから、当面は「以前のIMEを使う」
オプションで回避するのも手なのかな、という気がしてきました。(今どちらにするか悩んでいます。)

 ただ、自分の稚拙な知識で考えた解決策であっても、どなたかのお役に立てるかもしれないな、という気もしましたの
で、感謝の気持ちも込め、ひとまず投稿させていただきます。

procedure TEditor.WMImeComposition(var Msg: TMessage);
var
  Imc: HIMC;
  L: Integer;
  S: String;
begin
  inherited;
  if (Msg.LParam and GCS_RESULTSTR <> 0) then begin
    if not FReadOnly then begin
      Imc := ImmGetContext(Handle);
      L := ImmGetCompositionString(Imc, GCS_RESULTSTR, nil, 0);
      SetLength(S, L + 1);
      ImmGetCompositionString(Imc, GCS_RESULTSTR, PChar(S), L + 1);
      ImmReleaseContext(Handle, Imc);
      SetLength(S, L);
      FImeCount := L;
      PutStringToLine(S);
      // MDMain.Memo1.Lines.Add(S); // メッセージの流れを可視化したい場合のデバッグ用
    end;
    SendMessage(Handle, WM_IME_NOTIFY, IMN_CLOSECANDIDATE, 0); // 文字入力が完了したらメッセージを飛ばす。
  end else if Msg.LParam = 0 then begin
    SendMessage(Handle, WM_IME_NOTIFY, IMN_CLOSECANDIDATE, 0); // 変換がキャンセルされてもメッセージを飛ばす。
  end;
end;

procedure TEditor.WMImeNotify(var Msg: TMessage);
begin
  inherited;
  // FImeWindowShowingは自分で追加した新しい変数。TEditorのconstructor内でFalseに初期化しておく。
  if Msg.WParam = IMN_OPENCANDIDATE then begin
    if not FImeWindowShowing then begin // IMEの処理がまだ始まっていない場合に限り、1回だけ処理
      CaretBeginUpdate;
      FImeWindowShowing := True;
      MDMain.Memo1.Lines.Add('WM_Ime_Notify (IMN_OpenCANDIDATE)'); // メッセージの流れを可視化したい場合のデバッグ用
    end;
  end else if Msg.WParam = IMN_CLOSECANDIDATE then begin
    if FImeWindowShowing then begin
      CaretEndUpdate;
      FImeWindowShowing := False;
      MDMain.Memo1.Lines.Add('WM_Ime_Notify (IMN_CloseCANDIDATE)'); // メッセージの流れを可視化したい場合のデバッグ用
    end;
  end;
end;


risa  2021-05-29 21:32:49  No: 149686

 先日より、以下のコードで動作を確認しておりましたが、違和感のない動作を
確認できました。(既存の変数を使って条件判定を行うこととしました。短時間
ではありますが、新旧IMEのみならず、ATOK2017、最新のGoogleIMEでも使って
みて、違和感のない動作となっていることを確認しました。)
 そのため本件については一旦解決とさせていただきたいと思います。
 ご多忙にも関わらず貴重なご助言等をくださった皆様、誠にありがとうござい
ました。本当に助かりました。改めて深く御礼申し上げます。

※フィードバックHubには、ドキュメントどおりIMN_CloseCandidateのメッセージ
 が飛ばないのはバグの一種ではありませんか?というコメントを投稿させていた
 だきました。すぐに治るとは思いませんが、近い将来、本来あるべき形に戻って
 もらえるとよいなと思います。

procedure TEditor.WMImeComposition(var Msg: TMessage);
var
  Imc: HIMC;
  L: Integer;
  S: String;
begin
  inherited;
  // WM_IME_CHAR メッセージが発行され、IME 文字列長分の WM_CHAR が
  // Windows の DefWindowsProc によってポストされる。
  if (Msg.LParam and GCS_RESULTSTR <> 0) then begin
    if not FReadOnly then begin
      Imc := ImmGetContext(Handle);
      L := ImmGetCompositionString(Imc, GCS_RESULTSTR, nil, 0);
      SetLength(S, L + 1);
      ImmGetCompositionString(Imc, GCS_RESULTSTR, PChar(S), L + 1);
      ImmReleaseContext(Handle, Imc);
      SetLength(S, L);
      FImeCount := L;
      PutStringToLine(S);
    end;
    SendMessage(Handle, WM_IME_NOTIFY, IMN_CLOSECANDIDATE, 0);
  end else if Msg.LParam = 0 then begin
    SendMessage(Handle, WM_IME_NOTIFY, IMN_CLOSECANDIDATE, 0);
  end;
end;

procedure TEditor.WMImeNotify(var Msg: TMessage);
begin
  inherited;
  if Msg.WParam = IMN_OPENCANDIDATE then begin
    if FCaretUpdateCount = 0 then CaretBeginUpdate;
  end else if Msg.WParam = IMN_CLOSECANDIDATE then begin
    if FCaretUpdateCount = 1 then CaretEndUpdate;
  end;
end;


risa  2021-09-27 19:58:51  No: 149852

 先日の件で、知人経由でマイクロソフトに以下の報告をしましたところ、
「We made a fix for this issue in build 21327 in Dev channel.」
というご回答を頂戴できました。現時点で自分のPCのWindows10は最新版ですが、まだ
このスレッドの頭に書かせていただいた症状は残ったままです。このアップデートはいつ
頃一般公開されることになるのでしょうか。ご存じの方がいらっしゃればお教えいただけ
ますでしょうか。

 検索したところ、これは開発者向けのプレビューバージョンのようで、2021年3月頃に
公開された版のようです。半年くらいしてから大型バージョンアップに乗せられる感じな
のでしょうかね。当方は素人プログラマで、本格的な開発者向けのプレビュー版を評価で
きるようなスキルはありませんが、一般公開されたら真っ先に試してみたいと思っており
ました。

----------------以下Microsoftへの報告(該当部分)----------------------------------

Microsoft様のドキュメントでは、IMEのウィンドウが開く際、WM_Ime_Notify (IMN_OpenCANDIDATE)が飛び、
閉じる際にはWM_Ime_Notify (IMN_CloseCANDIDATE)が飛ぶという仕組みであると説明しているようですが、
実際にはIMN_OpenCANDIDATEはウィンドウが開いたときだけではなく、文字を入力するたびに飛び、
逆にIMN_CloseCANDIDATEはウィンドウを閉じても飛ばないという現状になっているようですね。
(大変恐縮なのですが、ドキュメントどおりの動作になっていない点を見ると、バグの一種かと思われます。)
上記のソフトでは、このメッセージに基づいてカーソルの表示非表示を判断しているようですが、新しいIMEでは
ドキュメントどおりにメッセージを飛ばさなくなったため、多くのソフトに誤作動を生じさせているようです。


※返信する前に利用規約をご確認ください。








  このエントリーをはてなブックマークに追加