繁体   English   中英

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

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

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