Laden...

[Tutorial] Keyword-Highlighting (Einfaches Syntax-Highlighting)

Erstellt von Pulpapex vor 18 Jahren Letzter Beitrag vor 15 Jahren 61.925 Views
P
Pulpapex Themenstarter:in
939 Beiträge seit 2003
vor 18 Jahren
[Tutorial] Keyword-Highlighting (Einfaches Syntax-Highlighting)

Einfaches Keyword-Highlighting mit der RichTextBox

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;

Jetzt kann das Wort vor der Cursorposition gesucht werden. Zuerst wird der Wortanfang ermittelt.

// Wortanfang ermitteln.
int wordStart = index;
Match m = wordStartRegex.Match(text, index);
if(m.Success) {
   wordStart = m.Index;
}

Hier der im Code verwendete, reguläre Ausdruck wordStartRegex. Er sucht ausgehend vom Cursor rückwärts:

private static readonly Regex wordStartRegex = new Regex(
   @"\\b\\w",
   RegexOptions.Compiled | RegexOptions.RightToLeft);

Ab Wortanfang wird wieder vorwärts nach dem Wortende gesucht.

// Wortende ermitteln.
int wordEnd = index - 1;
m = wordEndRegex.Match(text, wordStart);
if(m.Success) {
   wordEnd = m.Index;
}

Hier der zugehörige, reguläre Ausdruck wordEndRegex:

private static readonly Regex wordEndRegex = new Regex(
   @"\\w\\b",
   RegexOptions.Compiled);

Aus Wortanfang und Wortende wird die Wortlänge ermittelt. Ist sie 0, gibt es nichts zu highlighten und es kann sofort abgebrochen werden:

// Wortlänge ermitteln. Ist die Länge 0, gleich zurück.
int wordLength = wordEnd - wordStart + 1;
if(wordLength == 0) return;

Wurde ein Wort gefunden, wird überprüft ob es sich um ein Schlüsselwort handelt. Dementsprechend wird eine Textfarbe ausgewählt. KeywordColor ist wieder eine Eigenschaft von RichTextBoxKeywordHilighter.

// Das Wort am Index.
string word = text.Substring(wordStart, wordLength);

// Textfarbe ermitteln.
bool isKeyword = KeywordLookup.ContainsKey(word);
Color wordColor = isKeyword ? KeywordColor : Color.Black;

Nun folgt das eigentliche Hervorheben. Zuvor muss aber noch die aktuelle Selektion der RichTextBox gesichert werden. Sie legt auch die Cursorposition fest, die im Anschluss wieder hergestellt werden muss. Das geschied in den Methoden BeginUpdate und EndUpdate.

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();

Wie man am Code sieht, gibt es einen Sonderfall, der extra behandelt werden muss. Normalerweise wird nur das Wort vor dem Cursor beachtet. Wird jedoch ein Wort durch einen Tastendruck in zwei Wörter aufgetrennt, kann sich auch die Farbe des Wortes hinter dem Cursor ändern. In dem Fall wird HilightAt einfach ein zweites Mal aufgerufen.

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

1.985 Beiträge seit 2004
vor 18 Jahren

Hallo Pulpapex,

erstmal ein großes Danke für den Artikel. Ist sehr gut beschreiben und einfach erweiterbar.

Willst Du evtl. noch weiter dran arbeiten? Ich habe so ein paar Erweiterungen, die ich nicht ganz hinbekomme (liegt hauptsächlich daran, dass ich keine Ahnung von RegEx habe).

Ein Bereich, der auf Kommentare matched, wäre sehr gut. Ich komme leider nicht auf den entsprechenden RegEx-Ausdruck.

Auch das Laden von Dateien bzw. Copy & Paste wären nicht schlecht. Da komme ich auch nicht dahinter 😦.

Würde mich freuen, wenn Du noch mal Lust hast, daran weiter zu arbeiten.

Gruß,
Fabian

"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de

354 Beiträge seit 2004
vor 18 Jahren

Copy & Paste fehlt noch ein wenig in deiner Lösung, und bei ca. 4000 Zeilen in der RichTextBox flackert das Teil schon gewaltig.

.NET GUI - Die Community für grafische Oberflächen unter .NET
Jetzt kostenlos besorgen: .NET BlogBook
Norbert Eder
DasBackup

P
Pulpapex Themenstarter:in
939 Beiträge seit 2003
vor 18 Jahren

Hi nitronic,

Highlighting für Textbereiche, also für Copy & Paste, kommt im zweiten Teil. Das steht ganz unten im Artikel.

Im Moment wird nur in der Umgebung des Cursors hervorgehoben. Deshalb wundert es mich, dass es bei viel Text flimmern soll. Sollte eigentlich keine Rolle spielen. Ich werde das heute abend mal testen.

@Fabian - den zweiten Teil werde ich die nächsten Tage in Angriff nehmen. Mal schauen.

1.985 Beiträge seit 2004
vor 18 Jahren

Hallo Pulpapax,

das wäre auf jeden Fall sehr nett von Dir. Das Beispiel ist richtig gut.

Ein Problem habe ich noch beim Implementieren der Erkennung von Kommentaren. Kannst Du da vielleicht auch mal drüber gucken?

Gruß,
Fabian

"Eine wirklich gute Idee erkennt man daran, dass ihre Verwirklichung von vornherein ausgeschlossen erscheint." (Albert Einstein)

Gefangen im magischen Viereck zwischen studieren, schreiben, lehren und Ideen umsetzen…

Blog: www.fabiandeitelhoff.de

T
3 Beiträge seit 2007
vor 16 Jahren

Vielen dank für das Tutorial. Habs gerade gefunden und es hat mir sehr geholfen.
Allerdings hab ich einen Kritikpunkt. Deine Klasse kann keine Schlüsselwörter wie "<html>" hervorheben sondern nur "html"

mfg
TheProgrammer

Programmieren ist keine Tätigkeit. Es ist eine Lebenseinstellung!

49.485 Beiträge seit 2005
vor 16 Jahren

Hallo TheProgrammer,

dazu wäre nur eine kleinere Änderung an den verwendeten Regex-Pattern nötig. Die momentanen Pattern arbeiten ja absichtlich mit Wortgrenzen (\b), damit bei "forget" nicht fälschlich "for" innerhalb des Wortes hervorgehoben würde.

Da Pulpapex schon lange nicht mehr im Forum war, wirst du die Änderung selber durchführen müssen. Mach für evtl. Fragen zu Regex oder der überhaupt der Umsetzung bitte einen neuen Thread auf.

herbivore

J
1 Beiträge seit 2008
vor 15 Jahren

Hi,

für alle die es interessiert.. ich habe den RichTextBoxKeywordHighlighter etwas erweitert.. er kann jetzt den gesamten Text highlighten mit <html> und remarks..

Gruß

Joachim

5.299 Beiträge seit 2008
vor 15 Jahren

Hi!

Ich stelle grade fest, der RichtextboxUpdater funzt nur halb. In RichtextboxUpdater.BeginUpdate()


      // Prevent the control from raising any events.
      oldEventMask = SendMessage(
         new HandleRef(rtb, rtb.Handle),
         EM_SETEVENTMASK, 0, 0);
      //...

ließ mich ja erwarten, daß insbesondere die SelectionChanged-Events der Rtb unterdrückt würden. Werdenseabernich.

mein kleiner Test:


		public RichTextBox RichTextBox {
         get { return richTextBox; }
         set {
            richTextBox = value;
            richTextBox.SelectionChanged += new EventHandler(richTextBox_SelectionChanged);
         }
      }

      void richTextBox_SelectionChanged(object sender, EventArgs e) {
         if (updating > 0) {
            throw new Exception(
               "Dieses Ereignis sollte doch in diesem Zustand nicht gefeuert werden!");
         }
      }

Ich erweitere den Richtextbox-Setter, und abonniere das SelectionChanged.

Anbei das Werk in lauffähiger Vollständigkeit:

Der frühe Apfel fängt den Wurm.