Tutorial 34: RichEdit Control:
More Text Operations
Tutorial 35: RichEdit Control: Syntax Hilighting

Before reading this tutorial, let me warn you that it's a complicated subject: not suited for a beginner. This is the last in the richedit control tutorials.

Download the example.

Theory

Syntax hilighting is a subject of hot debate for those writing text editors. The best method (in my opinion) is to code a custom edit control and this is the approach taken by lots of commercial softwares. However, for those of us who don't have time for coding such control, the next best thing is to adapt the existing control to make it suit our need.

Let us take a look at what RichEdit control provides to help us in implementing syntax hilighting. I should state at this moment that the following method is not the "correct" path: I just want to show you the pitfall that many fall for. RichEdit control provides EM_SETCHARFORMAT message that you can use to change the color of the text. At first glance, this message seems to be the perfect solution (I know because I was one of the victim). However, a closer examination will show you several things that are undesirable:

With the above discussion, you can see that using EM_SETCHARFORMAT is a wrong choice. I'll show you the "relatively correct" choice.

The method I currently use is "syntax hilighting just-in-time". I'll hilight only the visible portion of text. Thus the speed of the hilighting will not be related to the size of the file at all. No matter how large the file, only a small portion of it is visible at one time.

How to do that ? The answer is simple:

  1. subclass the richedit control and handle WM_PAINT message within your own window procedure
  2. When it receives WM_PAINT message, it calls the original window procedure of the richedit control to let it update the screen as usual.
  3. After that, we overwrite the words to be hilighted with different color

Of course, the road is not that easy: there are still a couple of minor things to fix but the above method works quite nicely. The display speed is very satisfactory.

Now let's concentrate on the detail. The subclassing process is simple and doesn't require much attention. The really complicated part is when we have to find a fast way of searching for the words to be hilighted. This is further complicated by the need not to hilight any word within a comment block.

The method I use may not be the best but it works ok. I'm sure you can find a faster way. Anyway, here it is:

WORDINFO STRUCT WordLen dd ? ; the length of the word: used as ; a quick comparison pszWord dd ? ; pointer to the word pColor dd ? ; point to the dword that contains ; the color used to hilite the word NextLink dd ? ; point to the next WORDINFO structure WORDINFO ENDS

As you can see, I use the length of the word as the second quick comparison. If the first character of the word matches, we next compare its length to the available words. Each dword in ASMSyntaxArray contains a pointer to the head of the associated WORDINFO array. For example, the dword that represents the character "i" will contain the pointer to the linked list of the words that begin with "i". pColor member points to the dword that contains the color value used to hilight the word. pszWord points to the word to be hilighted, in lowercase.

The word list is stored in a file named "wordfile.txt" and I access it with GetPrivateProfileString APIs. I provide as many as 10 different syntax coloring, starting from C1 to C10. The color array is named ASMColorArray. pColor member of each WORDINFO structure points to one of the dwords in ASMColorArray. Thus it is easy to change the syntax coloring on the fly: you just change the dword in ASMColorArray and all words using that color will use the new color immediately.

Example

