0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WinForms IME不具合の切り分け|確定が飛ぶ/文字が消える原因【救急E05】

0
Last updated at Posted at 2026-01-20

連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

変換中に文字が消える。Enterで確定したはずが別の欄へ移る。突然「半角しか入らない」状態になる。
IMEの不具合に見える。だが、WinForms側の 入力イベント / フォーカス移動 / ImeModeの継承 / 画面遷移や終了の順序 が重なった結果、同じ見え方になることがある。

このページは、原因探しより先に 見る場所を揃える -> 止血 -> 恒久 を一直線で整理する救急メモ。


0. 30秒で結論(最初の5分を合わせる)

  • 再現手順は1本に絞る(「どの操作で壊れるか」を1行で言い切る)
  • フォーカス / ImeMode / 入力言語を時系列でログに残す(起きた順に追えるようにする)
  • 混入点を特定する(TextChanged / Binding / Timer / 遷移 / Keyの横取り)
  • 止血は次の順で当てる
    1. 入力中(変換中)に Text を書き換えない
    2. フォーカスの強奪(Timer / 完了コールバック / 遷移直後)を止める
    3. ImeModeは「画面の方針」と「入力欄の用途」で決める(親継承で Disable になっていないかも見る)
    4. 遷移/終了は Hide -> 後処理 -> Close に統一する(切り離し対策)

1. 見え方で切り分ける(見え方 -> 最初に見る場所)

見え方 具体例 最初に見る場所
文字が消える/巻き戻る 変換中に消える、確定前に戻る 入力中に Text= 再代入(TextChanged/Binding/整形)
確定が飛ぶ Enter確定が別欄へ入る、確定が抜ける フォーカス遷移(Timer/非同期完了/Shown/Leave)
半角しか入らない ひらがなへ戻らない、常に英数 ImeModeがOff/Disable側、親継承、入力言語切替
画面遷移後にだけ崩れる 遷移直後から不安定、別画面へ影響が持ち越される 遷移/終了順序+後処理不足(IMEコンテキスト切り離し)
端末差が強い AはOK、BだけNG IME種別/設定差+アプリ側混入点(上記)

2. 真犯人

IMEまわりの詰まりは、だいたい次のどれかに当たることが多い。

パターン 典型例 兆候(観測できるサイン)
入力中のText再代入 TextChangedでTrim/正規化/再バインド 変換中に文字が消える、巻き戻る
フォーカス強奪 Timer/完了コールバックで Focus() Enter/Leaveが割り込む、確定先が揺れる
ImeModeの継承ミス 親がDisable/Off、画面全体へ影響の広がり 特定画面だけ半角へ寄る
遷移/終了順序+後処理不足 Close直行、購読/Timer残存 遷移後から不安定、別画面へ持ち越す
低レイヤ介入 ProcessCmdKey/WndProcで横取り EnterやIME系キーの挙動が割れる

3. 処方箋(最短ルート)

3-1. 事実を集めて整合を取る:フォーカス/ImeMode/入力言語の時系列ログ

先に「何が分かるか」を合わせる。
このログは、IMEそのものを解析する道具ではなく アプリ側の混入点を特定する道具 になる。

このログで分かること

  • どの操作でフォーカスが揺れたか(Enter/Leave/GotFocus/LostFocus の時系列)
  • IMEが有効か無効か(ImeMode が On/Off/Disable へ寄っていないか)
  • 入力言語が切り替わったか(日本語 -> 英語配列など)

どの症状に効くか

  • 確定が飛ぶ: 入力中に Leave -> Enter が割り込んでいないかを見る
  • 半角しか入らない: ime=Disable/Offlang=英語配列 へ寄っていないかを見る
  • 遷移後から崩れる: Shown/Activated/FormClosing 直後のフォーカスと言語の揺れを見る

読み方(最短)

  • 「確定が飛んだ瞬間」の前後に、別コントロールの Enter が割り込んでいないか
  • ある画面だけ ime=Disable になっていないか(親継承が疑わしい)
  • 遷移直後に lang が切り替わっていないか(端末差の切り口)
using System.Collections.Generic;
using System.Windows.Forms;

// 意図: フォーカス/ImeMode/入力言語を時系列で揃え、確定飛び/半角化の混入点を短時間で特定する
// 落とし穴: 体感だけで追うと「たぶんIME」の結論に寄り、アプリ側の混入点が残りやすい
static class ImeTrace
{
    public static void Wire(Control root, System.Action<string> log)
    {
        foreach (Control c in Enumerate(root))
        {
            c.Enter     += (_, __) => Dump("Enter", c, log);
            c.Leave     += (_, __) => Dump("Leave", c, log);
            c.GotFocus  += (_, __) => Dump("GotFocus", c, log);
            c.LostFocus += (_, __) => Dump("LostFocus", c, log);
        }

        if (root.FindForm() is Form f)
        {
            f.Shown       += (_, __) => DumpForm("Shown", f, log);
            f.Activated   += (_, __) => DumpForm("Activated", f, log);
            f.Deactivate  += (_, __) => DumpForm("Deactivate", f, log);
            f.FormClosing += (_, __) => DumpForm("FormClosing", f, log);
        }
    }

