WPF TextBlock выделяет определенные детали в зависимости от условий поиска - PullRequest
14 голосов
/ 15 апреля 2009

У меня есть TextBlock, в который динамически добавляются строки (в основном это набор объектов 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

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

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

Ответы [ 8 ]

16 голосов
/ 03 марта 2011

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

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

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

9 голосов
/ 26 февраля 2014

Я взял dthrasers answer и убрал необходимость в парсере 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~|. И пусть начнется выделение!

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

3 голосов
/ 13 марта 2016

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

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

Текстовый блок WPF с соответствием строки поиска

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

SearchMatchTextblock (GitHub)

2 голосов
/ 13 июля 2012

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

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

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

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

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

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

1 голос
/ 08 марта 2018

Здесь я представляю другой подход для выделения текста. У меня был случай, когда мне нужно было декорировать кучу кода C # в WPF, однако я не хотел использовать синтаксис textBlock.Inlines.Add, вместо этого я хотел сгенерировать 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) синим цветом, а Console.WriteLine выделить желтым.

Шаг 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. Добавьте 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;
        }

    }
}
1 голос
/ 13 апреля 2016

Вот что я придумал, отстроив существующий 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}"/>
0 голосов
/ 14 октября 2015

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

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

0 голосов
/ 16 апреля 2009

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

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

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