.386 .model FLAT,STDCALL OPTION casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\comdlg32.inc include \masm32\include\gdi32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD WORDINFO STRUCT WordLen dd ? ; the length of the word: used as a quick ; comparison pszWord dd ? ; pointer to the word pColor dd ? ; point to the dword that contains the color ; used to hilite the word NextLink dd ? ; point to the next WORDINFO structure WORDINFO ENDS .const IDR_MAINMENU equ 101 IDM_OPEN equ 40001 IDM_SAVE equ 40002 IDM_CLOSE equ 40003 IDM_SAVEAS equ 40004 IDM_EXIT equ 40005 IDM_COPY equ 40006 IDM_CUT equ 40007 IDM_PASTE equ 40008 IDM_DELETE equ 40009 IDM_SELECTALL equ 40010 IDM_OPTION equ 40011 IDM_UNDO equ 40012 IDM_REDO equ 40013 IDD_OPTIONDLG equ 101 IDC_BACKCOLORBOX equ 1000 IDC_TEXTCOLORBOX equ 1001 IDR_MAINACCEL equ 105 IDD_FINDDLG equ 102 IDD_GOTODLG equ 103 IDD_REPLACEDLG equ 104 IDC_FINDEDIT equ 1000 IDC_MATCHCASE equ 1001 IDC_REPLACEEDIT equ 1001 IDC_WHOLEWORD equ 1002 IDC_DOWN equ 1003 IDC_UP equ 1004 IDC_LINENO equ 1005 IDM_FIND equ 40014 IDM_FINDNEXT equ 40015 IDM_REPLACE equ 40016 IDM_GOTOLINE equ 40017 IDM_FINDPREV equ 40018 RichEditID equ 300 .data ClassName db "IczEditClass",0 AppName db "IczEdit version 3.0",0 RichEditDLL db "riched20.dll",0 RichEditClass db "RichEdit20A",0 NoRichEdit db "Cannot find riched20.dll",0 ASMFilterString db "ASM Source code (*.asm)",0,"*.asm",0 db "All Files (*.*)",0,"*.*",0,0 OpenFileFail db "Cannot open the file",0 WannaSave db "The data in the control is modified. Want to save it ?",0 FileOpened dd FALSE BackgroundColor dd 0FFFFFFh ; default to white TextColor dd 0 ; default to black WordFileName db "\wordfile.txt",0 ASMSection db "ASSEMBLY",0 C1Key db "C1",0 C2Key db "C2",0 C3Key db "C3",0 C4Key db "C4",0 C5Key db "C5",0 C6Key db "C6",0 C7Key db "C7",0 C8Key db "C8",0 C9Key db "C9",0 C10Key db "C10",0 ZeroString db 0 ASMColorArray dd 0FF0000h,0805F50h,0FFh,666F00h,44F0h,5F8754h, 4 dup(0FF0000h) CommentColor dd 808000h .data? hInstance dd ? hRichEdit dd ? hwndRichEdit dd ? FileName db 256 dup(?) AlternateFileName db 256 dup(?) CustomColors dd 16 dup(?) FindBuffer db 256 dup(?) ReplaceBuffer db 256 dup(?) uFlags dd ? findtext FINDTEXTEX <;> ASMSyntaxArray dd 256 dup(?) hSearch dd ? ; handle to the search/replace dialog box hAccel dd ? hMainHeap dd ? ; heap handle OldWndProc dd ? RichEditVersion dd ? .code start: mov byte ptr [FindBuffer],0 mov byte ptr [ReplaceBuffer],0 invoke GetModuleHandle, NULL mov hInstance,eax invoke LoadLibrary,ADDR RichEditDLL .IF eax!=0 mov hRichEdit,eax invoke GetProcessHeap mov hMainHeap,eax call FillHiliteInfo invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT invoke FreeLibrary,hRichEdit .ELSE invoke MessageBox,0,ADDR NoRichEdit,ADDR AppName, \ MB_OK or MB_ICONERROR .ENDIF invoke ExitProcess,eax WinMain PROC hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:DWORD mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,IDR_MAINMENU mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, ADDR wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW, \ CW_USEDEFAULT,CW_USEDEFAULT, \ CW_USEDEFAULT,CW_USEDEFAULT, \ NULL,NULL,hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd invoke LoadAccelerators,hInstance,IDR_MAINACCEL mov hAccel,eax .WHILE TRUE invoke GetMessage, ADDR msg,0,0,0 .BREAK .IF (!eax) invoke IsDialogMessage,hSearch,ADDR msg .IF eax==FALSE invoke TranslateAccelerator,hwnd,hAccel,ADDR msg .IF eax==0 invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDIF .ENDIF .ENDW mov eax,msg.wParam ret WinMain ENDP StreamInProc PROC hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0 xor eax,1 ret StreamInProc ENDP StreamOutProc PROC hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0 xor eax,1 ret StreamOutProc ENDP CheckModifyState PROC hWnd:DWORD invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 .IF eax!=0 invoke MessageBox,hWnd,ADDR WannaSave,ADDR AppName, \ MB_YESNOCANCEL .IF eax==IDYES invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0 .ELSEIF eax==IDCANCEL mov eax,FALSE ret .ENDIF .ENDIF mov eax,TRUE ret CheckModifyState ENDP SetColor PROC LOCAL cfm:CHARFORMAT invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0, \ BackgroundColor invoke RtlZeroMemory,ADDR cfm,sizeof cfm mov cfm.cbSize,sizeof cfm mov cfm.dwMask,CFM_COLOR push TextColor pop cfm.crTextColor invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL, \ ADDR cfm ret SetColor ENDP OptionProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL clr:CHOOSECOLOR .IF uMsg==WM_INITDIALOG .ELSEIF uMsg==WM_COMMAND mov eax,wParam shr eax,16 .IF ax==BN_CLICKED mov eax,wParam .IF ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .ELSEIF ax==IDC_BACKCOLORBOX invoke RtlZeroMemory,ADDR clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push BackgroundColor pop clr.rgbResult mov clr.lpCustColors,OFFSET CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,ADDR clr .IF eax!=0 push clr.rgbResult pop BackgroundColor invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX invoke InvalidateRect,eax,0,TRUE .ENDIF .ELSEIF ax==IDC_TEXTCOLORBOX invoke RtlZeroMemory,ADDR clr,sizeof clr mov clr.lStructSize,sizeof clr push hWnd pop clr.hwndOwner push hInstance pop clr.hInstance push TextColor pop clr.rgbResult mov clr.lpCustColors,OFFSET CustomColors mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT invoke ChooseColor,ADDR clr .IF eax!=0 push clr.rgbResult pop TextColor invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX invoke InvalidateRect,eax,0,TRUE .ENDIF .ELSEIF ax==IDOK invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0 push eax invoke SetColor pop eax invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0 invoke EndDialog,hWnd,0 .ENDIF .ENDIF .ELSEIF uMsg==WM_CTLCOLORSTATIC invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX .IF eax==lParam invoke CreateSolidBrush,BackgroundColor ret .ELSE invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX .IF eax==lParam invoke CreateSolidBrush,TextColor ret .ENDIF .ENDIF mov eax,FALSE ret .ELSEIF uMsg==WM_CLOSE invoke EndDialog,hWnd,0 .ELSE mov eax,FALSE ret .ENDIF mov eax,TRUE ret OptionProc ENDP SearchProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD .IF uMsg==WM_INITDIALOG push hWnd pop hSearch invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0, \ ADDR FindBuffer .ELSEIF uMsg==WM_COMMAND mov eax,wParam shr eax,16 .IF ax==BN_CLICKED mov eax,wParam .IF ax==IDOK mov uFlags,0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0, \ ADDR findtext.chrg invoke GetDlgItemText,hWnd,IDC_FINDEDIT,ADDR FindBuffer, \ sizeof FindBuffer .IF eax!=0 invoke IsDlgButtonChecked,hWnd,IDC_DOWN .IF eax==BST_CHECKED or uFlags,FR_DOWN mov eax,findtext.chrg.cpMin .IF eax!=findtext.chrg.cpMax push findtext.chrg.cpMax pop findtext.chrg.cpMin .ENDIF mov findtext.chrg.cpMax,-1 .ELSE mov findtext.chrg.cpMax,0 .ENDIF invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE .IF eax==BST_CHECKED or uFlags,FR_MATCHCASE .ENDIF invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD .IF eax==BST_CHECKED or uFlags,FR_WHOLEWORD .ENDIF mov findtext.lpstrText,OFFSET FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags, \ ADDR findtext .IF eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, \ ADDR findtext.chrgText .ENDIF .ENDIF .ELSEIF ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .ELSE mov eax,FALSE ret .ENDIF .ENDIF .ELSEIF uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .ELSE mov eax,FALSE ret .ENDIF mov eax,TRUE ret SearchProc ENDP ReplaceProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL settext:SETTEXTEX .IF uMsg==WM_INITDIALOG push hWnd pop hSearch invoke SetDlgItemText,hWnd,IDC_FINDEDIT,ADDR FindBuffer invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,ADDR ReplaceBuffer .ELSEIF uMsg==WM_COMMAND mov eax,wParam shr eax,16 .IF ax==BN_CLICKED mov eax,wParam .IF ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .ELSEIF ax==IDOK invoke GetDlgItemText,hWnd,IDC_FINDEDIT,ADDR FindBuffer, \ sizeof FindBuffer invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,ADDR ReplaceBuffer, \ sizeof ReplaceBuffer mov findtext.chrg.cpMin,0 mov findtext.chrg.cpMax,-1 mov findtext.lpstrText,OFFSET FindBuffer mov settext.flags,ST_SELECTION mov settext.codepage,CP_ACP .WHILE TRUE invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN, \ ADDR findtext .IF eax==-1 .BREAK .ELSE invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, \ ADDR findtext.chrgText invoke SendMessage,hwndRichEdit,EM_SETTEXTEX, \ ADDR settext, ADDR ReplaceBuffer .ENDIF .ENDW .ENDIF .ENDIF .ELSEIF uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .ELSE mov eax,FALSE ret .ENDIF mov eax,TRUE ret ReplaceProc ENDP GoToProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL LineNo:DWORD LOCAL chrg:CHARRANGE .IF uMsg==WM_INITDIALOG push hWnd pop hSearch .ELSEIF uMsg==WM_COMMAND mov eax,wParam shr eax,16 .IF ax==BN_CLICKED mov eax,wParam .IF ax==IDCANCEL invoke SendMessage,hWnd,WM_CLOSE,0,0 .ELSEIF ax==IDOK invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE mov LineNo,eax invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0 .IF eax>LineNo invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0 invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax invoke SetFocus,hwndRichEdit .ENDIF .ENDIF .ENDIF .ELSEIF uMsg==WM_CLOSE mov hSearch,0 invoke EndDialog,hWnd,0 .ELSE mov eax,FALSE ret .ENDIF mov eax,TRUE ret GoToProc ENDP PrepareEditMenu PROC hSubMenu:DWORD LOCAL chrg:CHARRANGE invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0 .IF eax==0 ; no text in the clipboard invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED .ELSE invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED .ENDIF invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0 .IF eax==0 invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED .ELSE invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED .ENDIF invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0 .IF eax==0 invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED .ELSE invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED .ENDIF invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,ADDR chrg mov eax,chrg.cpMin .IF eax==chrg.cpMax ; no current selection invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED .ELSE invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED .ENDIF ret PrepareEditMenu ENDP 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 lea esi,buffer mov edi,pBuffer invoke CharLower,edi mov ecx,nSize SearchLoop: or ecx,ecx jz Finished cmp byte ptr [edi]," " je EndOfWord cmp byte ptr [edi],9 ; tab je EndOfWord mov InProgress,TRUE mov al,byte ptr [edi] mov byte ptr [esi],al inc esi SkipIt: inc edi dec ecx jmp SearchLoop EndOfWord: cmp InProgress,TRUE je WordFound jmp SkipIt WordFound: mov byte ptr [esi],0 push ecx invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO push esi mov esi,eax ASSUME esi:ptr WORDINFO invoke lstrlen,ADDR buffer mov [esi].WordLen,eax push ArrayOffset pop [esi].pColor inc eax invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax mov [esi].pszWord,eax mov edx,eax invoke lstrcpy,edx,ADDR buffer mov eax,pArray movzx edx,byte ptr [buffer] shl edx,2 ; multiply by 4 add eax,edx .IF dword ptr [eax]==0 mov dword ptr [eax],esi .ELSE push dword ptr [eax] pop [esi].NextLink mov dword ptr [eax],esi .ENDIF pop esi pop ecx lea esi,buffer mov InProgress,FALSE jmp SkipIt Finished: .IF InProgress==TRUE invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO push esi mov esi,eax ASSUME esi:ptr WORDINFO invoke lstrlen,ADDR buffer mov [esi].WordLen,eax push ArrayOffset pop [esi].pColor inc eax invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax mov [esi].pszWord,eax mov edx,eax invoke lstrcpy,edx,ADDR buffer mov eax,pArray movzx edx,byte ptr [buffer] shl edx,2 ; multiply by 4 add eax,edx .IF dword ptr [eax]==0 mov dword ptr [eax],esi .ELSE push dword ptr [eax] pop [esi].NextLink mov dword ptr [eax],esi .ENDIF pop esi .ENDIF ret ParseBuffer ENDP FillHiliteInfo PROC uses edi LOCAL buffer[1024]:BYTE LOCAL pTemp:DWORD LOCAL BlockSize:DWORD invoke RtlZeroMemory,ADDR ASMSyntaxArray,sizeof ASMSyntaxArray 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 invoke GetFileAttributes,ADDR buffer .IF eax!=-1 mov BlockSize,1024*10 invoke HeapAlloc,hMainHeap,0,BlockSize mov pTemp,eax @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C1Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C2Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,4 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C3Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,8 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C4Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,12 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C5Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,16 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C6Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,20 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C7Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,24 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C8Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,28 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C9Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,32 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF @@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C10Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0 inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF mov edx,OFFSET ASMColorArray add edx,36 invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray .ENDIF invoke HeapFree,hMainHeap,0,pTemp .ENDIF ret FillHiliteInfo ENDP NewRichEditProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL hdc:DWORD LOCAL hOldFont:DWORD LOCAL FirstChar:DWORD LOCAL rect:RECT LOCAL txtrange:TEXTRANGE LOCAL buffer[1024*10]:BYTE LOCAL hRgn:DWORD LOCAL hOldRgn:DWORD LOCAL RealRect:RECT LOCAL pString:DWORD LOCAL BufferSize:DWORD LOCAL pt:POINT .IF uMsg==WM_PAINT push edi push esi invoke HideCaret,hWnd invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam push eax mov edi,OFFSET ASMSyntaxArray invoke GetDC,hWnd mov hdc,eax invoke SetBkMode,hdc,TRANSPARENT 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 mov txtrange.chrg.cpMin,eax mov FirstChar,eax invoke SendMessage,hWnd,EM_CHARFROMPOS,0,ADDR rect.right mov txtrange.chrg.cpMax,eax 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 invoke SetTextColor,hdc,CommentColor lea eax,buffer mov txtrange.lpstrText,eax invoke SendMessage,hWnd,EM_GETTEXTRANGE,0,ADDR txtrange .IF eax>0 mov esi,eax ; esi=size of the text mov BufferSize,eax push edi push ebx lea edi,buffer mov edx,edi ; used as the reference point mov ecx,esi mov al,";" ScanMore: repne scasb je NextSkip jmp NoMoreHit NextSkip: dec edi inc ecx mov pString,edi mov ebx,edi sub ebx,edx add ebx,FirstChar mov txtrange.chrg.cpMin,ebx push eax mov al,0Dh repne scasb pop eax HiliteTheComment: .IF ecx>0 mov byte ptr [edi-1],0 .ENDIF mov ebx,edi sub ebx,edx add ebx,FirstChar mov txtrange.chrg.cpMax,ebx pushad mov edi,pString mov esi,txtrange.chrg.cpMax sub esi,txtrange.chrg.cpMin ; esi contains the length ; of the buffer mov eax,esi push edi .WHILE eax>0 .IF byte ptr [edi]==9 mov byte ptr [edi],0 .ENDIF inc edi dec eax .ENDW pop edi .WHILE esi>0 .IF byte ptr [edi]!=0 invoke lstrlen,edi push eax mov ecx,edi lea edx,buffer sub ecx,edx add ecx,FirstChar .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 invoke DrawText,hdc,edi,-1,ADDR rect,0 pop eax add edi,eax sub esi,eax .ELSE inc edi dec esi .ENDIF .ENDW mov ecx,txtrange.chrg.cpMax sub ecx,txtrange.chrg.cpMin invoke RtlZeroMemory,pString,ecx popad .IF ecx>0 jmp ScanMore .ENDIF NoMoreHit: pop ebx pop edi mov ecx,BufferSize lea esi,buffer .WHILE ecx>0 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 lea esi,buffer mov ecx,BufferSize .WHILE ecx>0 mov al,byte ptr [esi] .IF al!=0 push ecx invoke lstrlen,esi push eax mov edx,eax ; edx contains the length of the string movzx eax,byte ptr [esi] .IF al>="A" &;& al<="Z" sub al,"A" add al,"a" .ENDIF shl eax,2 add eax,edi ; edi contains the pointer to the ; WORDINFO pointer array .IF dword ptr [eax]!=0 mov eax,dword ptr [eax] ASSUME eax:ptr WORDINFO .WHILE eax!=0 .IF edx==[eax].WordLen pushad invoke lstrcmpi,[eax].pszWord,esi .IF eax==0 popad mov ecx,esi lea edx,buffer sub ecx,edx add ecx,FirstChar 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 mov edx,[eax].pColor invoke SetTextColor,hdc,dword ptr [edx] invoke DrawText,hdc,esi,-1,ADDR rect,0 .BREAK .ENDIF popad .ENDIF push [eax].NextLink pop eax .ENDW .ENDIF pop eax pop ecx add esi,eax sub ecx,eax .ELSE inc esi dec ecx .ENDIF .ENDW .ENDIF invoke SelectObject,hdc,hOldRgn invoke DeleteObject,hRgn invoke SelectObject,hdc,hOldFont invoke ReleaseDC,hWnd,hdc invoke ShowCaret,hWnd pop eax pop esi pop edi ret .ELSEIF uMsg==WM_CLOSE invoke SetWindowLong,hWnd,GWL_WNDPROC,OldWndProc .ELSE invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam ret .ENDIF NewRichEditProc ENDP WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD LOCAL ofn:OPENFILENAME LOCAL buffer[256]:BYTE LOCAL editstream:EDITSTREAM LOCAL hFile:DWORD LOCAL hPopup:DWORD LOCAL pt:POINT LOCAL chrg:CHARRANGE .IF uMsg==WM_CREATE invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR RichEditClass,0, \ WS_CHILD or WS_VISIBLE or ES_MULTILINE \ or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\ CW_USEDEFAULT,CW_USEDEFAULT, \ CW_USEDEFAULT, CW_USEDEFAULT, \ hWnd,RichEditID,hInstance,0 mov hwndRichEdit,eax invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS, \ TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1 .IF eax==0 ; means this message is not processed mov RichEditVersion,2 .ELSE mov RichEditVersion,3 invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE, \ SES_EMULATESYSEDIT,SES_EMULATESYSEDIT .ENDIF invoke SetWindowLong,hwndRichEdit,GWL_WNDPROC, \ ADDR NewRichEditProc mov OldWndProc,eax invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0 invoke SetColor invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0, \ ENM_MOUSEEVENTS invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0 .ELSEIF uMsg==WM_NOTIFY push esi mov esi,lParam ASSUME esi:ptr NMHDR .IF [esi].code==EN_MSGFILTER ASSUME esi:ptr MSGFILTER .IF [esi].msg==WM_RBUTTONDOWN invoke GetMenu,hWnd invoke GetSubMenu,eax,1 mov hPopup,eax invoke PrepareEditMenu,hPopup mov edx,[esi].lParam mov ecx,edx and edx,0FFFFh shr ecx,16 mov pt.x,edx mov pt.y,ecx invoke ClientToScreen,hWnd,ADDR pt invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN, \ pt.x,pt.y,NULL,hWnd,NULL .ENDIF .ENDIF pop esi .ELSEIF uMsg==WM_INITMENUPOPUP mov eax,lParam .IF ax==0 ; file menu .IF FileOpened==TRUE ; a file is already opened invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED .ELSE invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED .ENDIF .ELSEIF ax==1 ; edit menu invoke PrepareEditMenu,wParam .ELSEIF ax==2 ; search menu bar .IF FileOpened==TRUE invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED .ELSE invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED .ENDIF .ENDIF .ELSEIF uMsg==WM_COMMAND .IF lParam==0 ; menu commands mov eax,wParam .IF ax==IDM_OPEN invoke RtlZeroMemory,ADDR ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,OFFSET ASMFilterString mov ofn.lpstrFile,OFFSET FileName mov byte ptr [FileName],0 mov ofn.nMaxFile,sizeof FileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY \ or OFN_PATHMUSTEXIST invoke GetOpenFileName,ADDR ofn .IF eax!=0 invoke CreateFile,ADDR FileName, \ GENERIC_READ, \ FILE_SHARE_READ, \ NULL, \ OPEN_EXISTING, \ FILE_ATTRIBUTE_NORMAL, \ 0 .IF eax!=INVALID_HANDLE_VALUE mov hFile,eax mov editstream.dwCookie,eax mov editstream.pfnCallback,OFFSET StreamInProc invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT, \ ADDR editstream invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile mov FileOpened,TRUE .ELSE invoke MessageBox,hWnd,ADDR OpenFileFail,ADDR AppName, \ MB_OK or MB_ICONERROR .ENDIF .ENDIF .ELSEIF ax==IDM_CLOSE invoke CheckModifyState,hWnd .IF eax==TRUE invoke SetWindowText,hwndRichEdit,0 mov FileOpened,FALSE .ENDIF .ELSEIF ax==IDM_SAVE invoke CreateFile,ADDR FileName, \ GENERIC_WRITE, \ FILE_SHARE_READ, \ NULL, \ CREATE_ALWAYS, \ FILE_ATTRIBUTE_NORMAL, \ 0 .IF eax!=INVALID_HANDLE_VALUE @@: mov hFile,eax mov editstream.dwCookie,eax mov editstream.pfnCallback,OFFSET StreamOutProc invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT, \ ADDR editstream invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0 invoke CloseHandle,hFile .ELSE invoke MessageBox,hWnd,ADDR OpenFileFail,ADDR AppName, \ MB_OK or MB_ICONERROR .ENDIF .ELSEIF ax==IDM_COPY invoke SendMessage,hwndRichEdit,WM_COPY,0,0 .ELSEIF ax==IDM_CUT invoke SendMessage,hwndRichEdit,WM_CUT,0,0 .ELSEIF ax==IDM_PASTE invoke SendMessage,hwndRichEdit,WM_PASTE,0,0 .ELSEIF ax==IDM_DELETE invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0 .ELSEIF ax==IDM_SELECTALL mov chrg.cpMin,0 mov chrg.cpMax,-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,ADDR chrg .ELSEIF ax==IDM_UNDO invoke SendMessage,hwndRichEdit,EM_UNDO,0,0 .ELSEIF ax==IDM_REDO invoke SendMessage,hwndRichEdit,EM_REDO,0,0 .ELSEIF ax==IDM_OPTION invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd, \ ADDR OptionProc,0 .ELSEIF ax==IDM_SAVEAS invoke RtlZeroMemory,ADDR ofn,sizeof ofn mov ofn.lStructSize,sizeof ofn push hWnd pop ofn.hwndOwner push hInstance pop ofn.hInstance mov ofn.lpstrFilter,OFFSET ASMFilterString mov ofn.lpstrFile,OFFSET AlternateFileName mov byte ptr [AlternateFileName],0 mov ofn.nMaxFile,sizeof AlternateFileName mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY \ or OFN_PATHMUSTEXIST invoke GetSaveFileName,ADDR ofn .IF eax!=0 invoke CreateFile,ADDR AlternateFileName, \ GENERIC_WRITE, \ FILE_SHARE_READ, \ NULL, \ CREATE_ALWAYS, \ FILE_ATTRIBUTE_NORMAL, \ 0 .IF eax!=INVALID_HANDLE_VALUE jmp @B .ENDIF .ENDIF .ELSEIF ax==IDM_FIND .IF hSearch==0 invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd, \ ADDR SearchProc,0 .ENDIF .ELSEIF ax==IDM_REPLACE .IF hSearch==0 invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd, \ ADDR ReplaceProc,0 .ENDIF .ELSEIF ax==IDM_GOTOLINE .IF hSearch==0 invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd, \ ADDR GoToProc,0 .ENDIF .ELSEIF ax==IDM_FINDNEXT invoke lstrlen,ADDR FindBuffer .IF eax!=0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0, \ ADDR findtext.chrg mov eax,findtext.chrg.cpMin .IF eax!=findtext.chrg.cpMax push findtext.chrg.cpMax pop findtext.chrg.cpMin .ENDIF mov findtext.chrg.cpMax,-1 mov findtext.lpstrText,OFFSET FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN, \ ADDR findtext .IF eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, \ ADDR findtext.chrgText .ENDIF .ENDIF .ELSEIF ax==IDM_FINDPREV invoke lstrlen,ADDR FindBuffer .IF eax!=0 invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0, \ ADDR findtext.chrg mov findtext.chrg.cpMax,0 mov findtext.lpstrText,OFFSET FindBuffer invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0, \ ADDR findtext .IF eax!=-1 invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0, \ ADDR findtext.chrgText .ENDIF .ENDIF .ELSEIF ax==IDM_EXIT invoke SendMessage,hWnd,WM_CLOSE,0,0 .ENDIF .ENDIF .ELSEIF uMsg==WM_CLOSE invoke CheckModifyState,hWnd .IF eax==TRUE invoke DestroyWindow,hWnd .ENDIF .ELSEIF uMsg==WM_SIZE mov eax,lParam mov edx,eax and eax,0FFFFh shr edx,16 invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE .ELSEIF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax,eax ret WndProc ENDP END start

Analysis:

The first action before calling WinMain to to call FillHiliteInfo. This function reads the content of wordfile.txt and parses the content. FillHiliteInfo PROC uses edi LOCAL buffer[1024]:BYTE LOCAL pTemp:DWORD LOCAL BlockSize:DWORD invoke RtlZeroMemory,ADDR ASMSyntaxArray,sizeof ASMSyntaxArray

Initialize ASMSyntaxArray to zero.

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

Construct the full path name of wordfile.txt: I assume that it's always in the same folder as the program.

invoke GetFileAttributes,ADDR buffer .IF eax!=-1

I use this method as a quick way of checking whether a file exists.

mov BlockSize,1024*10 invoke HeapAlloc,hMainHeap,0,BlockSize mov pTemp,eax

Allocate the memory block to store the words. Default to 10K. The memory is allocated from the default heap.

@@: invoke GetPrivateProfileString,ADDR ASMSection,ADDR C1Key, \ ADDR ZeroString,pTemp, \ BlockSize,ADDR buffer .IF eax!=0

I use GetPrivateProfileString to retrieve the content of each key in wordfile.txt. The key starts from C1 to C10.

inc eax .IF eax==BlockSize ; the buffer is too small add BlockSize,1024*10 invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize mov pTemp,eax jmp @B .ENDIF

