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

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

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

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

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

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

Но он очень неустойчив: (

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

Ответ 1

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

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

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

Ответ 2

Я взял 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~|. И пусть начнется подсветка!

Примечания:
Вы можете изменить стиль в перспективе, чтобы определить, что и как выделяется ваш текст
Убедитесь, что вы добавили свой класс конвертера в пространство имен и ресурсы. Это также может потребовать перестройки для работы.

Ответ 3

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

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

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

И полный код в качестве решения здесь:

SearchMatchTextblock (GitHub)

Ответ 4

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

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

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

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

Вот упрощенная версия моего решения; класс, который происходит из TextBlock, который добавляет свойство зависимостей Type 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);
    }
}

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

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, так как нам не нужны изменения, которые должны быть мультилисты (например, один ведущий имеет несколько просмотров). Сначала я попытался сделать это, добавив уведомление об изменении события для текста и одно для списка (которому также необходимо вручную подписаться на событие INotifyCollectionChanged). Однако у меня были проблемы с утечками памяти из подписки на события, и тот факт, что обновления для текста и основных моментов не принесли в то же время, сделало его проблематичным.

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

Ответ 5

Вот что я придумал, построив exishing 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}"/>

Ответ 6

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

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

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

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

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

Шаг 1. Создайте класс Code Highlighter:

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. Добавьте XAML-тег Canvas в свой MainWindow.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 добавьте следующий код: (убедитесь, что test.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;
        }

    }
}

Ответ 7

Закончено создание следующего кода

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

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;
                }
            }
        }
    }
}

Ответ 8

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

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