简体   繁体   English

C#WPF DataGrid:如何在不破坏DataGrid iteslf的情况下为DataGrid单元内的文本绘制颜色?

[英]C# WPF DataGrid: how to color paint text inside DataGrid cells without breaking the DataGrid iteslf?

I'm developing an application for searching some text in a batch of bilingual (source/translation) XML files (Trados SDLXLIFF for that matter). 我正在开发一个应用程序,用于搜索一批双语(源/翻译)XML文件中的某些文本(就此而言,Trados SDLXLIFF)。 Since I want to be able to do quick edit in the translation text from search results, I have chosen WPF DataGrid to present the search results. 由于我希望能够对搜索结果中的翻译文本进行快速编辑,因此我选择了WPF DataGrid来显示搜索结果。

Among other things, I also want to highlight search phrase inside the search results with yellow background, and also to highlight with red font internal tags/text formatting placeholders that source/translation text may contain. 除其他外,我还想用黄色背景突出显示搜索结果中的搜索短语,并用红色字体突出显示源/翻译文本可能包含的内部标签/文本格式占位符。 After googling it I found this post and implemented the suggestion in my code. 谷歌搜索后,我发现了这篇文章,并在我的代码中实现了建议。

At first glance everything worked fine, but then I noticed that with a large number of search results when DataGrid needs to be scrolled it begins to display some random text from my search results, and each time when I scroll the DataGrid up and down it shows different text in cells where color painting was applied. 乍一看一切正常,但是后来我注意到,在需要滚动DataGrid的情况下,使用大量搜索结果,它开始显示搜索结果中的一些随机文本,并且每次我上下滚动DataGrid时,都会显示应用了颜色绘画的单元格中的不同文本。 Essentially, applying color painting to DataGrid cells breaks visual consistency of the DataGrid. 本质上,将彩色绘画应用于DataGrid单元会破坏DataGrid的视觉一致性。

To illustrate the issue I created a simple WFP application. 为了说明这个问题,我创建了一个简单的WFP应用程序。

XAML: 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#: 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;
        }
    }
}

Launch the application and then scroll the DataGrid repeatedly up and down, and you will see that with each scroll the DataGrid shows random text in cells that have been painted. 启动应用程序,然后反复上下滚动DataGrid,您将看到,每次滚动时,DataGrid都会在已绘制的单元格中显示随机文本。

I've done some experiments and can assure you that it does not matter how you add data to the DataGrid: either directly as in this example or via a bound data collection. 我已经做过一些实验,可以向您保证,将数据添加到DataGrid并不重要:直接如本例中那样或通过绑定的数据集合。 It does not matter how you apply text painting: either via connected "Loaded" events of TextBlocks (as in this example) or first adding data to the DataGrid and then walking over cells and applying painting to each cell individually. 无论如何应用文本绘画都无关紧要:通过连接的TextBlocks“加载”事件(如本例中所示),或者先将数据添加到DataGrid,然后遍历单元格并将绘画分别应用于每个单元格。 (For traversing the DataGrid cell by cell I used the code from here ). (为了按单元遍历DataGrid单元,我使用了来自此处的代码)。 As soon as the DataGrid is color-painted, it is broken. 一旦将DataGrid涂上颜色,它就会损坏。

UPD : I found out that the DataGrid contents get broken even if I just replace TextBlock.Inlines contents with a new Run object containing the same text with no coloring at all. UPD :我发现即使我仅用一个包含相同文本且完全不带颜色的新Run对象替换TextBlock.Inlines内容,DataGrid内容也会损坏。 So essentially a bound TextBlock in a DataGrid gets broken if we try to manipulate with its Inlines collection. 因此,如果尝试使用其Inlines集合进行操作, Inlines本质上讲,DataGrid中绑定的TextBlock会损坏。

So my question is: how to apply color painting to text in WPF DataGrid cells without breaking visual consistency of this DataGrid? 所以我的问题是:如何在WPF DataGrid单元中的文本上应用颜色绘画而又不破坏此DataGrid的视觉一致性?

After some googling I've found another solution for color-painting text inside a TextBlock. 经过一番谷歌搜索后,我发现了另一个在TextBlock中为文本着色的解决方案。 I subclassed TexbBlock to make its InlineCollection accessible via a new RichText bindable property and then wrote a value converter to convert plain text to a collection of rich text inlines based on regular expressions. 我将TexbBlock子类化,以使其可通过新的RichText可绑定属性访问其InlineCollection,然后编写了一个值转换器,将纯文本转换为基于正则表达式的富文本内联的集合。

The demonstration code is below. 演示代码如下。

XAML: 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# 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!");
        }
    }
}

Now color painting works fine on a DataGrid with a large numbers of rows. 现在,彩色绘画可以在具有大量行的DataGrid上正常工作。

PS. PS。 Some guy from MSDN forums also suggested to set VirtualizingPanel.IsVirtualizing property of a DataGrid to false . 来自MSDN论坛的一些人还建议将DataGrid的VirtualizingPanel.IsVirtualizing属性设置为false This solution works with the original code but, as I understand, this variant is not very good performance-wise. 此解决方案适用于原始代码,但据我了解,此变体不是非常好的性能。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM