簡體   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