[英]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.