Die RichTextBox ist ja im allgemeinen als träge und lahm verschrien. Ich möchte im folgenden zeigen, wie man dennoch schnelles, interaktives Syntax Highlighting hinbekommt. Schlüsselwörter werden bei der Eingabe hervorgehoben.
Die Demo-Anwendung besteht aus vier kleinen Klassen, die ich im folgenden im Detail beschreiben werde:
- KeywordHilightingTest
- KeywordHilightingForm
- RichTextBoxKeywordHilighter
- RichTextBoxUpdater
KeywordHilightingTest
Zu KeywordHilightingTest gibt es nicht viel zu sagen. Die Klasse enthält nur die Main-Methode:
class KeywordHilightingTest {
static void Main() {
Application.EnableVisualStyles();
Application.Run(new KeywordHilightingForm());
}
}
KeywordHilightingForm
KeywordHilightingForm ist auch sehr einfach. Als einziges Control befindet sich eine RichTextBox mit DockStyle.Fill auf der Form. Für das RichTextBox.TextChanged-Ereignis wurde ein EventHandler eingerichtet. Das Ereignis wird bei jedem Tastendruck ausgelöst und ruft die HilightAtCursor-Methode auf. Desweiteren enthält die Klasse noch ein String-Array mit den Schlüsselwörtern und verfügt über eine KeywordHilighter-Eigenschaft, die das eigentliche Hervorheben übernimmt.
public class KeywordHilightingForm : Form {
private RichTextBoxKeywordHilighter keywordHilighter;
private readonly string[] keywords = {
"abstract", "event", "new", "struct",
"as", "explicit", "null", "switch",
"base", "extern", "object", "this",
"bool", "false", "operator", "throw",
"break", "finally", "out", "true",
"byte", "fixed", "override", "try",
"case", "float", "params", "typeof",
"catch", "for", "private", "uint",
"char", "foreach", "protected", "ulong",
"checked", "goto", "public", "unchecked",
"class", "if", "readonly", "unsafe",
"const", "implicit", "ref", "ushort",
"continue", "in", "return", "using",
"decimal", "int", "sbyte", "virtual",
"default", "interface", "sealed", "volatile",
"delegate", "internal", "short", "void",
"do", "is", "sizeof", "while",
"double", "lock", "stackalloc",
"else", "long", "static",
"enum", "namespace", "string"
};
public KeywordHilightingForm() {
InitializeComponent();
}
private void InitializeComponent() {
// ...
}
// Wird bei jedem Tastendruck ausgelöst.
private void HandleTextChanged(object sender, EventArgs e) {
HilightAtCursor();
}
// Hebt den Text am Cursor farblich hervor.
public void HilightAtCursor() {
// Entspricht Cursorposition.
int index = this.richTextBox.SelectionStart;
// Wort am Cursor hervorheben.
KeywordHilighter.HilightAt(index);
}
// Der KeywordHilighter übernimmt das Highlighten.
// Er wird mit der RichTextBox und den Keywords initialisiert.
public RichTextBoxKeywordHilighter KeywordHilighter {
get {
if(keywordHilighter == null) {
keywordHilighter = new RichTextBoxKeywordHilighter();
keywordHilighter.RichTextBox = this.richTextBox;
keywordHilighter.Keywords = this.keywords;
}
return keywordHilighter;
}
}
}
RichTextBoxKeywordHilighter
Die RichTextBoxKeywordHilighter-Klasse ist wie gesagt für das eigentliche Hervorheben zuständig. Der Trick besteht darin, so wenig Text wie nur möglich zu bearbeiten. Deshalb wird auch nur das Wort vor dem Cursor untersucht, das gerade eingegeben wird. Die gesamte Textzeile zu aktualisieren wäre zwar einfacher, dauert aber schon zu lange.
Der Code für die Texthervorhebungen befindet sich in der HilightAt-Methode. Als Parameter wird ein Index auf ein Zeichen übergeben. Der Index darf um 1 grösser sein als die Position des letzten Zeichens im hervorzuhebenden Wort. Er entspricht damit der Cursorposition, wenn das Wort gerade eingegeben wird. Den Code der Methode habe ich hier in mehrere Blöcke aufgeteilt, um ihre Funktion besser beschreiben zu können.
Zunächst einmal wird der Text aus der RichTextBox benötigt. RichTextBox ist eine Eigenschaft von RichTextBoxKeywordHilighter.
// Der Text.
string text = RichTextBox.Text;
// Wortanfang ermitteln.
int wordStart = index;
Match m = wordStartRegex.Match(text, index);
if(m.Success) {
wordStart = m.Index;
}
private static readonly Regex wordStartRegex = new Regex(
@"\\b\\w",
RegexOptions.Compiled | RegexOptions.RightToLeft);
// Wortende ermitteln.
int wordEnd = index - 1;
m = wordEndRegex.Match(text, wordStart);
if(m.Success) {
wordEnd = m.Index;
}
private static readonly Regex wordEndRegex = new Regex(
@"\\w\\b",
RegexOptions.Compiled);
// Wortlänge ermitteln. Ist die Länge 0, gleich zurück.
int wordLength = wordEnd - wordStart + 1;
if(wordLength == 0) return;
// Das Wort am Index.
string word = text.Substring(wordStart, wordLength);
// Textfarbe ermitteln.
bool isKeyword = KeywordLookup.ContainsKey(word);
Color wordColor = isKeyword ? KeywordColor : Color.Black;
BeginUpdate();
// Textfarbe ändern.
RichTextBox.SelectionStart = wordStart;
RichTextBox.SelectionLength = wordLength;
RichTextBox.SelectionColor = wordColor;
// Spezialfall, wenn ein Wort mit Space in zwei Wörter
// aufgetrennt wird, muss zusätzlich das Wort
// hinter dem Index aktualisiert werden.
bool wordSplit = index < text.Length &&
index - 2 == wordEnd &&
Char.IsLetterOrDigit(text[index]);
if(wordSplit) {
HilightAt(index + 1);
}
EndUpdate();
Der Code, wie er jetzt ist, funktioniert eigentlich schon zufriedenstellend. Es gibt keine Verzögerungen beim Tippen und gleichzeitigem Highlighting. Allerdings sind die in HilightAt programmatisch vorgenommenen Selektionen für den Benutzer sichtbar. Sie fallen durch kurzes Aufblitzen unangenehm auf. Dieses Problem löst die RichTextBoxUpdater-Klasse. Den Code hierfür habe ich in Pete's Weblog gefunden:
Pete's Weblog: Extending RichTextBox, Part I
RichTextBoxUpdater
RichTextBoxUpdater definiert zwei Methoden, BeginUpdate und EndUpdate. BeginUpdate deaktiviert die Aktualisierung der RichTextBox-Anzeige während vom Programm Textänderungen vorgenommen werden. EndUpdate reaktiviert die Aktualisierung wieder. Leider gibt es in der RichTextBox-Klasse keine Methoden, die das erledigen. Es muss also wiedermal auf P/Invoke und die Win-Api zurückgegriffen werden.
Die Einzelheiten können auf der angegebenen Seite nachgelesen werden, hier nur noch der Code für die Klasse:
class RichTextBoxUpdater {
private const int EM_SETEVENTMASK = 1073;
private const int WM_SETREDRAW = 11;
private int updating;
private int oldEventMask;
// Deaktiviert Redraw und Events der RichTextBox.
public void BeginUpdate(RichTextBox rtb) {
// Deal with nested calls.
updating++;
if(updating > 1) return;
// Prevent the control from raising any events.
oldEventMask = SendMessage(
new HandleRef(rtb, rtb.Handle),
EM_SETEVENTMASK, 0, 0);
// Prevent the control from redrawing itself.
SendMessage(
new HandleRef(rtb, rtb.Handle),
WM_SETREDRAW, 0, 0);
}
// Reaktiviert Redraw und Events der RichTextBox.
public void EndUpdate(RichTextBox rtb) {
// Deal with nested calls.
updating--;
if(updating > 0) return;
// Allow the control to redraw itself.
SendMessage(
new HandleRef(rtb, rtb.Handle),
WM_SETREDRAW, 1, 0);
// Allow the control to raise event messages.
SendMessage(
new HandleRef(rtb, rtb.Handle),
EM_SETEVENTMASK, 0, oldEventMask);
}
[DllImport("user32", CharSet = CharSet.Auto)]
private static extern int SendMessage(
HandleRef hWnd,
int msg,
int wParam,
int lParam);
}
Das war's auch schon. So sieht das Ergebnis aus:

Wenn ihr Lust habt, probiert die angehängte Demo-Anwendung aus. Die Zip-Datei enthält auch den kompletten Quellcode, den ihr nach Belieben zerpflücken und in eigene Anwendungen einbauen könnt. Bei Pete's Code bin ich mir nicht so sicher, aber ich denke, dass eine Quellenangabe ausreichen sollte.
In einem zweiten Teil werde ich die RichTextBoxKeywordHilighter-Klasse nochmal um das Highlighten für Textbereiche erweitern, damit sie auch beim Laden von Textdateien und bei Copy&Paste funktioniert.
Gruss
Pulpapex
Suchhilfe: Syntax, Keyword, Keywords, Highlight, Hilight, Highlighter, Hilighter, Highlighting, Hilighting, Syntaxhighlight, Syntaxhilight, Syntaxhighlighter, Syntaxhilighter, Syntaxhighlighting, Syntaxhilighting, Keywordhighlight, Keywordhilight, Keywordhighlighter, Keywordhilighter, Keywordhighlighting, Keywordhilighting