[英]C# WPF DataGrid: how to color paint text inside DataGrid cells without breaking the DataGrid iteslf?
我正在开发一个应用程序,用于搜索一批双语(源/翻译)XML文件中的某些文本(就此而言,Trados SDLXLIFF)。 由于我希望能够对搜索结果中的翻译文本进行快速编辑,因此我选择了WPF DataGrid来显示搜索结果。
除其他外,我还想用黄色背景突出显示搜索结果中的搜索短语,并用红色字体突出显示源/翻译文本可能包含的内部标签/文本格式占位符。 谷歌搜索后,我发现了这篇文章,并在我的代码中实现了建议。
乍一看一切正常,但是后来我注意到,在需要滚动DataGrid的情况下,使用大量搜索结果,它开始显示搜索结果中的一些随机文本,并且每次我上下滚动DataGrid时,都会显示应用了颜色绘画的单元格中的不同文本。 本质上,将彩色绘画应用于DataGrid单元会破坏DataGrid的视觉一致性。
为了说明这个问题,我创建了一个简单的WFP应用程序。
XAML:
<Window x:Class="WPF.Tutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=system"
Title="MainWindow" Height="480" Width="640" WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid x:Name="testGrid" Grid.Row="0" AutoGenerateColumns="False" Background="White" CanUserAddRows="False" CanUserDeleteRows="False" Margin="2" SelectionUnit="FullRow" SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ID}" Header="ID" Width="30" IsReadOnly="True" />
<DataGridTemplateColumn Header="Source" Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="sourceTextBlock" Text="{Binding Path=SourceText}" TextWrapping="Wrap" Loaded="onTextLoaded"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Target" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="targetTextBlock" Text="{Binding Path=TargetText}" TextWrapping="Wrap" Loaded="onTextLoaded"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox x:Name="targetTextBox" Text="{Binding Path=TargetText}" TextWrapping="Wrap" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Label x:Name="statusLabel" Grid.Row="1" />
</Grid>
</Window>
C#:
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.ComponentModel;
namespace WPF.Tutorial
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int i = 1; i <= 100; i++)
{
testGrid.Items.Add(new Segment() { ID = i.ToString(),
SourceText = String.Format("Segment <b>{0}</b>", i),
TargetText = String.Format("Сегмент <b>{0}</b>", i) });
}
statusLabel.Content = String.Format("Items: {0}", testGrid.Items.Count);
}
// Text highlighting
private void HighlightText(TextBlock tb)
{
// The search pattern we need to highlight
string searchText = "сегмент";
var regex = new Regex("(" + searchText + ")", RegexOptions.IgnoreCase);
// We want to highlight tags inside text
var tagRegex = new Regex("(<[^>]*>)", RegexOptions.IgnoreCase);
string[] pieces = tagRegex.Split(tb.Text);
var subpieces = new List<string>();
foreach (var piece in pieces)
{
subpieces.AddRange(regex.Split(piece));
}
tb.Inlines.Clear();
foreach (var item in subpieces)
{
// We don't want to highlight search patterns inside tags
if (regex.Match(item).Success && !tagRegex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Yellow;
tb.Inlines.Add(runx);
}
else if (tagRegex.Match(item).Success)
{
Run runx = new Run(item);
runx.Foreground = Brushes.Red;
tb.Inlines.Add(runx);
}
else
{
tb.Inlines.Add(item);
}
}
}
private void onTextLoaded(object sender, EventArgs e)
{
var tb = sender as TextBlock;
if (tb != null)
{
HighlightText(tb);
}
}
}
class Segment : IEditableObject
{
string targetBackup = null;
public string ID { get; set; }
public string SourceText { get; set; }
public string TargetText { get; set; }
public void BeginEdit()
{
if (targetBackup == null)
targetBackup = TargetText;
}
public void CancelEdit()
{
if (targetBackup != null)
{
TargetText = targetBackup;
targetBackup = null;
}
}
public void EndEdit()
{
if (targetBackup != null)
targetBackup = null;
}
}
}
启动应用程序,然后反复上下滚动DataGrid,您将看到,每次滚动时,DataGrid都会在已绘制的单元格中显示随机文本。
我已经做过一些实验,可以向您保证,将数据添加到DataGrid并不重要:直接如本例中那样或通过绑定的数据集合。 无论如何应用文本绘画都无关紧要:通过连接的TextBlocks“加载”事件(如本例中所示),或者先将数据添加到DataGrid,然后遍历单元格并将绘画分别应用于每个单元格。 (为了按单元遍历DataGrid单元,我使用了来自此处的代码)。 一旦将DataGrid涂上颜色,它就会损坏。
UPD :我发现即使我仅用一个包含相同文本且完全不带颜色的新Run
对象替换TextBlock.Inlines
内容,DataGrid内容也会损坏。 因此,如果尝试使用其Inlines
集合进行操作, Inlines
本质上讲,DataGrid中绑定的TextBlock会损坏。
所以我的问题是:如何在WPF DataGrid单元中的文本上应用颜色绘画而又不破坏此DataGrid的视觉一致性?
经过一番谷歌搜索后,我发现了另一个在TextBlock中为文本着色的解决方案。 我将TexbBlock子类化,以使其可通过新的RichText可绑定属性访问其InlineCollection,然后编写了一个值转换器,将纯文本转换为基于正则表达式的富文本内联的集合。
演示代码如下。
XAML:
<Window x:Class="WPF.Tutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=system"
xmlns:local="clr-namespace:WPF.Tutorial"
Title="MainWindow" Height="480" Width="640" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:RichTextValueConverter x:Key="RichTextValueConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid x:Name="testGrid" Grid.Row="0" AutoGenerateColumns="False" Background="White" CanUserAddRows="False" CanUserDeleteRows="False" Margin="2" SelectionUnit="FullRow" SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ID}" Header="ID" Width="30" IsReadOnly="True" />
<DataGridTemplateColumn Header="Source" Width="*" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:RichTextBlock x:Name="sourceTextBlock" TextWrapping="Wrap"
RichText="{Binding Path=SourceText, Converter={StaticResource RichTextValueConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Target" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:RichTextBlock x:Name="targetTextBlock" TextWrapping="Wrap"
RichText="{Binding Path=TargetText, Converter={StaticResource RichTextValueConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox x:Name="targetTextBox" Text="{Binding Path=TargetText}" TextWrapping="Wrap" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Label x:Name="statusLabel" Grid.Row="1" />
</Grid>
</Window>
C#
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.ComponentModel;
namespace WPF.Tutorial
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public static Regex SearchRegex = new Regex("(segment)", RegexOptions.IgnoreCase);
public static Regex TagRegex = new Regex("(<[^>]*>)", RegexOptions.IgnoreCase);
ObservableCollection<Segment> segments;
public MainWindow()
{
InitializeComponent();
segments = new ObservableCollection<Segment>();
testGrid.ItemsSource = segments;
for (int i = 1; i <= 100; i++)
{
segments.Add(new Segment()
{
ID = i.ToString(),
SourceText = String.Format("Segment <b>{0}</b>", i),
TargetText = String.Format("Сегмент <b>{0}</b>", i)
});
}
statusLabel.Content = String.Format("Items: {0}", testGrid.Items.Count);
}
public class Segment : IEditableObject
{
string targetBackup = null;
public string ID { get; set; }
public string SourceText { get; set; }
public string TargetText { get; set; }
public void BeginEdit()
{
if (targetBackup == null)
targetBackup = TargetText;
}
public void CancelEdit()
{
if (targetBackup != null)
{
TargetText = targetBackup;
targetBackup = null;
}
}
public void EndEdit()
{
if (targetBackup != null)
targetBackup = null;
}
}
}
public class RichTextBlock : TextBlock
{
public static DependencyProperty InlineProperty;
static RichTextBlock()
{
//OverrideMetadata call tells the system that this element wants to provide a style that is different than in base class
DefaultStyleKeyProperty.OverrideMetadata(typeof(RichTextBlock), new FrameworkPropertyMetadata(
typeof(RichTextBlock)));
InlineProperty = DependencyProperty.Register("RichText", typeof(List<Inline>), typeof(RichTextBlock),
new PropertyMetadata(null, new PropertyChangedCallback(OnInlineChanged)));
}
public List<Inline> RichText
{
get { return (List<Inline>)GetValue(InlineProperty); }
set { SetValue(InlineProperty, value); }
}
public static void OnInlineChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == e.OldValue)
return;
RichTextBlock r = sender as RichTextBlock;
List<Inline> i = e.NewValue as List<Inline>;
if (r == null || i == null)
return;
r.Inlines.Clear();
foreach (Inline inline in i)
{
r.Inlines.Add(inline);
}
}
}
class RichTextValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string text = value as string;
var inlines = new List<Inline>();
if (text != null)
{
string[] pieces = MainWindow.TagRegex.Split(text);
var subpieces = new List<string>();
foreach (var piece in pieces)
{
subpieces.AddRange(MainWindow.SearchRegex.Split(piece));
}
foreach (var item in subpieces)
{
if (MainWindow.SearchRegex.Match(item).Success && !MainWindow.TagRegex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Yellow;
inlines.Add(runx);
}
else if (MainWindow.TagRegex.Match(item).Success)
{
Run runx = new Run(item);
runx.Foreground = Brushes.Red;
inlines.Add(runx);
}
else
{
inlines.Add(new Run(item));
}
}
}
return inlines;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException("Back conversion is not supported!");
}
}
}
现在,彩色绘画可以在具有大量行的DataGrid上正常工作。
PS。 来自MSDN论坛的一些人还建议将DataGrid的VirtualizingPanel.IsVirtualizing
属性设置为false
。 此解决方案适用于原始代码,但据我了解,此变体不是非常好的性能。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.