Iczelion - 35 - RichEdit Control: Syntax Hilighting (Teil 2)

Zurück zu Teil 1
 

Analyse:

Die erste Aktion nach dem Aufruf von WinMain ist der Aufruf von FillHiliteInfo. Diese Funktion liest den Inhalt der wordfile.txt ein und zerlegt den Inhalt.
 
FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
Initialisiere ASMSyntaxArray mit null.
 
    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName
Konstruiere den vollständigen Pfad-Namen der wordfile.txt. Ich gehe davon aus, dass sie immer im selben Verzeichnis wie das Programm liegt.
 
    invoke GetFileAttributes,addr buffer
    .if eax!=-1
Ich benutze diese Methode als schnelle überprüfung, ob eine Datei existiert.
 
        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax
Alloziiere den Speicherblock um die Wörter abzuspeichern. Standard ist 10K. Der Speicher wir vom Standard-Heap alloziiert.
 
@@:        
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
Ich benutze GetPrivateProfileString um den Inhalt jedes Keys aus wordfile.txt zu erhalten. Die Keys starten bei C1 bis C10.
 
            inc eax
            .if eax==BlockSize    ; der Buffer ist zu klein
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
überprüfung, ob der Speicherblock groß genug ist. Wenn nicht, inkrementieren wir die Größe um 10K bis der Block groß genug ist.
 
            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
übergebe die Wörter, das Speicherblock-Handle, die Größe der Daten die aus wordfile.txt gelesen wurden, die Adresse das Farb-DWords und die Adresse von ASMSyntaxArray.

Lassen Sie uns nun analysieren, was ParseBuffer macht. Kurz gesagt, akzeptiert diese Funktion den Buffer, der die zu hilightende Wörter enthält, teilt sie in individuelle Wörter und speichert jedes von ihnen in ein WORDINFO Struktur Array, auf das schnell mittels ASMSyntaxArray zugegriffen werden kann.
 
ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE
InProgress ist das Flag, das ich benutze, um zu indizieren, ob das Scan-Prozess schon begonnen hat. Wenn der Wert FALSE ist, haben wir noch kein nicht-space-Zeichen erreicht.
 
    lea esi,buffer    
    mov edi,pBuffer
    invoke CharLower,edi
ESI zeigt auf unseren lokalen Buffer, der die Wörter enthält, die wir in die Wörterliste zerlegt haben. EDI zeigt auf den Wörter-Listen-String. Um die Suche später zu vereinfachen, konvertieren wir alle Buchstaben in Kleinbuchstaben.
 
    mov ecx,nSize            
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9     ; tab
    je EndOfWord
Scanne die gesamte Wörter-Liste in dem Buffer, nach White-Spaces suchend. Wenn ein White-Space gefunden wird, müssen wir bestimmen, ob es den Anfang oder das Ende eines Wortes markiert.
 
    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop
Wenn das untersuchte Byte kein White-Space ist, kopieren wir es in den Buffer um das ein Wort zu konstruieren und fahren mit dem Scan fort.
 
EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt
Wenn ein White-Space gefunden wurde, überprüfen wir den Wert in InProgress. Wenn der Wert gleich TRUE ist, können wir davon ausgehen, dass das White-Space das Ende eines Wortes markiert und wir das Wort in den lokalen Buffer schreiben können (ESI zeigt darauf), in eine WORDINFO Struktur. Wenn der FALSE ist, fahren wir mit dem Scan fort, bis wir ein nicht-White-Space Zeichen finden.
 
WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
Wenn das Ende eines Wortes gefunden wurde, hängen wir eine 0 an den Buffer, um aus dem Wort ein ASCIIZ String zu machen. Wir alloziieren dann einen Speicherblock vom Heap in der Größe von WORDINFO für diese Wort.
 
    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax
Wir ermitteln die Länge des Wortes in dem lokalen Buffer und speicher sie im WordLen Element der WORDINFO Struktur, um sie als schnellen Vergleich zu nutzen.
 
    push ArrayOffset
    pop [esi].pColor
