loading ...
loading...

2006-07-03 | 罗云彬的汇编教程 教程 35: RichEdit 控件:语法高亮显示 (2)

分享

分析:

第一步,在调用 WinMain 之前先调用 FillHiliteInfo。这个函数读出 wordfile.txt 的内容然后解析该内容。

FillHiliteInfo proc uses edi
	LOCAL buffer[1024]:BYTE
	LOCAL pTemp:DWORD
	LOCAL BlockSize:DWORD
	invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray

初始化 ASMSyntaxArray 为 0 。

	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

构建 of wordfile.txt 的完全路径: 我假定该文件总是跟程序文件在同一个文件夹。

	invoke GetFileAttributes,addr buffer
	.if eax!=-1

我使用这个方法来快速检验一个文件是否存在。

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

分配内存块来保存高亮词。缺省是10K。是从缺省堆中分配的。

@@:		
		invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
		.if eax!=0

我使用 GetPrivateProfileString 来检索 wordfile.txt 中每一个关键字的内容。关键字从 C1 到 C10。

			inc eax
			.if eax==BlockSize	; 缓冲区太小
				add BlockSize,1024*10
				invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
				mov pTemp,eax
				jmp @B
			.endif

检查内存块是否足够大。如果不够的话,我们就增大10K,直到足够大为止。

			mov edx,offset ASMColorArray
			invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray

传递需要高亮显示的词的缓冲区, 内存块句柄,从WordFile.txt中读出的数据的大小,用来高亮显示词的颜色数组的地址,和ASMSyntaxArray的地址。

现在让我们来看看 ParseBuffer 做了什么。基本上,这个函数接收包含需要高亮显示词的缓冲区,将它们解析为单个的词并将它们每个都保存在一个 WORDINFO 结构数组中,这样可以从 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 是一个标志,我用来指示扫描处理是否已经开始。如果它的值是FALSE,表示我们还没遇到一个非空格字符。

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

ESI 指向一个局部缓冲区,用来保存从词表中分离出来的单个词。EDI 指向词表字符串。为了方便以后的搜索,我们将所有的词都转换为小写形式。

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

扫描缓冲区里的整个词表,搜索空格(White Space)字符。如果找到的话,我们必须判断它表示一个词的结束还是开始。 

	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

如果找到了一个空格字符,我们就检查 InProgress 的值。如果值为 TRUE ,我们可以假定空格标识一个词的结束,我们可以继续将解析出来的在局部缓冲区(由ESI指向)中的词保存到一个 WORDINFO 结构中。如果值为 FALSE ,我们继续扫描直到发现一个非空白(non-white space)的空格字符。

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

到达词的结尾后,我们就在词缓冲区尾加一个0,标识该词是一个 ASCIIZ 字符串。然后我们从堆中为该词分配一块 WORDINFO 内存。 

	push esi
	mov esi,eax
	assume esi:ptr WORDINFO
	invoke lstrlen,addr buffer
	mov [esi].WordLen,eax

我们取得局部变量中该词的长度,保存在 WORDINFO 结构的 WordLen 成员中,以后用来快速比较。 

	push ArrayOffset
	pop [esi].pColor

将包含用来高亮显示该词的颜色的双字的地址保存到 pColor 成员中。 

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

从堆中分配一块内存用来保存词本身。现在 WORDINFO 结构已经都填充好了,可以插入到适当的链表中去。 

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

pArray包含的是 ASMSyntaxArray 的地址。我们要移动到跟词中第一个字符具有相同索引的DWORD上。因此我们将词中的第一个字符放入EDX,然后将EDX乘以4(因为 ASMSyntaxArray 中的每个元素都是 4 字节大小),再将偏移量加到 ASMSyntaxArray 的地址上,我们就在EAX中得到相应DWORD的地址。 

	.if dword ptr [eax]==0
		mov dword ptr [eax],esi
	.else
		push dword ptr [eax]
		pop [esi].NextLink
		mov dword ptr [eax],esi
	.endif

检查那个DWORD的值,如果为 0 , 意味着当前链表中没有词是以这个字符开头的。因此我们将当前 WORDINFO 结构的地址保存到那个DWORD中。

如果值为非0, 意味着数组中至少有一个词是以这个字符开头的,因此我们将这个WORDINFO 结构插入到该链表头并更新其 NextLink 成员来指向下一个WORDINFO 结构。

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

完成这个操作后,我们就开始下一轮扫描循环直到到达缓冲区的尾。

		invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
		invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
		.if eax==0		; 说明该消息没被处理
			mov RichEditVersion,2
		.else
			mov RichEditVersion,3
			invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
		.endif

创建RichEdit控件之后,我们需要确定其版本。这一步很必要,因为EM_POSFROMCHAR 在 RichEdit 2.0 和 3.0 中是不同的,而 EM_POSFROMCHAR 对于我们的语法高亮显示程序是至关重要的。我从来没见到有文档介绍到检查RichEdit控件版本的方法,因此我必须使用一个解决办法(workaround)。在这种情况下,我设置一个选项指定 3.0 版本并立即检索它的值。如果我能检索到该值的话我就假设该控件是3.0版本的。

如果你使用 RichEdit 3.0 ,你会注意到更改一个大文件的正文颜色会花费很长的时间。这个问题好象是3.0版本特有的。我找到一个解决办法:通过发送EM_SETEDITSTYLE 消息,使控件模拟系统Edit控件行为。

取得版本信息后,我们继续下一步,子类化RichEdit控件。现在我们检查RichEdit的新的窗口过程。

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

