c# WPF DataGrid:如何在不破坏DataGrid本身的情况下为DataGrid单元格中的文本着色

本文关键字:DataGrid 单元格 情况下 文本 WPF | 更新日期: 2023-09-27 18:18:59

我正在开发一个应用程序,用于在一批双语(源/翻译)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内容,数据网格内容也会被破坏,根本没有着色。所以本质上,如果我们试图操纵它的Inlines集合,那么DataGrid中的绑定TextBlock就会被破坏。

所以我的问题是:如何在不破坏这个数据网格的视觉一致性的情况下将彩色绘画应用到WPF数据网格单元格中的文本?

c# WPF DataGrid:如何在不破坏DataGrid本身的情况下为DataGrid单元格中的文本着色

经过一番谷歌搜索,我找到了另一种解决方案,在TextBlock内着色文本。我子类化了texblock,使它的InlineCollection可以通过一个新的RichText可绑定属性访问,然后编写了一个值转换器,将纯文本转换为基于正则表达式的富文本内联集合。

示例代码如下。

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上工作得很好。

p。一些来自MSDN论坛的人也建议将数据网格的VirtualizingPanel.IsVirtualizing属性设置为false。此解决方案适用于原始代码,但据我所知,此变体在性能方面不是很好。