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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2017 年 11 月 27 日

CPOL

4分钟阅读

viewsIcon

46861

downloadIcon

667

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

引言

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

  1. 不带集合控件或者
  2. 在任何表示集合的控件内(例如:TreeView - 请参阅 [1] 中的奖励内容)

我希望支持的其他要求是

  • MVVM 架构模式
  • 对黑色和浅色皮肤的默认支持
  • 自定义高亮颜色配置

下面的截图展示了根据 [2] 中的想法开发的示例应用程序。你应该能够选择左侧列表中的一个 string,然后右侧列表应该高亮显示相应的项目。

背景

我们开发了这个 WPF 树视图 [1] filter 应用程序,它可以在大约 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 获得一些反馈。

论坛(Forum below)中有一位读者建议以不同的方式实现这一点 - 主要是在附加行为中进行字符串匹配,而不是在 viewmodel 中。 Meshack Musundi 沿着这条思路,在这里实现了 替代解决方案。目前我不确定哪个解决方案更好,我猜取决于 :-) 我总是喜欢在 viewmodel 中进行数据处理(字符串匹配),让视图关心显示,但是有不同的方法可以实现这一点,这里有 2 种有趣的替代方案供您选择。

参考

© . All rights reserved.