我们处理 WM_PAINT 消息。首先,我们隐藏光标以便避免一些高亮后的难看的gfx (ugly gfx)。我们发送WM_PAINT消息给 RichEdit的原来的处理过程,让它更新窗口。CallWindowProc 返回后,正文已经被使用原来的颜色/背景色更新过了。现在是我们做语法高亮显示的机会了! 

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

保存 ASMSyntaxArray 的地址到 EDI。然后我们取得DC的句柄,设置正文背景模式为 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

我们要取得可见的正文,因此我们首先必须发送 EM_GETRECT 消息给RichEdit控件来取得格式化矩形(formatting rectangle)。现在我们有了范围矩形,我们通过EM_CHARFROMPOS消息,取得矩形左上角的最接近的字符索引 。一旦我们有了字符索引(控件中第一个可见字符), 我们就可以从该位置开始做语法高亮显示了。但是效果可能没有从该字符所在行的第一个字符开始高亮显示的效果好。这就是为什么我需要通过 EM_LINEFROMCHAR 消息去取得第一个可见字符所在行的行号的原因。为了取得该行的第一个字符,我需要发送 EM_LINEINDEX 消息。 

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

取得第一个字符的索引后,将之保存到变量 FirstChar 中以便以后引用。跟着我们来取得最后一个可见字符的索引,通过发送 EM_CHARFROMPOS 消息,在 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

在进行语法高亮显示时,我注意到这个方法的一个不好看的边缘效果:如果RichEdit控件有页边空白的话(你可以通过发送 EM_SETMARGINS 消息给RichEdit控件来指定页边空白), DrawText 写出了页边空白。 因而我需要创建一个裁剪区域,大小为矩形框的大小,这可以通过调用 CreateRectRgn 来实现。这样 GDI 函数的输出就会被裁剪在“可写”区域内。

跟着,我们需要先高亮显示注释,并将它们排除在目标之外。我使用的方法是搜索 ";" 并用注释颜色来高亮显示那些正文,直到遇到回车符。我在这里不分析这个过程了:它相当的长和复杂。现在可以说,当所有的注释高亮显示后,我们就在缓冲区中用 0 来替换掉它们,以便注释中的词在以后不会再被重复高亮处理。

		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

将注释内容排除掉后,我们通过使用 0 替换掉分隔符,将缓冲区中的词表分开为一个个单个的词。用这个方法,我们在处理缓冲区中的词时就再不需要考虑分隔符了:只有一个分隔符,就是 NULL 。 

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

搜索缓冲区找到第一个非NULL字符,也就是某个词的第一个字符。 

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

取得词的长度并保存到 EDX

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

将字符转换为小写的 (如果是大写字符的话) 

				shl eax,2
				add eax,edi		; edi 为 WORDINFO 指针数组的指针
				.if dword ptr [eax]!=0

之后,我们跳到 ASMSyntaxArray 中的相应的DWORD,并检查该DWORD的值是否为 0 ,是的话我们就跳到下一个词。 

					mov eax,dword ptr [eax]
					assume eax:ptr WORDINFO
					.while eax!=0
						.if edx==[eax].WordLen

如果DWORD的值不为 0 ,它就指向一个 WORDINFO 结构的链表。我们跟着遍历该链表,将局部缓冲区中词的长度跟 WORDINFO 结构中词的长度进行比较。这是在我们比较词之前进行的快速测试,可以节约一些时钟周期。 

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

如果两个词的长度相等,我们就继续使用 lstrcmpi 比较两个词。 

								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

知道需要高亮显示的词的第一个字符的字符索引后,我们跟着通过发送 EM_POSFROMCHAR 消息取得它的位置。然而这个消息在RichEdit 2.0 和 3.0 中的解释是不同的。对RichEdit 2.0 来说,wParam 包含字符索引,lParam 没有使用。它在 EAX 返回 坐标。对 RichEdit 3.0 来说,wParam 是一个指向 POINT 结构的指针,该调用将会将结果坐标填入这里,而 lParam 则包含字符索引。

正如你看到的,如果传递错误的参数给 EM_POSFROMCHAR ,它可以对你的系统造成重大破坏。这个就是为什么我必须区分 RichEdit 控件的版本的缘故。 

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

一旦我们取得了开始坐标,我们就使用 WORDINFO 结构中指定的颜色来设置正文颜色。跟着使用新颜色值重写该词。

As the final words, 这个方法可以从几个方面提高。例如,我取得从第一个可见行到最后一个可见行的所有正文。如果那些行很长的话,处理那些不可见的词会降低性能。你可以通过一行一行地取得真正的可见行来进行优化。同样,搜索算法也可以使用更多的不同方法来提高速度。不要受我的错误影响(Don't take me wrong):这个例子用来进行语法高亮显示的方法是快,但是它可以更快!:)

分享 分享 |  评论 (0) |  阅读 (?)  |  固定链接 |  类别 (Tech) |  发表于 20:36
搜狐博客温馨提示:搜狐博客官方不会要求参加活动的各位博友缴纳任何的手续费用。请勿轻信留言、评论中的中奖信息,更不要拨打陌生电话及向陌生帐户汇款,谨防受骗!识别更多网络骗术,请 点击查看详情
您还未登录,只能匿名发表评论。或者您可以 登录 后发表。
 
  *中国人爱国心,搜狗输入法爱国主题皮肤下载>>
表  情:
加载中...
回复通知: 同时用小纸条通知对方该回复