65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2017 年 11 月 27 日

CPOL

4分钟阅读

viewsIcon

46867

downloadIcon

669

在 WPF TextBlock 控件中实现具有 MVVM 的文本高亮

引言

我们回顾了一个 WPF 行为 [3],它可以高亮显示 WPF TextBlock 控件中的文本。此行为几乎可以用于任何可以使用 TextBlock 控件的地方。因此,虽然我们在本文中会讨论如何在 ListBox 中使用它,但该行为也可以在

  1. 不使用集合控件的情况下使用,或者
  2. 在任何表示集合的控件(例如: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 变量包含表示左侧 ListBoxList1)中选定项的 stringforeach 循环遍历右侧 ListBoxList2)中的每个项,并调整 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

右侧 ListBoxList2)中的所有项都是 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 接口,以及
  • 实现 viewmodelSelectionRange

这 3 项的应用应该很简单且灵活。事实证明,我们还可以使用该接口在后台线程中计算匹配范围,而不会在绑定到 UI 集合项时遇到其他问题,正如我们在单独的奖励内容 [1] 部分中将看到的。

目前,我不确定这 3 项是否应包含在额外的 Nuget 包库中,但我会考虑一下。我很想就这个问题以及此处开发的 Code Project 获得一些反馈。

在下面的论坛中,有一位读者建议以不同的方式实现这一点 - 主要是在附加行为中进行字符串匹配,而不是在 viewmodel 中。 Meshack Musundi 遵循了这一思路,并在此处实现了 替代解决方案。目前我不确定哪种解决方案更好,我猜这取决于 :-) 我一直倾向于在 viewmodel 中进行数据处理(字符串匹配),让视图负责显示,但实现这一点有不同的方法,这里有 2 个有趣的替代方案供您选择。

参考

© . All rights reserved.