WPF TextBlock выделяет определенные части на основе условия поиска

У меня есть TextBlock, к которому динамично добавлены строки (в основном куча объектов Run, которые либо курсивом, либо полужирным шрифтом).

в моем приложении у меня есть функция поиска.

Я хочу иметь возможность выделить текст TextBlock, который находится в поиске.

под выделением я имею в виду изменение определенных частей цвета текста TextBlock (имея в виду, что он может выделить несколько разных объектов запуска одновременно).

Я пробовал этот пример http://blogs.microsoft.co.il/blogs/tamir/archive/2008/05/12/search-and-highlight-any-text-on-wpf-rendered-page.aspx

но швы очень нестабильная :(

есть ли простой способ решить эту проблему?

8 ответов


этот вопрос похож на Как отобразить результаты поиска в элементе управления WPF с выделенными условиями запроса

в ответ на этот вопрос я придумал подход, который использует IValueConverter. Конвертер берет фрагмент текста, форматирует его в допустимую разметку XAML и использует XamlReader для создания экземпляра разметки в объекты framework.

полное объяснение довольно длинное, поэтому я опубликовал его в своем блоге:Выделение Терминов Запроса в текстовом блоке WPF


взял dthrasers ответ и снял необходимость в синтаксическом анализаторе XML. Он делает большую работу, объясняя каждую из частей в блог, однако это не потребовало от меня добавления каких-либо дополнительных библиотек, вот как я это сделал.

Шаг первый, сделайте класс конвертера:

class StringToXamlConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string input = value as string;
            if (input != null)
            {
                var textBlock = new TextBlock();
                textBlock.TextWrapping = TextWrapping.Wrap;
                string escapedXml = SecurityElement.Escape(input);

                while (escapedXml.IndexOf("|~S~|") != -1) {
                //up to |~S~| is normal
                textBlock.Inlines.Add(new Run(escapedXml.Substring(0, escapedXml.IndexOf("|~S~|"))));
                //between |~S~| and |~E~| is highlighted
                textBlock.Inlines.Add(new Run(escapedXml.Substring(escapedXml.IndexOf("|~S~|") + 5,
                                          escapedXml.IndexOf("|~E~|") - (escapedXml.IndexOf("|~S~|") + 5))) 
                                          { FontWeight = FontWeights.Bold, Background= Brushes.Yellow });
                //the rest of the string (after the |~E~|)
                escapedXml = escapedXml.Substring(escapedXml.IndexOf("|~E~|") + 5);
                }

                if (escapedXml.Length > 0)
                {
                    textBlock.Inlines.Add(new Run(escapedXml));                      
                }
                return textBlock;
            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException("This converter cannot be used in two-way binding.");
        }

    }

Шаг второй: Вместо TextBlock используйте ContentBlock. Передайте строку (вы бы использовали ее для своего textBlock) в блок содержимого, например Итак:

<ContentControl
               Margin="7,0,0,0"
               HorizontalAlignment="Left"
               VerticalAlignment="Center"
               Content="{Binding Description, Converter={StaticResource CONVERTERS_StringToXaml}, Mode=OneTime}">
</ContentControl>

Шаг третий: Убедитесь, что тест, который вы проходите, обозначен |~S~| и |~E~|. И пусть подсветка начнется!

заметки:
вы можете изменить стиль в беге, чтобы определить, что и как ваш текст будет выделен
убедитесь, что вы добавили класс конвертера в пространство имен и ресурсы. Для этого также может потребоваться перестроение.


по странному совпадению, я недавно написал статью, которая решает ту же проблему. Это пользовательский элемент управления, который имеет те же свойства, что и TextBlock (так что вы можете поменять его на TextBlock везде, где вам это нужно), и у него есть дополнительное свойство, которое вы можете привязать к called HighLightText, и везде, где значение HighLightText находится в main Text свойство (без учета регистра), оно выделено.

это был довольно прямой контроль для создания, и вы можете найти статья здесь:

WPF TextBlock с соответствием строки поиска

и полный код как решение здесь:

HighlightSearchMatchTextBlock (GitHub)


у меня была аналогичная проблема - попытка реализовать текстовый поиск по нагрузке докладчиков, которые в основном представляют отчет. Отчет был первоначально записан в строку, и мы использовали встроенный в ctrl-F FlowDocumentViewer - это не очень хорошо и имеет некоторые параметры wierd, но было достаточно.

Если вы просто хотите что-то подобное можно сделать следующее:

        <FlowDocumentScrollViewer>
            <FlowDocument>
                <Paragraph FontFamily="Lucida Console" FontSize="12">
                    <Run Text="{Binding Content, Mode=OneWay}"/>
                </Paragraph>
            </FlowDocument>
        </FlowDocumentScrollViewer>

мы решили пойти на перезапись, а отчет хранится в синхронизации с остальной частью программа и в основном все изменения его, чтобы воссоздать весь отчет каждый раз означает, что это довольно медленно. Мы хотели улучшить это, перейдя к модели update-the-bits-you-need-to, но нужно было иметь модель представления (а не просто строку), чтобы иметь возможность делать это в здравом уме! Мы хотели сохранить функциональность поиска перед заменой отчета, однако, и пойти лучше и выделить "текущую" позицию поиска в одном цвете и другие поисковые хиты в другой.

вот упрощенная версия моего решения; класс, производный от TextBlock, который добавляет свойство зависимостей типа HighlightingInformation. Я не включил пространство имен и использование, поскольку они чувствительны.

public class HighlightingTextBlock : TextBlock
{
    public static readonly DependencyProperty HighlightingProperty =
        DependencyProperty.Register("Highlighting", typeof (HighlightingInformation), typeof (HighlightingTextBlock));

    public HighlightingInformation Highlighting
    {
        get { return (HighlightingInformation)GetValue(HighlightingProperty); }
        set { SetValue(HighlightingProperty, value); }
    }

    public HighlightingTextBlock()
    {
        AddValueChangedCallBackTo(HighlightingProperty, UpdateText);
    }

    private void AddValueChangedCallBackTo(DependencyProperty property, Action updateAction)
    {
        var descriptor = DescriptorFor(property);
        descriptor.AddValueChanged(this, (src, args) => updateAction());
    }

    private DependencyPropertyDescriptor DescriptorFor(DependencyProperty property)
    {
        return DependencyPropertyDescriptor.FromProperty(property, GetType());
    }

    private void UpdateText()
    {
        var highlighting = Highlighting;
        if (highlighting == null)
            return;
        highlighting.SetUpdateMethod(UpdateText);

        var runs = highlighting.Runs;
        Inlines.Clear();
        Inlines.AddRange(runs);
    }
}

тип, к которому этот класс может быть привязан, использует метод update при изменении текста и списка основных моментов для обновления списка запусков. Сами блики выглядят примерно так:

public class Highlight
{
    private readonly int _length;
    private readonly Brush _colour;

    public int Start { get; private set; }

    public Highlight(int start, int length,Brush colour)
    {
        Start = start;
        _length = length;
        _colour = colour;
    }

    private string TextFrom(string currentText)
    {
        return currentText.Substring(Start, _length);
    }

    public Run RunFrom(string currentText)
    {
        return new Run(TextFrom(currentText)){Background = _colour};
    }
}

для получения правильная коллекция бликов-это отдельная проблема, которую я в основном решил, рассматривая коллекцию докладчиков как дерево, в котором вы рекурсивно ищете контент - листовые узлы-это те, у которых есть контент, а у других узлов просто есть дети. Если вы ищете глубину-сначала вы получаете заказ, который ожидаете. Затем вы можете в основном написать обертку вокруг списка результатов, чтобы отслеживать позицию. Я не собираюсь публиковать весь код для этого - мой ответ здесь, чтобы документировать, как вы можете сделайте wpf многоцветной подсветкой в стиле MVP.

Я не использовал INotifyPropertyChanged или CollectionChanged здесь, поскольку нам не нужны были изменения, чтобы быть мульти-cast (например, один ведущий имеет несколько представлений). Первоначально я попытался сделать это, добавив уведомление об изменении события для текста и одно для списка (которое вы также должны вручную подписаться на событие INotifyCollectionChanged). Однако у меня были опасения по поводу утечек памяти из подзаголовков событий и тот факт, что обновления для текста и основные моменты не пришли в то же время сделали его проблематичным.

единственный недостаток этого подхода заключается в том, что люди не должны привязываться к свойству text элемента управления. В реальной версии я добавил некоторые проверки + исключения, чтобы остановить людей от этого, но ommitted его из примера для ясности!


