基于Caliburn Micro的WPF MVVM阿拉伯文本搜索定位滚动问询
解决方案:Caliburn Micro WPF MVVM阿拉伯文本搜索滚动与高亮
一、解决ScrollViewer+SelectableTextBlock的滚动绑定问题
ScrollViewer的ScrollableHeight是只读依赖属性,无法直接双向绑定,可通过附加属性实现VM与控件的状态同步,同时绑定VerticalOffset控制滚动:
1. 实现附加属性
public static class ScrollViewerExtensions { // 同步ScrollViewer的ScrollableHeight到VM public static readonly DependencyProperty ScrollableHeightProperty = DependencyProperty.RegisterAttached("ScrollableHeight", typeof(double), typeof(ScrollViewerExtensions), new PropertyMetadata(default(double), OnScrollableHeightChanged)); // 从VM控制ScrollViewer的VerticalOffset public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerExtensions), new PropertyMetadata(default(double), OnVerticalOffsetChanged)); private static void OnScrollableHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is ScrollViewer scrollViewer) { scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; } } private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { if (sender is ScrollViewer scrollViewer) { SetScrollableHeight(scrollViewer, scrollViewer.ScrollableHeight); } } private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is ScrollViewer scrollViewer) { scrollViewer.ScrollToVerticalOffset((double)e.NewValue); } } public static double GetScrollableHeight(DependencyObject obj) => (double)obj.GetValue(ScrollableHeightProperty); public static void SetScrollableHeight(DependencyObject obj, double value) => obj.SetValue(ScrollableHeightProperty, value); public static double GetVerticalOffset(DependencyObject obj) => (double)obj.GetValue(VerticalOffsetProperty); public static void SetVerticalOffset(DependencyObject obj, double value) => obj.SetValue(VerticalOffsetProperty, value); }
2. XAML绑定与ViewModel配置
<ScrollViewer local:ScrollViewerExtensions.ScrollableHeight="{Binding ScrollableHeight, Mode=OneWayToSource}" local:ScrollViewerExtensions.VerticalOffset="{Binding TargetVerticalOffset, Mode=OneWay}"> <local:SelectableTextBlock Text="{Binding ContentText}" HighlightedText="{Binding SearchKeyword}" /> </ScrollViewer>
ViewModel中定义对应属性,并在搜索结果点击时计算偏移:
public double ScrollableHeight { get; set; } public double TargetVerticalOffset { get; set; } public void OnSearchResultClicked(SearchResult result) { // 根据匹配文本位置计算目标偏移量(需适配阿拉伯文RTL布局的位置计算逻辑) TargetVerticalOffset = CalculateVerticalOffset(result.MatchPosition); NotifyOfPropertyChange(() => TargetVerticalOffset); }
二、更优替代方案:FlowDocumentScrollViewer + FlowDocument
原生FlowDocument对阿拉伯文RTL(从右到左)排版支持完善,自带滚动、文本选择功能,可轻松实现高亮与精准定位,完全符合MVVM模式:
1. ViewModel实现
public FlowDocument ContentDocument { get; set; } public TextRange TargetMatchRange { get; set; } public void OnSearchResultClicked(SearchResult result) { // 生成带高亮的FlowDocument ContentDocument = GenerateHighlightedDocument(result.FullText, result.SearchKeyword); // 定位到匹配文本范围 TargetMatchRange = FindMatchRange(ContentDocument, result.SearchKeyword); NotifyOfPropertyChange(() => ContentDocument); NotifyOfPropertyChange(() => TargetMatchRange); } private FlowDocument GenerateHighlightedDocument(string text, string keyword) { var doc = new FlowDocument(); var para = new Paragraph { FlowDirection = FlowDirection.RightToLeft }; // 开启阿拉伯文RTL布局 int index = 0; while (index < text.Length) { int keywordIndex = text.IndexOf(keyword, index, StringComparison.OrdinalIgnoreCase); if (keywordIndex == -1) { para.Inlines.Add(new Run(text.Substring(index))); break; } if (keywordIndex > index) { para.Inlines.Add(new Run(text.Substring(index, keywordIndex - index))); } // 高亮匹配文本 var highlightedRun = new Run(text.Substring(keywordIndex, keyword.Length)) { Background = Brushes.Yellow }; para.Inlines.Add(highlightedRun); index = keywordIndex + keyword.Length; } doc.Blocks.Add(para); return doc; } private TextRange FindMatchRange(FlowDocument doc, string keyword) { var range = new TextRange(doc.ContentStart, doc.ContentEnd); var position = range.Start; while (position != null) { if (position.GetTextInRun(LogicalDirection.Forward).Contains(keyword)) { var start = position; var end = position.GetPositionAtOffset(keyword.Length); return new TextRange(start, end); } position = position.GetNextContextPosition(LogicalDirection.Forward); } return null; }
2. 附加属性实现滚动定位
public static class FlowDocumentScrollViewerExtensions { public static readonly DependencyProperty TargetMatchRangeProperty = DependencyProperty.RegisterAttached("TargetMatchRange", typeof(TextRange), typeof(FlowDocumentScrollViewerExtensions), new PropertyMetadata(null, OnTargetMatchRangeChanged)); private static void OnTargetMatchRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.NewValue is TextRange range) { range.BringIntoView(); } } public static TextRange GetTargetMatchRange(DependencyObject obj) => (TextRange)obj.GetValue(TargetMatchRangeProperty); public static void SetTargetMatchRange(DependencyObject obj, TextRange value) => obj.SetValue(TargetMatchRangeProperty, value); }
3. XAML使用
<FlowDocumentScrollViewer local:FlowDocumentScrollViewerExtensions.TargetMatchRange="{Binding TargetMatchRange}" Document="{Binding ContentDocument}" />
该方案优势:
- 原生支持阿拉伯文RTL排版,无需额外解析处理
- 自带文本选择、复制等功能,无需自定义控件
- 通过
TextRange.BringIntoView实现精准滚动,无需手动计算偏移量
内容的提问来源于stack exchange,提问作者Everyone
相关产品推荐
相关产品推荐