Checking whether the memory block is large enough. If it is not, we increment the size by 10K until the block is large enough.

mov edx,OFFSET ASMColorArray invoke ParseBuffer,hMainHeap,pTemp,eax,edx,ADDR ASMSyntaxArray

Pass the words, the memory block handle, the size of the data read from wordfile.txt, the address of the color dword that will be used to hilight the words and the address of ASMSyntaxArray.

Now, let's examine what ParseBuffer does. In essence, this function accepts the buffer containing the words to be hilighted, parses them to individual words and stores each of them in a WORDINFO structure array that can be accessed quickly from ASMSyntaxArray.

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 is the flag I use to indicate whether the scanning process has begun. If the value is FALSE, we haven't encountered a non-white space character yet.

lea esi,buffer mov edi,pBuffer invoke CharLower,edi

ESI points to our local buffer that will contain the word we have parsed from the word list. edi points to the word list string. To simplify the search later, we convert all characters to lowercase.

mov ecx,nSize SearchLoop: or ecx,ecx jz Finished cmp byte ptr [edi]," " je EndOfWord cmp byte ptr [edi],9 ; tab je EndOfWord

Scan the whole word list in the buffer, looking for the white spaces. If a white space is found, we have to determine whether it marks the END or the beginning of a word.

mov InProgress,TRUE mov al,byte ptr [edi] mov byte ptr [esi],al inc esi SkipIt: inc edi dec ecx jmp SearchLoop

