トップ回答者
WPFのTextBox内に日本語入力する時のキャレットの挙動がおかしい

質問
-
TextBoxコントロールをXAML内に
<TextBox AcceptsReturn="True" Height="100" />
と記述して配置したとします。
TextBox 内の入力で、
1行目の入力は Enter で改行し、
2行目の先頭で日本語入力(IME)を On にして、
「あ」と入力した後、確定せずに Back Space キーを押した場合、
「あ」の文字は削除され、キャレットの
位置は2行目の先頭に移動するのが正常な動作だと思いますが、
どういうわけか、キャレットの位置は1行目に移動してしまいます。
また、IME の代わりに ATOK (手元にあるのは ATOK 2014) を
使った場合も挙動がおかしいことがあります。
1行目の入力は Enter で改行し、
2行目の先頭で日本語入力(ATOK)を On にして、
「あい」と入力して、スペースキーで「愛」に変換した時、
キャレットの位置が1行目に移動してしまいます。
TextBoxコントロール側のバグだと思うのですが、いかがでしょうか?
前者のIMEの場合は、CaretIndex に
PreviewTextInput のタイミングで正しい値をセットすること
でなんとか回避できたのですが、
後者のATOKの場合は、お手上げ状態です。
キャレットの挙動を正常にしたいのですが、
なにかよい解決方法をご存知でしたらご教授ください。
よろしくお願いいたします。
なお、こちらの開発環境は以下の通りです。
Visual Studio 2017 Community
.NET 4.5.1
WPF
Windows7 Pro
ATOK の動作確認については Windows 10 上で行いました。
.NET 4.7 も試してみましたが、症状は同じでした。
回答
-
その後、ゼロ幅スペースを使わない方法を思いつきました。
一時的にキャレットを透明にすることで不具合を回避できました。
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:WpfApp1" Title="MainWindow" Height="350" Width="525"> <StackPanel> <TextBlock Text="対策なし"/> <TextBox x:Name="textBox1" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.ImeFix="false"/> <TextBlock Text="対策あり"/> <TextBox x:Name="textBox2" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.ImeFix="true"/> </StackPanel> </Window>
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace WpfApp1 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } class TextBoxTool { private static bool imeStart; private static bool fixTarget; static TextBoxTool() { imeStart = false; fixTarget = false; } public static bool GetImeFix(DependencyObject obj) { return (bool)obj.GetValue(ImeFixProperty); } public static void SetImeFix(DependencyObject obj, bool value) { obj.SetValue(ImeFixProperty, value); } public static readonly DependencyProperty ImeFixProperty = DependencyProperty.RegisterAttached ("ImeFix" , typeof(bool) , typeof(TextBoxTool) , new UIPropertyMetadata(default(bool), new PropertyChangedCallback(OnImeFixPropertyChanged))); private static void OnImeFixPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) { if (dpo is TextBox target) { bool newValue = (bool)e.NewValue; bool oldValue = (bool)e.OldValue; if (oldValue) { target.TextChanged -= TextBox_TextChanged; TextCompositionManager.RemovePreviewTextInputHandler(target, TextBox_PreviewTextInput); TextCompositionManager.RemoveTextInputUpdateHandler(target, TextBox_TextInputUpdate); } if (newValue) { target.TextChanged += TextBox_TextChanged; TextCompositionManager.AddPreviewTextInputHandler(target, TextBox_PreviewTextInput); TextCompositionManager.AddTextInputUpdateHandler(target, TextBox_TextInputUpdate); } } } static void TextBox_TextChanged(object sender, TextChangedEventArgs e) { var tb = sender as TextBox; if (fixTarget) { tb.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => { int i = tb.CaretIndex; tb.CaretBrush = null; tb.CaretIndex = i; })); } } static void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { if (imeStart) { imeStart = false; fixTarget = false; } } static void TextBox_TextInputUpdate(object sender, TextCompositionEventArgs e) { var tb = sender as TextBox; if (imeStart) { if (fixTarget) { tb.CaretBrush = new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)); } } else { int i = tb.CaretIndex; if (i > 0 && tb.Text[i - 1] == '\n') { fixTarget = true; } imeStart = true; } } } }
- 回答としてマーク 立花楓Microsoft employee, Moderator 2017年10月23日 4:33
すべての返信
-
.NET Framework 4.5.1辺りには、確かにTextBox周りでキャレット系の不具合はあったと記憶していますが、.NET Framework 4.7では解消しているはずです。
#確か、.NET Framework 4.6.2で解消されたはずです。
プロジェクトのターゲットではなく、実際に動作するクライントに.NET Framework 4.7がインストールされている必要がありますが、その辺りは大丈夫でしょうか?
私の方で以下の環境で試してみましたが、おっしゃるような不具合は再現できませんでした。
・Windows 7 pro 32bit, Visual Studio 2017 Community, .NET Framework 4.7
・Windows 10 pro 64bit, Visual Studio Enterprise 2017, .NET Framework 4.7★良い回答には回答済みマークを付けよう! MVP - .NET http://d.hatena.ne.jp/trapemiya/
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年10月12日 1:35
-
trapemiya 様
ご回答ありがとうございます。
クライントに.NET Framework 4.7はインストールしてあります。
こちらのWindows 7 Pro のレジストリ
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full
の値は
Release: 0x00070805 (460805)
Version: 4.7.02053
となっています。(他になにか確認すべき点がありましたら教えてください。)
以下の3つの環境で再度試してみましたが、IME入力の症状は100%再現します。
Windows 7 pro 32bit, Visual Studio 2017 Community, .NET Framework 4.7
Windows 7 pro 64bit, Visual Studio 2017 Community, .NET Framework 4.7
Windows 10 Home 64bit, Visual Studio 2017 Community, .NET Framework 4.7
他の方はいかがでしょうか?- 編集済み murataya 2017年10月16日 1:56
-
以下の組み合わせ全てで再現しました。
MS-IME+Win7 x86/x64 , MS-IME+Win8.1 x64 , MS-IME+Win10 x64
ATOK2017(体験版)+Win10 x64変換中の状態で、入力行の行頭にカーソルが移動できないんですかね
とりあえず改行コードにゼロ幅スペースをくっつけて、行頭にも移動するように見せかけてみる。(言語不明なのでC#)
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"> <StackPanel> <TextBlock Text="未対策"/> <TextBox x:Name="textBox1" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.AppendZeroWidthReturn="false"/> <TextBlock Text="ゼロ幅スペース付き"/> <TextBox x:Name="textBox2" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.AppendZeroWidthReturn="true"/> </StackPanel> </Window>
using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; namespace WpfApplication1 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } class TextBoxTool { #region 添付プロパティ public static bool GetAppendZeroWidthReturn(DependencyObject obj) { return (bool)obj.GetValue(AppendZeroWidthReturnProperty); } public static void SetAppendZeroWidthReturn(DependencyObject obj, bool value) { obj.SetValue(AppendZeroWidthReturnProperty, value); } /// <summary>改行コードにゼロ幅を付加するか指定する</summary> public static readonly DependencyProperty AppendZeroWidthReturnProperty = DependencyProperty.RegisterAttached ("AppendZeroWidthReturn" , typeof(bool) , typeof(TextBoxTool) , new UIPropertyMetadata(default(bool), new PropertyChangedCallback(OnAppendZeroWidthReturnPropertyChanged))); private static void OnAppendZeroWidthReturnPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) { TextBox target = dpo as TextBox; if (target != null) { bool newValue = (bool)e.NewValue; bool oldValue = (bool)e.OldValue; if (oldValue) { target.PreviewKeyDown -= TextBox_PreviewKeyDown; target.CommandBindings.Remove(cbPaste); target.CommandBindings.Remove(cbCopy); TextCompositionManager.RemoveTextInputStartHandler(target, TextBox_TextInputStart); TextCompositionManager.RemovePreviewTextInputHandler(target, TextBox_PreviewTextInput); } if (newValue) { target.PreviewKeyDown += TextBox_PreviewKeyDown; target.CommandBindings.Add(cbPaste); target.CommandBindings.Add(cbCopy); TextCompositionManager.AddTextInputStartHandler(target, TextBox_TextInputStart); TextCompositionManager.AddPreviewTextInputHandler(target, TextBox_PreviewTextInput); } } } static void TextBox_TextInputStart(object sender, TextCompositionEventArgs e) { var txb = (TextBox)sender; SetComposition(txb, e.TextComposition); System.Diagnostics.Debug.WriteLine(GetComposition(txb)); } static void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { var txb = (TextBox)sender; SetComposition(txb, null); System.Diagnostics.Debug.WriteLine(GetComposition(txb)); } #endregion private static TextComposition GetComposition(DependencyObject obj) { return (TextComposition)obj.GetValue(CompositionProperty); } private static void SetComposition(DependencyObject obj, TextComposition value) { obj.SetValue(CompositionProperty, value); } private static readonly DependencyProperty CompositionProperty = DependencyProperty.RegisterAttached("Composition", typeof(TextComposition), typeof(TextBoxTool), new UIPropertyMetadata(default(TextComposition))); ///// <summary>ゼロ幅スペース</summary> //#if DEBUG // public const char ZEROWIDTH = '#'; //#else public const char ZEROWIDTH = (char)0x200B; //#endif /// <summary>通常の改行コードの後ろにゼロ幅スペース(2行目以降の行頭にゼロ幅スペースを埋め込む)</summary> public static readonly string NewLineZero = System.Environment.NewLine + ZEROWIDTH; public static readonly int NewLineZeroLength = NewLineZero.Length; private static System.Reflection.PropertyInfo piTextSelectionInternal; private static System.Reflection.PropertyInfo piAnchorPosition; static TextBoxTool() { piTextSelectionInternal = typeof(TextBox).GetProperty("TextSelectionInternal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy); piAnchorPosition = typeof(System.Windows.Documents.TextSelection).GetProperty("AnchorPosition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); } /// <summary>範囲選択のアンカー位置を取得</summary> public static TextPointer GetAnchorPointer(TextBox txb) { if (piTextSelectionInternal != null && piAnchorPosition != null) { var sel = piTextSelectionInternal.GetValue(txb) as System.Windows.Documents.TextSelection; if (sel != null) { var anchor = piAnchorPosition.GetValue(sel) as System.Windows.Documents.TextPointer; return anchor; } } return null; } /// <summary>ペーストコマンド</summary> private static CommandBinding cbPaste = new System.Windows.Input.CommandBinding(ApplicationCommands.Paste, (s, e) => { var t = (TextBox)s; ExpandNewline(t); var text = Clipboard.GetText().Replace(System.Environment.NewLine, NewLineZero); t.SelectedText = text; e.Handled = true; }); /// <summary>コピーコマンド</summary> private static CommandBinding cbCopy = new System.Windows.Input.CommandBinding(ApplicationCommands.Copy, (s, e) => { var t = (TextBox)s; var text = t.SelectedText.Replace(ZEROWIDTH.ToString(), string.Empty); Clipboard.SetText(text); e.Handled = true; }); /// <summary>改行コードの選択をゼロ幅スペースまで広げる</summary> /// <param name="t"></param> private static void ExpandNewline(TextBox t) { if (t.SelectedText.Length > 0 && t.SelectedText[0] == ZEROWIDTH) { t.SelectionStart++; t.SelectionLength--; } if (t.SelectedText.EndsWith(System.Environment.NewLine)) { var x = GetTailNext(t); if (x.Length > 0 && x[0] == ZEROWIDTH) { t.SelectionLength++; } } } /// <summary>選択範囲の次の文字</summary> private static string GetTailNext(TextBox txb) { int p = txb.SelectionStart + txb.SelectionLength; int len = Math.Max(0, Math.Min(System.Environment.NewLine.Length + 1, txb.Text.Length - p)); return txb.Text.Substring(p, len); } private static void TextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { var txb = (TextBox)sender; if (!txb.AcceptsReturn) { return; } switch (e.Key) { case System.Windows.Input.Key.Return: { ExpandNewline(txb); var comp = GetComposition(txb); if (Keyboard.Modifiers == ModifierKeys.Shift && comp != null && InputMethod.GetIsInputMethodEnabled(txb)) { //IME入力途中でShit+Enter InputMethod.SetIsInputMethodEnabled(txb, false); InputMethod.SetIsInputMethodEnabled(txb, true); } int p = txb.SelectionStart; txb.SelectedText = NewLineZero; txb.Select(p + NewLineZeroLength, 0); e.Handled = true; } break; case System.Windows.Input.Key.Back: { ExpandNewline(txb); if (txb.SelectionStart < NewLineZeroLength) { return; } if (txb.Text.Substring(txb.SelectionStart - NewLineZeroLength, NewLineZeroLength) == NewLineZero) { int p = txb.SelectionStart; if (p == txb.Text.Length) { txb.Text = txb.Text.Substring(0, p - NewLineZeroLength); } else { txb.Text = txb.Text.Substring(0, p - NewLineZeroLength) + txb.Text.Substring(p); } txb.Select(p - NewLineZeroLength, 0); e.Handled = true; } } break; case System.Windows.Input.Key.Delete: { if (txb.SelectionLength > 0) { ExpandNewline(txb); } else { string tail = GetTailNext(txb); if (tail.StartsWith(System.Environment.NewLine)) { txb.SelectionLength = System.Environment.NewLine.Length; tail = GetTailNext(txb); if (tail.Length > 0 && tail[0] == ZEROWIDTH) { txb.SelectionLength++; } } } } break; case Key.Left: { if (txb.SelectionStart > 0 && txb.SelectionLength > 0) { var pAnchor = GetAnchorPointer(txb); if (pAnchor != null) { var pCaret = pAnchor.DocumentStart.GetPositionAtOffset(txb.CaretIndex); if (pCaret.CompareTo(pAnchor) < 0) { var list = System.Environment.NewLine.Skip(1).ToList(); list.Add(ZEROWIDTH); //行頭に向かって範囲拡張の場合 do { EditingCommands.SelectLeftByCharacter.Execute(null, txb); } while (txb.SelectionStart > 0 && list.Contains(txb.SelectedText[0])); e.Handled = true; } } } } break; default: ExpandNewline(txb); break; } } } }
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年10月12日 1:35
-
以下の組み合わせ全てで再現しました。
MS-IME+Win7 x86/x64 , MS-IME+Win8.1 x64 , MS-IME+Win10 x64
ATOK2017(体験版)+Win10 x64変換中の状態で、入力行の行頭にカーソルが移動できないんですかね
とりあえず改行コードにゼロ幅スペースをくっつけて、行頭にも移動するように見せかけてみる。(言語不明なのでC#)
gekka 様
ご回答ありがとうございます。書き忘れていたのですが言語はC#であっています。
再現したとのことで、とりあえず自分だけの症状ではないことがわかり安心しました。
ソースコードまで付けていただいて感激です。
ゼロ幅スペースをくっつけるという発想はなかったので勉強になりました。
載せていただいたコードを早速コピペして、実行してみたのですが、
どうも副作用があるようです。
「あ」+「Back Space」の入力を「ゼロ幅スペース付き」TextBoxの中で行った後、
「未対策」TextBoxの中で行い、もう1度「ゼロ幅スペース付き」TextBoxの中で行う
と症状が再発します。
また、「ゼロ幅スペース付き」TextBox内でカーソルキーの左(←)、右(→)を連射して
キャレットを移動させていると、たまにキャレットが移動しなくなることがあります。- 編集済み murataya 2017年10月16日 1:52
-
副作用とうのは、CrLfとゼロ幅スペースの間にカーソルがある場合の処理が足らなかったからですかね
private static void TextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { var txb = (TextBox)sender; if (!txb.AcceptsReturn) { return; } if (e.Key != Key.Left) { if (txb.SelectionLength == 0) { if (GetTailNext(txb).StartsWith(ZEROWIDTH.ToString())) { txb.SelectionStart++; System.Diagnostics.Debug.WriteLine("HIT"); } } else if (txb.SelectedText[0] == ZEROWIDTH) { txb.SelectionStart++; txb.SelectionLength--; System.Diagnostics.Debug.WriteLine("HIT"); } } (略)
ゼロ幅スペースに対する処理は不完全なので、実用したい場合はそれなりに手を入れてください。
ZEROWIDTHを目に見える文字に設定すればデバッグしやすくなります。個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年10月12日 7:12
-
.NET Framework 4.5.1辺りには、確かにTextBox周りでキャレット系の不具合はあったと記憶していますが、.NET Framework 4.7では解消しているはずです。
#確か、.NET Framework 4.6.2で解消されたはずです。
プロジェクトのターゲットではなく、実際に動作するクライントに.NET Framework 4.7がインストールされている必要がありますが、その辺りは大丈夫でしょうか?
私の方で以下の環境で試してみましたが、おっしゃるような不具合は再現できませんでした。
・Windows 7 pro 32bit, Visual Studio 2017 Community, .NET Framework 4.7
・Windows 10 pro 64bit, Visual Studio Enterprise 2017, .NET Framework 4.7
trapemiya 様
.NET Framework 4.6.2 Release Notes
を確認してみましたが、該当する記述は見つかりませんでした。
([177621]や[245230] とかは別の不具合です。)
その他(net46, net461, net47 と net471)のリリースノートも確認しましたが、
該当する記述は見つかりませんでした。
「.NET Framework 4.7では解消しているはず」との情報は本当なのでしょうか?
また、そちらの環境では再現しないとのことですが、なにか特殊な環境
(例えば、英語版のOSやキーボードを使用しているとか...)
ということはないでしょうか?
<本記事をご覧になった方へ>
今のところ、再現したという人はgekka様のみで、
TextBox側の不具合なのかどうかが明確になっていません。
「再現した」あるいは「再現しない」の情報をいただけると幸いです。
- 編集済み murataya 2017年10月16日 2:03
-
-
その後、ゼロ幅スペースを使わない方法を思いつきました。
一時的にキャレットを透明にすることで不具合を回避できました。
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:WpfApp1" Title="MainWindow" Height="350" Width="525"> <StackPanel> <TextBlock Text="対策なし"/> <TextBox x:Name="textBox1" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.ImeFix="false"/> <TextBlock Text="対策あり"/> <TextBox x:Name="textBox2" Height="100" AcceptsReturn="true" Margin="5" app:TextBoxTool.ImeFix="true"/> </StackPanel> </Window>
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace WpfApp1 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } class TextBoxTool { private static bool imeStart; private static bool fixTarget; static TextBoxTool() { imeStart = false; fixTarget = false; } public static bool GetImeFix(DependencyObject obj) { return (bool)obj.GetValue(ImeFixProperty); } public static void SetImeFix(DependencyObject obj, bool value) { obj.SetValue(ImeFixProperty, value); } public static readonly DependencyProperty ImeFixProperty = DependencyProperty.RegisterAttached ("ImeFix" , typeof(bool) , typeof(TextBoxTool) , new UIPropertyMetadata(default(bool), new PropertyChangedCallback(OnImeFixPropertyChanged))); private static void OnImeFixPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e) { if (dpo is TextBox target) { bool newValue = (bool)e.NewValue; bool oldValue = (bool)e.OldValue; if (oldValue) { target.TextChanged -= TextBox_TextChanged; TextCompositionManager.RemovePreviewTextInputHandler(target, TextBox_PreviewTextInput); TextCompositionManager.RemoveTextInputUpdateHandler(target, TextBox_TextInputUpdate); } if (newValue) { target.TextChanged += TextBox_TextChanged; TextCompositionManager.AddPreviewTextInputHandler(target, TextBox_PreviewTextInput); TextCompositionManager.AddTextInputUpdateHandler(target, TextBox_TextInputUpdate); } } } static void TextBox_TextChanged(object sender, TextChangedEventArgs e) { var tb = sender as TextBox; if (fixTarget) { tb.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => { int i = tb.CaretIndex; tb.CaretBrush = null; tb.CaretIndex = i; })); } } static void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { if (imeStart) { imeStart = false; fixTarget = false; } } static void TextBox_TextInputUpdate(object sender, TextCompositionEventArgs e) { var tb = sender as TextBox; if (imeStart) { if (fixTarget) { tb.CaretBrush = new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)); } } else { int i = tb.CaretIndex; if (i > 0 && tb.Text[i - 1] == '\n') { fixTarget = true; } imeStart = true; } } } }
- 回答としてマーク 立花楓Microsoft employee, Moderator 2017年10月23日 4:33