C#/VB.Net 中可高亮显示的 WPF/MVVM TextBlock





5.00/5 (12投票s)
在 WPF TextBlock 控件中实现具有 MVVM 的文本高亮
引言
我们回顾了一个 WPF 行为 [3],它可以高亮显示 WPF TextBlock
控件中的文本。此行为几乎可以用于任何可以使用 TextBlock
控件的地方。因此,虽然我们在本文中会讨论如何在 ListBox
中使用它,但该行为也可以在
- 不使用集合控件的情况下使用,或者
- 在任何表示集合的控件(例如:
TreeView
- 请参阅 [1] 中的奖励内容)中
我希望支持的其他要求是
- MVVM 架构模式
- 对黑色和浅色皮肤的默认支持
- 自定义高亮颜色配置
下面的截图显示了基于 [2] 中的想法开发的示例应用程序。您应该能够选择左侧列表中的一个 string
,右侧列表应高亮显示相应的项。
背景
我们开发了一个 WPF Tree View [1] 过滤应用程序,该应用程序在大约 100,000 个节点树上运行速度相当快,我当时想知道如何高亮显示 TextBlock
项目,因为有时很难真正看到匹配的文本。因此,我进行了一些搜索,找到了一些有前景的示例,但没有一个支持 MVVM 或暗/浅色皮肤,但 [2] 中的示例对我来说似乎最有前景,所以我试图将此解决方案开发成一个更健壮且符合 MVVM 的解决方案。
Using the Code
本文附带的示例代码包含 2 个项目
WpfApplication4
HighlightableTextBlock
其中第一个 WpfApplication4
项目几乎是 [2] 中的原始代码,而 HighlightableTextBlock
项目包含我想在本文中开发的这个行为。您应该能够将每个项目设置为 Visual Studio 中的启动项目,启动项目并单击列表框以感受这些示例的工作方式。
关注点
我附上了这两个版本,希望它们能帮助其他人了解开发标准(符合 Winforms)解决方案到符合 MVVM 的解决方案时有时所需的转换过程。
那么,让我们检查 HighlightableTextBlock
项目中的代码以了解它是如何工作的。MainWindow.xaml 文件中左侧 ListBox
的定义是一个很好的起点。
<ListBox Grid.Column="0" Grid.Row="1"
ItemsSource="{Binding List1}"
SelectionMode="Single"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="3"
behav:SelectedItemsBahavior.ChangedCommand="{Binding ListSelectionChangedCommand}"
/>
上面的 XAML 指定 ListBox
应从 AppViewModel
对象中的 List1
属性获取其项。当左侧 ListBox
中的选择发生更改时,会调用 SelectedItemsBehavior
。当用户单击 MainWindow
左侧列表中的一项时,该行为会调用 AppViewModel
对象中的 ListSelectionChangedCommand
(将选定的项作为命令参数发送)。ListSelectionChangedCommand
中的代码如下所示:
public ICommand ListSelectionChangedCommand
{
get
{
if (_ListSelectionChangedCommand == null)
{
_ListSelectionChangedCommand = new RelayCommand<object>((p) =>
{
var spara = p as string;
if (spara == null)
return;
foreach (var item in List2) // Evaluate selected string in left List1
{ // against each item in List2
item.MatchString(spara);
}
});
}
return _ListSelectionChangedCommand;
}
}
Public ReadOnly Property ListSelectionChangedCommand As ICommand
Get
If _ListSelectionChangedCommand Is Nothing Then
_ListSelectionChangedCommand = New RelayCommand(Of Object)(
Sub(p)
Dim spara = TryCast(p, String)
If spara Is Nothing Then Return
For Each item In List2 '' Evaluate selected string in left List1
item.MatchString(spara) '' against each item in List2
Next
End Sub)
End If
Return _ListSelectionChangedCommand
End Get
End Property
spara
变量包含表示左侧 ListBox
(List1
)中选定项的 string
。foreach
循环遍历右侧 ListBox
(List2
)中的每个项,并调整 StringMatchItem
类中的匹配指示器属性 Range
属性
public bool MatchString(string searchString)
{
if (string.IsNullOrEmpty(DisplayString) == true &&
string.IsNullOrEmpty(searchString) == true)
{
Range = new SelectionRange(0,0);
return true;
}
else
{
if (string.IsNullOrEmpty(DisplayString) == true ||
string.IsNullOrEmpty(searchString) == true)
{
Range = new SelectionRange(-1, -1);
return false;
}
}
int start;
if ((start = DisplayString.IndexOf(searchString)) >= 0)
{
Range = new SelectionRange(start, start + searchString.Length);
return true;
}
else
{
Range = new SelectionRange(start, -1);
return false;
}
}
Public Function MatchString(ByVal searchString As String) As Boolean
If String.IsNullOrEmpty(DisplayString) = True AndAlso String.IsNullOrEmpty(searchString) = True Then
Range = New SelectionRange(0, 0)
Return True
Else
If String.IsNullOrEmpty(DisplayString) = True OrElse String.IsNullOrEmpty(searchString) = True Then
Range = New SelectionRange(-1, -1)
Return False
End If
End If
Dim start As Integer
start = DisplayString.IndexOf(searchString)
If start >= 0 Then
Range = New SelectionRange(start, start + searchString.Length)
Return True
Else
Range = New SelectionRange(start, -1)
Return False
End If
End Function
右侧 ListBox
(List2
)中的所有项都是 StringMatchItem
类型的对象。该类包含一个 Range
属性,该属性符合 ISelectionRange
接口定义。右侧 ListBox
中每个项的 TextBlock
绑定到 StringMatchItem
类中每个 viewmodel
项的 Range
属性,如下所示:
<ListBox Grid.Column="1" Grid.Row="1"
ItemsSource="{Binding List2}"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="3">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayString}"
behav:HighlightTextBlockBehavior.Range="{Binding Range}"
/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
HighlightTextBlockBehavior
行为会侦听 Range
属性的变化,并对此做出反应,从而高亮显示我们希望引起特别关注的区域。
private static void OnRangeChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TextBlock txtblock = d as TextBlock;
if (txtblock == null)
return;
var range = GetRange(d); // Get the bound Range value to do highlighting
// Standard background is transparent
SolidColorBrush normalBackGround = new SolidColorBrush(Color.FromArgb(00, 00, 00, 00));
if (range != null)
{
if (range.NormalBackground != default(Color))
normalBackGround = new SolidColorBrush(range.NormalBackground);
}
var txtrange = new TextRange(txtblock.ContentStart, txtblock.ContentEnd);
txtrange.ApplyPropertyValue(TextElement.BackgroundProperty, normalBackGround);
if (range == null)
return;
if (range.Start < 0 || range.End < 0) // Nothing to highlight here :-(
return;
try
{
Color selColor = (range.DarkSkin ? Color.FromArgb(255, 254, 252, 200) :
Color.FromArgb(255, 208, 247, 255));
Brush selectionBackground = new SolidColorBrush(selColor);
if (range != null)
{
if (range.SelectionBackground != default(Color))
selectionBackground = new SolidColorBrush(range.SelectionBackground);
}
TextRange txtrangel = new TextRange(
txtblock.ContentStart.GetPositionAtOffset(range.Start + 1)
, txtblock.ContentStart.GetPositionAtOffset(range.End + 1));
txtrangel.ApplyPropertyValue(TextElement.BackgroundProperty, selectionBackground);
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);
Console.WriteLine(exc.StackTrace);
}
}
Private Sub OnRangeChanged(ByVal d As DependencyObject,
ByVal e As DependencyPropertyChangedEventArgs)
Dim txtblock As TextBlock = TryCast(d, TextBlock)
If txtblock Is Nothing Then Return
Dim range = GetRange(d) '' Get the bound Range value to do highlighting
Dim normalBackGround As SolidColorBrush = New SolidColorBrush(Color.FromArgb(0, 0, 0, 0))
If range IsNot Nothing Then
If range.NormalBackground <> Nothing Then normalBackGround = New SolidColorBrush(range.NormalBackground)
End If
Dim txtrange = New TextRange(txtblock.ContentStart, txtblock.ContentEnd)
txtrange.ApplyPropertyValue(TextElement.BackgroundProperty, normalBackGround)
If range Is Nothing Then Return
If range.Start < 0 OrElse range.[End] < 0 Then Return
Try
Dim selColor As Color = (If(range.DarkSkin, Color.FromArgb(255, 254, 252, 200),
Color.FromArgb(255, 208, 247, 255)))
Dim selectionBackground As Brush = New SolidColorBrush(selColor)
If range IsNot Nothing Then
If range.SelectionBackground <> Nothing Then selectionBackground = New SolidColorBrush(range.SelectionBackground)
End If
Dim txtrangel As TextRange = New TextRange(txtblock.ContentStart.GetPositionAtOffset(range.Start + 1), txtblock.ContentStart.GetPositionAtOffset(range.[End] + 1))
txtrangel.ApplyPropertyValue(TextElement.BackgroundProperty, selectionBackground)
Catch exc As Exception
Console.WriteLine(exc.Message)
Console.WriteLine(exc.StackTrace)
End Try
End Sub
我们可以在这里看到,该行为使用了 WPF 的 TextRange 类,它通常用于在 WPF FlowDocument
中选择文本,但也可以在此处使用,因为 WPF TextBlock
也提供了所需的属性和方法。
结论
重用代码不应该需要比
HighlightTextBlockBehavior
行为ISelectionRange
接口,以及- 实现
viewmodel
类SelectionRange
这 3 项的应用应该很简单且灵活。事实证明,我们还可以使用该接口在后台线程中计算匹配范围,而不会在绑定到 UI 集合项时遇到其他问题,正如我们在单独的奖励内容 [1] 部分中将看到的。
目前,我不确定这 3 项是否应包含在额外的 Nuget 包库中,但我会考虑一下。我很想就这个问题以及此处开发的 Code Project 获得一些反馈。
在下面的论坛中,有一位读者建议以不同的方式实现这一点 - 主要是在附加行为中进行字符串匹配,而不是在 viewmodel 中。 Meshack Musundi 遵循了这一思路,并在此处实现了 替代解决方案。目前我不确定哪种解决方案更好,我猜这取决于 :-) 我一直倾向于在 viewmodel 中进行数据处理(字符串匹配),让视图负责显示,但实现这一点有不同的方法,这里有 2 个有趣的替代方案供您选择。