    private static void Dump(string ev, Control c, System.Action<string> log)
    {
        var f = c.FindForm();
        var active = f?.ActiveControl?.Name ?? "<null>";
        var lang = InputLanguage.CurrentInputLanguage?.LayoutName ?? "<null>";
        log($"[IME] {ev,-9} c={c.Name} focused={c.Focused} ime={c.ImeMode} active={active} lang={lang}");
    }

    private static void DumpForm(string ev, Form f, System.Action<string> log)
    {
        var active = f.ActiveControl?.Name ?? "<null>";
        var lang = InputLanguage.CurrentInputLanguage?.LayoutName ?? "<null>";
        log($"[IME] {ev,-11} form={f.Name} active={active} lang={lang}");
    }

    private static IEnumerable<Control> Enumerate(Control root)
    {
        var stack = new Stack<Control>();
        stack.Push(root);
        while (stack.Count > 0)
        {
            var c = stack.Pop();
            yield return c;
            foreach (Control child in c.Controls) stack.Push(child);
        }
    }
}

3-2. 混入点を点検する(最初に見る場所)

  • TextChanged / Validating / Validated / BindingComplete 内で Text= が混入していないか
  • Timer.Tick や非同期完了後(await 後)に Focus()/Select()/ActiveControl= が混入していないか
  • 親コンテナ(Panel/UserControl/Form)の ImeModeDisable/Off 側へ寄っていないか
  • 画面遷移/終了の順序で「後処理」が散っていないか(Timer停止、購読解除、入力補助解除など)

4. 失敗パターン別:見つけ方 -> 直し方(この章が核)

4-1. 文字が消える:入力中(変換中)に Text を書き換える

見つけ方(観測)

  • 変換中に消える/巻き戻る
  • ImeTraceでフォーカスが揺れていない
  • TextChanged系で Text= が走っている
// 意図: 入力を即時に整形したい(Trim/正規化/桁区切りなど)
// 落とし穴: 変換中にTextを再代入すると、IMEの合成状態が崩れやすい
private void textBox1_TextChanged(object sender, System.EventArgs e)
{
    textBox1.Text = textBox1.Text.Trim(); // 変換中にも走り得る(混入点になりやすい)
    textBox1.SelectionStart = textBox1.TextLength;
}

直し方(止血)
整形は「確定後」に合わせる(Validated/Leave)。入力中は触らない。

// 意図: 入力中はIMEへ任せ、確定後にだけ整形する
// 落とし穴: TextChanged内での整形は、別画面や別入力でも同種の壊れ方を呼びやすい
private void textBox1_Validated(object sender, System.EventArgs e)
{
    var raw = textBox1.Text;
    var normalized = raw.Trim();
    if (raw != normalized) textBox1.Text = normalized; // 確定後の1回だけ更新する
}

4-2. 確定が飛ぶ:フォーカスが奪われる(Timer/遷移/非同期完了)

見つけ方(観測)

  • ImeTraceで入力中にEnter/Leaveが割り込む
  • ActiveControl が一瞬別へ移る
  • その直後の確定が別欄へ入る
// 意図: 入力支援で自動的に次の入力欄へ進めたい
// 落とし穴: 変換確定のタイミングと競合し、確定先が揺れやすい
private void timer1_Tick(object sender, System.EventArgs e)
{
    nextTextBox.Focus(); // 入力中に割り込むと確定飛びの混入点になる
}

直し方(止血)
フォーカス制御の役割を絞る。初回表示の1回だけに絞るなど、割り込みを減らす。

// 意図: 初回表示の1回だけフォーカスを合わせ、入力中の奪取を避ける
// 落とし穴: 「便利」目的のFocus乱用は、確定飛び・変換途切れの温床になる
private bool _focusInitialized;

protected override void OnShown(System.EventArgs e)
{
    base.OnShown(e);
    if (_focusInitialized) return;
    _focusInitialized = true;

    BeginInvoke((System.Action)(() =>
    {
        textBox1.Focus(); // 表示直後へ寄せ、入力中の割り込みになりにくくする
    }));
}

4-3. 半角しか入らない:ImeModeがOff/Disable側へ寄る(親継承含む)

見つけ方(観測)

  • ImeTraceログで ime=Disable/Off が出る
  • 特定画面だけ起きる(親コンテナ継承が混入点になりやすい)
  • 入力言語が英語配列へ寄っているケースもある(langも合わせて見る)

直し方(止血)
親は NoControl へ寄せ、入力欄で用途を決める。画面全体で振り回さない。

// 意図: 親は継承に任せ、入力欄で用途を決める(画面全体の揺れを減らす)
// 落とし穴: 親がDisable/Offだと、配下が意図せず半角側へ寄ることがある
panel1.ImeMode = ImeMode.NoControl;

// 日本語入力が要る欄
textBoxKana.ImeMode = ImeMode.On;

// 数値専用欄(IME無効)
textBoxNumber.ImeMode = ImeMode.Disable;

4-4. 画面遷移/終了で崩れる:コントロールとIMEが切り離される

