我有一个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的完整代码。
有关默认插入符导航如何工作的任何帮助或信息都会有所帮助。谢谢你的时间。
答案 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的一些奇怪的行为。在编辑插入位置时,似乎删除了默认行为(为什么它会移到行的开头)。所以我假设你必须始终控制插入位置。因此,我在else
和Key_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
次检查。