鱼C论坛

 找回密码
 立即注册
查看: 2360|回复: 0

[学习笔记] 琢石成器WIN32汇编程序设计学习(三)-第4章 第一个窗口程序(三)

[复制链接]
发表于 2018-3-12 16:54:19 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 兰陵月 于 2018-3-12 16:58 编辑

4.2.3  消息循环
    1.消息循环的一般格式

    程序中的以下代码就是通常的消息循环
.while        TRUE
invoke        GetMessage,addr @stMsg,NULL,0,0
.break        if eax == 0
invoke        TranslateMessage,addr @stMsg
invoke        DispatchMessage,addr @stMsg
.endw
    消息循环中的几个函数要用到一个MSG结构,用来做消息传递:
MSG        STRUCT
  hwnd       DWORD        ?
  message    DWORD        ?
  wParam     DWORD        ?
  lParam     DWORD        ?
  time       DWORD        ?
  pt         DWORD        ?
MSG        ENDS
    它的各个字段的含义是:
    hwnd—消息要发向的窗口的句柄;
    message—消息标识符,在头文件中以WM_开头的预定义值(意思是Windows Message);
    wParam—消息的参数之一;
    lParam—消息的参数之二;
    time—消息放入消息队列的时间;
    pt—这是一个POINT数据结构,表示消息放入消息队列时的鼠标坐标。
    这个结构定义了消息的所有属性,GetMessage函数就是从消息队列中取出这样一条消息的:
    invoke GetMessage,lpMsg,hWnd,wMsgFilterMin,wMsgFilterMax
    函数的lpMsg指向一个MSG结构,函数会在这里返回取到的消息,hWnd参数指定要获取哪个窗口的消息,例子中指定为NULL,表示获取的是所有本程序所属窗口的消息,wMsgFilterMin和wMsgFilterMax为0表示获取所有编号的消息。
    GetMessage函数从消息队列里取得消息,填写好MSG结构并返回,如果获取的消息是WM_QUIT消息,那么eax中的返回值是0,否则eax返回非零值,所以用.break .if eax==0来检查返回值,如果消息队列中有WM_QUIT,则退出消息循环。
    TranslateMessage将MSG结构传给Windows进行一些键盘消息的转换,当有键盘按下和放开时,Windows产生WM_KEYDOWN和WM_KEYUP或WM_SYSKEYDOWN和WM_SYSKEYUP消息,但这些消息的参数中包含的是按键的扫描码,转换成常用的ASCII码要经过查表,很不方便,TranslateMessage遇到键盘消息则将扫描码转换成ASCII码并在消息队列中插入WM_CHAR或WM_SYSCHAR消息,参数就是转换好的ASCII码,如此一来,要处理键盘消息的话只要处理WM_CHAR消息就好了。遇到非键盘消息则TranslateMessage不做处理。
    最后由DispatchMessage将消息发送到窗口对应的窗口过程去处理。窗口过程返回后DispatchMessage函数才返回,然后开始新一轮消息循环。
    2.其他形式的消息循环
    GetMessage函数是程序空闲的时候主动将控制权交还给Windows的一种方式,Windows是一个抢占式的多任务系统,任务之间每20ms切换一次,试想一下,如果窗口程序在主窗口中采用死循环等待,消息由Windows直接发送到窗口过程,那么程序会是下列这种样子:
invoke                CreateWindowEx,…
invoke                ShowWindow,…
invoke                UpdateWindow,…
.while                dwQuitFlag==0    ;要退出时在窗口过程中设置dwQuitFlag
.endw
invoke                ExitProcess,…
    但这样一来,即使程序在空闲状态,轮到自己的20ms时间片的时候,CPU时间就会全部消耗在.while循环中,使用GetMessage的时候,轮到应用程序时间片的时候,如果消息队列里还没有消息,那么程序还是停留在GetMessage内部,这是就可以由Windows当家作主没收这20ms的时间片,这样保证了CPU资源的合理应用。
如果应用程序想把所有时间充分利用回来,消息队列里没有消息的时候不让GetMessage在Windows内部等待,拱手交出属于自己的CPU时间,那么消息循环可以是下列这种样子:
.while     TRUE
    invoke  PeekMessage,addr @stMsg,NULL,0,0,PM_REMOVE
    .if     eax
            .break   .if @stMsg.message==WM_QUIT
            invoke   TranslateMessage,addr @stMsg
            invoke   DispatchMessage,addr @stMsg
    .else
             <做其他工作>
    .endif
.endw
    PeekMessage是一个类似于GetMessage的函数,区别在于当消息队列里有消息的时候,PeekMessage取回消息,并在eax中返回非零值,没有消息的时候它会直接返回,并在eax中返回零。所以在返回非零值的时候,程序检查消息是否是WM_QUIT,是则结束消息循环,不是则用标准流程处理消息;返回零的时候,表示是空闲时间,程序就可以做其他工作了,但插入做其他工作的代码执行时间不能过长,以不超过10ms为好,否则会影响正常的消息处理,使窗口的反应看起来很迟钝。如果必须处理很长时间的工作,那么应该将它分成很多小部分处理,以便有足够的频率用PeekMessage来检查消息。
    PeekMessage的前面4个参数和GetMessage是相同的,增加的最后一个参数表示在取回消息以后,对消息队列中消息是否保留。当这个参数是PM_REMOVE时,消息被取回的同时也被从消息队列里删除,而用PM_NOREMOVE的时候,被取回的消息不会从消息队列里删除,函数相当于“偷看”了这条消息。例子程序中用了PM_REMOVE,否则每次看到的都是队列中的第一条消息。
4.2.4  窗口过程
    窗口过程是给Windows回调用的,它必须遵循规定的格式。对窗口过程的子程序名称没有规定,对Windows来说,窗口过程的地址才是唯一需要的,例子程序中的子程序名是_ProcWinMain,读者可以改用任何名称。窗口过程子程序的参数格式为:
    WindowProc     proc   hwnd,uMsg,wParam,lParam
    第一个参数是窗口句柄,由于一个窗口过程可能为多个基于同一个窗口类的窗口服务,所以Windows回调的时候必须指出要操作的窗口,FirstWindow只建立了一个窗口,所以每次传递过来的hwnd和用CreateWindowEx函数返回的窗口句柄是一样的;第二个参数是消息标识,后面两个参数是消息的两个参数。这4个参数和消息循环中的MSG结构中的前4个字段是一样的。
    1.窗口过程的结构
    窗口过程一般有如下的结构:
WindowProc  proc  uses ebx edi esi hWnd,uMsg,wParam,lParam
    mov       eax,uMsg
    .if       eax == WM_XXX
              <处理WM_XXX消息>
    .elseif   eax == WM_YYY
              <处理WM_YYY消息>
    .elseif   eax == WM_CLOSE
              invoke   DestroyWindow,hWinMain
              invoke   postQuitMessage,NULL
    .else
              invoke   DefWindowProc,hWnd,uMsg,wParam,lParam
              ret
    .endif
    xor       eax,eax
    ret
WindowProc    endp
    该过程主要是对uMsg参数中的消息编号构成一个分支结构,对于需要处理的消息分别处理。不感兴趣的则交给DefWindowProc来处理
    要注意的是,窗口过程中要注意保存ebx,edi,esi和ebp寄存器,高级程序中不用自己操心这一点,汇编中就要注意了,Windows内部将这4个寄存器当指针使用,如果返回时改变了它们的值,程序会马上崩溃。proc后面的uses伪操作在子程序进入和退出时自动安插上push和pop寄存器指令,来保护这些寄存器的值。其实不仅是在窗口过程中是这样,所有WIN32 API也遵循这个规定,所以调用API后,ebx,edi,esi和ebp寄存器的值总是不会被改变的,但ecx和edx的值就不一定了。
    uMsg参数指定的消息有一定的范围,Windows标准窗口中已经预定义的值在0~03ffh之间,用户可以自定义一些消息,通过SendMessage等函数传递给窗口过程做自定义的处理工作,这时可以使用的值是从0400h开始的,WM_USER就定义为00000400h,当程序员定义多个用户消息的时候,一般使用WM_USER+1,WM_USER+2,…之类的定义方法。
    wParam和lParam参数是消息所附带的参数,它随消息的不同而不同,对于不同的消息,它们的含义必须分别从手册中查明:如WM_MOUSEMOVE消息中,wParam是标志,lParam是鼠标位置;而在WM_GETTEXT消息中,wParam是要获取的字符数,lParam是缓冲区地址;而对于WM_COPY消息来说,它不需要额外的信息,所以两个参数都没有定义。
    处理了不同的消息,必须返回规定的值给Windows,返回值也需要分别从手册中查明,比如,处理WM_CREATE消息的时候,返回0表示成功;如果程序无法初始化,如申请内存失败,那么可以返回-1,Windows就不会继续窗口的创建过程。一些消息的返回值则没有定义,但大部分的消息处理以后都已返回0表示成功,所以程序中把默认的返回语句放在最后,将eax清零后返回,如果在处理某个消息的时候需要返回不同的值,可以在分支中将eax赋值后直接用ret指令返回。对于DefWindowProc的返回值,我们不对它进行干涉,所以直接将eax不做修改地用ret返回。
    WM_CLOSE消息是按下窗口右上角的“关闭”按钮后收到的,程序在这里可以处理和关闭窗口相关的事情,一般是相关资源的释放工作,如释放内存、保存工作和提示用户是否保存工作等,如记事本程序在未保存的时候单击“关闭”按钮,会有提示框提示是否先保存文件,单击“取消”按钮的话,记事本不会关闭,这个步骤就是在WM_CLOSE消息处理中完成的。如果处理WM_CLOSE消息时直接返回,那么窗口不会关闭,因为这个消息只是Windows通知窗口用户单击了“关闭”按钮而已,窗口采取什么样的行为是窗口的事情。当窗口决定关闭的时候,需要程序自己调用DestroyWindow来摧毁窗口,并用PostQuitMessage向消息循环发送WM_QUIT消息来退出消息循环。调用PostQuitMessage时的参数是退出码,就是GetMessage收到WM_QUIT后MSG结构中wParam字段中的内容,在这里使用NULL。
    【很重要】PostQuitMessage是初学者容易遗漏的函数,如果没有这条语句,外观上窗口是被摧毁掉了,从屏幕上消失了,但主程序中的消息循环却没有收到WM_QUIT,结果还在那里打转。常有人调试的时候丢了这条语句,结果再一次编译的时候就收到错误:LINK fatal error :LNK1104:cannot open file ”XXX.exe”,这就表示exe文件仍然被使用中。Window为什么不在窗口摧毁的时候自动发送一个WM_QUIT消息,而必须由用户程序自己通过PostQuitMessage函数发送呢?其实很好理解:因为屏幕上可能不止一个窗口,Windows无法确定哪个窗口关闭代表着程序结束。试想一下,用户打开了一个输入参数的小窗口,单击“确定”按钮后关闭并回到主窗口,Windows却部分三七二十一自动发送了一个WM_QUIT消息,程序就会莫名其妙地退出了。
    2.收到消息的顺序
    窗口过程收到消息是有一定顺序的,收到第一条消息并不是从消息循环开始以后,而是在CreatWindowEx中就开始了,显示和刷新窗口的函数ShowWindow和UpdateWindow也向窗口过程发送消息,这一点并不奇怪,因为Windows在CreateWindowEx前调用RegisterClassEx的时候就已经得到窗口过程的地址了。并且在建立窗口的过程中需要窗口过程的配合。表4.6和表4.7分别列出了调用CreateWindowEx和ShowWindow的时候窗口过程收到的消息。
009.png

010.png

    然后程序执行UpdateWindow,这个函数仅仅向窗口过程发送一条WM_PAINT消息,接着,主程序开始进入消息循环,Windows根据各种因素给窗口过程发送相应的消息,一直到调用DestroyWindow为止。表4.8列出了DestroyWindow向窗口过程发送的消息。
011.png

    在所有这些阶段的消息中,大部分的消息都不需要程序自己关心,Windows只是尽义务通知窗口过程而已,窗口过程转手就交给DefWindowProc去处理了。程序需要关心的消息有下面这些,可以根据需要选择使用:
WM_CREATE—放置窗口初始化代码,如建立各种子窗口(状态栏和工具栏等)。
WM_SIZE—放置位置安排的代码,因为建立的子窗口可能需要随窗口大小的改变而移动位置。
WM_PAINT—如果需要自己绘制客户区,则在这里安排代码。
WM_CLOSE—向用户确认是否退出,如果退出则摧毁窗口并发送WM_QUIT消息。
WM_DESTROY—窗口摧毁,在这里放置释放资源等扫尾代码。

    在例子程序中,我们处理了WM_PAINTA消息来绘制客户区,功能就是在窗口的中间写上一行字:“Win32 Assembly,Simple and Powerful!”。窗口过程先通过BeginPaint获取窗口客户区的“设备环境”句柄,然后通过GetClientRect获取客户区的大小,最后通过DrawText函数将字符串按照取得的屏幕大小居中写到“设备环境”中,也就是窗口上。如果不需要显示这个字符串,则连WM_PAINT消息也不用处理。
    3.消息的默认处理—DefWindowProc
    Windows预定义的消息范围是0~03ffh,共预留了1024个消息编号,查看一下头文件Windows.inc,可以发现实际已定义的消息数目有几百个,这些消息中的大部分对于窗口的运行来说都是必需的,如果窗口过程要处理每一种消息,那么窗口过程中的elseif语句就会绵延数千行,但是窗口的行为就是由处理这些消息的方法来表现的,不处理又不行,怎么办呢?
     实际上,大部分窗口的行为都是差不多的,这意味着如果要窗口过程处理全部的消息,不同窗口的窗口代码过程应该是大同小异的,完全可以用一个通用的模块来以默认的方式处理消息,Win32中的DefWindowProc函数实现的就是这个功能。
    不要小看了这个DefWindowProc,正是它用默认的方式处理了几百种消息,才使用户能用区区百来行代码写出一个全功能的窗口。也正是所有的窗口都用DefWindowProc默认处理程序自己不处理的消息,才使它们的行为看不上大同小异,因为它们背后实际上是同一块代码在处理。
    在窗口过程的分支语句中,用户处理所有需要个性化处理的消息,对于表现行为是默认行为的消息,则在else分支中用DefWindowProc来处理。由于对于Windows消息来说,它并不关心消息在窗口过程中是程序用自己的代码处理的还是用DefWindowProc处理的,它只看eax中的返回值来了解处理结果,所以不管消息是谁处理的,都必须在eax中返回正确的值。DefWindowProc返回时eax中就是它对消息的处理结果,程序只要直接把eax传回给Windows就行了,所以在例子程序中,DefWindowProc后面直接用一句ret指令返回。
注意:例子中DefWindowProc后面直接使用的这句ret非常重要,如果丢失了这一句,那么相当于处理大多数消息时没有返回正确的值,窗口将不会正常工作。表4.9中列出了DefWindowProc中对一些消息的处理方法,如果与用户期望的不同,就必须在窗口过程中自己处理。
012.png

    从这些默认的处理方法可以看出,想要一个窗口和别的窗口看起来不一样,比如,想要窗口看起来像苹果机的窗口一样,并且把关闭按钮移到标题栏最左边去,那么可以自己处理WM_NCPAINT消息,把非客户区画成苹果机窗口的样子,并把关闭按钮画到标题栏左边去,并且自己处理WM_NCLBUTTONUP消息,当检测到鼠标按下的位置在自己的关闭按钮上的时候,则发送WM_CLOSE消息。对别的消息的处理思路也可以按这种方法类推。
    另外,可以发现DefWindowProc对WM_CLOSE的默认处理是调用DestroyWindow摧毁窗口,DestroyWindow会引发一个WM_DESTROY消息,WM_CLOSE和WM_DESTROY的不同之处:WM_CLOSE代表用户有关闭窗口的意向,窗口过程有权不“服从”,但收到WM_DESTROY的时候窗口已经在关闭过程中了,不管窗口过程愿不愿意,窗口的关闭已经是不可挽回的事了。
    对于这两个消息,窗口过程必须处理其中的一个,因为必须有个地方发送WM_QUIT消息来结束消息循环,例子程序中处理WM_CLOSE消息,在其中用DestroyWindow摧毁窗口,再调用PostQuitMessage结束消息循环;程序也可以不处理WM_CLOSE消息,让DefWindowProc以默认处理的方式摧毁窗口,但这时候必须处理WM_DESTROY消息,在其中调用PostQuitMessage发送WM_QUIT以结束消息循环。

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-3-28 19:54

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表