我正在开发一个应用程序,用于搜索一批双语(源/翻译)XML文件(Trados SDLXLIFF)中的一些文本。由于我希望能够从搜索结果中快速编辑翻译文本,因此我选择了WPF DataGrid来显示搜索结果。
除此之外,我还想在黄色背景的搜索结果中突出显示搜索短语,并且还要突出显示源/翻译文本可能包含的红色字体内部标记/文本格式占位符。在谷歌搜索后,我找到this post并在我的代码中实施了建议。
乍一看一切正常,但后来我注意到当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单元格,我使用了here中的代码)。一旦DataGrid被彩色绘制,它就会被破坏。
UPD :我发现即使我只是将TextBlock.Inlines
内容替换为包含相同文本而没有着色的新Run
内容,DataGrid内容也会被破坏。因此,如果我们尝试使用其Inlines
集合进行操作,那么DataGrid中的绑定TextBlock基本上就会被破坏。
所以我的问题是:如何在不破坏此DataGrid的视觉一致性的情况下将颜色绘制应用于WPF DataGrid单元格中的文本?
答案 0 :(得分:1)
经过一些谷歌搜索后,我在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
。这个解决方案适用于原始代码,但据我所知,这种变体在性能方面不是很好。