如何获取RichTextBox垂直滚动位置 避免FlowDocument重建后滚动跳转
解决方案
核心思路
你之前的方案只保存了选中行行号,调用BringIntoView()会强制目标行对齐视口顶部,导致浏览位置偏移。我们可以在文档变更前额外保存视口顶部可见行的行号和该行与视口顶部的像素偏移量,重建文档后通过微调滚动控件的偏移值即可1:1还原原来的浏览位置。
实现步骤
1. 获取RichTextBox内置的ScrollViewer
WPF的RichTextBox模板内置了ScrollViewer控件,我们可以通过可视化树查询拿到它,直接控制滚动偏移:
private ScrollViewer GetRichTextBoxScrollViewer(RichTextBox rtb) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(rtb); i++) { var child = VisualTreeHelper.GetChild(rtb, i); if (child is ScrollViewer sv) return sv; } return null; }
2. 文档变更前保存滚动状态
调用以下方法在重建FlowDocument前保存当前的滚动状态:
// 返回值:视口顶部可见行行号、该行顶部距离视口顶部的像素偏移 private (int topVisibleLine, double topOffset) SaveScrollState(RichTextBox rtb) { var sv = GetRichTextBoxScrollViewer(rtb); if (sv == null) return (0, 0); // 获取视口左上角点对应的文本指针 var viewportTopPoint = new Point(0, 0); var topPointer = rtb.GetPositionFromPoint(viewportTopPoint, true); int topLine = FindLineNumber(ref rtb, topPointer); // 计算该行顶部和视口顶部的偏移差 var charRect = topPointer.GetCharacterRect(LogicalDirection.Forward); double offset = charRect.Top; return (topLine, offset); }
3. 文档重建后恢复滚动状态
新的FlowDocument加载完成后,调用以下方法还原滚动位置:
private void RestoreScrollState(RichTextBox rtb, int topVisibleLine, double topOffset) { var sv = GetRichTextBoxScrollViewer(rtb); if (sv == null) return; // 定位到之前保存的顶部可见行 var targetLinePointer = GetTextPosition(ref rtb, topVisibleLine); if (targetLinePointer == null) return; // 计算目标行的坐标,调整滚动偏移还原原始位置 var targetRect = targetLinePointer.GetCharacterRect(LogicalDirection.Forward); sv.ScrollToVerticalOffset(targetRect.Top - topOffset); }
4. 业务代码修改
把原来的业务逻辑替换为以下代码即可:
// 变更前保存滚动状态和选中行(可选) var scrollState = SaveScrollState(RichTextListing.RichText); var selectedLine = FindLineNumber(ref RichTextListing.RichText, RichTextListing.RichText.Selection.Start); // 重建文档 View.Flow = doc; // 恢复滚动位置 RestoreScrollState(RichTextListing.RichText, scrollState.topVisibleLine, scrollState.topOffset); // 可选:恢复原来的选中状态 var selectedPos = GetTextPosition(ref RichTextListing.RichText, selectedLine); if (selectedPos != null) { var lineEnd = selectedPos.GetLineStartPosition(1) ?? selectedPos.DocumentEnd; RichTextListing.RichText.Selection.Select(selectedPos, lineEnd); }
注意事项
- 该方案要求格式化前后的行号是一一对应的,如果格式化操作会新增/删除行,你需要额外维护格式化前后的行号映射关系,替换
topVisibleLine为对应新文档的行号即可。 - RichTextBox是引用类型,你原代码中方法参数的
ref关键字可以省略,不影响逻辑运行。
内容的提问来源于stack exchange,提问作者Paul Baxter
相关产品推荐
相关产品推荐

