MultiLine WPF TextBox中的键盘插入导航

时间:2017-06-28 19:28:43

标签: c# wpf

我有一个ListView使用多行TextBox作为其DataTemplate。

默认情况下,在多行TextBox中,启用向上和向下箭头导航。如果您的TextBox有两行,插入符号位于第一行,您按下向下箭头,它将插入符号放在第二行的相同位置。

我还在ListView中的TextBox之间添加了光标导航。如果您位于TextBox的第一行并按向上箭头,则会将焦点设置为ListView中的上一个TextBox。同样,如果您在最后一行并按下,它将转到下一个TextBox。但由于这必须手动完成,我必须编写自己的逻辑来维持相对位置。但它很复杂并且有一些问题。

private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
    var tb = (sender as TextBox);
    var textBeforeCursor = tb.Text.Substring(0, tb.SelectionStart);
    var textAfterCursor = tb.Text.Substring(tb.SelectionStart);

    if (e.Key == Key.Up && !textBeforeCursor.Contains("\r\n"))
    {
        var caretIndex = GetTextBoxCaretIndex();
        listView.SelectedIndex--;

        var lastLineRegex = new Regex("(.*)(\r\n.*$)", RegexOptions.Singleline);
        var previousString = listView.SelectedItem as string;
        var lines = lastLineRegex.Match(previousString);
        var offset = lines.Groups[1].Length;

        FocusTextBox(caretIndex + offset + 2);
    }
    if (e.Key == Key.Down && !textAfterCursor.Contains("\r\n"))
    {
        var caretIndex = GetTextBoxCaretIndex();
        var lastLineRegex = new Regex("(.*)(\r\n.*$)", RegexOptions.Singleline);
        var previousString = listView.SelectedItem as string;
        var lines = lastLineRegex.Match(previousString);
        var offset = lines.Groups[1].Length;
        listView.SelectedIndex++;

        Console.WriteLine($"CaretIndex: {caretIndex}, Offset: {offset}");
        FocusTextBox(caretIndex - offset - 2);
    }
}

private int GetTextBoxCaretIndex()
{
    var item = listView.ItemContainerGenerator.ContainerFromItem(listView.SelectedItem) as ListViewItem;
    var textBox = GetVisualChildOfType<TextBox>(item);
    return textBox.CaretIndex;
}
private void FocusTextBox(int caretIndex = 0)
{
    var item = listView.ItemContainerGenerator.ContainerFromItem(listView.SelectedItem) as ListViewItem;
    var textBox = GetVisualChildOfType<TextBox>(item);

    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
    {
        textBox.CaretIndex = Math.Min(caretIndex, textBox.Text.Length);
        textBox.SelectionStart = textBox.CaretIndex;
        textBox.Focus();
    }));
}

这种逻辑类型有效,但在某些情况下会破坏行之间的默认插入符号导航。

Here's a .gif of one example case

插入符号位于顶部文本框的最后一行,8个字符。我按下,它转到第二个TextBox,第一行有插入符号,8个字符;预期的行为。

然后我再次按下,然后进入第二行,但是在第一个字符而不是第8个字符。我的代码在这种情况下没有执行,因此默认逻辑会发生异常。

我甚至不知道从哪里开始。通过测试,似乎就像TextBox在每一行上有一些关于插入位置的内部状态,但是通过查看TextBox文档,我没有看到任何与此有关的属性。

您可以查看简化的示例项目以及演示问题on GitHub的完整代码。

有关默认插入符导航如何工作的任何帮助或信息都会有所帮助。谢谢你的时间。

2 个答案:

答案 0 :(得分:1)

解决方案最终是在所有情况下手动控制光标,但需要单独的逻辑。想法是,相对于当前行的开头得到插入位置,并将它的新位置设置为下一行的第一个字符加上相对位置,考虑下一行是否小于当前行。

if (e.Key == Key.Up)
{
    if (!textBeforeCursor.Contains("\r\n"))
    {
        var caretIndex = GetTextBoxCaretIndex();
        listView.SelectedIndex--;

        var lastLineRegex = new Regex("(.*)(\r\n.*$)", RegexOptions.Singleline);
        var previousString = listView.SelectedItem as string;
        var lines = lastLineRegex.Match(previousString);
        var offset = lines.Groups[1].Length;

        FocusTextBox(caretIndex + offset + 2);
    }
    else
    {
        var item = listView.ItemContainerGenerator.ContainerFromItem(listView.SelectedItem) as ListViewItem;
        var textBox = GetVisualChildOfType<TextBox>(item);
        var currentLineIndex = textBox.GetLineIndexFromCharacterIndex(textBox.CaretIndex);
        var positionOnCurrentLine = textBox.CaretIndex - textBox.GetCharacterIndexFromLineIndex(currentLineIndex);

        var nextLineIndex = currentLineIndex - 1;
        var lineStartIndex = textBox.GetCharacterIndexFromLineIndex(nextLineIndex);
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
        {
            var modifier = textBox.GetLineText(nextLineIndex).Contains("\r\n") ? 2 : 0;
            textBox.CaretIndex = Math.Min(
                lineStartIndex + positionOnCurrentLine,
                lineStartIndex + textBox.GetLineLength(nextLineIndex) - modifier);
        }));
    }
}

向下箭头键的逻辑相同,但您将nextLineIndex更改为currentLineIndex + 1.

此解决方案不如默认插入符管理,因为默认管理会考虑您是否在一行(无论长度),并在您手动更改之前将其保持在行尾。此解决方案有时也会选择略微意外的位置,因为字符具有不同的宽度。

我尝试了一个使用TextBox.GetRectFromCharacterIndex和TextBox.GetCharacterIndexFromPoint的解决方案,但它似乎并没有改进功能。也许有更好的方法。

答案 1 :(得分:0)

我可以看到你的沮丧。当我尝试这个时,我发现了Caret的一些奇怪的行为。在编辑插入位置时,似乎删除了默认行为(为什么它会移到行的开头)。所以我假设你必须始终控制插入位置。因此,我在elseKey_Up项检查中添加了Key_Down条款。我重复了你的逻辑,但是它使得插入符位置在文本框中被明确控制。

     //...if (e.Key == Key.Up && !textBeforeCursor.Contains("\r\n")){...}
     else if (e.Key == Key.Up)
     {
        var caretIndex = GetTextBoxCaretIndex();

        var lastLineRegex = new Regex("(.*)(\r\n.*$)", RegexOptions.Singleline);
        var previousString = listView.SelectedItem as string;
        var lines = lastLineRegex.Match(previousString);
        var offset = lines.Groups[1].Length;

        FocusTextBox(caretIndex - offset - 2);
     }
    //...if (e.Key == Key.Down && !textAfterCursor.Contains("\r\n")){...}
    else if(e.Key ==  Key.Down)
    {
        var caretIndex = GetTextBoxCaretIndex();
        var lastLineRegex = new Regex("(.*)(\r\n.*$)", RegexOptions.Singleline);
        var previousString = listView.SelectedItem as string;
        var lines = lastLineRegex.Match(previousString);
        var offset = lines.Groups[1].Length;
        FocusTextBox(caretIndex + offset + 2);
    }

代码绝对可以清理。我保持原样,因为它允许在每个阶段进行调试。因此,我会根据您的需要将其留给您进行重构。您还必须在null上添加previousString次检查。