If the byte under scrutiny is not a white space, we copy it to the buffer to construct a word and then continue the scan.

EndOfWord: cmp InProgress,TRUE je WordFound jmp SkipIt

If a white space is found, we check the value in InProgress. If the value is TRUE, we can assume that the white space marks the END of a word and we may proceed to put the word currently in the local buffer (pointed to by ESI) into a WORDINFO structure. If the value is FALSE, we continue the scan until a non-white space character is found.

WordFound: mov byte ptr [esi],0 push ecx invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO

When the END of a word is found, we append 0 to the buffer to make the word an ASCIIZ string. We then allocate a block of memory from the heap the size of WORDINFO for this word.

push esi mov esi,eax ASSUME esi:ptr WORDINFO invoke lstrlen,ADDR buffer mov [esi].WordLen,eax

We obtain the length of the word in the local buffer and store it in the WordLen member of the WORDINFO structure, to be used as a quick comparison.

push ArrayOffset pop [esi].pColor

Store the address of the dword that contains the color to be used to hilight the word in pColor member.

inc eax invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax mov [esi].pszWord,eax mov edx,eax invoke lstrcpy,edx,ADDR buffer

Allocate memory from the heap to store the word itself. Right now, the WORDINFO structure is ready to be inserted into the appropriate linked list.

