連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index
変換中に文字が消える。Enterで確定したはずが別の欄へ移る。突然「半角しか入らない」状態になる。
IMEの不具合に見える。だが、WinForms側の 入力イベント / フォーカス移動 / ImeModeの継承 / 画面遷移や終了の順序 が重なった結果、同じ見え方になることがある。
このページは、原因探しより先に 見る場所を揃える -> 止血 -> 恒久 を一直線で整理する救急メモ。
0. 30秒で結論(最初の5分を合わせる)
- 再現手順は1本に絞る(「どの操作で壊れるか」を1行で言い切る)
- フォーカス / ImeMode / 入力言語を時系列でログに残す(起きた順に追えるようにする)
- 混入点を特定する(TextChanged / Binding / Timer / 遷移 / Keyの横取り)
- 止血は次の順で当てる
- 入力中(変換中)に
Textを書き換えない - フォーカスの強奪(Timer / 完了コールバック / 遷移直後)を止める
- ImeModeは「画面の方針」と「入力欄の用途」で決める(親継承で Disable になっていないかも見る)
- 遷移/終了は 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/Offやlang=英語配列へ寄っていないかを見る - 遷移後から崩れる: 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)の
ImeModeがDisable/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. 関連リンク
禁書庫: 現場の即効チェック
- 再現手順を1本に短文化する(操作と壊れ方を1行にする)
- ImeTraceでフォーカス/ImeMode/入力言語を時系列で合わせる
- TextChanged/Binding/整形で
Text=の混入を点検する - Timer/非同期完了で
Focus()の混入を点検する - 親コンテナのImeMode継承(Disable/Off)を点検する
- 遷移/終了で崩れる場合、Hide -> 後処理 -> Closeにまとめる
帰還先(読む順・公開済リンクが最新): S00_門前の誓い_総合Index