вот что я придумал, построив из exisiting TextBlock и добавление нового свойства зависимостей с именем SearchText:

public class SearchHightlightTextBlock : TextBlock
{
    public SearchHightlightTextBlock() : base() { }

    public String SearchText { get { return (String)GetValue(SearchTextProperty); }
                               set { SetValue(SearchTextProperty, value); } }      

    private static void OnDataChanged(DependencyObject source,
                                      DependencyPropertyChangedEventArgs e)
    {
        TextBlock tb = (TextBlock)source;

        if (tb.Text.Length == 0)
            return;

        string textUpper = tb.Text.ToUpper();
        String toFind = ((String) e.NewValue).ToUpper();
        int firstIndex = textUpper.IndexOf(toFind);
        String firstStr = tb.Text.Substring(0, firstIndex);
        String foundStr = tb.Text.Substring(firstIndex, toFind.Length);
        String endStr = tb.Text.Substring(firstIndex + toFind.Length, 
                                         tb.Text.Length - (firstIndex + toFind.Length));

        tb.Inlines.Clear();
        var run = new Run();
        run.Text = firstStr;
        tb.Inlines.Add(run);
        run = new Run();
        run.Background = Brushes.Yellow;
        run.Text = foundStr;
        tb.Inlines.Add(run);
        run = new Run();
        run.Text = endStr;

        tb.Inlines.Add(run);
    }

    public static readonly DependencyProperty SearchTextProperty =
        DependencyProperty.Register("SearchText", 
                                    typeof(String), 
                                    typeof(SearchHightlightTextBlock), 
                                    new FrameworkPropertyMetadata(null, OnDataChanged));
}

и на ваш взгляд, это:

<view:SearchHightlightTextBlock SearchText="{Binding TextPropertyContainingTextToSearch}" 
                                Text="{Binding YourTextProperty}"/>

здесь я представляю другой подход для выделения текста. У меня был случай использования, когда мне нужно было украсить кучу кода C# в WPF, однако я не хотел использовать textBlock.Встроенный.Добавьте тип синтаксиса, вместо этого я хотел создать выделение XAML на лету, а затем динамически добавить его в холст или какой-либо другой контейнер в WPF.

предположим, вы хотите раскрасить следующий фрагмент кода, а также выделить его часть:

public static void TestLoop(int count)
{ 
   for(int i=0;i<count;i++)
     Console.WriteLine(i);
}

предположим, что приведенный выше код находится в файле под названием Test.формат txt. Предположим, вы хотите раскрасить все ключевые слова C# (public, static, void и т. д..) и простые типы (int, string) в синем цвете и консоли.Строку выделить желтым цветом.

Шаг 0. Создайте новое приложение WPF и включите в файл Test пример кода, подобный приведенному выше.txt

Шаг 1. Создайте класс выделения кода:

using System.IO;
using System.Text;

public enum HighLightType
{
    Type = 0,
    Keyword = 1,
    CustomTerm = 2
}

public class CodeHighlighter
{
    public static string[] KeyWords = { "public", "static", "void", "return", "while", "for", "if" };
    public static string[] Types = { "string", "int", "double", "long" };

    private string FormatCodeInXaml(string code, bool withLineBreak)
    {
        string[] mapAr = { "<","&lt;" , //Replace less than sign
                            ">","&gt;" }; //Replace greater than sign
        StringBuilder sb = new StringBuilder();

        using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
        {
            while (!sr.EndOfStream)
            {
                string line = sr.ReadLine();

                line = line.Replace("\t", "&#160;&#160;&#160;&#160;"); //Replace tabs
                line = line.Replace(" ", "&#160;"); //Replace spaces

                for (int i = 0; i < mapAr.Length; i += 2)
                    line = line.Replace(mapAr[i], mapAr[i + 1]);

                if (withLineBreak)
                    sb.AppendLine(line + "<LineBreak/>"); //Replace line breaks
                else
                    sb.AppendLine(line);
            }

        }
        return sb.ToString();
    }


    private string BuildForegroundTag(string highlightText, string color)
    {
        return "<Span Foreground=\"" + color + "\">" + highlightText + "</Span>";
    }

    private string BuildBackgroundTag(string highlightText, string color)
    {
        return "<Span Background=\"" + color + "\">" + highlightText + "</Span>";
    }