mov eax,pArray movzx edx,byte ptr [buffer] shl edx,2 ; multiply by 4 add eax,edx

pArray contains the address of ASMSyntaxArray. We want to move to the dword that has the same index as the value of the first character of the word. So we put the first character of the word in EDX then multiply EDX by 4 (because each element in ASMSyntaxArray is 4 bytes in size) and then add the OFFSET to the address of ASMSyntaxArray. We have the address of the corresponding dword 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

Check the value of the dword. If it's 0, it means there is currently no word that begins with this character in the list. We thus put the address of the current WORDINFO structure in that dword.

If the value in the dword is not 0, it means there is at least one word that begins with this character in the array. We thus insert this WORDINFO structure to the head of the linked list and update its NextLink member to point to the next WORDINFO structure.

pop esi pop ecx lea esi,buffer mov InProgress,FALSE jmp SkipIt

After the operation is complete, we begin the next scan cycle until the END of buffer is reached.

invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS, \ TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1 .IF eax==0 ; means this message is not processed mov RichEditVersion,2 .ELSE mov RichEditVersion,3 invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE, \ SES_EMULATESYSEDIT, \ SES_EMULATESYSEDIT .ENDIF

After the richedit control is created, we need to determine the its version. This step is necessary since EM_POSFROMCHAR behaves differently for RichEdit 2.0 and 3.0 and EM_POSFROMCHAR is crucial to our syntax hilighting routine. I have never seen a documented way of checking the version of richedit control thus I have to use a workaround. In this case, I set an option that is specific to version 3.0 and immediately retrieve its value. If I can retrieve the value, I assume that the control version is 3.0.