Speichern Sie die Adresse des DWord, dass die Farbe enthält, die zum hilighten benutzt werden soll, im pColor Element.
 
    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer
Alloziieren Speicher vom Heap um das Wort selbst zu speichern. Genau jetzt, ist die WORDINFO Struktur bereit, um sie in die entsprechende verkettete Liste einzufügen.
 
    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2        ; multipliziere mit 4
    add eax,edx
pArray enthält die Adresse von ASMSyntaxArray. Wir wollen zu dem DWord gehen, das den selben Index wie der Wert des ersten Zeichens im Wort hat. Deshalb speichern wir das erste Zeichen des Wortes in EDX, multiplizieren EDX mit 4 (da jedes Element in ASMSyntaxArray 4 Bytes groß ist) und addieren dann den Offset zu der Adresse von ASMSyntaxArray. Wir haben die Adresse des korrespondierenden DWords in EAX.
 
    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif
überprüfe den Wert des DWord. Wenn er 0 ist, bedeutet das, dass zur Zeit kein Wort vorliegt, dass mit dem Zeichen aus der Liste beginnt. Deshalb speichern wir die Adresse der aktuellen WORDINFO Struktur in diesem DWord.

Wenn der Wert in dem DWord nicht 0 ist, bedeutet das, dass mindest ein Wort mit diesem Zeichen in dem Array beginnt. Deshalb fügen wir diese WORDINFO Struktur am Kopf der verketteten Liste ein und aktuallisieren das NextLink-Element, damit es auf die nächste WORDINFO Struktur zeigt.
 
    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt
Nachdem diese Operation beendet ist, beginnen wir mit dem nächsten Scan-Zyklus, bis das Ende des Buffers erreicht wurde.
 
        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0        ; bedeutet, dass die Nachricht nicht verarbeitet wurde
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif
Nachdem das RichEdit Steuerelement erzeugt wurde, müsse wir seine Version bestimmen. Dieser Schritt ist notwendig, da EM_POSFROMCHAR sich für RichEdit 2.0 und 3.0 unterschiedlich verhält und EM_POSFROMCHAR ist entscheidend für unsere Syntax Hilighting Routine. Ich habe niemals einen dokumentierten Weg gesehen, wie man die Version des RichEdit Steuerelementes überprüft, weshalb ich eine wenig tricksen muss. In diesem Fall setze ich eine Option, die Versions 3.0 spezifisch ist und ermittele umgehend seinen Wert. Wenn ich den Wert ermitteln kann, nehme ich an, dass das Steuerelement in der Version 3.0 vorliegt.

Wenn Sie das RichEdit Steuerelement in der Version 3.0 benutzen, werden Sie feststellen, dass das aktuallisieren der Schrift-Farbe für eine größere Datei ziemlich lange dauert. Dieses Problem scheint Versions 3.0 spezifisch. Ich habe dazu einen Trick gefunden: lassen Sie das Steuerelement das Verhalten des System Edit Steuerelementes emulieren, in dem Sie eine EM_SETEDITSTYLE Nachricht senden.

Danach können wir die Versions-Information erhalten, wir fahren fort und leiten das RichEdit Steuerelement ab (subclassing). Nun werden wir die neue Fenster-Prozedur für das RichEdit Steuerelement unter die Lupe nehmen.
 
NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    ........
    .......
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax
Wir behandeln die WM_PAINT Nachricht. Als erstes verstecken wir den Cursor, um so einige unschöne gfx nach dem Hilighting zu vermeiden. Danach übergeben wir die Nachricht der original RichEdit Prozedur um sie das Fenster aktuallisieren zu lassen. Wenn CallWindowProc zurückkehrt, wird der Text mit seiner gewöhnlichen Farbe/Hintergrund aktualisiert. Nun ist unsere Möglichkeit um das Syntax-Hilighting zu machen
 
        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT
Speichern Sie die Adress von ASMSyntaxArray in EDI. Dann erhalten wir das Handle des Device Kontextes und setzen den Text Hintergrund auf transparent, so dass der Text, den wir schreiben werden, die Standard-Hintergrund-Farbe benutzt.
 
        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
Wir wollen den sichtbaren Text ermitteln, weshalb wir als erstes das formatierende Rechteck ermitteln müssen, in dem wir eine EM_GETRECT Nachricht an das RichEdit Steuerelement senden. Nun, wo wir das begrenzende Rechteck haben, ermitteln wir den nächsten Zeichen-Index zu linken oberen Ecke des Rechtecks mit EM_CHARFROMPOS. Wenn wir einmal den Zeichen-Index haben (das erste sichtbare Zeichen im Steuerelement), können wir mit dem Syntax Hilighting von dieser Position aus starten. Aber der Effekt mag nicht so gut sein, als wenn wir mit dem ersten Buchstaben des Zeile, in dem das Zeichen ist, beginnen. Das ist der Grund, warum ich die Zeilen-Nummer von diesem ersten Zeichen ermittele, indem eine EM_LINEFROMCHAR Nachricht gesendet wird. Um das erste Zeichen dieser Linie zu erhalten, sende ich eine EM_LINEINDEX Nachricht.
 
        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax
Wenn wir den ersten Zeichen-Index haben, speichern wir ihn für zukünftige Referenzen in der FirstChar Variable. Als nächstes ermitteln wir den zu letzt sichtbaren Zeichen-Index, in dem wir eine EM_CHARFROMPOS Nachricht senden, und die untere rechte Ecke des Rechtecks in lParam übergeben.
 
        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax
Während des Syntax Hilightings, habe ich einen unansehnlichen Seiteneffekt dieser Methode bemerkt: wenn das RichEdit Steuerelement eine Begrenzung (Margin) hat (sie können eine Begrenzung spezifizieren, in dem Sie eine EM_SETMARGINS Nachricht an das RichEdit Steuerelement senden), DrawText schreibt über diese Begrenzung. Deshalb muss ich eine Clipping-Region erzeugen, die Größe des formatierenden Rechtecks, indem CreateRectRgn aufgerufen wird. Die Ausgabe der GDI Funktionen werden auf die "schreibbare" Fläche geclippt.

Als nächstes müssen wir die Kommentare hilighten und sie aus dem Weg räumen. Meine Methode ist nach einem ";" zu suchen und den Text mit der Kommentar-Farbe zu hilighten, bis ein Carriage Return gefunden wird. Ich werde die Routine hier nicht analysieren: sie ist ziemlich lang und kompliziert. Soviel sei hier gesagt, dass, wenn alle Kommentare gehilighted sind, wir sie mit nullen im Buffer ersetzen, so dass die Wörter in den Kommentaren später nicht bearbeitet / gehilighted werden.
 
        mov ecx,BufferSize
        lea esi,buffer
        .while ecx0
            mov al,byte ptr [esi]
            .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al=="" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                mov byte ptr [esi],0
            .endif
            dec ecx
            inc esi
        .endw
Nachdem die Kommentare erst einmal aus dem Weg sind, seperieren wir die Wörter im Buffer, indem wir die "Seperatoren" mit 0 ersetzen. Mit dieser Methode müssen wir uns nicht um das Seperator-Zeichen kümmern, während wir die Wörter in dem Buffer weiterbearbeiten: Es gibt nur ein Seperator-Zeiche, NULL.
 
        lea esi,buffer
        mov ecx,BufferSize
        .while ecx0
            mov al,byte ptr [esi]
            .if al!=0
Durchsuchen Sie den Buffer nach dem ersten Zeichen, der nicht NULL ist, d.h. das erste Zeichen eines Wortes.
 
                push ecx
                invoke lstrlen,esi
                push eax
                mov edx,eax
Ermitteln Sie die Länge des Wortes und speichern Sie sie in EDX
 
                movzx eax,byte ptr [esi]
                .if al="A" && al<="Z"
                    sub al,"A"
                    add al,"a"
                .endif