    private string HighlightTerm(HighLightType type, string term, string line)
    {
        if (term == string.Empty)
            return line;

        string keywordColor = "Blue";
        string typeColor = "Blue";
        string statementColor = "Yellow";

        if (type == HighLightType.Type)
            return line.Replace(term, BuildForegroundTag(term, typeColor));
        if (type == HighLightType.Keyword)
            return line.Replace(term, BuildForegroundTag(term, keywordColor));
        if (type == HighLightType.CustomTerm)
            return line.Replace(term, BuildBackgroundTag(term, statementColor));

        return line;
    }

    public string ApplyHighlights(string code, string customTerm)
    {
        code = FormatCodeInXaml(code, true);
        customTerm = FormatCodeInXaml(customTerm, false).Trim();

        StringBuilder sb = new StringBuilder();
        using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
        {
            while (!sr.EndOfStream)
            {
                string line = sr.ReadLine();

                line = HighlightTerm(HighLightType.CustomTerm, customTerm, line);

                foreach (string keyWord in KeyWords)
                    line = HighlightTerm(HighLightType.Keyword, keyWord, line);

                foreach (string type in Types)
                    line = HighlightTerm(HighLightType.Type, type, line);

                sb.AppendLine(line);
            }
        }

        return sb.ToString();

    }
}

Шаг 2. Добавьте тег Canvas XAML в Главное окно.в XAML

<Window x:Class="TestCodeVisualizer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestCodeVisualizer"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Canvas Name="canvas" />
</Window>

Шаг 3. В приложении WPF добавьте следующий код: (убедитесь, что этот тест.txt находится в правильном месте):

using System.Text;
using System.IO;
using System.Windows;
using System.Windows.Markup;

namespace TestCodeVisualizer
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            string testText = File.ReadAllText("Test.txt");
            FrameworkElement fe = GenerateHighlightedTextBlock(testText, "Console.WriteLine");
            this.canvas.Children.Add(fe);
        }


        private FrameworkElement GenerateHighlightedTextBlock(string code, string term)
        {
            CodeHighlighter ch = new CodeHighlighter();
            string uc = "<UserControl xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>[CONTENT]</UserControl>";

            string content = "<TextBlock>" + ch.ApplyHighlights(code, term) + "</TextBlock>";
            uc = uc.Replace("[CONTENT]", content);

            FrameworkElement fe = XamlReader.Load(new System.IO.MemoryStream(Encoding.UTF8.GetBytes(uc))) as FrameworkElement;
            return fe;
        }

    }
}

закончил писать следующий код

на данный момент есть несколько ошибок, но решает проблему

if (Main.IsFullTextSearch)
{
    for (int i = 0; i < runs.Count; i++)
    {
        if (runs[i] is Run)
        {
            Run originalRun = (Run)runs[i];

            if (Main.SearchCondition != null && originalRun.Text.ToLower()
                .Contains(Main.SearchCondition.ToLower()))
            {
                int pos = originalRun.Text.ToLower()
                          .IndexOf(Main.SearchCondition.ToLower());

                if (pos > 0)
                {
                    Run preRun = CloneRun(originalRun);
                    Run postRun = CloneRun(originalRun);

                    preRun.Text = originalRun.Text.Substring(0, pos);
                    postRun.Text = originalRun.Text
                        .Substring(pos + Main.SearchCondition.Length);

                    runs.Insert(i - 1 < 0 ? 0 : i - 1, preRun);
                    runs.Insert(i + 1, new Run(" "));
                    runs.Insert(i + 2, postRun);

                    originalRun.Text = originalRun.Text
                        .Substring(pos, Main.SearchCondition.Length);

                    SolidColorBrush brush = new SolidColorBrush(Colors.Yellow);
                    originalRun.Background = brush;

                    i += 3;
                }
            }
        }
    }
}

Если вы обрабатываете ContainerContentChanging для ListViewBase, вы можете использовать следующий подход:выделение текстовых блоков для WinRT / ContainerContentChanging

обратите внимание, что этот код предназначен для Windows RT. Синтаксис WPF будет немного отличаться. Также обратите внимание, что если вы используете привязку для заполнения TextBlock.Свойство Text, текст, сгенерированный моим подходом, будет перезаписан. Я использую ContainerContentChanging для заполнения целевых полей, потому что радикально-увеличение производительности и улучшение использования памяти по сравнению с обычной привязкой. Я использую привязку только для управления исходными данными, а не представлением данных.