【问题标题】:Keyboard Caret Navigation in MultiLine WPF TextBoxMultiLine WPF 文本框中的键盘插入符号导航
【发布时间】:2017-06-28 19:28:42
【问题描述】:

我有一个使用多行文本框作为其 DataTemplate 的 ListView。

默认情况下,在多行文本框中,启用向上和向下箭头导航。如果您的 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 个字符。我按下,它转到第二个文本框,第一行有插入符号,超过 8 个字符;预期的行为。

然后我再次按下,它转到第二行,但在第一个字符而不是第 8 行。我的代码在这种情况下没有执行,所以默认逻辑发生了一些不寻常的事情。

我什至不知道从哪里开始。通过测试它似乎 TextBox 对每一行的插入符号位置有一些内部状态,但通过查看 TextBox 文档,我没有看到任何关于此的属性。

您可以查看一个精简的示例项目和完整的代码来演示问题on GitHub

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

【问题讨论】:

    标签: c# wpf


    【解决方案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 的解决方案,但它似乎并没有改进功能。也许有更好的方法。

    【讨论】:

      【解决方案2】:

      我能看出你的沮丧。当我尝试这个时,我发现插入符号有一些奇怪的行为。似乎当您编辑插入符号位置时,默认行为已被删除(为什么它会转到行首)。所以我假设你必须始终控制插入符号的位置。因此,我在您的 Key_UpKey_Down 检查中添加了 else 子句。我重复了您的逻辑,但这样做是为了在文本框中明确控制插入符号的位置。

           //...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);
          }
      

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

      【讨论】:

      • 提供的代码不能满足我的需要,因为它永远不会改变 ListView 的选定索引。但是,我没想过手动处理所有箭头键逻辑。这似乎是一个合理的解决方案。感谢您的建议。
      • 当前代码中的 ListView 选定索引已更改。在您的 if 语句下(正如我在代码中评论的那样)。您的问题仅表明您正在尝试解决您在 GIF 中看到的问题。此代码旨在插入您的代码中
      猜你喜欢
      • 1970-01-01
      • 2010-10-05
      • 2011-01-08
      • 2011-09-05
      • 2016-07-26
      • 1970-01-01
      • 2012-10-29
      • 2011-04-25
      • 1970-01-01
      相关资源
      最近更新 更多