Una funzionalità fondamentale per text-editor/reader è l’evidenziazione programmatica di testo, magari utilizzando stili di formattazione diversi. In questo post vorrei mostrare una possibile implementazione di tale funzionalità utilizzando il controllo RichTextBox di WPF. Immaginando un approccio top-down, potremmo partire da un metodo HilightText(…), il cui compito sia proprio quello di evidenziare del testo all’interno di una RichTextBox.
public void HilightText(string pattern)
{
List<TextRange> ranges = GetMatchingTextRanges(pattern);
foreach (TextRange textRange in ranges) HilightTextRange(textRange, Brushes.Black, Brushes.Yellow);
}
L’idea è quella di applicare un background giallo ed un foreground nero a tutti gli intervalli di testo (
TextRange) che soddisfano un dato pattern-matching.
private void HilightTextRange(TextRange textRange, Brush foregroundBrush, Brush backgroundBrush)
{
if (foregroundBrush != null) textRange.ApplyPropertyValue(TextElement.ForegroundProperty, foregroundBrush);
if (backgroundBrush != null) textRange.ApplyPropertyValue(TextElement.BackgroundProperty, backgroundBrush);
}
Ora ci troviamo di fronte all’esigenza di determinare gli intervalli di testo coinvolti nel matching. Definiamo dunque un metodo
GetMatchingTextRanges(…), il cui compito è di navigare l’intero documento della RichTextBox (un
FlowDocument) analizzando esclusivamente gli elementi (inlines) di tipo
Run, ovvero quelli che contengono testo.
private List<TextRange> GetMatchingTextRanges(string pattern)
{
List<TextRange> ranges = new List<TextRange>();
TextPointer textNavigator = richTextBox.Document.ContentStart;
while (textNavigator.CompareTo(richTextBox.Document.ContentEnd) < 0)
{
TextPointerContext context = textNavigator.GetPointerContext(LogicalDirection.Backward);
if (context == TextPointerContext.ElementStart && textNavigator.Parent is Run)
{
List<TextRange> runRanges = GetMatchingTextRanges((Run)textNavigator.Parent, pattern);
if (runRanges.Count > 0) ranges.AddRange(runRanges);
}
textNavigator = textNavigator.GetNextContextPosition(LogicalDirection.Forward);
}
return ranges;
}
A questo punto, per ogni Run trovato, è possibile controllare se esistono porzioni di testo in esso contenuti che soddisfano il pattern di ricerca. In caso positivo, possiamo aggiungerle alla lista dei TextRange da evidenziare.
private List<TextRange> GetMatchingTextRanges(Run run, string pattern)
{
List<TextRange> ranges = new List<TextRange>();
MatchCollection matches = Regex.Matches(run.Text, pattern);
foreach (Match match in matches)
{
TextPointer startPosition = GetContentTextPointer(run.ContentStart, match.Index - 1);
TextPointer endPosition = GetContentTextPointer(run.ContentStart, match.Index + match.Length - 1);
ranges.Add(new TextRange(startPosition, endPosition));
}
return ranges;
}
Come si stabiliscono gli estremi dei TextRange da evidenziare all’interno di ciascun Run? Anzitutto, tramite la classe
Regex riusciamo a capire per ciascun match quali sono gli indici dei caratteri iniziali all’interno del testo del Run. Poi, ricordando che per calcolare l’offset corretto all’interno di ciascun Run non dobbiamo considerare i caratteri (un Run può contenere altri tipi di simboli) bensì i
TextPointer che sono adiacenti al testo di interesse, definiamo il seguente metodo:
private TextPointer GetContentTextPointer(TextPointer contentStart, int offset)
{
TextPointer result = contentStart;
int i = 0;
while (i < offset && result != null)
{
if (result.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text ||
result.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.None) i++;
if (result.GetPositionAtOffset(1, LogicalDirection.Forward) == null) return result;
result = result.GetPositionAtOffset(1, LogicalDirection.Forward);
}
return result;
}
Finalmente il gioco è fatto. Di seguito riportiamo un esempio di output ottenuto invocando HilightText(“labor”);
Technorati Tags:
WPF,
RichTextBox