WPF(.NET6 C#)基于字符串索引的RichTextBox搜索实现优化问询
WPF(.NET 6) RichTextBox搜索功能标准实现方案
现有实现的问题
你当前的变通方案可以满足基础场景需求,但存在三个明显缺陷:
- 全量提取
TextRange.Text做字符串匹配,大文档场景下会产生不必要的内存开销 - 通过循环调用
GetNextInsertionPosition定位匹配终点,时间复杂度和关键词长度正相关,长文本+长关键词场景下卡顿明显 - 文档存在超链接、内联UI元素、段落格式标记等非纯文本内容时,容易出现匹配位置错位——
TextRange.Text返回的纯文本会过滤部分内部元素标记,和FlowDocument内部的符号计数规则不匹配。
标准实现方案
WPF FlowDocument的文本内容是按TextRun分块存储的,最规范高效的搜索实现是直接沿TextPointer遍历文本块,在遍历过程中完成匹配,从根源上避免纯文本索引和文档内部偏移不对应的问题。
可直接复用的实现代码如下:
/// <summary> /// 在FlowDocument中搜索指定文本 /// </summary> /// <param name="doc">待搜索的FlowDocument实例</param> /// <param name="searchPattern">搜索关键词</param> /// <param name="comparison">字符串匹配规则</param> /// <param name="startPosition">搜索起始位置,不传默认从文档开头开始</param> /// <returns>搜索结果</returns> public static SearchResultValue FindText(FlowDocument doc, string searchPattern, StringComparison comparison, TextPointer startPosition = null) { if (doc == null || string.IsNullOrEmpty(searchPattern)) return new SearchResultValue(false, null); TextPointer currentPointer = startPosition ?? doc.ContentStart; int patternLength = searchPattern.Length; while (currentPointer != null && currentPointer.CompareTo(doc.ContentEnd) < 0) { // 跳过非文本类型的元素边界 if (currentPointer.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text) { currentPointer = currentPointer.GetNextContextPosition(LogicalDirection.Forward); continue; } // 读取当前文本块的内容 string currentRunText = currentPointer.GetTextInRun(LogicalDirection.Forward); if (string.IsNullOrEmpty(currentRunText)) { currentPointer = currentPointer.GetNextContextPosition(LogicalDirection.Forward); continue; } // 单文本块内匹配 int matchIndex = currentRunText.IndexOf(searchPattern, comparison); if (matchIndex >= 0) { TextPointer matchStart = currentPointer.GetPositionAtOffset(matchIndex, LogicalDirection.Forward); TextPointer matchEnd = matchStart.GetPositionAtOffset(patternLength, LogicalDirection.Forward); return new SearchResultValue(true, new TextRange(matchStart, matchEnd)); } // 处理跨文本块匹配(关键词被格式标记拆分到两个相邻Run的场景) int preCheckLength = Math.Min(patternLength - 1, currentRunText.Length); TextPointer crossCheckStart = currentPointer.GetPositionAtOffset(currentRunText.Length - preCheckLength, LogicalDirection.Forward); string crossCheckText = new TextRange(crossCheckStart, doc.ContentEnd).Text; int crossMatchIndex = crossCheckText.IndexOf(searchPattern, comparison); if (crossMatchIndex >= 0 && crossMatchIndex < preCheckLength) { TextPointer matchStart = crossCheckStart.GetPositionAtOffset(crossMatchIndex, LogicalDirection.Forward); TextPointer matchEnd = matchStart.GetPositionAtOffset(patternLength, LogicalDirection.Forward); return new SearchResultValue(true, new TextRange(matchStart, matchEnd)); } // 移动到下一个文本块 currentPointer = currentPointer.GetPositionAtOffset(currentRunText.Length, LogicalDirection.Forward); } return new SearchResultValue(false, null); } /// <summary> /// 搜索操作返回结果 /// </summary> /// <param name="Successful">是否匹配到内容</param> /// <param name="Selection">匹配到的文本范围,无匹配时为null</param> internal record struct SearchResultValue(bool Successful, TextRange? Selection);
实现优势
- 不需要提前提取全量文档文本,大文档下内存占用更低
- 不需要逐字符循环跳转定位终点,匹配到内容后直接通过偏移拿到起止位置,性能远高于逐位遍历的实现
- 自动处理带格式文本、跨文本块关键词、内嵌元素等场景,不会出现位置错位
- 完全兼容原有业务逻辑,支持从任意指定位置向后搜索。
内容的提问来源于stack exchange,提问作者Bjoern
相关产品推荐
相关产品推荐