見え方

  • 遷移元を閉じた直後から遷移先で変換が途切れる/半角へ寄る
  • 「前画面の入力状態」が残りとして残るように見える

止血として効きやすい型:Hide -> 後処理 -> Close

// 意図: Close前にHideを挟み、遷移中の破棄タイミングをずらす(切り離し系の壊れ方を減らす)
// 落とし穴: 後処理不足(Timer/購読/入力補助)が残ると、遷移先へ影響が持ち越される
private bool _closingByHide;

protected override void OnFormClosing(FormClosingEventArgs e)
{
    base.OnFormClosing(e);

    if (_closingByHide) return;

    _closingByHide = true;
    e.Cancel = true;

    Hide(); // 先に見え方を落とす(遷移中の入力状態を落ち着かせる狙い)

    // 後処理をここへ集約する(あちこちに置くと持ち越しやすい)
    // timer1.Stop();
    // someControl.TextChanged -= OnTextChanged;
    // CancelBackgroundJobs();

    BeginInvoke((System.Action)(() =>
    {
        Close(); // 次のループでCloseへ寄せる
    }));
}

混入点になりやすい条件

  • 遷移元が Close() 直行で、同じメッセージループ内に破棄が走る
  • 遷移元でTimer/イベント購読/入力補助が残ったまま
  • 遷移中にフォーカス操作が割り込む

補足
WinFormsでは入力の受け皿が「コントロール(ハンドル)」に寄る。遷移中に破棄と入力処理が競合すると、IME側の合成状態や候補窓の紐付けがズレ、見え方として「IMEが壊れた」になりやすい。


4-5. キー処理の横取り:ProcessCmdKey/WndProc/AcceptButton

見つけ方(観測)

  • ProcessCmdKey / PreviewKeyDown / WndProc が存在する
  • Enterや変換関連キーで return true している
  • base呼び出しが抜けている
// 意図: Enterで画面操作を統一したい
// 落とし穴: 変換確定と競合すると、確定飛びや確定抜けが発生しやすい
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == Keys.Enter)
    {
        // ここでフォーカス移動や確定処理を走らせると、IME確定と競合しやすい
        return true; // 横取り(影響範囲が広い)
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

直し方(恒久)

  • 横取り条件を狭める(必要な場面だけ)
  • 変換と絡むキーは原則baseへ流す方向にまとめる(副作用が大きい)

4-6. DataGridView編:編集途中に差し替えが走る

見つけ方(観測)

  • グリッド編集時だけ「文字が消える/確定が飛ぶ」が出る
  • DataSource再設定、行再生成、EndEdit無しの更新が混入している

直し方(恒久)

  • 更新前に編集内容を確定する(EndEdit/CommitEdit
  • 編集中は差し替えを避け、確定後に反映にまとめる

4-7. 非同期完了で崩れる:完了後にUI更新とフォーカスが割り込む

見つけ方(観測)

  • await 後に Text=Focus() が居る
  • 入力中にバックグラウンド完了が割り込む

直し方(恒久)

  • 入力中はUI更新を遅延し、確定後にまとめる
  • 完了通知は表示だけに留め、フォーカス操作を排除する

4-8. 端末差(最後に扱う):IME種別/設定/配布・依存差

切り分けの順

  • 先にアプリ側の混入点(4-1〜4-4)を潰す
  • その上で端末条件を分解する

端末差の例

  • Windows IME / Google日本語入力
  • 言語バー設定、入力言語切替の癖
  • RDP/仮想環境
  • 成果物の上書き運用で古いdllが残る(配布差)
  • OS/ランタイム/依存dllの差(依存差)

5. 再発防止の掟

観点 ルール案 レビューで見るポイント
入力中Text再代入 変換中は Text= をしない TextChanged/Binding内で Text= が混入していないか
フォーカス Focus/Selectの役割を絞る Timer/完了コールバックからFocusが飛んでいないか
ImeMode 親はNoControl、入力欄で用途決定 親Disable継承、Enter/Leaveで切替乱用がないか
遷移/終了 Hide -> 後処理 -> Closeにまとめる 後処理が散って持ち越されていないか
低レイヤ介入 ProcessCmdKey/WndProcは最小 base呼び出し、条件の狭さ、影響範囲が明確か

6. 関連リンク

  • ログで詰まりを止める
    • 止血と計測(ログ/タイムアウト/計測の型): E04
  • UIスレッドと「待ち」の構造を掴む
    • 構造理解(メッセージループ/帰還先): G11
    • 同期ブロック整理(待ち方/合成/デッドロック回避): G13
  • チーム基準へ反映する
    • 規約化(レビュー観点として扱う): R06

禁書庫: 現場の即効チェック

  • 再現手順を1本に短文化する(操作と壊れ方を1行にする)
  • ImeTraceでフォーカス/ImeMode/入力言語を時系列で合わせる
  • TextChanged/Binding/整形で Text= の混入を点検する
  • Timer/非同期完了で Focus() の混入を点検する
  • 親コンテナのImeMode継承(Disable/Off)を点検する
  • 遷移/終了で崩れる場合、Hide -> 後処理 -> Closeにまとめる

帰還先(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?