If you use RichEdit control version 3.0, you will notice that updating the font color for a large file takes quite a long time. This problem seems to be specific to version 3.0. I found a workaround: making the control emulate the behavior of the system edit control by sending EM_SETEDITSTYLE message.

After we can obtain the version information, we proceed to subclass the richedit control. We will now examine the new window procedure for the richedit control.

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

We handle WM_PAINT message. First, we hide the caret so as to avoid some ugly gfx after the hilighting. After that we pass the message to the original richedit procedure to let it update the window. When CallWindowProc returns, the text is updated with its usual color/background. Now is our opportunity to do syntax hilighting.

mov edi,OFFSET ASMSyntaxArray invoke GetDC,hWnd mov hdc,eax invoke SetBkMode,hdc,TRANSPARENT

Store the address of ASMSyntaxArray in edi. Then we obtain the handle to the device context and set the text background mode to transparent so the text that we will write will use the default background color.

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

We want to obtain the visible text so we first have to obtain the formatting rectangle by sending EM_GETRECT message to the richedit control. Now that we have the bounding rectangle, we obtain the nearest character index to the upper left corner of the rectangle with EM_CHARFROMPOS. Once we have the character index (the first visible character in the control), we can start to do syntax hilighting starting from that position. But the effect might not be as good as when we start from the first character of the line that the character is in. That's why I need to obtain the line number of that the first visible character is in by sending EM_LINEFROMCHAR message. To obtain the first character of that line, I send EM_LINEINDEX message.

mov txtrange.chrg.cpMin,eax mov FirstChar,eax invoke SendMessage,hWnd,EM_CHARFROMPOS,0,ADDR rect.right mov txtrange.chrg.cpMax,eax

Once we have the first character index, store it for future reference in FirstChar variable. Next we obtain the last visible character index by sending EM_CHARFROMPOS, passing the lower-right corner of the formatting rectangle in lParam.

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

While doing syntax hilighting, I noticed an unsightly side-effect of this method: if the richedit control has a margin (you can specify margin by sending EM_SETMARGINS message to the richedit control), DrawText writes over the margin. Thus I need to create a clipping region, the size of the formatting rectangle, by calling CreateRectRgn. The output of GDI functions will be clipped to the "writable" area.

Next, we need to hilight the comments first and get them out of our way. My method is to search for ";" and hilight the text with the comment color until the carriage return is found. I will not analyze the routine here: it's fairly long and complicated. Suffice here to say that, when all the comments are hilighted, we replace them with 0s in the buffer so that the words in the comments will not be processed/hilighted later.

mov ecx,BufferSize lea esi,buffer .WHILE ecx>0 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

Once the comments are out of our way, we separate the words in the buffer by replacing the "separator" characters with 0s. With this method, we need not concern about the separator characters while processing the words in the buffer anymore: there is only one separator character, NULL.

lea esi,buffer mov ecx,BufferSize .WHILE ecx>0 mov al,byte ptr [esi] .IF al!=0

Search the buffer for the first character that is not null,ie, the first character of a word.

push ecx invoke lstrlen,esi push eax mov edx,eax

Obtain the length of the word and put it in EDX

movzx eax,byte ptr [esi] .IF al>="A" && al<="Z" sub al,"A" add al,"a" .ENDIF

Convert the character to lowercase (if it's an uppercase character)

shl eax,2 add eax,edi ; edi contains the pointer to the ; WORDINFO pointer array .IF dword ptr [eax]!=0

After that, we skip to the corresponding dword in ASMSyntaxArray and check whether the value in that dword is 0. If it is, we can skip to the next word.

mov eax,DWORD ptr [eax] ASSUME eax:ptr WORDINFO .WHILE eax!=0 .IF edx==[eax].WordLen

If the value in the dword is non-zero, it points to the linked list of WORDINFO structures. We process to walk the linked list, comparing the length of the word in our local buffer with the length of the word in the WORDINFO structure. This is a quick test before we compare the words. Should save some clock cycles.

pushad invoke lstrcmpi,[eax].pszWord,esi .IF eax==0

If the lengths of both words are equal, we proceed to compare them with lstrcmpi.

popad mov ecx,esi lea edx,buffer sub ecx,edx add ecx,FirstChar

We construct the character index from the address of the first character of the matching word in the buffer. We first obtain its relative OFFSET from the starting address of the buffer then add the character index of the first visible character to it.

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

Once we know the character index of the first character of the word to be hilighted, we proceed to obtain the coordinate of it by sending EM_POSFROMCHAR message. However, this message is interpreted differently by richedit 2.0 and 3.0. For richedit 2.0, wParam contains the character index and lParam is not used. It returns the coordinate in eax. For richedit 3.0, wParam is the pointer to a POINT structure that will be filled with the coordinate and lParam contains the character index.

As you can see, passing the wrong arguments to EM_POSFROMCHAR can wreak havoc to your system. That's why I have to differentiate between RichEdit control versions.

mov edx,[eax].pColor invoke SetTextColor,hdc,dword ptr [edx] invoke DrawText,hdc,esi,-1,ADDR rect,0

Once we got the coordinate to start, we set the text color with the one specified in the WORDINFO structure. And then proceed to overwrite the word with the new color.

As the final words, this method can be improved in several ways. For example, I obtain all the text starting from the first to the last visible line. If the lines are very long, the performance may hurt by processing the words that are not visible. You can optimize this by obtaining the really visible text line by line. Also the searching algorithm can be improved by using a more efficient method. Don't take me wrong: the syntax hilighting method used in this example is FAST but it can be FASTER. :)


Tutorial 32: Multiple Document Interface (MDI) Overview Tutorial 34: RichEdit Control: More Text Operations
Software » wiki » Assembler » X86 » icz » uk » tute035.html