// Здесь запускается механизм подсвечивания в созданном мною диспетчере задач. // Смысл в том, что если текст вводится/стирается слишком быстро, // предыдущая подсветка не успеет закончить работу, поэтому новая подсветка // добавляется в очередь. Если в очереди уже что то есть, то это удаляется из очереди // и вставляется новая задача. Для каждого правила очередь своя. void BeginHighlight(HighlightRule rule) { _ruleTasks[rule].Add(new Action(() => Highlight(rule))); }
// Механизм подсветки void Highlight(HighlightRule rule) { // Если передали не существующее правило, покидаем процедуру if (rule == null) { return; } // Так как правила у нас задаются в Xaml коде, они будут принадлежать основному потоку, в котором крутится форма, // поэтому некоторые свойства можно достать/положить только таким образом ObservableCollection <Highlight> highlights = null; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { highlights = rule.Highlights; })); // Даже если существует правило, но в нем не задано, чем подсвечивать, покидаем процедуру подсветки if (highlights.Count == 0) { return; } // Еще ряд условий для выхода из процедуры подсветки var exitFlag = false; exitFlag = exitFlag || string.IsNullOrWhiteSpace(_content); Application.Current.Dispatcher.Invoke(new ThreadStart(() => { exitFlag = exitFlag || Inlines.IsReadOnly || Inlines.Count == 0 || HighlightRules == null || HighlightRules.Count == 0; })); if (exitFlag) { return; } // Создадим параграф. Все манипуляции будем проводить внутри него, потому что выделить что либо // непосредственно в TextBlock нельзя, если это выделение затрагивает несколько элементов var par = new Paragraph(); // Парсим _content, в котором у нас сериализованный Span с оригинальным содержимым TextBlock'a. var parsedSp = (Span)XamlReader.Parse(_content); // Сам Span нам не нужен, поэтому сливаем все его содержимое в параграф par.Inlines.AddRange(parsedSp.Inlines.ToArray()); // Обозначаем стартовую позицию (просто для удобства) и выдергиваем из TextBlock'a голый текст. // Искать вхождения искомой строки будем именно в нем var firstPos = par.ContentStart; var curText = string.Empty; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { curText = Text; })); // Выдергиваем из основного потока текст для подсветки var hlText = string.Empty; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { hlText = rule.HightlightedText; })); // Если текст для подсветки не пустой и его длина не превышает длину текста, в котором ищем, // то продолжим, иначе просто выведем в конце оригинал if (!string.IsNullOrEmpty(hlText) && hlText.Length <= curText.Length) { // Выдергиваем в основном потоке из правила свойство IgnoreCase. // Решил логику оставиьт в основном потоке, потому что нагрузка операции очень низкая // и не стоит моего пота :) var comparison = StringComparison.CurrentCulture; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { comparison = rule.IgnoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture; })); // Формируем список индексов, откуда начинаются вхождения искомой строки в тексте var indexes = new List <int>(); var ind = curText.IndexOf(hlText, comparison); while (ind > -1) { indexes.Add(ind); ind = curText.IndexOf(hlText, ind + hlText.Length, StringComparison.CurrentCultureIgnoreCase); } TextPointer lastEndPosition = null; // Проходим по всем индексам начала вхождения строки поиска в текст foreach (var index in indexes) { // Эта переменная нужна была в моих соисканиях наилучшего места для начала поиска, // ведь индекс положения в string не соответствует реальному положению TextPointer'a. // Поиск продолжается, поэтому переменную я оставил. var curIndex = index; // Начинаем поиск с последней найденной позиции либо перемещаем TextPointer вперед // на значение, равное индексу вхождения подстроки в текст var pstart = lastEndPosition ?? firstPos.GetPositionAtOffset(curIndex); // startInd является длиной текста между начальным TextPointer и текущей точкой начала подсветки var startInd = new TextRange(pstart, firstPos.GetInsertionPosition(LogicalDirection.Forward)).Text.Length; // В результате нам нужно, чтобы startInd был равен curIndex while (startInd != curIndex) { // Если честно, мне неще не встречались случаи, когда я обгонял startInd обгонял curIndex, однако // решил оставить продвижение назад на случай более оптимизированного алгоритма поиска if (startInd < curIndex) { // Смещаем точку начала подсветки на разницу curIndex - startInd var newpstart = pstart.GetPositionAtOffset(curIndex - startInd); // Иногда TextPointer оказывается между \r и \n, в этом случае начало подсветки // сдвигается вперед. Чтобы этого избежать, двигаем его в следующую позицию для вставки //if (pstart.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) // pstart = newpstart.GetNextInsertionPosition(LogicalDirection.Backward); var len = new TextRange(pstart, newpstart).Text.Length; startInd += len; pstart = newpstart; } else { var newpstart = pstart.GetPositionAtOffset(curIndex - startInd); var len = new TextRange(pstart, newpstart).Text.Length; startInd -= len; pstart = newpstart; } } // Ищем конечную точку подсветки аналогичным способом, как для начальной var pend = pstart.GetPositionAtOffset(hlText.Length); var delta = new TextRange(pstart, pend).Text.Length; while (delta != hlText.Length) { if (delta < hlText.Length) { var newpend = pend.GetPositionAtOffset(hlText.Length - delta); var len = new TextRange(pend, newpend).Text.Length; delta += len; pend = newpend; } else { var newpend = pend.GetPositionAtOffset(hlText.Length - delta); var len = new TextRange(pend, newpend).Text.Length; delta -= len; pend = newpend; } } // К сожалению, предложенным способом не получается разделить Hyperlink. // Скорее всего это придется делать вручную, но пока такой необходимости нет, // поэтому, если начальной или конечной частью подсветки мы режем гиперссылку, // то просто сдвигаем эти позиции. В общем ссылка либо полностью попадает в подсветку, // либо не попадает совсем var sHyp = (pstart?.Parent as Inline)?.Parent as Hyperlink; var eHyp = (pend?.Parent as Inline)?.Parent as Hyperlink; if (sHyp != null) { pstart = pstart.GetNextContextPosition(LogicalDirection.Forward); } if (eHyp != null) { pend = pend.GetNextContextPosition(LogicalDirection.Backward); } // Ну а тут применяем к выделению подсветки. if (pstart.GetOffsetToPosition(pend) > 0) { var sp = new Span(pstart, pend); foreach (var hl in highlights) { hl.SetHighlight(sp); } } lastEndPosition = pend; } } // Здесь сериализуем получившийся параграф и в основном потоке помещаем его содержимое в TextBlock var parStr = XamlWriter.Save(par); Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { Inlines.Clear(); Inlines.AddRange(((Paragraph)XamlReader.Parse(parStr)).Inlines.ToArray()); })).Wait(); }
// Отписка от событий правила подсветки void UnsubscribeRuleNotifies(HighlightRule rule) { rule.HighlightTextChanged -= Rule_HighlightTextChanged; rule.IgnoreCaseChanged -= Rule_IgnoreCaseChanged; }