Konvertiere den Buchstaben in einen Kleinbuchstaben (wenn es ein Großbuchstabe ist)
 
                shl eax,2
                add eax,edi        ; edi enthält den Zeiger auf das WORDINFO Zeiger Array
                .if dword ptr [eax]!=0
Danach, springen wir zum korrespondierenden DWord in ASMSyntaxArray und überprüfen, ob der Wert in diesem DWord 0 ist. Wenn ja, können wir zum nächsten Wort springen.
 
                    mov eax,dword ptr [eax]
                    assume eax:ptr WORDINFO
                    .while eax!=0
                        .if edx==[eax].WordLen
Wenn der Wert im DWord ungleich null ist, zeigt es auf die verkettete Liste der WORDINFO Strukturen. Wir überprüfen die Länge des Wortes in unserem lokalen Buffer mit dem Wort in der WORDINFO Struktur. Das ist ein schneller Test, bevor wir die Wörter vergleichen. Sollte einigen Rechenzyklen sparen.
 
                            pushad
                            invoke lstrcmpi,[eax].pszWord,esi
                            .if eax==0
Wenn die Länge beider Wörter gleich ist, fahren wir fort und vergleichen Sie mit lstrcmpi.
 
                                popad
                                mov ecx,esi
                                lea edx,buffer
                                sub ecx,edx
                                add ecx,FirstChar
Wir konstruieren den Zeichen-Index aus der Adresse des ersten Buchstbens des ersten übereinstimmenden Wortes aus dem Buffer. Wir emitteln als erstes seinen relativen Offset von der Start-Adresse des Buffers us und addieren dann den Zeichen-Index des ersten sichtbaren Buchstabens hinzu.
 
                                pushad
                                .if RichEditVersion==3
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                .else
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                    mov ecx,eax
                                    and ecx,0FFFFh
                                    mov rect.left,ecx
                                    shr eax,16
                                    mov rect.top,eax
                                .endif
                                popad
Wenn wir erst einmal den Zeichen-Index des ersten Buchstabens des ersten Wortes, das gehilighted werden soll, kennen, fahren wir fort und ermitteln die Koordinaten, in dem wir eine EM_POSFROMCHAR Nachricht senden. Diese Nachricht wird allerdings von RichEdit 2.0 und 3.0 unterschiedlich interpretiert. Bei RichEdit 2.0 enthält wParam den Zeichen-Index und lParam wird nicht benutzt. Die Koordinaten werden in EAX zurückgegeben. Bei RichEdit 3.0 ist wParam der Zeiger auf eine POINT Struktur, die mit den Koordinaten gefüllt wird und lParam enthält den Zeichen-Index.

Wie Sie sehen, kann die übergabe falscher Parameter bei EM_POSFROMCHAR verheerende Folgen für Ihr System haben. Das ist der Grund warum ich zwichen den RichEdit Steuerelement Versionen unterscheiden muss.
 
                                mov edx,[eax].pColor
                                invoke SetTextColor,hdc,dword ptr [edx]                                
                                invoke DrawText,hdc,esi,-1,addr rect,0
Wenn wir erst einmal die Koordinaten des Anfangs haben, setzen wir die Text-Farbe, die in der WORDINFO Struktur spezifiziert ist. Und dann überschreiben wir das Wort mit der neuen Farbe.

Als Schlusswort: diese Methode kann in verschiedene Dingen verbessert werden. Ich ermittle zum Beispiel den ganzen Text von der ersten bis zur letzten sichtbaren Zeile. Wenn diese Zeilen sehr lang sind, kann die Performance leiden, indem Wörter bearbeitet werden, die nicht sichtbar sind. Sie können das optimieren, indem Sie nur den wirklich sichtbaren Text ermitteln. Auch der Such-Algorithmus kann verbessert werden, indem eine effizientere Methode benutzt wird. Verstehen Sie mich nicht falsch: die Syntax-Hilighting Methode die ich in diesem Beispiel verwendet habe ist SCHNELL, aber sie kann SCHNELLER sein. :)
 
Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage