Entwickler-Ecke

Open Source Units - HTML-Editor im Eigenbau


MSCH - Di 25.10.05 20:02
Titel: HTML-Editor im Eigenbau
Hallo Community,
ich möcht' euch die Verwendung des HTML-Editors, welcher beim IE verwendet wird, vorstellen und einige Anregungen bieten.
habt Verständnis, dass hier nicht der gesamte Quelltext steht.(Admins, verzeiht mir).

das Ganze funzt nachweislich unter D5, D6 und D2000. Andere Umgebungen stehen mir nicht zur Verfügung.
Sodele, nun gehts los.

Als erstes benötigt Ihr das OCX und die TLB des DHTML-Editors, welches Ihr in der Regel und auf deutschen Systemen unter dem Verzeichnis C:\Programme\Gemeinsame Dateien\Microsoft Shared\Triedit als DHTMLED.OCX findet. Wie ihr diese einbaut, beschreibe ich jetzt nicht, ist in der Hilfe gut beschrieben.

Als nächstes patchen wir die Olectrls.pas, da MS mit der KB891781 eine Sicherheitslücke gestopft hat, die in Delphi leider zu der bekannten Meldung "Schnittstelle nicht unterstützt" führt. Diese Lösung findet ihr auch bei Google, ist nicht auf meinem Mist gewachsen.
Achtung: Macht vorher eine Sicherheitskopie!!

Fügt im Interface der OleCltrs.pas folgendes ein:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
Type
  TImpIOleContainer = class(TObject, IOleContainer)
   protected
      m_cRef: DWORD;
   public
      function _AddRef: Integer; overloadstdcall;
      function _Release: Integer; overloadstdcall;

      constructor Create; virtual;
      destructor Destroy; override;

      function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

      function ParseDisplayName(const bc: IBindCtx; pszDisplayName: POleStr;
       out chEaten: Longint; out mkOut: IMoniker): HResult; stdcall;

      function EnumObjects(grfFlags: Longint; out Enum: IEnumUnknown): 
HResult;
         stdcall;
      function LockContainer(fLock: BOOL): HResult; stdcall;
 end;

const
 IID_IUnknown : TGUID = '{00000000-0000-0000-C000-000000000046}';
 IID_IOleContainer : TGUID = '{0000011B-0000-0000-C000-000000000046}';


Im Implementationsteil:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
constructor TImpIOleContainer.Create;
begin
   inherited;
   m_cRef := 0;
end;

destructor TImpIOleContainer.Destroy;
begin
  inherited;
end;

function TImpIOleContainer.EnumObjects(grfFlags: Integer;
  out Enum: IEnumUnknown): HResult;
begin
  Result := E_NOINTERFACE;
end;

function TImpIOleContainer.LockContainer(fLock: BOOL): HResult;
begin
   Result := E_NOINTERFACE;
end;

function TImpIOleContainer.ParseDisplayName(const bc: IBindCtx;
  pszDisplayName: POleStr; out chEaten: Integer;
  out mkOut: IMoniker): HResult;
begin
  Result := E_NOINTERFACE;
end;

function TImpIOleContainer.QueryInterface(const IID: TGUID;
  out Obj): HResult;

begin
   If (IsEqualGUID(IID, IID_IOleContainer)) Then
      begin
        GetInterface(IID_IOleContainer, Obj);
        Result := S_OK;
        Exit;
      end
      else
      If (IsEqualGUID(IID, IID_IUnknown)) Then
      begin
         GetInterface(IID_IUnknown, Obj);
         Result := S_OK;
         Exit;
      end;

   Result := E_NOINTERFACE;
end;

function TImpIOleContainer._AddRef: Integer;
begin
  Inc(m_cRef);
  Result := m_cRef;
end;

function TImpIOleContainer._Release: Integer;
begin
   If m_cRef > 0 Then
      Dec(m_cRef)
      else
        m_cRef := 0;
   Result := m_cRef;
end;



Jetzt sucht bitte folgende Funktion und ergänzt sie wie dargestellt:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
function TOleControl.GetContainer(out container: IOleContainer): HResult;
Var
 pIContainer : TImpIOleContainer;

begin
   pIContainer := TImpIOleContainer.Create;
   If (pIContainer <> NILThen
     begin
      Result := pIContainer.QueryInterface(IID_IOleContainer, container);
      Exit;
     end;
   container := NIL;
   Result := E_NOINTERFACE;
//  Result := E_NOINTERFACE;<<-- das stand dort vorher drinne und gab besagte Meldung aus
end;


so, nun basteln wir den HTML-Editor
Ich habe dazu eine Form gebastet mit verschiedenen Buttons wie Fett, Kursiv etc. sowie dem importierten OCX-Control.
Das control hat hier den Namen WebEdit

Folgende Units werden u.a. benötigt:

Delphi-Quelltext
1:
2:
3:
4:
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, OleServer, ExtDlgs, OleCtrls, DHTMLEDLib_TLB, ActiveX, ExtCtrls, SHDocVw, MSHTML_TLB, ActnList,
  StdCtrls, Menus, ComCtrls;


Nun erweitert bitte mal die Klassendeklaration
wie folgt:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
  TMain = class(TForm,IHTMLEditDesigner,IHTMLEditHost)
...
    // *** Implementation of IHTMLHost
    function SnapRect(const pIElement: IHTMLElement; var prcNew: tagRECT; eHandle: _ELEMENT_CORNER): HResult; stdcall;
    // *** Implementation of IHTMLEditDesigner ***
    function PreHandleEvent(inEvtDispId: Integer; const pIEventObj: IHTMLEventObj): HResult; stdcall;
    function PostHandleEvent(inEvtDispId: Integer; const pIEventObj: IHTMLEventObj): HResult; stdcall;
    function TranslateAccelerator(inEvtDispId: Integer; const pIEventObj: IHTMLEventObj): HResult; stdcall;
    function PostEditorEventNotify(inEvtDispId: Integer; const pIEventObj: IHTMLEventObj): HResult; stdcall;
...
end

// und die zugehörige Implementation dazu:

function TMain.PostHandleEvent(inEvtDispId: Integer;
  const pIEventObj: IHTMLEventObj): HResult;
begin
  Result := S_FALSE;
end;

function TMain.PreHandleEvent(inEvtDispId: Integer;
  const pIEventObj: IHTMLEventObj): HResult;
begin
  Result := S_FALSE;
end;

function TMain.TranslateAccelerator(inEvtDispId: Integer;
  const pIEventObj: IHTMLEventObj): HResult;
begin
  Result := S_FALSE;
end;

function TMain.PostEditorEventNotify(inEvtDispId: Integer;
  const pIEventObj: IHTMLEventObj): HResult;
begin
  if inEvtDispId = -606 then begin
    sb.Panels[0].Text := IntToStr(pIEventObj.clientX) + ':' + IntToStr(pIEventObj.clientY);
  end;
  Result := S_FALSE;
end;

function TMain.SnapRect(const pIElement: IHTMLElement; var prcNew: tagRECT;
  eHandle: _ELEMENT_CORNER): HResult;
begin
  prcNew.left := 20 * (prcNew.left div 20);
  prcNew.top := 20 * (prcNew.top div 20);
  prcNew.right := 20 * (prcNew.right div 20);
  prcNew.bottom := 20 * (prcNew.bottom div 20);
  Result := S_OK;
end;


am Ende des Programms muss stehen:

Delphi-Quelltext
1:
2:
3:
4:
5:
initialization
OleInitialize(nil);

finalization
OleUninitialize;

In der Create() Methode der Form ergänzen wir folgende Einträge:

Delphi-Quelltext
1:
2:
3:
4:
5:
procedure TMain.FormCreate(Sender: TObject);
begin
  WebEdit.DefaultInterface._AddRef;
...
end;


Um eine Datei zu Laden:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
var
  O:OleVariant;
begin
    O:='test.html';
    WebEdit.LoadDocument(O);
    ...
end;


Um eine Datei zu Sichern, gehe ich hier jetzt einen alternativen Weg via Textfile:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
var
  W:WideString;
  F:TextFile;
begin
  S:=WebEdit.DocumentHTML; // gesamter Quelltext, nicht .InnerHTML !!
  AssignFile(F,'Test.html');
  Rewrite(F);
  Writeln(F,S);
  closeFile(F);
end;


Alternativ wäre zum Beispiel auch sinnvoll:

Delphi-Quelltext
1:
2:
   StringToWideChar(Filename,Wide,Sizeof(Wide));
  (WebEdit.DOM as IPersistFile).Save(Wide,false);


oder via Dialog zum Beispiel:

Delphi-Quelltext
1:
2:
3:
4:
procedure TMain.SaveAsClick(Sender: TObject);
begin
  WebEdit.DOM.execCommand('SaveAs',false,0);
end;


Damit diverse Buttons (Fett, Kursiv) nur aktiv sind, wenn der Nutzer auch sinnvoll das anwenden kann, habe ich eine Funktion "UpdateButton", die genau prüft, was kann, was kann nicht.
Zur Lesart:
ak... sind TAktions, die mit den Buttons gleichnamiger Bedeutung verknüpft sind.
die cmdID findet ihr auch im MSDN


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
function TMain.GetProperties(const Name: String): OLEVariant;
// liefert einen Rückgabewert, ob in der aktuellen Selektion das Kommando "Name" enthalten ist
// sprich, ist der Text der Selektion "Fett" gibts einen Rückgabewert [fett] 
// d.h. der Buttons "Fett" ist selektiert (Down) wenn der Cursor auf einen Fetten Text geht
 var
  V: OleVariant;
begin
  V := WebEdit.DOM.selection.createRange;
  Result := V.queryCommandValue(Name);
end;


procedure TMain.UpdateButtons;
  function QueryStatus(cmdID: OleVariant): OleVariant;
  // liefert den Status eines Kommandos 
  begin
    Result := WebEdit.QueryStatus(cmdID);
  end;

begin
  // Aktion  = [Kommando erlaubt], simple Bitschieberei
  akAusschneiden.Enabled  := (QueryStatus(DECMD_CUT) and DECMDF_ENABLED) = DECMDF_ENABLED;
  akKopieren.Enabled := (QueryStatus(DECMD_COPY)and DECMDF_ENABLED) = DECMDF_ENABLED;
  akEinfuegen.Enabled:= (QueryStatus(DECMD_PASTE) and DECMDF_ENABLED) = DECMDF_ENABLED;
  akEinfuegenAlsText.Enabled:= (QueryStatus(DECMD_PASTE) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aBold.Enabled := (QueryStatus(DECMD_BOLD) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aBold.Checked := GetProperties('Bold');
  aUnderline.Enabled := (QueryStatus(DECMD_UNDERLINE) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aUnderline.Checked := GetProperties('Underline');
  aItalic.Enabled := (QueryStatus(DECMD_ITALIC) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aItalic.Checked := GetProperties('Italic');
  aLeft.Checked   := (GetProperties('JustifyLeft'));
  aCenter.Checked := (GetProperties('JustifyCenter'));
  aRight.Checked  := (GetProperties('JustifyRight'));
  aUndo.Enabled:= (QueryStatus(DECMD_UNDO) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aRedo.Enabled:= (QueryStatus(DECMD_REDO) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aNumber.Checked:= (QueryStatus(DECMD_ORDERLIST) and DECMDF_LATCHED) = DECMDF_LATCHED;
  aBullet.Checked:= (QueryStatus(DECMD_UNORDERLIST) and DECMDF_LATCHED) = DECMDF_LATCHED;
  aIndent.Enabled:= (QueryStatus(DECMD_INDENT) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aOutdent.Enabled:= (QueryStatus(DECMD_OUTDENT) and DECMDF_ENABLED) = DECMDF_ENABLED;
  aURL.Enabled:=  WebEdit.Dom.queryCommandEnabled('CreateLink');
  aUp.Checked:=(GetProperties('Superscript'));
  aDown.Checked:=(GetProperties('Subscript'));
end;


nun wollen wir aber auch, dass der Anwender Text z.b. Fett machen kann
hier die zugehörige TAction.Das gilt übrigens für alle Aktions. Ihr müsst nur die CMDID
wie Bold, Underline etc. entsprechend verwenden.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
procedure TMain.SetProperties(const Name: String; Value: OleVariant);
// dem HTMLEditor eine Nachricht schicken.
var
  V: OleVariant;
  S: OleVariant;
begin
  if GetProperties(Name) <> Value then begin // nur wenn sowieso nicht schon gesetzt
    V := WebEdit.DOM.selection.createRange; // Range erstellen
    S := Value; // a bissle rumschieben
    V.execCommand(Name , False, S); // und ab damit
  end;
end;

procedure TMain.aBoldExecute(Sender: TObject);
begin
  SetProperties('Bold',iBold.Down); // Je nach Status des Buttons "Down" Text Fett machen oder Text Fett aufheben
  UpdateButtons; // mir ist noch nichts besseres eingefallen, Buttons aktualisieren
end;
...

// hier zum beispiel Text unterstreichen
procedure TMain.btnUnderlineClick(Sender: TObject);
begin
  SetProperties('Underline',iUnderline.Down);
  UpdateButtons;
end;


Damit der Anwender auch immer weiss welche Funktionen er nutzen kann, verknüpfen wir die
Ereignisse ONonClick(), ONonKeyDown() und ONonKeyUp() mit der Funktion UpdateButtons:
z.b.


Delphi-Quelltext
1:
2:
3:
4:
procedure TMain.WebEditonclick(Sender: TObject);
begin
  UpdateButtons;
end;


Noch ein Problem haben wir, welcher Zeichensatz soll verwendet werden?
Ist nicht grad unwichtig beim Speichern etc.

Ich habe dazu einfach in der Create() Methode geschrieben :


Delphi-Quelltext
1:
2:
3:
    while Webedit.Busy do Application.ProcessMessages;
      if not WebEdit.Busy then
        WebEdit.Dom.charset:='iso-8859-1';//'utf-8';//'unicode';


fertig.
Ich bastle hier noch etwas rum, kommen also noch ein paar Ergänzungen.

grez
MSCH


LH_Freak - Di 25.10.05 20:08

das klingt super. Könntest du vll. noch eine Demo posten (also im Anhang, und mit Quelltext)?


F34r0fTh3D4rk - So 30.10.05 17:19

ich würde es ja gerne verwenden, aber wie weiß ich auch nicht, ne demo wäre schon das non-plus-ultra ;)

um das einfach mal so zu testen, extra sich einarbeiten selbst n demo programm schreiben ist net immer so das wahre finde ich 8)


MSCH - So 30.10.05 19:20

oki doki,
ich setz das morgen mal rein, muss erstmal wieder mein D installieren, :-( isch abgekackt.

grez
msch

oje, immer diese ausdrücke.


MSCH - Mo 31.10.05 18:12

sodele, hier kömmt das versprochene Progrämmchen.
Es besteht aus 3 teilen, die Exe, eine Konfig und eine Hilfedatei.
Viel Spass beim probieren.

Hier noch ein paar infos:
Das Programm bietet die Möglichkeit, Sonderzeichen jedweder Art (auch Unicode) einzufügen.
Damit ihr das richtig seht, müßt ihr beim IE den Default-Zeichensatz auf Arial Unicode MS einstellen.
(extras-Optionen beim IE).

Leider kann ich die Quelltexte nur in Auszügen wiedergeben; ich verwendete auf mich lizensierte Kompos
von DevEx deren Weitergabe untersagt ist. Allerdings lässt sich das auch mit den Standard-Kompos von D
bewerkstelligen.

Die Unicode-Teile sind aus den TNT-Controls übernommen.

Und das übliche, die Verwendung ist frei jedoch auf eigene Verantwortung, Haftung und Schadensersatz
wird nicht übernommen. Ich hab nen Virenscanner am laufen, sollte also auch keine "Erreger" beinhalten.

Wird mich über Feedback freuen.

Grez
MSch


smiegel - Fr 11.11.05 13:57

Hallo,

entweder bin ich blind oder habe Tomaten auf den Augen! Wo ist der angekündigte Download-Link? Ich kann keinen finden :(


raziel - Fr 11.11.05 14:38

Er meint den Anhang, du musst wohl kurz "F5" drücken, damit der angezeigt wird. Ist ein bereits bekannter Bug.


stifflersmom - Fr 11.11.05 14:52

Auch wenn ich F5 länger drücke,
den Anhang finde ich nicht.

Moin


raziel - Fr 11.11.05 14:55

Dann klick auf "Seite neu laden", am Ende des letzten Beitrags von MSCH ist dann ein Anhang zu finden.


retnyg - Fr 11.11.05 15:05

falls du opera verwendest sind die anhänge manchmal ned ersichtlich
hier ein direktlink http://www.delphi-forum.de/download.php?id=1753


Bennle - Fr 11.11.05 16:05

Hallo,
Kann denn keiner mal den Quellcode anhängen, dann wäre das nur halbsoviel arbeit :D

MfG
Bennle


MSCH - Fr 11.11.05 18:45



Delete - Di 14.02.06 00:21

Hi,

wie ich schon in meiner PN geschrieben hab, würde ich gerne Funktionen wie Folgende ans laufen bekommen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
function TMain.PostEditorEventNotify(inEvtDispId: Integer;  
  const pIEventObj: IHTMLEventObj): HResult;  
begin  
  if inEvtDispId = -606 then begin  
    // onmousemove  
    sb.Panels[0].Text := IntToStr(pIEventObj.clientX) + ':' + IntToStr(pIEventObj.clientY);  
  end;  
  Result := S_FALSE; 
end;


Allerdings fehlt mir jeglicher Ansatz wie ich das bewerkstelligen soll, da die entsprechenden Ereignisse nicht aufgerufen werden. Ich hab zwar Lösungen für den TWebBrowser gefunden, allerdings es nicht geschafft das auf das TDhtmlEdit zu übertragen.
Wär deshalb echt nett, falls du oder wer anders wissen würden wie ich die Funktionen dafür integriere, das die Ereignisse aufgerufen und genutzt werden können (z.B. eben für X/Y-Anzeige oder dergleichen).

Danke und Gruß
Benedikt


MSCH - Di 14.02.06 18:57

hallo,
eine entsprechende Lösung findest du unter:

http://www.mswil.ch/websvn/filedetails.php?repname=devphp&path=%2Fsource%2Ffrmhtmledit.pas&rev=0&sc=0

Du musst deinen Quelltext entsprechend den dortig deklarierten Eventhandler anpassen.

ich hoffe, es hilft dir weiter.
grez
msch


Delete - Mi 15.02.06 00:51

Eine Frage bleibt mir dabei irgendwie noch, was ist die Entsprechung von TDHTMLEdit zufolgender, auf den TWebBrowser bezogenen Funktion:


Delphi-Quelltext
1:
2:
3:
4:
function Tform1.GetHTMLDocument2Ifc: IHTMLDocument2;
begin  
   Result := WebBrowser1.Document as IHTMLDocument2;
end;



Irgendwie steh ich da grad voll aufm Schlauch, aber ich denke ich benötige diese Funktion? Lieg ich da falsch bzw. wenn es doch richtig ist, was muss ich statt WebBrowser1.Document nutzen?
Oder denk ich das Ganze komplett falsch?

MfG Benedikt


MSCH - Mi 15.02.06 18:36

im DHTML-Editor wäre die Entsprechung:

result:= Webedit.DOM;

grez
msch


Delete - Mi 15.02.06 18:42

Mhhh verdammt, das hatte ich auch schon aber trotzdem irgendeinen Fehler (Schwerwiegender Fehler: TOleException) - naja, ich werd nochmal genauer schauen was sich da machen lässt - trotzdem Danke, evtl. kann ich mein Problem ja auch so irgendwie lösen :wink:

//EDIT: Hat sich erledigt...