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

时间:2015-10-24 07:19:56

标签: c# wpf xaml datagrid

我正在开发一个应用程序,用于搜索一批双语(源/翻译)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单元格中的文本?

1 个答案:

答案 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。这个解决方案适用于原始代码,但据我所知,这种变体在性能方面不是很好。