📄 附录a-c.txt
字号:
时候,系统将设置这个子程序为Ctrl+C 处理程序,如果Add 参数指定为FALSE ,系统将取
消这个设置。
HandlerRoutine 参数也可以为NULL,这时当Add 参数设置为TRUE 时,系统将忽略对
Ctrl+C 的处理,设置为FALSE 的话系统将恢复原来的处理方式。
例子程序中使用SetConsoleCtrlHandler 函数将处理子程序设置到_CtrlHandler 中。
Ctrl+C 的处理子程序必须按照规定的格式定义:
HandlerRoutine proc dwCtrlType
该子程序有一个输入参数dwCtrlType ,系统调用该子程序的时候将使用这个参数指明发
生事件的类型,事件类型可能是以下几种。
● CTRL_C_EVENT——收到Ctrl+C 字符。
● CTRL_BREAK_EVENT——收到Ctrl+Break 字符。
● CTRL_CLOSE_EVENT——用户关闭了控制台窗口(比如按下了控制台窗口上面的关
闭按钮或在控制台窗口的菜单上选择了“关闭”等)。
● CTRL_LOGOFF_EVENT——当前用户注销。
● CTRL_SHUTDOWN_EVENT——系统准备关闭。
在例子代码中,_CtrlHandler 子程序仅处理CTRL_BREAK_EVENT 事件以及
CTRL_C_EVENT 事件,并在检测到这两个事件的时候用CloseHandle 函数将输入句柄关闭,
这样在后面的循环中ReadConsole 函数就会出错返回,程序即可退出循环。
3. 控制台窗口的输入和输出
从控制台窗口接收键盘输入可以用ReadConsole 或ReadFile 两种函数来完成(在第10
章中有对ReadFile 函数的详细介绍),ReadConsole 函数的使用方法如下:
invoke ReadConsole,hConsoleInput,lpBuffer,\
nNumberOfCharsToRead,lpNumberOfCharsRead,lpReserved
hConsoleInput 参数为控制台的标准输入句柄,lpBuffer 指向用来接收输入数据的缓冲区,
nNumberOfCharsToRead 参数指定要读取的数据长度,lpNumberOfCharsRead 指向一个双字,
函数在这里返回实际读取的字节数。如果函数读取输入成功,则返回非零值,如果读取失败,
则返回零。
ReadConsole 和ReadFile 函数的区别在于能否支持重定向,ReadFile 函数允许使用“<”
符号将一个文本文件的内容作为输入重定向到输入句柄中,而ReadConsole 函数不支持重定
向。
两个函数读取输入字符的方式取决于SetConsoleMode 函数对输入句柄工作模式的设置,
当工作模式指定为行读取模式的时候,只有用户输入的字符数等于nNumberOfCharsToRead
参数指定的数量时,或者用户输入回车后函数才返回;当工作模式设置为非行读取模式时,
即使nNumberOfCharsToRead 参数指定的值很大,只要有任何字符输入,函数即返回,函数
实际读取的字符数量放在lpNumberOfCharsRead 参数指向的双字中。
如果需要向控制台窗口输出文本,同样可以使用WriteConsole 函数或者WriteFile 函数(在
第10 章中有WriteFile 函数的详细介绍),WriteConsole 函数的用法如下:
invoke WriteConsole,hConsoleOutput,lpBuffer,\
nNumberOfCharsToWrite,lpNumberOfCharsWritten,lpReserved
参数hConsoleOutput 指定为前面获取的标准输出句柄,lpBuffer 参数指向输出的内容,
nNumberOfCharsToWrite 参数为要输出的数据长度,lpNumberOfCharsWritten 指向一个双字变
量,用来返回实际输出的数据长度。
同样,两种方法的区别在于WriteConsole 函数不支持输出内容的重定向,而WriteFile 函
数支持重定向,这样使用命令行方式的管道操作符可以将内容重定向到一个文件中,如下面
的命令将Ping 的结果存放到result.txt 文件中:
ping www.yahoo.com > result.txt
如果程序希望某些内容可以重定向,而某些内容不允许重定向,那么可以混合使用这两
个函数来输出文本。
例子程序中循环使用ReadConsole 函数来读入用户的输入,并用WriteConsole 函数来将
输入的字符串回显在控制台窗口中。当用户输入Ctrl+C 组合键的时候,Ctrl+C 的处理子程
序中将输入句柄关闭,这样ReadConsole 会返回失败,循环退出。
4. 设置控制台窗口文本的颜色
例子程序在用户输入的时候显示的字符颜色是白色的,但是在将同样的内容输出时,字
符颜色却是加亮的蓝色,这是因为在调用ReadConsole 和WriteConsole 函数前分别用
SetConsoleTextAttribute 函数设置了不同的文本颜色。
SetConsoleTextAttribute 函数用于设置控制台窗口中将要显示的文本颜色,一旦设置完毕
后,以后显示的字符将全部使用新的颜色,但是最初显示的字符颜色不受影响,函数的用法
如下:
invoke SetConsoleTextAttribute,hConsoleHandle,dwColor
hConsoleHandle 参数是需要设置的输出句柄,dwColor 参数指定颜色值,颜色值可以是
下列取值的组合。
● FOREGROUND_BLUE,FOREGROUND_GREEN,FOREGROUND_RED ——分别表
示字符颜色为蓝色、绿色和红色。
● FOREGROUND_INTENSITY ——字符颜色加亮。
● BACKGROUND_BLUE,BACKGROUND_GREEN,BACKGROUND_RED ——分别
表示字符的背景色为蓝色、绿色和红色。
● BACKGROUND_INTENSITY ——字符背景颜色加亮。
如果需要其他颜色,可以将这些颜色值按照三原色相加的方式组合起来,比如设置字符
颜色为加亮的白色时,因为白色是由红绿蓝三色组合而成的,所以dwColor 参数可以指定为
FOREGROUND_INTENSITY or FOREGROUND_RED or FOREGROUND_GREEN or
FOREGROUND_BLUE ;需要红色加亮的字符,但是背景是黄色的时候,可以指定为
FOREGROUND_RED or FOREGROUND_INTENSITY or BACKGROUND_GREEN or
BACKGROUND_RED ,这是因为背景的黄色是由红色和绿色组合而成的。
将控制台程序和普通的窗口程序对比就可以发现,由于不必处理复杂的消息机制,控制
台程序的流程可以使用和DOS 程序类似的架构,也就是说程序可以按顺序化的方式来写,
而不是以消息驱动的方式来写。这种架构可以让程序的编写更加简单。
附录B
窗口消息实验
在本节中,将通过不同的实验来了解常见的窗口消息,并进一步理解窗口的工作机制。
我们将构造一个程序,在程序中将收到的窗口消息查表翻译成文本以“WM_XXX”格式显
示出来,同时将和窗口相关的API 函数的调用也显示出来,这样可以分析窗口的各种行为和
消息之间的关系。
实验用到的源代码请参考附书光盘的Appendix B 目录。
B.1 MsgWindow 程序
为了把窗口消息翻译成文本信息显示出来,我们可以选择在窗口的客户区中显示文本,
但这样会引入新的消息,干扰实验,所以,这里选择了一种新的方法,就是先打开Windows
附件中自带的Notepad 记事本程序,然后在程序中将要显示的内容通过SendMessage 发送到
记事本中,这样可以通过查看记事本中的内容来了解MsgWindow 的运行情况。
程序以Chapter04\FirstWindow 例子为模板,在此基础上增加了一些功能。增加的内容共
有3 个部分。
第一部分是将消息查表转换为字符串,首先在 .const 段中增加两个表:十六进制的消息
编号列表dwMsgTable 和字符串列表szStringTable ,两表中的项目一一对应,代码如下:
.const
dwMsgTable dd WM_NULL
dd WM_CREATE
dd WM_DESTROY
dd WM_MOVE
.
dd WM_EXITSIZEMOVE
MSG_TABLE_LEN equ ($ - dwMsgTable)/sizeof dword
MSG_STRING_LEN equ sizeof szStringTable
szStringTable db 'WM_NULL ',0
db'WM_CREATE ',0
db 'WM_DESTROY ',0
db 'WM_MOVE ',0
.
db 'WM_EXITSIZEMOVE ',0
szFormat db 'WndProc: [%04x]%s %08x %08x',0dh,0
MSG_TABLE_LEN 定义了表的项数,MSG_STRING_LEN 定义了字符串表中每一项的
长度。sizeof 操作符取的是szStringTable 这一行中的数据长度,而非包括下面全部的字符串
行。为了简化处理,全部字符串的长度保持相等。由于篇幅所限,这里没有列出全部的消息
列表,完整的源代码可以在本书附带光盘的Appendix B\MsgWindow01 目录中找到。
程序在窗口过程的入口处调用_ShowMessage 子程序来翻译消息并传给记事本:
_ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam
invoke _ShowMessage,uMsg,wParam,lParam
mov eax,uMsg
.if eax == WM_XXX
.
_ShowMessage 子程序用来将消息查表翻译成字符串,源程序如下:
_ShowMessage proc _uMsg,_wParam,_lParam
local @szBuffer[128]:byte
pushad
;********************************************************************
; 查找消息的说明字符串
;********************************************************************
mov eax,_uMsg
mov edi,offset dwMsgTable
mov ecx,MSG_TABLE_LEN
cld
repnz scasd
.if ZERO?
sub edi,offset dwMsgTable + sizeof dword
shr edi,2
mov eax,edi
mov ecx,MSG_STRING_LEN
mul ecx
add eax,offset szStringTable
;********************************************************************
; 翻译格式并发送到 Notepad 窗口
;********************************************************************
invoke wsprintf,addr@szBuffer,addr szFormat,\
_uMsg,eax,_wParam,_lParam
invoke _SendtoNotepad,addr @szBuffer
.endif
popad
ret
_ShowMessage endp
在这里要用到repnz scasd 指令,scasd 指令是把eax 中的值从[edi]开始的内存中按双字比
较,同时将edi 加4,如果相等,则ZR 标志置位,否则为NZ,repnz 表示如果标志为NZ,
则以ecx 为重复次数重复搜索,直到相等或ecx 为零为止。
将ecx 赋值为消息表的项数MSG_TABLE_LEN ,将edi 赋值为消息表的开始地址offset
dwMsgTable ,然后开始查找,停止后可以查看标志Zero 位,如果是非ZERO,表示查完全部
都没有找到,如果是ZERO ,则表示找到表项。
当标志为ZERO 时,edi 指向找到项目的后一项,将edi 减去一项的长度(sizeof dword)
以及表的基址,再除以表项的长度(sizeof dword 等于4,除以4 等于右移两位,所以程序中
用shr edi,2),就是消息在表中的索引了,接下来算出消息字符串的位置,位置等于:索引×
字符串长+字符串表基址,代码如下:
mov ecx,MSG_STRING_LEN
mul ecx
add eax,offset szStringTable
这样,eax 中就是字符串的地址了。最后将消息编号、名称和参数用wsprintf 函数格式化
成可以发送的字符串存放到@szBuffer 中,并用_SendtoNotepad 子程序将@szBuffer 中的内容
发送到记事本中。
程序增加的第二部分就是下面这个_SendtoNotepad 子程序:
szDestClass db 'Notepad',0
_SendtoNotepad proc _lpsz
local @hWinNotepad
pushad
invoke FindWindow,addr szDestClass,NULL
.if eax
mov ecx,eax
invoke ChildWindowFromPoint,ecx,20,20
.endif
.if eax
mov @hWinNotepad,eax
mov esi,_lpsz
@@:
lodsb
or al,al
jz @F
movzx eax,al
invoke PostMessage,@hWinNotepad,WM_CHAR,eax,1
jmp @B
@@:
.endif
popad
ret
_SendtoNotepad endp
该子程序中首先用FindWindow 函数来查找记事本程序是否已经运行,记事本程序的窗
口类名称为“Notepad”,FindWindow 可以用窗口类当做第一个参数来查找,如果找到,返回
的是记事本程序的主窗口句柄,否则返回0。
要发送的是模拟键盘按键的消息WM_CHAR,这样就好像在记事本中人工键入字符,但
直接向记事本主窗口发送WM_CHAR 消息是不行的,要向记事本窗口客户区中的编辑子窗
口发送消息才行,所以程序中又用从位置获取子窗口句柄的函数ChildWindowFromPoint 来获
得编辑子窗口的句柄。
锁定了最后的目标即记事本中的编辑子窗口后,程序用PostMessage 向它发送消息,根
据字符串的长度,用一个循环每次发送一个WM_CHAR 消息,WM_CHAR 消息的wParam
和lParam 的含义如下:
wParam = chCharCode // wParam 是键值
lParam = lKeyData // lParam 是键数据(重复次数)
程序中用mov eax,al 将键值扩展到参数所需的32 位,当做wParam 参数发送,lParam 为
1,表示键的重复次数为1 次,这样一来,记事本中就源源不断地显示出MsgWindow 程序的
运行轨迹了。
MsgWindow 程序增加的第三部分是在每个函数的前后增加了显示状态的语句,它们只是
简单地把一个字符串发送到记事本中。
;定义一些字符串
szCreateWindow1 db 'Creating Window...',0dh,0
szCreateWindow2 db 'CreateWindow end',0dh,0
szShowWindow1 db 'Showing Window...',0dh,0
szShowWindow2 db 'ShowWindow end',0dh,0
szUpdateWindow1 db 'Updating Window...',0dh,0
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -