📄 附录a-c.txt
字号:
szUpdateWindow2 db 'UpdateWindow end',0dh,0
szGetMsg1 db 'Getting Message...',0dh,0
szGetMsg2 db '[%04x]Message gotten',0dh,0
szDispatchMsg1 db 'Dispatching Message...',0dh,0
szDispatchMsg2 db 'DispatchMessage end',0dh,0
.
invoke _SendtoNotepad,addr szCreateWindow1
invoke CreateWindowEx,.
invoke _SendtoNotepad,addr szCreateWindow2
invoke _SendtoNotepad,addr szShowWindow1
invoke ShowWindow,hWinMain,SW_SHOWNORMAL
invoke _SendtoNotepad,addr szShowWindow2
invoke _SendtoNotepad,addr szUpdateWindow1
invoke UpdateWindow,hWinMain
invoke _SendtoNotepad,addr szUpdateWindow2
.
上面代码中的粗体部分就是相对于FirstWindow 程序增加的内容,好了,现在DOS 控制
台上键入nmake 将MsgWindow 程序编译出来,然后打开记事本,再运行MsgWindow.exe,
如果记事本上出现一大堆的东西,就说明实验可以开始了!
B.2 开始实验
实验1. 验证收到消息的顺序
打开记事本,然后运行MsgWindow 程序,记事本上出现的内容如下:
Creating Window...
WndProc: [0024]WM_GETMINMAXINFO 00000000 0012fda4
WndProc: [0081]WM_NCCREATE 00000000 0012fd8c
WndProc: [0083]WM_NCCALCSIZE 00000000 0012fdc4
WndProc: [0001]WM_CREATE 00000000 0012fd68
CreateWindow end
Showing Window...
WndProc: [0018]WM_SHOWWINDOW 00000001 00000000
WndProc: [0046]WM_WINDOWPOSCHANGING 00000000 0012fec0
WndProc: [0046]WM_WINDOWPOSCHANGING 00000000 0012fec0
WndProc: [001c]WM_ACTIVATEAPP 00000001 00000450
WndProc: [0086]WM_NCACTIVATE 00000001 00000000
WndProc: [000d]WM_GETTEXT 000001fe 0012f52c
WndProc: [0006]WM_ACTIVATE 00000001 00000000
WndProc: [0007]WM_SETFOCUS 00000000 00000000
WndProc: [0085]WM_NCPAINT 00000001 00000000
WndProc: [000d]WM_GETTEXT 000001fe 0012f52c
WndProc: [0014]WM_ERASEBKGND e3010449 00000000
WndProc: [0047]WM_WINDOWPOSCHANGED 00000000 0012fec0
WndProc: [0005]WM_SIZE 00000000 00450064
WndProc: [0003]WM_MOVE 00000000 004b0038
ShowWindow end
Updating Window...
WndProc: [000f]WM_PAINT 00000000 00000000
UpdateWindow end
Getting Message...
以WndProc 带头的是在窗口过程中收到的消息,显然,和4.2.4 节中讲述的是一致的,
在调用CreateWindowEx 时,窗口过程就开始接收消息,里面有重要的WM_CREATE ,然后
在ShowWindow 的时候,Windows 向窗口过程发送了很多的消息,而UpdateWindow 只给窗
口过程发送了一条WM_PAINT 消息,接下来就进入了消息循环。
可以看到,GetMessage 函数是程序主动上交空闲时间的办法之一,因为显示出Getting
Message..以后,程序就等着那里了,这表示程序的空闲时间并不浪费在消息循环中,而是
在GetMessage 函数内部由Windows 自己分配了。
接下来把鼠标移过MsgWindow 窗口,在记事本上看到了什么?用户一个小小的动作就
够窗口过程忙的——我们看到了多次重复的下列内容:
WndProc: [0084]WM_NCHITTEST 00000000 00830096
WndProc: [0020]WM_SETCURSOR 001b0304 02000001
[0200]Message gotten
Dispatching Message...
WndProc: [0200]WM_MOUSEMOVE 00000000 0038005e
DispatchMessage end
Getting Message...
首先,Windows 在GetMessage 没有返回的时候就调用了两次窗口过程,分别是处理
WM_NCHITTEST 和WM_SETCURSOR ,它们并不经过消息循环;然后,GetMessage 取到
[0200] 消息并返回,0200 是WM_MOUSEMOVE 消息的编号;接下来,DispatchMessage 函
数开始工作,在这个函数的内部,消息被Windows 发送给窗口过程处理,最后DispatchMessage
返回,然后开始新的GetMessage。
最后在MsgWindow 上单击“关闭”按钮,看发生了什么:
[00a1]Message gotten
Dispatching Message...
WndProc: [00a1]WM_NCLBUTTONDOWN 00000014 003d0097
WndProc: [0215]WM_CAPTURECHANGED 00000000 00000000
WndProc: [0112]WM_SYSCOMMAND 0000f060 003d0097
WndProc: [0010]WM_CLOSE 00000000 00000000
WndProc: [0046]WM_WINDOWPOSCHANGING 00000000 0012fad8
WndProc: [0047]WM_WINDOWPOSCHANGED 00000000 0012fad8
WndProc: [0086]WM_NCACTIVATE 00000000 00000000
WndProc: [0006]WM_ACTIVATE 00000000 00000000
WndProc: [001c]WM_ACTIVATEAPP 00000000 00000450
WndProc: [0008]WM_KILLFOCUS 00000000 00000000
WndProc: [0002]WM_DESTROY 00000000 00000000
WndProc: [0082]WM_NCDESTROY 00000000 00000000
DispatchMessage end
Getting Message...
[0012]Message gotten
GetMessage 收到的是按下鼠标的WM_NCLBUTTONDOWN 的消息,由DispatchMessage
转给窗口过程处理后,窗口过程将它转手给了DefWindowProc,DefWindowProc 根据鼠标的
位置得出结论:用户按的是“关闭”按钮,放开鼠标后,它就给窗口过程发送WM_CLOSE
消息,当窗口过程调用DestroyWindow 后,窗口被摧毁,窗口过程最后收到的是
WM_DESTROY 消息和WM_NCDESTROY 消息,而消息循环中GetMessage 最后收到的是
0012 号WM_QUIT 消息,消息循环结束。
实验2. 全部消息都经过消息循环吗
在做这个实验之前,读者已经知道并不是所有的消息都是经过消息循环的,它们中有
些是Windows 直接发送到窗口过程的,上一个实验中就已经可以看到GetMessage 返回的
次数明显比调用窗口过程的次数少,这意味着窗口过程有很多次是由Windows 直接调用
的。
这次,我们用极端的方式来验证,先把消息循环中的DispatchMessage 去掉,这样
GetMessage 得到的消息将不会再被送到窗口过程了,窗口过程收到的就是由Windows 直接调
用的。改变后的源代码见所附光盘中的Appendix B\MsgWindow02 目录。
编译后,同样先打开记事本,再执行MsgWindow ,然后将鼠标移过MsgWindow 窗口,
并尝试着单击“关闭”按钮和双击等各种动作,结果是窗口过程还是在被调用,如下所示:
WndProc: [0084]WM_NCHITTEST 00000000 007e0088
WndProc: [0020]WM_SETCURSOR 0026030c 02000001
WndProc: [0084]WM_NCHITTEST 00000000 006c0070
WndProc: [0020]WM_SETCURSOR 0026030c 02000001
.
由于没有了DispatchMessage ,大部分消息被忽略了,窗口就停在了屏幕上,不能进行移
动、缩放或关闭等操作,但还是有一部分消息直接由Windows 发送给窗口过程,它们是鼠标
位置测试的WM_NCHITTEST 消息和要求设置光标的WM_SETCURSOR 消息,所以在鼠标
移动到边框的时候,鼠标光标还是会变成双箭头的样子。
另外,尝试着单击其他窗口来切换焦点,然后再单击标题栏来重新激活窗口,可以发
现WM_MOUSEACTIVATE,WM_ACTIVATE 和WM_KILLFOCUS 等消息也是不经过消
息循环的。接下来,把一个窗口移动到MsgWindow 窗口前覆盖它的位置,再移开,可以
发现WM_SYNCPAINT 和WM_ERASEBKGND 等消息也是由Windows 直接发给窗口过
程的。
最后,关闭窗口,当然这个窗口只能用Ctrl+Alt+Del 组合键在任务管理器中关闭了!
实验3. TranslateMessage 有什么用
首先执行实验1 的MsgWindow ,在窗口上敲几个键,每次敲一个键,得到的消息是:
WM_KEYDOWN,WM_CHAR 和WM_KEYUP 。如果按下键盘不放,则首先得到一个
WM_KEYDOWN,接下来就是重复的WM_CHAR 和WM_KEYUP 消息,直到放开键盘为止,
最后才会看到一个WM_KEYUP。显示如下:
WndProc: [0100]WM_KEYDOWN 00000041 001e0001
WndProc: [0102]WM_CHAR 00000061 001e0001
WndProc: [0101]WM_KEYUP 00000041 c01e0001
在WM_KEYDOWN 和WM_KEYUP 消息中,wParam 中是按键的扫描码,上面的数据
是按下了“A”键得到的,00000041h 是“A”的扫描码,到了WM_CHAR 消息中,wParam
中就是已经转换过的ASCII 码61 了,代表输入的是小写的字母“a”。
好!现在从程序中去掉TranslateMessage 语句(修改以后的源代码放在Appendix
B\MsgWindow03 目录中),然后看这个程序的运行结果,同样,按几次键以及按下键盘不放,
我们发现:这中间的区别就是少了WM_CHAR,所以只有在处理键盘输入要用到转换后的
ASCII 码的时候,TranslateMessage 函数才是有用的,在别的时候完全可以省略这个语句。这
个函数的功能就是看到WM_KEYDOWN 的时候把消息检查一下,然后根据键值将一条新的
WM_CHAR 或WM_SYSCHAR 消息放入消息循环中。
实验4. DefWindowProc 做了什么工作
现在把DefWindowProc 语句去掉(源代码详见Appendix B\MsgWindow04 目录),然后
再以同样的方法运行,窗口根本就没有出现!看记事本中出现了什么:
Creating Window...
WndProc: [0024]WM_GETMINMAXINFO 00000000 0012fda4
WndProc: [0081]WM_NCCREATE 00000000 0012fd8c
WndProc: [0082]WM_NCDESTROY 00000000 00000000
CreateWindow end
Showing Window...
ShowWindow end
Updating Window...
UpdateWindow end
Getting Message...
原来在建立窗口的时候执行到WM_NCCREATE 消息后窗口就摧毁掉了,看
WM_NCCREATE 的说明:The DefWindowProc function returns TRUE ,原来需要返回1 来表
示执行成功,所以需要处理WM_NCCREATE 并返回1,现在在窗口过程中加上下列分支:
.elseif eax == WM_NCCREATE
mov eax,1
ret
接着编译后执行,怎么编译不成功了?不能写exe 文件?原来上次的程序还停留在消息循
环中没有退出来,让我们在任务管理器中将它终止再编译,成功了!
好!现在继续执行,窗口成功建立了,但似乎陷入了死循环,因为记事本上不停地有消
息冒出来,而且只是冒出WM_PAINT 消息来,为什么呢?原来WM_PAINT 消息是不能不处
理的,也不能丢弃,只要Windows 认为窗口的客户区需要绘画(或者说是无效的),它就会
不停地向窗口发送WM_PAINT 消息,一般WM_PAINT 消息的处理中用BeginPaint 和EndPaint
会隐含地让客户区有效,如果不用BeginPaint/EndPaint ,程序必须显式地把客户区设置为有
效,Windows 才不会再发送WM_PAINT 消息。这个函数是ValidateRect ,现在在分支中再加
上处理WM_PAINT 的代码:
.elseif eax == WM_PAINT
invoke ValidateRect,hWnd,NULL
再编译执行,现在程序可以正常执行下去了,记事本上出现的信息也显示程序停留在了
GetMessage 处,一切正常。但是,窗口在哪里呢,屏幕上什么都没有,隐身了?把鼠标移到
窗口原来应该出现的地方,记事本中熟悉的WM_NCHITTEST 和WM_SETCURSOR 消息出
现了,原来窗口还在那里,只不过没有了DefWindowProc 的处理,窗口的绘画等所有工作都
没有做,窗口的边框与客户区等所有东西连画都没有画上去,所以窗口是存在的,但我们看
不到它!
是不是再加上WM_NCPAINT 消息自己画边框呢,这就不是这个实验的内容了。我们已
经知道,DefWindowProc 做的工作太多了,缺了它我们要补上的代码可不是一两个分支的问
题,而是上百个分支了!在这个实验中,我们根本不可能把它补全。
附录C
浏览目录对话框
C.1 浏览目录对话框简介
在众多由系统提供的对话框中,除了第8 章中介绍的众多通用对话框外,还有一个很常
用的浏览目录对话框,该对话框如图C.1 所示,这个对话框虽然也是通用型的,但是它是由
Shell32.dll 提供的,而不是由Comdlg32.dll 提供的,在实现的方法上也和上面介绍的通用对
话框有很大的不同,本节以一个例子来演示它的使用。
图C.1 浏览目录对话框
例子程序的源代码位于所附光盘的Appendix C\BrowseFolder 目录中,目录中包含了
BrowseFolder.asm 文件和_BrowseFolder.asm 文件。BrowseFolder.asm 文件的内容很简单,如
下所示:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include shell32.inc
includelib shell32.lib
include ole32.inc
includelib ole32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
szPath db MAX_PATH dup (?)
.data
szSelect db '您选择的目录',0
szNoSelect db '您按下了取消键',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
include _BrowseFolder.asm
start:
invoke GetCurrentDirectory,sizeof szPath,addr szPath
invoke _BrowseFolder,NULL,addr szPath
.if eax
invoke MessageBox,NULL,offset szPath,\
offset szSelect,MB_OK
.else
invoke MessageBox,NULL,offset szNoSelect,NULL,MB_OK
.endif
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
主文件中仅包含了几句调用和显示结果的代码,全部的功能集中在_BrowseFolder.asm
中,该文件用include 语句包含进主文件中,这样安排代码的原因是目录浏览对话框的实现比
较复杂,把功能模块写成一个单独的文件可以便于在其他文件中引用,读者也可以直接把这
个源文件不加修改地用在其他地方。_BrowseFolder.asm 文件的内容如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; IUnknown 接口定义
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -