Jörg Siebrands
Die Website wurde mit de Standard-Version des Webeditors "WebAdditor" erstellt
Diese Dokumentation befindet sich im Aufbau

Delphi bietet mit der RichEdit-Komponente eine Möglichkeit mit wenig Aufwand einen Editor oder eine kleine Textverarbeitung zu programmieren. Die Komponente enthält Zugriff auf alle wichtigen Text-Formatierungen. Was jedoch fehlt ist die Möglichkeit Links in den Text zu integrieren. Leider bin ich auch im Internet nicht wirklich fündig geworden. Lediglich die Möglichkeit automatisch Internetadressen als Link umzuwandeln wird dort beschrieben - zu finden unter anderen unter https://delphi.about.com/od/vclusing/l/aa111803a.htm.

Eine einfache und elegante Lösung um Links in die RichEdit-Komponente zu integrieren habe ich leider nicht gefunden. Es gibt jedoch die Möglichkeit innerhalb des RichEdit-Textes versteckte Daten einzufügen, die dann nicht auf dem Bildschirm erscheinen. Für derartige versteckte Daten gibt es offensichtlich mehrere Möglichkeiten. Ich habe mich für die HIDDEN-Kennzeichnung für versteckte Daten (z. B. die zugehörige URL) sowie für die PROTECTED-Kennzeichnung für den gesamten Link (Linktext + HIDDEN-Abschnitt) entschieden.

Zu berücksichtigen ist dabei, dass man den Cursor mit RichEdit.SelStart und RichEdit.SelLength nicht innerhalb des HIDDEN-Abschnitts positionieren kann - dies führt regelmäßig dazu, dass anschließend in RichEdit.SelLength der Wert '0' steht und nichts markiert wird. Darüber hinaus gibt es offensichtlich keine einheitliche Regelung für den Fall, dass die Text-Markierung direkt vor dem HIDDEN-Abschnitt endet oder direkt nach dem HIDDEN-Abschnitt beginnt. Manchmal ist der HIDDEN-Bereich dann in RichEdit.SelText enthalten und manchmal nicht. Auch wenn man eventuell nachvollziehen kann nach welchen Regeln dies geschieht dürfte es zu unsicher sein, sich darauf zu verlassen: In einer neueren Delphi- bzw. Windows-Version könnte dies dann wieder anders geregelt sein.

Die hier vorgestellte Lösung wurde zunächst mit der Starter-Version von Delphi XE2 entwickelt - für diese Dokumentation werde ich jedoch die Pro-Version von Delphi 7 verwenden. Ausgangsbasis ist dabei die Demo-Anwendung 'RichEdit' die sich üblicherweise im Programmverzeichnis von Delphi im Ordner 'Demos/RichEdit' befindet. Da es sich bei der Demo um ein mehrsprachiges Projekt handelt habe ich einfach aus dem oberen Verzeichnis die Dateien mit den Endungen 'pas', 'dfm', 'res' und 'dpr' in ein neues Verzeichnis kopiert - ansonsten müßte man wohl die sprachenspezifischen Formulare anpassen.
Die zugehörige Projektdatei heisst 'richedit.dpr'. Die Unit die hier nachfolgend angepasst wird heisst 'remain.pas'.

Das Beispiel läßt sich aber sicher auch mit einem neuen Projekt nachvollziehen. Dabei wird dann eine RichEdit-Komponente und der Link-Button benötigt - alles andere dürfte sich nicht allzusehr unterscheiden.

Die Richedit-Komponente im Demo hat den Namen 'Editor'. Zunächst muss dafür gesorgt werden, dass auch PROTECTED--Abschnitte in der Richedit-Komponente bearbeitet werden können - standardmäßig ist dies nicht der Fall. Dazu wird die Komponente 'Editor' aktiviert und im Objektinspektor mit einem Doppelklick auf das Ereignis 'OnProtectChange' die zugehörige Ereignis-Prozedur erstellt. Im Prozedurrumpf wird lediglich eine Zeile eingetragen:

procedure TMainForm.EditorProtectChange(Sender: TObject; StartPos,
  EndPos: Integer; var AllowChange: Boolean);
begin
  AllowChange := true;
end;


Für die neue Link-Funktion wird nun noch ein neuer Schalter benötigt. Hierzu wird ein neuer SpeedButton in der Toolbar hinzugefügt. Als Caption kann man einfach ein grosses 'L' für Link eingeben. Als Name habe ich 'LinkButton' verwendet.



Für die Bearbeitung der Links habe ich die Prozedur 'SetLink' geschrieben und dem Hauptformular 'TMainForm' hinzugefügt::

procedure SetLink();


Mit einem Doppelklick auf den LinkButton wird die Prozedur dem Button zugewiesen:

procedure TMainForm.LinkButtonClick(Sender: TObject);
begin
  SetLink;
end;


Innerhalb der Prozedur SetLink wird später die Funktion GetLinkStatus aufgerufen, die (u. a.) den aktuellen Status des markierten Bereichs auf Verlinkung testet:

function GetLinkStatus(var LinkInfoSize: integer): TLinkStatus;  


Der Typ 'TLinkStatus' muss natürlich auch noch definiert werden. Die Möglichen Zustände dieser Variablen werden weiter unten noch genauer erläutert:

TLinkStatus  =  (IsLink, IsNotLink, IsNotLinkNearLink, IsLinkAndNotLink, IsLinkPart, IsLinkError);
// Bedeutung: Link, kein Link, kein Link aber neben Link, Teilweise Link, unvollständiger Link, Fehler


Der Prozedurrumpf für SetLink beginnt folgendermaßen - die Bedeutung der Variablen wird später noch erläutert:

(* Setzt oder löscht Link im markierten Bereich (falls möglich) *)
procedure TMainForm.SetLink;
  begin
var
  v: integer;
  s: string;
  oldstart, oldlength: integer; // Index und Länge des selektierten Textausschnitts merken
  infolength: integer; // Länge des Info-Textes zum Link
  slLinkInfo: TStringList; // für Link-Info (URL,
  LinkInfoSize: integer; // Grö0e des Linkinfo-Textes in RichEdit
  cf: TCharFormat2;
  LinkStatus: TLinkStatus;


In der Demo-Anwendung dient die Variable FUpdating dazu, anzuzeigen, ob die Editor-Komponente gerade aktualisiert wird - ist dies der Fall wird die Prozedur SetLink sofort beendet. Ansonsten wird zunächst die Größe von TCharFormat2 zugewiesen. TCharFormat2 ist in der Unit RichEdit enthalten die im Beispielprojekt bereits eingebunden wurde. Die Variable cf wird für nachfolgende SendMessage-Aufrufe benötigt. Anschließend wird die Funktion GetLinkStatus aufgerufen. Diese gibt neben den Linkstatus bei gefundenen Link auch die Größe des LinkInfo-Textes im übergebenen var-Parameter LinkInfoSize zurück.

begin
  if FUpdating then Exit;
  cf.cbSize := SizeOf(TCharFormat2);    // Charformat-Größe für SendMessage setzen
  // Testen, ob protect
  LinkStatus := GetLinkStatus(LinkInfoSize); // Linkstatus ermitteln und ggfs. Markierung um HIDDEN-Bereich erweitern


Anschließend wird der LinkStatus ausgewertet. In folgenden Fällen wird ein Warnton (Beep) ausgegeben und die Prozedur verlassen:
  • Wenn es sich nicht um einen Link handelt, aber direkt daneben ein Link beginnt.
  • Wenn es sich bei einem Teil des Textes um einen Link handelt.
  • Wenn es sich nicht um einen vollständigen Link handelt.
  • Wenn es bei der Auswertung zu einem Fehler kam.

case LinkStatus of
  // kein Link - aber neben Link, Teilweise Link, unvollständiger Link, Fehler
  IsNotLinkNearLink, IsLinkAndNotLink, IsLinkPart, IsLinkError: begin      SysUtils.Beep;
    exit;
  end;
  IsNotLink: begin  // kein Link -> Link erstellen
    ...
  end;
  IsLink: begin     // Link -> Link löschen
    ...
  end;   


Wird 'IsNotLink' zurückgegeben bedeutet dies, das ein Link erstellt werden kann. Als erstes wird die Stringliste 'slLinkInfo' erstellt, in die die benötigten Link-Informationen gespeichert werden. Anschließend wird der Wert für den Start des selektierten Bereichs in oldstart und der Wert für die Länge des selektierten Bereichs in oldlength zwischengespeichert. Mit Hilfe der InputBox-Funktion wird nun die Adresse für den Link abgefragt. Dieser sollte normalerweise noch auf Fehler getestet werden - dies habe ich hier zur Vereinfachung weggelassen. Die Funktion InputBox ist in der Unit 'Dialogs' enthalten. Gegebenenfalls kann man noch in einem Dialogfenster abfragen ob der Link im gleichen oder neuen Fenster geöffnet werden soll - in diesem Beispiel wird vom aktuellen Fenster ausgegangen.

  IsNotLink: begin  // kein Link -> Link erstellen
    slLinkInfo := TStringList.Create;
    oldstart :=  Editor.SelStart;   // Markierungs-Start merken
    oldlength := Editor.SelLength;  // Markierungs-Länge merken
    s := InputBox('URL-Eingabe', 'Bitte geben Sie die Link-Adresse ein',
                  'http:www.google.de');


Nun wird die Stringliste slLinkInfo mit den benötigten Information für den Link gefüllt.

    slLinkInfo.Add('size=');      // für Size - Wert wird unten eingesetzt
    slLinkInfo.Add('type=link');
    slLinkInfo.Add('url=' + s);
    slLinkInfo.Add('target=self');  // in neuen Fenster: slLinkInfo.Add('target=blank');


Diese Informationen sollen später im RichEdit-Text im HIDDEN-Bereich gespeichert werden. Daher wird zunächst mit Length() ermittelt wieviel Platz für den Text benötigt wird. In der RichEdit-Komponente wird für das Zeilenende nur das Zeichen #13 statt #10#13 gespeichert, daher wird pro Eintrag ein Zeichen weniger benötigt.
Zur Länge wird dann noch die 'Länge der Länge' hinzugefügt, da dieser Wert ja die Länge erhöht. Anschließend wird noch geprüft, ob das Hinzufügen des Längenwertes dazu führt, dass mehr Platz benötigt wird - wenn die Länge 99 Zeichen beträgt und für die Speicherung des wertes 2 Zeichen benötigt werden beträgt die Länge anschließend 101 Zeichen und es müssen daher 3 Zeichen für den Längenwert reserviert werden. Die mathematische Formel um die Anzahl der Stellen einer Zahl zu ermitteln lautet: 'floor(1+log10(abs(Zahl)))'. Da es einfacher lesbar ist habe jedoch die Length-Funktion mit vorheriger String-Umwandlung benutzt. Für die spätere Analyse ist noch wichtig das diese Länge auf jeden Fall größer als 29 sein muss, da die eingefügten Strings ohne URL und SIZE-Wert bereits 29 Zeichen enthalten.
Der erste Wert von slLinkInfo wird anschließend mit diesem errechneten Wert für 'size' überschrieben.

    // Platzbedarf in RichEdit!    Mindestgröße ist über 29, daher: wenn 1. Ziffer = 1
    // oder 2 dann Wert = 3-stellig
    v := Length(slLinkInfo.Text) - slLinkInfo.Count;       
    LinkInfosize := v + Length(IntToStr(v));
    if LinkInfoSize <> (v + Length(IntToStr(LinkInfoSize))) then Inc(LinkInfoSize);
    // bisherige Größe + Stringlänge von Größe
    slLinkInfo.Strings[0] := 'size=' + IntToStr(LinkInfoSize);


Um zu kontrollieren was nach dem Hinzufügens des Links im RTF-Quelltext ändert habe ich nun zunächst den aktuellen Quelltext gespeichert.

    Editor.Lines.SaveToFile('test_davor.txt'); // nur zur Kontrolle, ggfs. auskommentieren


Nun wird die Linkfarbe gesetzt indem Currtext die gewünschte Farbe zugewiesen wird. Bei Currtext handelt es sich um eine Funktion die im Beispielprogramm enthalten ist. Diese gibt eine Variable vom Typ TTextAttributes zurück: Ist kein Text markiert wird das Attribut für den Standardtext geändert, bei markierten Text das Attribut für den ausgewählten Text. In unserem Fall wird also das Texxtattribut des markierten Textes geändert. Wir könnten ebenso auch direkt Editor.SelAttributes statt Currtext verwenden (Editor.SelAttributes.Color := clBlue). Auf die gleiche Art ändern wir die Style-Eigenschaft durch Hinzufügen des Werts für 'unterstrichen'.
Nun setzen wir die den Cursor auf das Ende des markierten Bereichs und hängen dort den LinkInfo-Text aus slLinkInfo an.

    CurrText.Color := clBlue;   // Linkfarbe setzen
    CurrText.Style := CurrText.Style + [fsUnderline];
    Editor.SelStart := oldstart + oldlength;
    Editor.SelLength := 0;
    Editor.SelText := slLinkInfo.Text;  // Link-Info einfügen


Die Länge des LinkInfo-Textes wird nun in infolength gespeichert und Der Cursor wird wieder auf die ursprüngliche Startposition gesetzt und Linktext sowie der hinzugefügte LinkInfo-Text ausgewählt und anschließend auf PROTECTED gesetzt. Wie oben beschrieben dient die PROTECTED-Markierung dazu den gesamten Link zu kennzeichnen..

    // Länge des LinkInfo-Textes
    Editor.SelStart := oldstart;
    // Vor Linktext positionieren
    infolength := Editor.SelStart - (oldstart + oldlength);
    // bis hinter Linkinfo-Text markieren
    Editor.SelLength := oldlength + infolength;
    // gesamter Link als protect
    CurrText.Protected := true;


Als nächstes wird der LinkInfo-Text als HIDDEN markiert - dieser wird dadurch im Editor nicht angezeigt. Dazu wird der Cursor vor dem LinkInfo-Text positioniert und der LinkInfo-Text ausgewählt. Anschließend wird per 'SendMessage' die Markierung vorgenommen. Dazu müssen zuvor die zugehörigen Variablen dwMask und dwEffects der CharFormat-Variablen cf gesetzt werden.

    Editor.SelStart := oldstart + oldlength;                  // vor Linkinfo-Text
    Editor.SelLength := infolength;                           // Linkinfo-Text markieren
    cf.dwMask := CFM_HIDDEN;      // für Hidden-Markierung
    cf.dwEffects := CFE_HIDDEN;   // für Hidden-Markierung
    // als HIDDEN (/v) markieren
    SendMessage(Editor.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(@cf));


Wie zuvor können wir nun den RTF-Quelltext speichern um zu kontrollieren welche Änderungen vorgenommen wurden.

    // nur zur Kontrolle, ggfs. auskommentieren
    Editor.Lines.SaveToFile('test_danach.txt');


Zum Schluss wird die Stringliste freigegeben - damit ist die Erstellung des Links abgeschlossen.

    slLinkInfo.Free
  end;  // IsNotLink   
 


Es folgt der Abschnitt zum Löschen des Links wenn der LinkStatus auf 'IsLink' steht. Zunächst wird wieder der Wert für den Start des selektierten Bereichs in oldstart und der Wert für die Länge des selektierten Bereichs in oldlength zwischengespeichert.

  IsLink: begin // Link -> Link löschen
    oldstart :=  Editor.SelStart;   // Markierungs-Start merken
    oldlength := Editor.SelLength;  // Markierungs-Länge merken


Anschließend wird per 'SendMessage' die Hidden-Markierung zurückgenommen. Dazu müssen zuvor wieder die zugehörigen Variablen dwMask und dwEffects der CharFormat-Variablen cf gesetzt werden. Außerdem wird die PROTECTED-Markierung aufgehoben, die den gesamten Link kennzeichnet.

    cf.dwMask := CFM_HIDDEN;      // für Hidden-Markierung bearbeiten
    cf.dwEffects := 0;            // für Hidden-Markierung aus!
    // HIDDEN (/v) Markierung aufheben
    SendMessage(Editor.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(@cf));
    CurrText.Protected := false;


Nun muss der LinkInfo-Text gelöscht werden. Dazu wird dieser zunächst markiert und dann mit einem Leerstring überschrieben.

    // vor LinkInfo positionieren und löschen
    Editor.SelStart := oldstart+oldlength-LinkInfoSize;     
    // LinkInfoSize enthält Länge des LinkInfo-Textes (s.o.)
    Editor.SelLength := LinkInfoSize;
    Editor.SelText := '';


Anschließend wird der Text wieder markiert. Der ursprüngliche Wert von SelLength wird dabei um die Länge des gelöschten LinkInfo-Textes verringert. Die lezte Zeile dient wieder nur dazu den RTF-Quelltext zu speichern um die Änderungen zu überprüfen.

    Editor.SelStart := oldstart; // nach Linktext positionieren
    Editor.SelLength := oldlength-LinkInfoSize;
    // nur zur Kontrolle - ggfs. auskommentieren 
    Editor.Lines.SaveToFile('test_danach.txt');
  end; // IsLink
end;  // procedure SetLink()

 

Es fehlt nun noch die Funktion GetLinkStatus() die den selektierten Text analysiert. Als Rückgabewert erhält man einen zuvor (s.o.) definierten Werte vom Typ TLinkStatus. Die Bedeutung wird noch einmal in den ersten beiden Kommentarzeilen erläutert. Wenn es sich um einen Link handelt steht anschließend die Größe des LinkInfoTextes in der als var-Parameter übergebenen Variablen LinkInfoSize. Anschließend wird die CharFormat-Variable mit der richtigen Größe initalisiert und die Position und Länge des selektierten Textes in oldstart bzw. oldlength gespeichert.

function TMainForm.GetLinkStatus(var LinkInfoSize: integer): TLinkStatus;
var
  b: boolean;
  v: integer;
  s: string;
  oldstart, oldlength: integer;
  LinkStatus: TLinkStatus;
  cf: TCharFormat2;
begin
  // Link, kein Link, kein Link aber neben Link, Teilweise Link, unvollständiger Link, Fehler
  // TLinkStatus = (IsLink, IsNotLink, IsNotLinkNearLink, IsLinkAndNotLink, IsLinkPart, IsLinkError);
  cf.cbSize := SizeOf(TCharFormat2);    // Charformat-Größe für SendMessage setzen
  oldstart :=  Editor.SelStart;   // Markierungs-Start merken
  oldlength := Editor.SelLength;  // Markierungs-Länge merken


Es folgt ein SendMessage-Aufruf um zu ermitteln ob der selektierte Text mit Protected markiert ist. Dazu wird die CharFormat-Variable cf vorher entsprechend initialisiert.

  (* Aktuelle Link-Formatierung ermitteln *)
  cf.dwMask := CFM_PROTECTED;      // Protected-Markierung testen
  cf.dwEffects := 0;               // enthält danach Ergebnis der Get-Abfrage
  // Aktuelle Formatierung ermitteln
  SendMessage(Editor.Handle, EM_GETCHARFORMAT, SCF_SELECTION, LPARAM(@cf));


Anschließend erstellen wir einen if-Zweig für den Fall, das der selektierte Text nur teilweise PROTECTED ist. In diesem Fall ist in cd.dwmask das entsprechende Bit nicht gesetzt.
Im if-Zweig wird nun das erste Zeichen des zuvor selektierten Textes ausgewählt.

  // teilweise verlinkt?
 
  // Selektion ist teilweise PROTECTED
  if not ((CFM_PROTECTED and cf.dwMask) = CFM_PROTECTED) then begin    
  // Testen ob HIDDEN-Berich von Link davor mit markiert ist ->
  // Zeichen davor muss PROTECTED sein.
  Editor.SelStart := oldstart; // 1. Zeichen des markierten Textes
  Editor.SelLength := 1; // 1. Zeichen testen


Anschließend wird ein weiterer if-Zweig für den Fall eingefügt, dass die Markierung des 1. Zeichens nicht funktioniert hat - dann muss es sich um ein Zeichen im HIDDEN-Format handeln.

  // Markierung funktioniert nicht: 1. Zeichen muss HIDDEN-Teil von Link sein
  if ((Editor.SelStart <> oldstart) or (Editor.SelLength <> 1)) then begin


In diesem Fall markieren wir zunächst den ursprünglichen Text mit Hilfe von SelStart und SelLength und suchen anschließend die Zeichenkette 'size='. Dort finden wir die Gesamtlänge der zusätzlichen Informationen die zuvor im HIDDEN-Bereich gespeichert wurden. Falls 'size=' nicht gefunden wird geben wir einen Fehler zurück.

    // Size-Wert ermitteln
    Editor.SelStart := oldstart;    // wieder wie vorher markieren
    Editor.SelLength := oldlength;  // "
    v := Pos('size=', Editor.SelText);    
    if (v = 0) then begin   // Hidden-Bereich ohne SIZE-Wert (sollte nicht vorkommen)
      result := IsLinkError;  // size nicht gefunden
      exit;
    end;


Ansonsten holen wir uns den Wert von size um die Länge des HIDDEN-Bereichs zu ermitteln. Sollte keine Zahl hinter 'size=' stehen wird ebenfalls ein entsprechender Fehler zurückgegeben. Für die MidStr-Funktion mus die Unit StrUtils eingebunden werden. Wie oben beschrieben gehen wir davon aus, dass die Länge größer als 29 sein muss. Zahlen die ein 1 oder 2 als erste Ziffer haben müssen daher 2-stellig sein. Die Länge des LinkInfo-textes kann als zwischen 30 und 299 sein, was für diesen Fall ausreichend sein sollte.

    s := MidStr(Editor.SelText, v+5, 3);   // Zahl in s
    v := StrToIntDef(s[1], -1);  // erste Ziffer
    if (v = -1) then begin
      result := IsLinkError;  // keine Zahl
      exit;
    end;
    // wenn 2-stellig -> nur zwei Zeichen nehmen
    if (v > 2) then s := LeftStr(s, 2);              
    v := StrToIntDef(s, -1);  // Zahl?
    if (v = -1) then begin
      result := IsLinkError;  // keine Zahl
      exit;
    end;


Nun selektieren wir den Bereich nochmal ohne den Hidden-Abschnitt. Anschließend wird nochmal überprüft ob die Selektion erfolgreich. Falls nicht wird die Funktion abgebrochen und IsLinkError zurückgegeben.

    Editor.SelStart := oldstart + v;    //
    Editor.SelLength := oldlength - v;  //
    if ((Editor.SelStart <> (oldstart+v)) or (Editor.SelLength <> (oldlength-v))) then begin // Fehler bei Selektion
      Editor.SelStart := oldstart;      // auf Ursprungswert setzen
      Editor.SelLength := oldlength;    // "
      result := IsLinkError;       // Fehler zurückgeben
      exit;
    end;


Nun testen wir die PROTECTED-Markierung des selektierten Textes.

      // neuen Zustand ermitteln
      cf.dwMask := CFM_PROTECTED;      // für Protected-Markierung
      cf.dwEffects := 0;                // enthält danach Ergebnis der Get-Abfrage
      SendMessage(Editor.Handle, EM_GETCHARFORMAT, SCF_SELECTION, LPARAM(@cf));   // Aktuelle Formatierung ermitteln



...wird fortgesetzt 
...

Delphi (1) - RichEdit mit Links