《WINDOWS 环境下32位汇编语言程序设计》第9章 通用控件(2)
9.4 使用Richedit控件
Richedit控件与Edit控件类似,可以用于文本的输入和编辑。但两者在功能上各有侧重点。Edit控件广泛使用于对话框中,用来供用户输入少量的文字,因此加快速度和减少资源的占用是最重要的,而各种高级编辑功能不是主要的,也正因此,Edit控件在短小精悍的同时,也存在诸多限制,最主要的就是在单行模式下,能容纳的文本不能超过32KB,在多行模式下也不能超过64 KB。
Richedit控件则侧重于文字的高级编辑功能,控件能够容纳的文本长度可以支持操作系统中的最大文件尺寸,并内置了很多高级编辑器才具有的特征,如多级的撤销或重做,向前或向后搜索,支持Unicode编辑等,最重要的就是支持RTF(Rich Text Format)格式的带段落格式的文本编辑。由于实现这些功能的代码比较复杂,所以Richedit控件的规模比较大,以至于Windows将它划分出来以一个单独的DLL库文件方式提供。
到目前为止,Richedit控件总共有3个版本,这些版本的功能有所不同,总的来说高版本包括了低版本的所有功能,但在某些细节的实现上又有些不同,随着版本的升高,一些设置工作也随之增多,所以如果不需要某些特殊功能的话,使用最高的版本可能并不是最适合的。
1.0版本的Richedit控件对应的库文件是Riched32.dll,Windows 95只提供1.0版本,文件名中的32是32位版本的意思(不过并没有一个Riched16.dll)。从Windows 98开始,系统中多了一个2.0版本的Richedit控件,Windows 2000开始则有了3.0版本。2.0版本和3.0版本的库文件名都是Riched20.dll,同时Riched32.dll文件仍然存在于系统中,不过Riched20.dll文件名中的20总是让人迷惑,很多人第一次使用Richedit控件的时候误认为Riched32.dll的版本要比Riched20.dll的版本高。
除了在功能上的不同外,不同版本Richedit控件的类名称也有所不同,表9.4列出了3个版本之间的一些区别。
表9.4中列出的仅是一些最重要的区别,很多细微的区别并没有列出来,比如,每个版本都可以为文本设定下划线,但3.0版比2.0版又增加了点、划、划-点、划-点-点等多种样式的下划线。
表9.4 不同版本Richedit控件之间的区别
Richedit控件的2.0版本和3.0版本使用的控件名和类名是相同的,有时候为了使用某些版本特有的功能,需要预先检测版本号,但由于Microsoft并没有提供一个官方的检测方法,所以必须利用一些版本之间的区别来进行检测(这种方法好像在检测不同的CPU),比如,排版样式功能(TYPOGRAPHY)是3.0版本才支持的,设置排版样式选项使用EM_SETTYPOGRAPHYOPTIONS消息,如果排版样式被设置后能够再检测到,说明控件的版本肯定是3.0的,代码如下:
invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,\TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
.if eax==0 ;说明设置消息没被处理,版本是2.0版mov dwVersion,2
.elsemov dwVersion,3
.endif
另外,也可以通过检测操作系统来确定Richedit控件的版本,如2.0版本在Windows 98和Windows NT 4.0中使用,而Windows 2000使用的是3.0版本。
在本节中,用一个简单的例子来演示Richedit控件的使用,包括如何创建控件、如何使用流操作装入和输出文件,以及如何进行文本查找等,由于篇幅有限,程序并没有演示所有的高级编辑功能,程序代码存放在所附光盘的Chapter09\Richedit目录中,其中汇编源代码Richedit.asm文件的内容如下:
;Richedit.asm ------------ “丰富编辑”控件的使用例子
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Richedit.asm
;rc Richedit.rc
;Link /subsystem:windows Richedit.obj Richedit.res
;--------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib
include c:/masm32/include/comdlg32.inc
includelib c:/masm32/lib/comdlg32.lib ;equ 等值定义
ICO_MAIN equ 1000
IDA_MAIN equ 2000
IDM_MAIN equ 2000
IDM_OPEN equ 2100
IDM_SAVE equ 2102
IDM_EXIT equ 2103
IDM_UNDO equ 2201
IDM_REDO equ 2202
IDM_SELALL equ 2203
IDM_COPY equ 2204
IDM_CUT equ 2205
IDM_PASTE equ 2206
IDM_FIND equ 2207
IDM_FINDPREV equ 2208
IDM_FINDNEXT equ 2209;数据段
.data?
hInstance dword ?
hWinMain dword ?
hMenu dword ?
hWinEdit dword ?
hFile dword ?
hFindDialog dword ?
idFindMessage dword ?
szFileName byte MAX_PATH dup(?)
szFindText byte 100 dup(?)
.data
stFind FINDREPLACE <sizeof FINDREPLACE, 0, 0, FR_DOWN, szFindText, \0, sizeof szFindText, 0, 0, 0, 0>
.const
FINDMSGSTRING byte 'commdlg_FindReplace',0
szClassName byte 'Wordpad',0
szCaptionMain byte '记事本',0
szDllEdit byte 'RichEd20.dll',0
szClassEdit byte 'RichEdit20A',0
szNotFound byte '字符串未找到!',0
szFilter byte 'Text Files(*.txt)', 0, '*.txt', 0byte 'All Files(*.*)', 0, '*.*', 0, 0
szDefaultExt byte 'txt',0
szErrOpenFile byte '无法打开文件!',0
szModify byte '文件已修改,是否保存?',0
szFont byte '宋体',0;代码段
.code
; Richedit的流操作
_ProcStream proc uses ebx edi esi, _dwCookie, _lpBuffer, _dwBytes, _lpBytes .if _dwCookie invoke ReadFile, hFile, _lpBuffer, _dwBytes, _lpBytes, 0 .else invoke WriteFile, hFile, _lpBuffer, _dwBytes, _lpBytes, 0.endif xor eax, eax ret
_ProcStream endp ; 保存文件,如果没有打开或创建文件则调用“另存为”子程序
_SaveFile proc local @stES:EDITSTREAM invoke SetFilePointer, hFile, 0, 0, FILE_BEGIN invoke SetEndOfFile, hFile mov @stES.dwCookie, FALSE mov @stES.pfnCallback, offset _ProcStream invoke SendMessage, hWinEdit, EM_STREAMOUT, SF_TEXT, addr @stES invoke SendMessage, hWinEdit, EM_SETMODIFY, FALSE, 0ret
_SaveFile endp ; 打开及输入文件
_OpenFile proc local @stOF:OPENFILENAME local @stES:EDITSTREAM ;显示“打开文件”对话框invoke RtlZeroMemory, addr @stOF, sizeof @stOF mov @stOF.lStructSize, sizeof @stOF push hWinMain pop @stOF.hwndOwner mov @stOF.lpstrFilter, offset szFilter mov @stOF.lpstrFile, offset szFileName mov @stOF.nMaxFile, MAX_PATH mov @stOF.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST mov @stOF.lpstrDefExt, offset szDefaultExt invoke GetOpenFileName, addr @stOF .if eax ;创建文件invoke CreateFile, addr szFileName, GENERIC_READ or GENERIC_WRITE, \FILE_SHARE_READ or FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0.if eax == INVALID_HANDLE_VALUE invoke MessageBox, hWinMain, addr szErrOpenFile, NULL, MB_OK or MB_ICONSTOP ret .endif push eax .if hFile invoke CloseHandle, hFile .endif pop eax mov hFile, eax ; 读入文件mov @stES.dwCookie, TRUE mov @stES.pfnCallback, offset _ProcStream invoke SendMessage, hWinEdit, EM_STREAMIN, SF_TEXT, addr @stES invoke SendMessage, hWinEdit, EM_SETMODIFY, FALSE, 0 .endif ret
_OpenFile endp ;允许继续操作则返回TRUE
_CheckModify proc local @dwReturn mov @dwReturn, TRUE invoke SendMessage, hWinEdit, EM_GETMODIFY, 0, 0.if eax && hFile invoke MessageBox, hWinMain, addr szModify, addr szCaptionMain, \MB_YESNOCANCEL or MB_ICONQUESTION .if eax == IDYES call _SaveFile .elseif eax == IDCANCEL mov @dwReturn, FALSE .endif.endif mov eax, @dwReturn ret
_CheckModify endp
;查找文字
_FindText proc local @stFindText:FINDTEXTEX ;设置查找范围invoke SendMessage, hWinEdit, EM_EXGETSEL, 0, addr @stFindText.chrg .if stFind.Flags & FR_DOWN push @stFindText.chrg.cpMax pop @stFindText.chrg.cpMin .endif mov @stFindText.chrg.cpMax, -1 ;设置查找选项mov @stFindText.lpstrText, offset szFindText mov ecx, stFind.Flags and ecx, FR_MATCHCASE or FR_DOWN or FR_WHOLEWORD ; 查找并把光标设置到找到的文本上invoke SendMessage, hWinEdit, EM_FINDTEXTEX, ecx, addr @stFindText .if eax == -1 mov ecx, hWinMain .if hFindDialog mov ecx, hFindDialog .endif invoke MessageBox, ecx, addr szNotFound, NULL, MB_OK or MB_ICONINFORMATION ret .endif invoke SendMessage, hWinEdit, EM_EXSETSEL, 0, addr @stFindText.chrgText invoke SendMessage, hWinEdit, EM_SCROLLCARET, NULL, NULL ret
_FindText endp ;根据情况改变菜单项状态
_SetStatus proc local @stRange:CHARRANGE invoke SendMessage, hWinEdit, EM_EXGETSEL, 0, addr @stRange mov eax, @stRange.cpMin .if eax == @stRange.cpMax invoke EnableMenuItem, hMenu, IDM_COPY, MF_GRAYED invoke EnableMenuItem, hMenu, IDM_CUT, MF_GRAYED .else invoke EnableMenuItem, hMenu, IDM_COPY, MF_ENABLED invoke EnableMenuItem, hMenu, IDM_CUT, MF_ENABLED .endif invoke SendMessage, hWinEdit, EM_CANPASTE, 0, 0.if eax invoke EnableMenuItem, hMenu, IDM_PASTE, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_PASTE, MF_GRAYED .endif invoke SendMessage, hWinEdit, EM_CANREDO, 0, 0.if eax invoke EnableMenuItem, hMenu, IDM_REDO, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_REDO, MF_GRAYED .endif invoke SendMessage, hWinEdit, EM_CANUNDO, 0, 0 .if eax invoke EnableMenuItem, hMenu, IDM_UNDO, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_UNDO, MF_GRAYED .endif invoke GetWindowTextLength, hWinEdit .if eax invoke EnableMenuItem, hMenu, IDM_SELALL, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_SELALL, MF_GRAYED .endif invoke SendMessage, hWinEdit, EM_GETMODIFY, 0, 0.if eax && hFile invoke EnableMenuItem, hMenu, IDM_SAVE, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_SAVE, MF_GRAYED .endif .if szFindText invoke EnableMenuItem, hMenu, IDM_FINDNEXT, MF_ENABLED invoke EnableMenuItem, hMenu, IDM_FINDPREV, MF_ENABLED .else invoke EnableMenuItem, hMenu, IDM_FINDNEXT, MF_GRAYED invoke EnableMenuItem, hMenu, IDM_FINDPREV, MF_GRAYED .endif ret
_SetStatus endp _Init proc local @stCf:CHARFORMAT ;注册“查找”对话框消息,初始化“查找”对话框的结构push hWinMain pop stFind.hwndOwner invoke RegisterWindowMessage, addr FINDMSGSTRING mov idFindMessage, eax ;建立输出文本窗口invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassEdit, NULL, \WS_CHILD or WS_VISIBLE or WS_VSCROLL or WS_HSCROLL \or ES_MULTILINE or ES_NOHIDESEL, \0, 0, 0, 0, \hWinMain, 0, hInstance, NULL mov hWinEdit, eax invoke SendMessage, hWinEdit, EM_SETTEXTMODE, TM_PLAINTEXT, 0invoke RtlZeroMemory, addr @stCf, sizeof @stCf mov @stCf.cbSize, sizeof @stCf mov @stCf.yHeight, 9 * 20 mov @stCf.dwMask, CFM_FACE or CFM_SIZE or CFM_BOLD invoke lstrcpy, addr @stCf.szFaceName, addr szFont invoke SendMessage, hWinEdit, EM_SETCHARFORMAT, 0, addr @stCf invoke SendMessage, hWinEdit, EM_EXLIMITTEXT, 0, -1 ret
_Init endp _Quit proc invoke _CheckModify .if eax invoke DestroyWindow, hWinMain invoke PostQuitMessage, NULL .if hFile invoke CloseHandle, hFile .endif .endif ret
_Quit endp _ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam local @stRange:CHARRANGE local @stRect:RECT mov eax, uMsg .if eax == WM_SIZEinvoke GetClientRect, hWinMain, addr @stRect invoke MoveWindow, hWinEdit, 0, 0, @stRect.right, @stRect.bottom, TRUE ;处理菜单、加速键及工具栏消息.elseif eax == WM_COMMAND mov eax, wParam movzx eax, ax .if eax == IDM_OPEN invoke _CheckModify .if eax call _OpenFile .endif .elseif eax == IDM_SAVE call _SaveFile .elseif eax == IDM_EXIT invoke _Quit .elseif eax == IDM_UNDO invoke SendMessage, hWinEdit, EM_UNDO, 0, 0.elseif eax == IDM_REDO invoke SendMessage, hWinEdit, EM_REDO, 0, 0.elseif eax == IDM_SELALL mov @stRange.cpMin, 0mov @stRange.cpMax, -1 invoke SendMessage, hWinEdit, EM_EXSETSEL, 0, addr @stRange .elseif eax == IDM_COPY invoke SendMessage, hWinEdit, WM_COPY, 0, 0.elseif eax == IDM_CUT invoke SendMessage, hWinEdit, WM_CUT, 0, 0.elseif eax == IDM_PASTE invoke SendMessage, hWinEdit, WM_PASTE, 0, 0.elseif eax == IDM_FIND and stFind.Flags, not FR_DIALOGTERM invoke FindText, addr stFind .if eax mov hFindDialog, eax .endif .elseif eax == IDM_FINDPREV and stFind.Flags, not FR_DOWN invoke _FindText .elseif eax == IDM_FINDNEXT or stFind.Flags, FR_DOWN invoke _FindText .endif ;-----------------------------------------------------------------------------------------------------.elseif eax == WM_INITMENU call _SetStatus .elseif eax == idFindMessage .if stFind.Flags & FR_DIALOGTERM mov hFindDialog, 0 .else invoke _FindText .endif ;--------------------------------------------------------------------------------------------------------.elseif eax == WM_ACTIVATE mov eax, wParam .if (ax == WA_CLICKACTIVE) || (ax == WA_ACTIVE)invoke SetFocus, hWinEdit .endif;----------------------------------------------------------------------------------------------------------.elseif eax == WM_CREATE push hWnd pop hWinMain invoke _Init ;-----------------------------------------------------------------------------------------------------------.elseif eax == WM_CLOSE call _Quit ;-------------------------------------------------------------------------------------------------------------.else invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret .endif xor eax, eax ret
_ProcWinMain endp
;----------------------------------------------------------------------------------------------------------
_WinMain proc local @stWndClass:WNDCLASSEX local @stMsg:MSG local @hAccelerator, @hRichEdit invoke LoadLibrary, offset szDllEdit mov @hRichEdit, eax invoke GetModuleHandle, NULL mov hInstance, eax invoke LoadMenu, hInstance, IDM_MAIN mov hMenu, eax invoke LoadAccelerators, hInstance, IDA_MAIN mov @hAccelerator, eax ;注册窗口类invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass invoke LoadIcon, hInstance, ICO_MAIN mov @stWndClass.hIcon, eax mov @stWndClass.hIconSm, eax invoke LoadCursor, 0, IDC_ARROW mov @stWndClass.hCursor, eax push hInstance pop @stWndClass.hInstance mov @stWndClass.cbSize, sizeof WNDCLASSEX mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW mov @stWndClass.lpfnWndProc, offset _ProcWinMain mov @stWndClass.hbrBackground, COLOR_BTNFACE + 1 mov @stWndClass.lpszClassName, offset szClassName invoke RegisterClassEx, addr @stWndClass ;建立并显示窗口invoke CreateWindowEx, NULL, \offset szClassName, offset szCaptionMain, \WS_OVERLAPPEDWINDOW, \CW_USEDEFAULT, CW_USEDEFAULT, 700, 500, \NULL, hMenu, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMAL invoke UpdateWindow, hWinMain ;消息循环 .while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0 .break .if eax == 0invoke TranslateAccelerator, hWinMain, @hAccelerator, addr @stMsg .if eax == 0 invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endif .endw invoke FreeLibrary, @hRichEdit ret
_WinMain endp;main函数
main proc call _WinMain invoke ExitProcess, 0
main endp
end main
编译运行结果:
由于篇幅所限,例子中仅演示了最基本的功能,一些附加的功能并没有写进去,如没有“另存为”功能(对打开的文件编辑后可以存盘,但是对直接在RichEdit控件中输入的内容没有设计存盘功能),也没有提供选择字体和颜色的对话框等,读者如果有兴趣的话,可以分析所附光盘的Chapter09\Wordpad目录中的程序,这是一个完整得多的编辑器,界面中使用了工具栏和状态栏,但由于全部代码的长度有900多行,所以在书中并没有列出它的代码。
Richedit.asm程序中使用了一些第8章中介绍的通用对话框:当选择“打开”文件菜单的时候,使用GetOpenFileName显示一个“打开”文件对话框来供用户选择文件;另外,在选择“查找”菜单的时候,使用FindText函数显示一个“查找文本”对话框,所以在WM_CREATE消息中程序预先使用RegisterWindowMessage函数为“查找文本”对话框注册FINDMSGSTRING消息。对于这些内容,读者可以参看第8章中的相关章节。
9.4.1 创建Richedit控件
1.装入Richedit控件
由于Richedit控件存在于一个单独的DLL库文件中,所以在使用前也要显式地装入库文件,装入Richedit库文件并不使用InitCommonControls之类的专用函数,一般使用通用的LoadLibrary函数来装入它,LoadLibrary函数的用法是:
invoke LoadLibrary,addr szDllName
mov hDllInstance,eax
函数返回装入DLL的模块实例句柄,当不再使用库文件的时候,需要使用FreeLibrary函数将库释放。当库被装入时,库中的初始化代码会注册Richedit控件的窗口类,这样就可以在程序中利用Richedit的类名来创建控件。例子程序的_WinMain子程序中是这样进行库的装入和释放工作的:
.const
szDllEdit db 'RichEd20.dll',0
...
.code
invoke LoadLibrary,offset szDllEdit
mov @hRichEdit,eax
...
;主程序代码——创建窗口、消息循环等
...
invoke FreeLibrary,@hRichEdit
当使用不同版本的Richedit控件时,注意要装入的库文件名是不同的。由于例子中使用2.0版本或3.0版本,所以装入的是RichEd20.dll文件。
2.创建Richedit控件
创建Richedit控件的工作一般在主窗口的WM_CREATE消息中完成,创建的办法是使用CreateWindowEx函数:
.const
szClassEdit db 'RichEdit20A',0
...
.code
invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClassEdit,NULL,\WS_CHILD OR WS_VISIBLE OR WS_VSCROLL OR WS_HSCROLL \OR ES_MULTILINE or ES_NOHIDESEL,\0,0,0,0,hWinMain,0,hInstance,NULL
mov hWinEdit,eax
注意类名使用上的区别:1.0版使用“RichEdit”,2.0和3.0版本的类名有两种,ANSI版本使用的类名是“RichEdit20A”,Unicode版本使用的类名是“RichEdit20W”,例子中使用的是ANSI版本。
Richedit控件可以使用的风格有3组:标准的窗口风格、Edit控件风格和Richedit控件特有的风格。
在Edit控件可以使用的风格中,可供Richedit控件使用的有:
● ES_MULTILINE——可以编辑多行文字。
● ES_AUTOHSCROLL和ES_AUTOVSCROLL——自动滚动。
● ES_NOHIDESEL——失去键盘输入焦点的时候仍然显示选择区域。
● ES_READONLY——只读属性。
● ES_CENTER,ES_RIGHT和ES_LEFT——文本的对齐方式。
● ES_WANTRETURN——允许用户按回车键插入新的行。
但下列Edit控件风格不能在Richedit控件中使用:
● ES_LOWERCASE或ES_UPPERCASE——将控件中的文字全部转换成小写或大写。
● ES_PASSWORD——将控件中的文字显示为密码方式(显示为星号)。
下面列出的是部分Richedit控件特有的风格:
● ES_DISABLENOSCROLL——指定这个风格后,控件在不需要滚动条的时候(文字没有超出客户区大小)显示灰化状态的滚动条,而默认情况下,当不需要滚动条的时候,控件根本不会显示它。
● ES_NOIME——禁止输入法(IME)操作,仅用于亚洲语言版本。
● ES_SAVESEL——在失去键盘输入焦点的时候保存当前选择区域,默认状态下当控件在重新获得焦点的时候会将全部文本选中。
创建控件以后,需要发送EM_EXLIMITTEXT消息设置控件中能够容纳字符的总数,虽然Richedit控件中的文字长度可以支持到最大的文件尺寸,但由于默认情况下,控件还是将最大字符数限制为64 KB,所以如果读者发现Richedit控件也只能编辑64 KB字符的话,那并不是控件的错,而是因为你没有告诉它具体的要求,EM_EXLIMITTEXT消息的使用方法是:
invoke SendMessage,hWinEdit,EM_EXLIMITTEXT,0,dwTextMax
其中dwTextMax指定了最大字符数。
9.4.2 Richedit控件的控制消息
1.选择区域
选择区域就是用户在控件中通过拖动鼠标来选定的多个文字,选择了一段文字以后,用户以后的操作就是针对这段文字的,如按下Delete键可以删除整段文字,按Ctrl+C键将这段文字拷贝到剪贴板中,按Ctrl+V键用剪贴板中的内容替换这段文字等。
在程序中,选择区域可以被程序获取,也可以由程序自由设置,从编程角度来看,选择区域的用处有两个:
● 选定操作文本——与用户手工选定一段文本以便进行各种操作类似,程序在对文本进行操作之前也需要预先设置选择区域。
● 定位光标——控件中并没有专门的用来定位光标的控制消息,定位光标也是靠设置选择区域完成的。如果把选择区域的起始位置和结束位置设置为相同的,那么就相当于把光标定位到这个位置而不选定任何文字。
在Edit控件中,获取选择区域可以通过向控件发送EM_GETSEL消息:
invoke SendMessage,hRichedit,EM_GETSEL,lpdwStart,lpdwEnd
lpdwStart和lpdwEnd指向两个用来返回选定区域起始位置和结束位置的双字变量,也可以将这两个参数全部设置为NULL,因为消息的返回值也是位置数据,返回值的低16位是选定区域的起始位置,高16位是结束位置。
但是EM_GETSEL消息仅适用于控件中文本长度不超过64 KB的情况,如果Richedit中选择区域的起始位置或结束位置有一个落在了64 KB以外,那么消息仅返回−1,而不是正确的数值,所以最好还是使用EM_EXGETSEL消息,EM_EXGETSEL消息是Richedit的特有消息,不能在Edit控件中使用:
invoke SendMessage,hRichedit,EM_EXGETSEL,0,lpchr
lpchr参数指向一个CHARRANGE结构,用来接收选择区域的起始和结束位置,该结构定义如下:
CHARRANGE STRUCTcpMin DWORD ? ;选择区域的起始位置cpMax DWORD ? ;选择区域的结束位置CHARRANGE ENDS
如果cpMin字段等于cpMax字段,表示选择区域的长度为0,而光标位于这个位置;如果cpMin等于0而cpMax等于−1,表示选定的是控件中的所有内容。
程序也可以通过发送对应的消息来设置选择区域:
invoke SendMessage,hRichedit,EM_SETSEL,dwStart,dwEnd
invoke SendMessage,hRichedit,EM_EXSETSEL,0,lpchr
EM_SETSEL的参数中直接用dwStart和dwEnd指定选择区域的开始和结束位置,但是这个消息同样有64 KB长度的限制;EM_EXSETSEL消息没有这个限制,lpchr参数同样指向一个CHARRANGE结构,结构中包含了需要设定的位置。如果仅是移动光标而不选择任何区域,可以将起始位置和结束位置设置为相同的数值。
在程序中设置了选择区域(或改变了光标位置)后,可能这个区域与原来的选择区域位置相差太多,以至于落在了客户区的外面,用户已经看不到它了,如果希望控件能够卷动文字以便将新的位置落在客户区中,可以发送EM_SCROLLCARET消息,这个消息没有任何参数:
invoke SendMessage,hRichedit,EM_SCROLLCARET,0,0
在例子程序中,根据是否存在选择区域来决定是否允许拷贝和剪切功能。因为如果不存在选择区域,就没有文本可供拷贝或剪切,所以在_SetStatus子程序中使用下面的代码首先获取选择区域,并根据情况允许或禁止拷贝和剪切菜单项:
invoke SendMessage,hWinEdit,EM_EXGETSEL,0,addr @stRangemov eax,@stRange.cpMin.if eax == @stRange.cpMax ;不存在选择区域invoke EnableMenuItem,hMenu,IDM_COPY,MF_GRAYEDinvoke EnableMenuItem,hMenu,IDM_CUT,MF_GRAYED.else ;存在选择区域invoke EnableMenuItem,hMenu,IDM_COPY,MF_ENABLEDinvoke EnableMenuItem,hMenu,IDM_CUT,MF_ENABLED.endif
另外,在查找文本的_FindText子程序中,一开始也通过发送EM_EXGETSEL消息获取选择区域,这是为了获得光标位置以便设置查找的起始点,当找到文本以后,文本的位置在FINDTEXTEX结构的chrgText字段中返回,chrgText字段本身是一个CHARRANGE结构,所以直接在EM_EXSETSEL消息中使用它就可以将选择区域设置到找到的文字上:
invoke SendMessage,hWinEdit,EM_EXSETSEL,0,addr @stFindText.chrgText
invoke SendMessage,hWinEdit,EM_SCROLLCARET,NULL,NULL
最后程序发送EM_SCROLLCARET消息卷动文字,以便找到的文本能够出现在用户的视野中。
2.文本管理
文本管理涉及获取文本、设置文本,以及一些辅助操作。
因为Richedit控件本身就是一个窗口,所以可以通过常规的函数对其中的文本进行操作,比如,要获取和设置文本,可以调用GetWindowText或SetWindowText函数,也可以通过发送WM_GETTEXT和WM_SETTEXT消息来完成;如果需要获取控件中的文本长度,可以通过GetWindowTextLength函数或发送WM_GETTEXTLENGTH消息,不过所有这些操作针对的都是控件中的全部文字,无法实现细微的操作。
向控件发送控制消息仅针对选择区域操作则灵活得多,当通过EM_EXSETSEL消息设置好选择区域后,再通过EM_GETSELTEXT消息就可以获取当前选定的文本:
invoke SendMessage,hWinEdit,EM_GETSELTEXT,0,lpBuffer
lpBuffer用来指定接收文本的缓冲区,由于没有参数指定缓冲区的大小,所以程序必须使用足够大的缓冲区,不过这不是问题,因为通过检查选择区域可以预先得知返回文本的大小,消息的返回值是返回到缓冲区中的字符串的长度(不包括末尾的0字符)。
通过发送EM_REPLACESEL消息可以替换选择区域中的文本,如果当前的选择区域长度不为0的话,选择区域的文本被消息指定的字符串所代替,如果选择区域长度为0,则指定的字符串被插入到当前光标位置:
invoke SendMessage,hWinEdit,EM_REPLACESEL,fCanUndo,lpString
其中fCanUndo参数指出本次替换操作是否可以撤销,如果指定TRUE,则控件保存撤销信息以便用户可以按Ctrl+Z键进行撤销,指定FALSE的话操作就不能被撤销。lpString参数指向插入或替换用的字符串,字符串以0结尾。
除了获取和设置文字,还有一系列的控制消息可以用来进行定位操作,比如,想要选定一整行内容,就必须知道某一行的起始位置和结束位置;另外,有时候也需要在字符位置和行号之间进行转换计算,对于这些要求,把所有文本从控件中读出来再自己进行处理显然是很麻烦的,幸好Richedit控件已经提供了这些功能。
通过发送EM_EXLINEFROMCHAR消息可以得知指定的字符位于哪一行中:
invoke SendMessage,hWinEdit,EM_EXLINEFROMCHAR,0,dwCharPos
mov dwLine,eax
其中dwCharPos指出字符的位置(以0开始),消息将返回字符所处的行号。在所有这些消息中,字符位置和行号都是从0开始计算的,也就是说第1行的行号用0表示。
EM_LINEINDEX消息则完成逆运算,它返回指定行号的第一个字符的位置,dwLine参数为输入的行号,如果dwLine参数输入−1的话,代表的是当前行(光标所在的行):
invoke SendMessage,hWinEdit,EM_LINEINDEX,dwLine,0
mov dwCharPos,eax
该消息返回指定行的起始字符的位置,如果指定的行号超过了控件中文本的总行数,那么消息将返回−1。控件中包含文本的总行数可以通过EM_GETLINECOUNT消息获取:
invoke SendMessage,hWinEdit,EM_GETLINECOUNT,0,0
mov dwTotalLines,eax
如果想获取某一行的长度,有好几种方法,比如,可以两次使用EM_LINEINDEX消息获取本行和下一行文字的起始位置,再相减就得出了行的长度;也可以用EM_LINELENGTH直接获取:
invoke SendMessage,hWinEdit,EM_LINELENGTH,ich,0
mov dwLineLength,eax
不过,由于EM_LINELENGTH消息中的ich参数并不是行号,而是行中任意一个字符的位置,所以想以行号为参数获取行长度的话,还需要先用EM_LINEINDEX消息将行号转换到字符位置后再使用EM_LINELENGTH消息。这个消息还有个特殊用途,当ich参数指定为-1的时候,返回值是选定区域跨越的多个行中没有被选定的字符的总数,这有什么用处呢?显然,当按下了Delete键删除了选择区域时,剩下的行的长度就是这个返回值。
要想获取某一行的内容也有多种办法,比如,可以先选定某个行,再用EM_GETSELTEXT消息来完成,但最简单的办法是使用EM_GETLINE消息:
mov word ptr szBuffer,sizeof szBuffer
invoke SendMessage,hWinEdit,EM_GETLINE,dwLine,addr szBuffer
其中dwLine参数指定要获取的行号,szBuffer是用来接收字符串的缓冲区,注意:缓冲区的第一个字(不是双字!)必须预先指定为缓存区的长度!另外,由于接收的字符串并不包括结束符0,所以在发送消息之前最好先把缓冲区全部清零,否则会和缓冲区中原有的数据混在一起。消息的返回值是返回到缓冲区中的字符串的长度。
3.设置文本格式
Richedit控件支持两种模式:带格式文本RTF(Rich Text Format)模式和不带格式文本(Plain Text)模式。在默认状态下控件处于RTF模式,在这种模式下,程序可以对控件中的不同文字分别设置不同的格式,这些格式可以被保存到*.rtf文件中。而在Plain Text模式下,只能将控件中的全部文字设置统一的格式,而且这些格式仅表现在“显示”上,不会被保存到*.txt文件中。
在控件窗口被创建后可以通过发送EM_SETTEXTMODE消息来设置工作模式,这条消息仅对2.0版本以上的Richedit控件有效:
invoke SendMessage,hWinEdit,EM_SETTEXTMODE,dwTextMode,0
当dwTextMode参数指定为TM_PLAINTEXT的时候,控件切换到不带格式模式;指定为TM_RICHTEXT的时候,控件切换到RTF模式。这个消息也可以用来设置重做/撤销的模式,在dwTextMode参数中同时指定TM_SINGLELEVELUNDO标志可以将控件设置为单级重做/撤销模式;指定TM_MULTILEVELUNDO标志则设置为多级重做/撤销模式。
要设置文本格式可以通过发送EM_SETCHARFORMAT消息,这个消息设置控件中一段选定的文本或者全部正文的格式,消息的用法如下:
invoke SendMessage,hWinEdit,EM_SETCHARFORMAT,uFlags,lpFmt
uFlags参数表示指定的格式所应用的范围,它可以是下面的数值。
● SCF_ALL——为控件中的全部文本设置指定的格式。
● SCF_SELECTION——仅为选择区域设置指定的格式,如果选择区域为空,则以后在此位置插入的新字符使用此格式。
● SCF_WORD与SCF_SELECTION——将格式应用到选定的单词上,如果选择区域没有落在整个单词上,那么格式会扩展到整个单词上,SCF_WORD标志必须和SCF_SELECTION标志一起使用。
lpFmt参数则指向一个CHARFORMAT或CHARFORMAT2结构,CHARFORMAT结构可以在所有版本中使用,而CHARFORMAT2结构仅可以在2.0及以上版本使用,CHARFORMAT2结构是CHARFORMAT结构的扩展,下面是CHARFORMAT2结构的定义:
CHARFORMAT2 STRUCTcbSize DWORD ? ;结构长度dwMask DWORD ? ;字段掩码dwEffects DWORD ? ;文字效果yHeight DWORD ? ;文字高度yOffset DWORD ?crTextColor DWORD ? ;文本颜色bCharSet BYTE ?bPitchAndFamily BYTE ?szFaceName BYTE LF_FACESIZE dup(?) ;字体名称
;CHARFORMAT结构的定义到此为止wWeight WORD ?sSpacing WORD ?crBackColor DWORD ?lcid DWORD ?dwReserved DWORD ?sStyle WORD ?wKerning WORD ?bUnderlineType BYTE ?bAnimation BYTE ?bRevAuthor BYTE ?bReserved1 BYTE ?
CHARFORMAT2 ENDS
CHARFORMAT2结构中szFaceName字段以前的内容就是CHARFORMAT结构,结构中各字段的含义如下。
● cbSize——结构的大小,由于控件使用该字段来判断结构的版本是CHARFORMAT还是CHARFORMAT2,所以在将结构传递给控件前必须将这个字段设置为正确的数值。
● dwMask——字段掩码,用来指定结构中哪些字段是有效的,如果没有使用对应的标志,即使某些字段的内容被设置,控件也不会使用它,dwMask中可以使用的标志可以是下面数值的组合:
■ CFM_BOLD——dwEffects字段中CFE_BOLD值是有效的。
■ CFM_CHARSET——bCharSet字段是有效的。
■ CFM_COLOR——crTextColor字段和dwEffects中的CFE_AUTOCOLOR值是有效的。
■ CFM_FACE——szFaceName字段的值是有效的。
■ CFM_ITALIC——dwEffects字段中的CFE_ITALIC值是有效的。
■ CFM_OFFSET——yOffset字段是有效的。
■ CFM_PROTECTED——dwEffects字段中的CFE_PROTECTED值是有效的。
■ CFM_SIZE——yHeight字段是有效的。
■ CFM_STRIKEOUT——dwEffects字段中的CFE_STRIKEOUT值是有效的。
■ CFM_UNDERLINE——dwEffects字段中的CFE_UNDERLINE值是有效的。
● dwEffects——字符效果,可以是以下值的组合:
■ CFE_AUTOCOLOR——使用系统正文颜色。
■ CFE_BOLD,CFE_ITALIC,CFE_STRIKEOUT和CFE_UNDERLINE——粗体字符、斜体字符、带删除线和带下划线。
■ CFE_PROTECTED——字符是受保护的,企图改变字符的话,控件会向父窗口发送一个EN_PROTECTED通知消息。
● yHeight——字符高度,单位是1/1440英寸(或1/20磅),如果这里是180,换算到“字体选择”通用对话框中的尺寸就是9磅(180×1/20=9)。
● yOffset——从基线算起的字符偏移,单位同上,如果该成员是正值,字符显示为上标;如果是负值,字符显示为下标。
● crTextColor——正文颜色,如果在dwEffects字段中指定了CFE_AUTOCOLOR标志,那么这个值会被忽略。
● bCharSet和bPitchAndFamily——字符集。
● szFaceName——用字符串表示的字体名字。
通过填充这个结构并将它通过EM_SETCHARFORMAT消息传送给控件,可以改变文字的效果(粗体、斜体、带删除线、带下划线等),如正文颜色(crTextColor)、字体外观(szFaceName)、字体大小(yHeight),以及使用的字符集等。使用CHARFORMAT2结构可以设置更多的文本风格,如字间距与正文背景色等。如果不需要这些额外的功能,那么只要使用CHARFORMAT结构就可以了。
例子程序Richedit.asm中只演示了使用Plain Text模式为所有文本设置字体的方法:
.const
szFont db '宋体',0
.codeinvoke SendMessage,hWinEdit,EM_SETTEXTMODE,TM_PLAINTEXT,0invoke RtlZeroMemory,addr @stCf,sizeof @stCfmov @stCf.cbSize,sizeof @stCfmov @stCf.yHeight,9 * 20mov @stCf.dwMask,CFM_FACE or CFM_SIZE or CFM_BOLDinvoke lstrcpy,addr @stCf.szFaceName,addr szFontinvoke SendMessage,hWinEdit,EM_SETCHARFORMAT,0,addr @stCf
程序首先将控件的模式设置为Plain Text模式,然后定义了一个名为@stCf的CHARFORMAT结构,将结构长度设置为CHARFORMAT结构的长度,然后在dwMask字段中使用CFM_FACE,CFM_SIZE和CFM_BOLD标志,表示只使用字体、字体大小和字体粗细参数,最后发送EM_SETCHARFORMAT消息将控件中的全部文字设置为大小为9磅(小五号)的“宋体”。
4.装入和保存文本
显然,使用GetWindowText和SetWindowText函数来保存和装入文本是可行的,但是RichEdit控件支持很大的文件,当文件足够大的时候,使用这种方法就很麻烦,因为必须首先要分配一块足够大的内存用做缓冲区,为了解决这个问题,Richedit控件提供了一种新方法,那就是文本流(Text Streaming)。
考虑这样一种情况:为了装入文本,可以申请一块大小合适的缓冲区(当然不会大到能容纳全部文本),每次从文件中读入缓冲区大小的文本并添加到控件中,如此循环直到读入全部文本;保存文本的时候,同样可以每次从控件中取出缓冲区大小的文本,然后写入文件,如此循环直到处理完控件中的全部文本。
文本流的操作方法与此类似,只不过缓冲区和循环都封装在控件的内部,而我们通过提供回调函数的办法提供读写文件的功能模块,Richedit控件循环调用这个模块直到处理完全部的内容。每次调用的时候,控件通过参数告诉回调函数要读写的字节数和缓冲区的地址。文本的流入和流出使用EM_STREAMIN和EM_STREAMOUT消息,这两个消息的使用格式是一样的:
invoke SendMessage,hWinEdit,EM_STREAMIN,uFormat,lpStream
invoke SendMessage,hWinEdit,EM_STREAMOUT,uFormat,lpStream
wParam参数中的uFormat指定需要流入/流出的内容,它可以是以下的取值:
● SF_RTF——文本格式是RTF格式。
● SF_TEXT——文本是Plain Text格式,也就是简单的文本格式。
● SFF_SELECTION——流操作的范围是当前选择区域。如果将文本流入,当前选择区域就会被替换;如果是流出,则只有那些当前选定的文本才流出。如果没有指定这个标志,操作范围是控件中的所有文本。
● SF_UNICODE——指定的是Unicode文本(2.0及以上版本提供)。
lParam参数中的lpStream指向一个EDITSTREAM结构,该结构定义如下:
EDITSTREAM STRUCTdwCookie DWORD ? ;用户自定义值dwError DWORD ? ;用来返回流操作过程中的错误信息pfnCallback DWORD ? ;回调函数地址EDITSTREAM ENDS
结构中各字段的说明如下:
● dwCookie——应用程序自定义的数值,这个数值将会传递给回调函数。
● dwError——指示流操作的结果,0说明没有错误。
● pfnCallback——指向回调函数,该函数由用户定义,并由RichEdit控件调用来传输文本。RichEdit控件将文本分成多个部分,每次调用该函数处理一个部分,直到全部文本被处理为止。
回调函数的定义如下:
EditStreamCallback proc dwCookie,lpBuffer,NumBytes, pBytesTransferred
控件传递给回调函数的参数说明如下:
● dwCookie——就是消息中指定的EDITSTREAM结构中定义的dwCookie值。
● lpBuffer——指向Richedit控件提供的缓冲区。对于流入操作,这里用来接收流入的文本;对于流出操作,这里存放要流出的文本。
● NumBytes——本次调用中可以操作的字节数。对于流入操作,表示可以写入缓冲区的最大字节数;对于流出操作,表示需要流出的文本的长度。
● pBytesTransferred——指向一个双字,由于实际操作的字节数不一定等于NumBytes参数指定的数值(比如,流入时读到了文件尾部),操作后必须在这里返回实际操作的字节数。
回调函数返回0说明操作成功,这样如果还有数据需要读写,RichEdit控件就会继续调用它。如果操作中发生了错误,而且程序需要停止操作的话,可以返回一个非0值,这时Richedit控件就会丢弃lpBuffer指向的数据并停止对回调函数的继续调用。当消息返回的时候,由回调函数返回的错误/成功值会在EDITSTREAM结构的dwError字段中返回,所以可以在SendMessage返回后以此检查流操作是否成功。
在例子文件中,进行流操作如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 回调函数;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>_ProcStream proc uses ebx edi esi_dwCookie,_lpBuffer,_dwBytes,_lpBytes.if _dwCookieinvoke ReadFile,hFile,_lpBuffer,_dwBytes,_lpBytes,0.else.endifinvoke WriteFile,hFile,_lpBuffer,_dwBytes,_lpBytes,0xor eax,eaxret_ProcStream endp...;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 流出操作;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>invoke SetFilePointer,hFile,0,0,FILE_BEGINinvoke SetEndOfFile,hFilemov @stES.dwCookie,FALSE ;@stES是一个EDITSTREAM结构mov @stES.pfnCallback,offset _ProcStreaminvoke SendMessage,hWinEdit,EM_STREAMOUT,SF_TEXT,addr @stES...;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 流入操作;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>mov @stES.dwCookie,TRUEmov @stES.pfnCallback,offset _ProcStreaminvoke SendMessage,hWinEdit,EM_STREAMIN,SF_TEXT,addr @stES
程序中流入和流出操作的回调函数使用同一个子程序,通过设置EDITSTREAM结构的自定义值dwCookie字段来表示进行的是流入还是流出的操作,子程序中通过判断dwCookie参数是TRUE还是FALSE来决定进行读文件还是写文件的操作。
存盘时,程序使用流出操作,这时控件会将所有的文本输出一遍,如果不将原文件中的内容清除,那么输出的文本就会叠加在原始文件后面,这显然不是我们需要的结果,所以程序使用SetFilePointer和SetEndOfFile首先将原文件清空(这两个函数以及读写文件的函数ReadFile和WriteFile的用法请参考第10章的10.2.2小节)。
5.查找和替换
在Richedit控件中可以通过发送EM_FINDTEXT或者EM_FINDTEXTEX消息来完成查找字符串的功能,EM_FINDTEXTEX消息是EM_FINDTEXT消息的扩展,它们的用法是:
invoke SendMessage,hWinEdit,EM_FINDTEXT,uFlags,lpFindText
invoke SendMessage,hWinEdit,EM_FINDTEXTEX,uFlags,lpFindTextEx
消息的wParam参数中的uFlags指定查找的选项,它可以是以下取值的组合:
● FR_DOWN(2.0版本及以上使用)——向后查找,不设置的话表示向前查找。
● FR_MATCHCASE——查找字符串区分大小写。
● FR_WHOLEWORD——匹配整个单词。
EM_FINDTEXT的lParam参数指向一个FINDTEXT结构,而EM_FINDTEXTEX消息的lParam参数指向一个FINDTEXTEX结构,这两个结构的定义如下:
FINDTEXT STRUCTchrg CHARRANGE <> ;查找区域lpstrText DWORD ? ;查找字符串地址
FINDTEXT ENDS
FINDTEXTEX STRUCTchrg CHARRANGE <> ;查找区域lpstrText DWORD ? ;查找字符串地址chrgText CHARRANGE <> ;如果找到则在这里返回找到文字的起始/结束位置
FINDTEXTEX ENDS
可以看出,EM_FINDTEXTEX消息的扩展之处在于直接在chrgText字段中返回找到文字的区域,程序可以马上使用这个区域数据进行其他操作,而EM_FINDTEXT消息必须根据找到的位置和查找字符串的长度自己计算这个区域。
两个消息的返回值是一样的,如果没有找到指定文字则返回−1,否则返回找到文字的起始位置。例子程序在_FindText子程序中完成查找功能,这个子程序分别在“查找下一个”、“查找上一个”和“查找文字”通用对话框的自定义消息中被调用,子程序中通过下面的代码来设置查找区域:
invoke SendMessage,hWinEdit,EM_EXGETSEL,0,addr @stFindText.chrg.if stFind.Flags & FR_DOWNpush @stFindText.chrg.cpMaxpop @stFindText.chrg.cpMin.endifmov @stFindText.chrg.cpMax,-1
当找到一个匹配字符串后,字符串被设置为选择区域,如果向下继续查找下一个的话,316必须将这个选择区域的结束位置用做下一次查找的起始点,所以程序发送EM_EXGETSEL消息获取选择区域并将cpMax字段放到cpMin字段中,并将cpMax字段设置为-1,表示一直查找到全部文本的最后。
在第8章中已经有所介绍:“查找文字”通用对话框使用FINDREPLACE结构,不知道是巧合还是Microsoft的故意安排,FINDREPLACE结构中的Flags字段和EM_FINDTEXTEX消息中的wParam参数的标志定义是一样的,所以程序直接取出Flags字段,然后屏蔽掉FR_MATCHCASE,FR_DOWN和FR_WHOLEWORD等不需要使用的标志以后,就可以直接在消息的wParam参数中使用了:
mov ecx,stFind.Flagsand ecx,FR_MATCHCASE or FR_DOWN or FR_WHOLEWORDinvoke SendMessage,hWinEdit,EM_FINDTEXTEX,ecx,addr @stFindText
找到字符串以后,进行替换操作就不是一件复杂的事情了,因为FINDTEXTEX结构的chrgText字段中已经返回了找到文本的起始和结束位置,将它设置为选择区域以后就可以通过发送EM_REPLACESEL消息进行替换操作了。
9.4.3 Richedit控件的通知消息
Richedit控件也可以向父窗口发送多种通知消息,使用“可以”一词的意思是,控件在默认状态下并不发送通知消息,如果需要控件发送某个消息,必须首先对控件进行设置。通过向Richedit控件发送EM_SETEVENTMASK消息可以激活需要的通知消息:
invoke SendMessage,hWinEdit,EM_SETEVENTMASK,0,dwMask
dwMask是事件掩码,可以是下面标志的组合,分别代表激活不同的通知消息:
● ENM_CHANGE——允许EN_CHANGE通知码,本消息在用户的操作可能改变控件中的文本的时候发送。
● ENM_CORRECTTEXT——允许EN_CORRECTTEXT通知码。
● ENM_DRAGDROPDONE——允许EN_DRAGDROPDONE通知码,本消息在用户完成了一个拖放操作后发送。
● ENM_DROPFILES——允许EN_DROPFILES通知码,本消息在用户将一个文件拖放进控件后发送。
● ENM_KEYEVENTS——允许为键盘消息发送EN_MSGFILTER通知码。
● ENM_MOUSEEVENTS——允许为鼠标消息发送EN_MSGFILTER通知码。
● ENM_PROTECTED——允许发送EN_PROTECTED通知码。
● ENM_SCROLL——允许发送EN_HSCROLL和EN_VSCROLL通知码。
● ENM_SCROLLEVENTS——允许为鼠标滑轮发送EN_MSGFILTER通知码。
● ENM_SELCHANGE——允许发送EN_SELCHANGE通知码,本消息在选择区域改变以后发送(包括光标位置改变)。
● ENM_UPDATE——允许发送EN_UPDATE通知码,本消息在控件将要显示被改变的文本之前发送。
当这些通知消息被激活的时候,父窗口就可以收到包含相应通知码的WM_NOTIFY消息。一般来说,将控件设置为仅发送程序感兴趣的通知消息,如当在Richedit控件中按下右键需要弹出一个菜单的时候,需要检测鼠标消息,那么就需要指定ENM_MOUSEEVENTS标志;另外,如果需要随时检测选择区域的状态,以便随时设置工具栏中“拷贝”与“剪切”等按钮的状态,那么就要使用ENM_SELCHANGE标志,这样光标一移动或者选择区域一改变,父窗口就可以收到EN_SELCHANGE通知码,于是程序就可以在WM_NOTIFY消息中随时改变工具栏上各按钮的状态。
由于Richedit.asm例子中没有使用工具栏,所以没有对通知消息进行示例,有兴趣的读者可以查看所附光盘的Chapter09\Wordpad目录中的例子文件,这个例子中使用工具栏和状态栏等控件,需要随时显示光标位置等信息,程序中就包含了处理通知消息的代码。
9.5 窗口的子类化
9.5.1 什么是窗口的子类化
在使用控件的过程中,常常会遇到这样一种情况:我们需要一种窗口,它与某种现成的控件提供的功能很相似,如果使用现成控件的话,那么控件几乎能提供所有需要的功能,仅我们要求的某个细节无法实现。举例来说,要编写一个十六进制与十进制的转换程序,程序中需要两个编辑控件来输入数值,输入十进制数值可以使用现成的Edit控件,只要指定ES_NUMBER风格就能让编辑框只能输入数字0~9,但输入十六进制数值的时候就不行了,因为指定ES_NUMBER风格的话就无法输入A~F,不指定的话用户就可能输入0~9和A~F之外的字符,那么该如何处理呢?
解决的办法有两种,第一种当然是自己创建一个窗口类,然后在自己的窗口过程中完成所有的功能,这显然是一项费时又费力的工作,因为我们几乎要自己重新写一遍Edit控件的全部功能;第二种方法就是使用本节要介绍的窗口子类化,窗口子类化最适合做的就是这一类工作。
如图9.8所示,窗口子类化的含义是接管被子类化的控件窗口,以达到对它进行控制的目的。虽然控件的窗口过程被封装在Windows内部,无法对它进行直接修改,但只要能截获Windows给控件的窗口过程发送的消息,就能够控制控件窗口。以上面的要求为例,只要截获Windows向编辑控件发送的WM_CHAR消息,就能够根据需要丢弃包含非十六进制字符的WM_CHAR消息,只把包含十六进制字符的WM_CHAR转发给控件的窗口过程,这样编辑控件将根本收不到十六进制字符之外的字符,我们的要求也就达到了。
图9.8 窗口子类化的工作原理子类化的操作并不局限于控件窗口,实际上任何窗口都可以子类化。但是对于应用程序自身使用的窗口类来说,它的控制权本来就是100%属于应用程序自身的,要实现某种功能就直接修改源代码好了,没有必要再进行一个子类化的过程,所以子类化的操作往往是对“黑匣子”类型的控件窗口进行的。
9.5.2 窗口子类化的实现
窗口子类化的要点是截获窗口的窗口过程,如何实现这一点呢?每个窗口的内部都保存有它所属的窗口类的WNDCLASSEX结构,结构中的lpfnWndProc字段指出了窗口过程的地址,如果能用自己的窗口过程地址来替换这个地址,那么Windows就会把消息发送到自定义的窗口过程中来了。通过调用函数SetWindowLong可以实现这个功能,SetWindowLong函数的用法是这样的:
invoke SetWindowLong,hWnd,nIndex,dwNewLong
mov dwOldLong,eax
hWnd参数指定要子类化窗口的窗口句柄,nIndex参数指定需要修改窗口的哪个属性,它可以是以下的取值:
● GWL_EXSTYLE——窗口的扩展风格。
● GWL_STYLE——窗口风格。
● GWL_WNDPROC——窗口过程地址(这就是我们需要的)。
● GWL_HINSTANCE——窗口所属的模块实例句柄。
● GWL_ID——窗口ID。
● GWL_USERDATA——窗口附带的32位自定义数值。
dwNewLong参数指定新的属性值。如果nIndex为GWL_WNDPROC,dwNewLong表示新的窗口过程地址;如果nIndex为GWL_STYLE,dwNewLong则表示新的窗口风格,依此类推。函数的返回值是指定属性的原先数值。当函数用于窗口子类化的时候,在nIndex参数中使用GWL_WNDPROC,以便将窗口过程地址设置到自定义的子程序中,这时函数返回的是控件窗口原来的窗口过程地址,由于窗口子类化的出发点就是为了尽量使用控件窗口原有的功能,程序为了“偷懒”而不去处理的大部分消息还要靠原来的窗口过程来处理,所以这个地址必须被保存下来。
让我们通过一个简单的例子来演示窗口子类化的实现过程,程序实现的就是前面介绍的十六进制与十进制转换的程序,源代码位于所附光盘的Chapter09\SubClass目录中。程序首先在资源脚本文件SubClass.rc中定义了一个对话框,对话框中包括两个编辑控件,IDC_DEC用来输入十进制数值,它包含ES_NUMBER风格,只能输入0~9的数值;而IDC_HEX用来输入十六进制数值,代码如下:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#include <resource.h>//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#define ICO_MAIN 1000#define DLG_MAIN 1000#define IDC_HEX 1001#define IDC_DEC 1002//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN ICON "Main.ico"//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>DLG_MAIN DIALOG 107, 102, 129, 42STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "Hex <> Dec"FONT 9, "宋体"{LTEXT "Hex", -1, 7, 9, 15, 8EDITTEXT IDC_HEX, 27, 7, 94, 12LTEXT "Dec", -1, 7, 26, 15, 8EDITTEXT IDC_DEC, 27, 24, 94, 12, ES_NUMBER}
汇编源程序SubClass.asm的内容如下:
; SubClass.asm
; 窗口子类化例子 —— 将一个编辑框子类化为只接收16进展字符
;----------------------------------------------------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff SubClass.asm
; rc SubClass.rc
; Link /subsystem:windows SubClass.obj SubClass.res
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib ;equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_HEX equ 1001
IDC_DEC equ 1002 ;数据段
.data?
hInstance dword ?
hWinMain dword ?
dwOption dword ?
lpOldProcEdit dword ?
.const
szFmtDecToHex byte '%08X',0
szFmtHexToDec byte '%u',0
szAllowedChar byte '0123456789ABCDEFabcdef',08h;代码段
.code
;IDC_HEX编辑框的新窗口过程
_ProcEdit proc uses ebx edi esi, hWnd, uMsg, wParam, lParam mov eax, uMsg .if uMsg == WM_CHAR mov eax, wParam mov edi, offset szAllowedCharmov ecx, sizeof szAllowedChar repnz scasb .if ZERO?.if al > '9'and al, not 20h .endif invoke CallWindowProc, lpOldProcEdit, hWnd, uMsg, eax, lParam ret .endif .else invoke CallWindowProc, lpOldProcEdit, hWnd, uMsg, wParam, lParam ret .endif xor eax, eax ret
_ProcEdit endp ;计算16进制到10进制
_HexToDec proc local @szBuffer[512]:byte invoke GetDlgItemText, hWinMain, IDC_HEX, addr @szBuffer, sizeof @szBuffer lea esi, @szBuffer cld xor eax, eax mov ebx, 16 .while TRUE movzx ecx, byte ptr[esi]inc esi .break .if !ecx .if cl > '9'sub cl, 'A' - 0ah .else sub cl, '0'.endif mul ebx add eax, ecx .endw invoke wsprintf, addr @szBuffer, addr szFmtHexToDec, eax invoke SetDlgItemText, hWinMain, IDC_DEC, addr @szBuffer ret
_HexToDec endp ;计算10进制到16进制
_DecToHex proc local @szBuffer[512]:byte invoke GetDlgItemInt, hWinMain, IDC_DEC, NULL, FALSE invoke wsprintf, addr @szBuffer, addr szFmtDecToHex, eax invoke SetDlgItemText, hWinMain, IDC_HEX, addr @szBuffer ret
_DecToHex endp
;-------------------------------------------------------------------
_ProcDlgMain proc uses ebx edi esi, hWnd, wMsg, wParam, lParam mov eax, wMsg .if eax == WM_CLOSE invoke EndDialog, hWnd, NULL .elseif eax == WM_INITDIALOG mov eax, hWnd mov hWinMain, eax invoke SendDlgItemMessage, hWnd, IDC_HEX, EM_LIMITTEXT, 8, 0invoke SendDlgItemMessage, hWnd, IDC_DEC, EM_LIMITTEXT, 10, 0invoke GetDlgItem, hWnd, IDC_HEXinvoke SetWindowLong, eax, GWL_WNDPROC, addr _ProcEdit mov lpOldProcEdit, eax .elseif eax == WM_COMMAND mov eax, wParam .if !dwOption mov dwOption, TRUE .if ax == IDC_HEX invoke _HexToDec .elseif ax == IDC_DEC invoke _DecToHex .endif mov dwOption, FALSE .endif .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp
;main函数
main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
程序运行后将显示如图9.9所示的对话框,上面的编辑控件是经过子类化的,只能输入十六进制字符,当输入字母a~f的时候,不管输入的是大写还是小写字母,都会被控件转换成大写字母。下面的编辑框是ES_NUMBER风格的,可以输入数字。不管在哪个编辑框中输入数值,程序会马上进行转换并将结果在另一个编辑框中显示出来。
图9.9 窗口子类化例子的运行界面
程序在对话框的初始化消息WM_INITDIALOG中对这两个编辑控件发送EM_LIMITTEXT消息,以此限制能够输入的最大字符长度,接下来程序通过GetDlgItem获取IDC_HEX编辑框的窗口句柄,并使用SetWindowLong函数将编辑框的新窗口过程设置到_ProcEdit子程序中,返回的原窗口过程地址被保存到lpOldProcEdit变量中:
invoke GetDlgItem,hWnd,IDC_HEX
invoke SetWindowLong,eax,GWL_WNDPROC,addr _ProcEdit
mov lpOldProcEdit,eax
这样,当Windows向IDC_HEX编辑框发送消息时,_ProcEdit子程序就会收到消息,在_ProcEdit中我们仅处理感兴趣的WM_CHAR消息,程序在数据段中定义了一个允许输入的字符表,表中包括0~9、大小写的A~F,以及退格键(如果不允许输入退格键的话将无法修正输入错误)。然后在处理WM_CHAR消息时使用scasb指令进行查表,如果字符在表中,则将WM_CHAR消息通过CallWindowProc函数转发给原来的窗口过程,如果不在表中则直接返回,相当于丢弃了这个按键动作。代码如下:
.const
szAllowedChar db '0123456789ABCDEFabcdef',08h
.code...mov eax,wParam;WM_CHAR消息中的一段代码mov edi,offset szAllowedCharmov ecx,sizeof szAllowedCharrepnz scasb.if ZERO?.if al > '9'and al,not 20h.endifinvoke CallWindowProc,lpOldProcEdit,hWnd,uMsg,eax,lParamret.endif
在转发之前,程序还对字符进行判断,如果字符是小写的a~f的话(表中的字符中大于“9”的肯定是字母),则通过and al,not 20h语句将字母转换成大写,因为大写字母的ASCII码从41h开始,小写字母从61h开始,这样的计算方法对大写字母没有影响,对小写的则刚好能够转换成大写的。程序将其他所有的消息原封不动地转发给原来的窗口过程,这样才能让编辑控件的窗口过程为我们完成控件的其他功能。
转发消息使用了CallWindowProc函数,这个函数仅起到将参数入栈和调用指定地址的作用,对于下面的语句我们完全可以用自己调用lpOldProcEdit的方法来代替它:
invoke CallWindowProc,lpOldProcEdit,hWnd,uMsg,eax,lParam
下面的代码就可以完成同样的功能:
push lParam
push eax
push uMsg
push hWnd
call lpOldProcEdit
程序中的其他代码应该算是相当简单的,_DecToHex子程序是十进制到十六进制的转换子程序,子程序中用GetDlgItemInt函数读入编辑框中的十进制数值,并用wsprintf转换成十六进制数值的字符串并显示到IDC_HEX编辑框中;_HexToDec是十六进制到十进制的转换子程序,由于并没有现成的转换函数,所以在子程序中顺序读入字符并每次通过乘以16来进行计算。
对控件窗口进行子类化,影响的只是被操作的窗口,并不会影响基于这种控件的其他窗口,因为SetWindowLong函数操作的对象只是单个窗口而不是窗口类,所以要对多个控件窗口进行同样的子类化就必须对每个窗口都进行子类化操作。
程序中还有一个技巧。由于使用SetDlgItemText设置编辑框文本的时候,编辑框会发送WM_COMMAND消息,又由于一收到某个WM_COMMAND消息就进行转换计算,并再次使用SetDlgItemText函数将计算结果显示在另一个编辑框中,这样就会进入发送WM_COMMAND消息的死循环中。为此程序中定义了一个dwOption变量,当正在处理某个WM_COMMAND消息的时候,将这个变量设置为1来防止重入,这样就能够防止死循环的发生,代码如下:
.elseif eax == WM_COMMANDmov eax,wParam.if ! dwOptionmov dwOption,TRUE.if ax == IDC_HEXinvoke _HexToDec.elseif ax == IDC_DECinvoke _DecToHex.endifmov dwOption,FALSE.endif
9.6 控件的超类化
9.6.1 什么是控件的超类化
子类化是对窗口功能的调整和扩展,那么超类化是什么呢?超类化是对类的调整和扩展,在C++中,可以通过继承和扩展某个基类来形成一个派生的类,超类化可以完成的功能与这相似。
超类化主要用在什么地方呢?举例来说,如果需要一个只能输入十六进制字符的编辑框,那么可以通过对编辑框窗口子类化来实现,9.5节的例子就是如此,但是当应用程序需要大量使用这种十六进制编辑框时,该如何处理呢?方法有3种:
● 创建自己的类,自己书写所有的功能代码。
● 创建多个Edit控件,并把它们全部子类化。
● 超类化Edit控件,用Edit控件当做基类派生出一个新的类,并用这个类来建立多个“新Edit”控件窗口。
第一种方法在9.5节中就被“枪毙”了,几乎没有人去干这种吃力不讨好的事情;第二种方法要好一点,但子类化一大堆的控件也是一件令人头痛的事情;这时就应该使用超类化Edit类方法,当从Edit类派生出一个新的“十六进制编辑类”后,接下来直接使用这个类就能够创建出一大堆的十六进制编辑框。
9.6.2 控件超类化的实现
各种自定义的窗口和不同的控件窗口之所以看上去千姿百态,功能也各不相同是因为两个原因:首先用来表示类属性的WNDCLASSEX结构定义不同,造成窗口的风格与形状等各不相同;其次,不同窗口类使用的窗口过程不同,这些不同的窗口过程对各种消息的处理方法各不相同,造成窗口的功能不同。
设想这样一种情况:如果自定义一个类,这个类的WNDCLASSEX结构中定义的风格、形状、光标与图标等所有属性都和Edit类相同,然后在自己的类中将窗口过程地址指向Edit类的窗口过程(或者原样拷贝Edit类的窗口过程代码),这个类会实现什么样的功能呢?答案是:除了类的名称不同之外,用这个类创建的窗口的形状和所有功能将和Edit框一模一样!这时候,就相当于从Edit类派生出了一个相同的类来。
这样一来就不难设想从基类派生出新类的方法,我们可以获取基类的WNDCLASSEX结构,然后保持结构中的大部分字段不变,仅修改个别需要自定义的属性,那么新类的窗口风格就和基类类似,如果还需要扩充某些功能的话,可以将结构中的窗口过程地址指到自己的子程序中,这样就可以扩展基类的功能。最后,使用这个修改后的结构以自定义的名称注册一个类,一个新的类就派生出来了。
对基类进行超类化的时候,如果修改的仅是窗口风格,那么使用派生类建立的窗口和基类窗口会实现同样的功能,但外观上会有所不同;如果仿照子类化窗口的方法修改窗口过程,那么使用派生类建立的窗口和基类窗口的外形是相同的,但是功能上会有所不同。
使用GetClassInfoEx函数可以获取现存的类的属性,对基类进行超类化的第一步就是使用这个函数获取基类的WNDCLASSEX结构。GetClassInfoEx函数的使用方法是:
invoke GetClassInfoEx,hinst,lpszClass,lpwcx
hinst参数是创建这个类的应用程序的实例句柄,如果要获取某个Windows预定义类的属性,那么这个参数使用NULL。
lpszClass参数指向一个字符串,用来定义类的名称。lpwcx参数指向一个WNDCLASSEX结构,用来返回指定类的属性。在调用函数之前,结构中的cbSize必须正确地设置为WNDCLASSEX结构的长度,否则函数的执行可能失败。
获取WNDCLASSEX结构以后,可以根据需要修改结构的内容。如果需要派生出一个功能不同的新类,可以将窗口过程地址设置到自己的程序中,当然原来的地址应该被保存下来,以便收到不感兴趣的消息时转发到原来的窗口过程中。除了修改需要自定义的属性外,还有两个字段是必须修改的:hInstance字段必须设置为应用程序的实例句柄;lpszClassName必须指向新的派生类的类名。完成了这些修改后,使用经过修改的WNDCLASSEX注册一个新的类就大功告成了。
让我们通过一个简单的例子来演示超类化的过程,例子中建立了一个对话框,并在对话框中定义了多个从Edit类派生出来的十六进制编辑类,这些类实现的功能和9.5节中的例子是一样的,全部的源代码在所附光盘的Chapter09\SuperClass目录中,SuperClass.rc文件的定义如下:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#include <resource.h>//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#define ICO_MAIN 1000#define DLG_MAIN 1000//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN ICON "Main.ico"//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>DLG_MAIN DIALOG 107, 102, 126, 82STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "SuperClass"FONT 9, "宋体"{CONTROL "",-1,"HexEdit",ES_LEFT | WS_BORDER | WS_TABSTOP,5,5,115,12CONTROL "",-1,"HexEdit",ES_LEFT | WS_BORDER | WS_TABSTOP,5,20,115,12CONTROL "",-1,"HexEdit",ES_LEFT | WS_BORDER | WS_TABSTOP,5,35,115,12CONTROL "",-1,"HexEdit",ES_LEFT | WS_BORDER | WS_TABSTOP,5,50,115,12CONTROL "",-1,"HexEdit",ES_LEFT | WS_BORDER | WS_TABSTOP,5,65,115,12}//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
读者可以看到,对话框中定义了多个HexEdit类,但是系统中并没有预定义这种名称的类,这就是将要从Edit类中派生的类。SuperClass.asm文件的内容如下:
;SuperClass.asm
;超类化例子 —— 从编辑控件派生一个只接收16进展字符的新控件
;--------------------------------------------------------------------
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff SuperClass.asm
;rc SuperClass.rc
;Link /subsystem:windows SuperClass.obj SuperClass.res
.386
.model flat,stdcall
option casemap:none ;include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib ;equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 1000;数据段
.data?
hInstance dword ?
hWinMain dword ?
lpOldProcEdit dword ?
.const
szAllowedChar byte '0123456789ABCDEFabcdef',08h
szEditClass byte 'Edit',0
szClass byte 'HexEdit',0;代码段
.code
;HexEdit控件的新窗口过程
_ProcEdit proc uses ebx edi esi hWnd, uMsg, wParam, lParam mov eax, uMsg.if uMsg == WM_CHAR mov eax, wParam mov edi, offset szAllowedChar mov ecx, sizeof szAllowedChar repnz scasb .if ZERO?.if al > '9'and al, not 20h .endif invoke CallWindowProc, lpOldProcEdit, hWnd, uMsg, eax, lParam ret .endif .else invoke CallWindowProc, lpOldProcEdit, hWnd, uMsg, wParam, lParam ret .endif xor eax, eax ret
_ProcEdit endp ;基于Edit类建立一个新的类:HexEdit
_SuperClass proc local @stWC:WNDCLASSEX mov @stWC.cbSize, sizeof @stWC invoke GetClassInfoEx, NULL, addr szEditClass, addr @stWC push @stWC.lpfnWndProc pop lpOldProcEdit mov @stWC.lpfnWndProc, offset _ProcEdit push hInstance pop @stWC.hInstance mov @stWC.lpszClassName, offset szClass invoke RegisterClassEx, addr @stWC ret
_SuperClass endp _ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam mov eax, wMsg .if eax == WM_CLOSE invoke EndDialog, hWnd, NULL .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp ;main函数
main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke _SuperClassinvoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, NULL
main endp
end main
由于在对话框初始化的时候,对话框管理器就要根据对话框资源的内容创建每个子窗口控件,所以在调用DialogBoxParam函数显示对话框之前,“HexEdit”类就必须存在,否则初始化工作会失败。因此,程序在DialogBoxParam函数之前调用_SuperClass子程序进行超类化的工作。
在_SuperClass子程序中,程序通过GetClassInfoEx函数获取Edit控件的类结构,然后将结构中的窗口过程指到自己的_ProcEdit子程序中,接下来将hInstance字段设置为自己的实例句柄,并将类的名称改为“HexEdit”,最后用RegisterClassEx函数注册一个新的“HexEdit”类:
.const
szEditClass db 'Edit',0
szClass db 'HexEdit',0
.code...
mov @stWC.cbSize,sizeof @stWC;@stWC是一个WNDCLASSEX结构
invoke GetClassInfoEx,NULL,addr szEditClass,addr @stWC
push @stWC.lpfnWndProc
pop lpOldProcEdit
mov @stWC.lpfnWndProc,offset _ProcEdit
push hInstance
pop @stWC.hInstance
mov @stWC.lpszClassName,offset szClass
invoke RegisterClassEx,addr @stWC
新的窗口过程和子类化窗口中的例子是一样的,在这里就不进行分析了,程序的结果就是:用HexEdit类建立的所有窗口的功能与子类化窗口例子中IDC_HEX编辑框的功能是一样的。
本程序演示的是派生类在对话框中的使用情况,在这里基于派生类创建的窗口是由对话框管理器自动调用CreateWindowEx函数创建的,如果将派生类使用在普通窗口中的话,可以通过指定派生类的名称,自己使用CreateWindowEx函数来创建。