




已阅读5页,还剩28页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1DELPHI的消息机制浅探SAVETIME2KYAHOOCOM200419我从去年12月上旬开始等待李维的INSIDEVCL。我当时的计划是,在这本书的指导下深入学习DELPHI。到了12月底,书还没有出来,我不愿再等,开始阅读VCL源代码。在读完TOBJECT、TPERSISTANT和TCOMPONENT的代码之后,我发现还是不清楚DELPHI对象到底是怎样被创建的。于是我查看DELPHI生成的汇编代码,终于理解了对象创建的整个过程这里要特别感谢BOOK523的帮助。此后我就开始学习DELPHIVCL的消息处理机制。自从我写下DELPHI的对象机制浅探,至今正好一个星期,我也基本上把DELPHIVCL的消息处理框架读完了。我的学习方法就是阅读源代码,一开始比较艰苦,后来线索逐渐清晰起来。在此把自己对DELPHIVCL消息机制的理解记录下来,便于今后的复习,也给初学DELPHI或没有时间阅读VCL源代码的朋友参考毕竟没有几个程序员像我这样有时间。由于学习时间较短,一定会有错误,请大家指正。我在分析VCL消息机制的过程中,基本上只考查了三个类TOBJECT、TCONTROL和TWINCONTROL。虽然我没有阅读上层类如TFORM的代码,但我认为这些都是实现的细节。我相信VCL消息系统中最关键的东西都在这三个类中。纲举而目张,掌握基础类的消息处理方法之后再读其他类的消息处理过程就容易得多了。要想读懂本文,最低配置为了解WIN32消息循环和窗口过程基本了解TOBJECT、TCONTROL和TWINCONTROL实现的内容熟悉DELPHI对象的重载与多态推荐配置为熟悉WIN32SDK编程熟悉DELPHI的对象机制熟悉DELPHI内嵌汇编语言推荐阅读DELPHI的原子世界HTTP/WWWCODELPHICOM/VCL窗口函数注册机制研究手记,兼与MFC比较HTTP/WWWDELPHIBBSCOM/DELPHIBBS/DISPQASPLID584889DELPHI的对象机制浅探HTTP/WWWDELPHIBBSCOM/DELPHIBBS/DISPQASPLID2390131本文排版格式为正文由窗口自动换行;所有代码以80字符为边界;中英文字符以空格符分隔。作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。目录一个GUIAPPLICATION的执行过程消息循环的建立TWINCONTROLCREATE、注册窗口过程和创建窗口补充知识TWNDMETHOD概述VCL的消息处理从TWINCONTROLMAINWNDPROC开始TWINCONTROLWNDPROCTCONTROLWNDPROCTOBJECTDISPATCHTWINCONTROLDEFAULTHANDLER2TCONTROLPERFORM和TWINCONTROLBROADCASTTWINCONTROLWMPAINT以TWINCONTROL为例描述消息传递的路径正文一个GUIAPPLICATION的执行过程消息循环的建立通常一个WIN32GUI应用程序是围绕着消息循环的处理而运行的。在一个标准的C语言WIN32GUI程序中,主程序段都会出现以下代码WHILEGETMESSAGE/转换消息中的字符集DISPATCHMESSAGE/把MSG参数传递给LPFNWNDPROCLPFNWNDPROC是WIN32API定义的回调函数的地址,其原型如下INT_STDCALLWNDPROCHWNDHWND,UINTUMSG,WPARAMWPARAM,LPARAMLPARAMWINDOWS回调函数CALLBACKFUNCTION也通常被称为窗口过程WINDOWPROCEDURE,本文随意使用这两个名称,代表同样的意义。应用程序使用GETMESSAGE不断检查应用程序的消息队列中是否有消息到达。如果发现了消息,则调用TRANSLATEMESSAGE。TRANSLATEMESSAGE主要是做字符消息本地化的工作,不是关键的函数。然后调用DISPATCHMESSAGEBEGINAPPLICATIONINITIALIZEAPPLICATIONCREATEFORMTFORM1,FORM1APPLICATIONRUNEND在PROJECT1的APPLICATIONINITIALIZE之前,DELPHI编译器会自动插入一行代码3SYSINIT_INITEXE。_INITEXE主要是初始化HINSTANCE和模块信息表等。然后_INITEXE调用SYSTEM_STARTEXE。SYSTEM_STARTEXE调用SYSTEMINITUNIT;SYSTEMINITUNIT调用项目中所有被包含单元的INITIALIZATION段的代码;其中有CONTROLSINITIALIZATION段,这个段比较关键。在这段代码中建立了MOUSE、SCREEN和APPLICATION三个关键的全局对象。APPLICATIONCREATE调用APPLICATIONCREATEHANDLE。APPLICATIONCREATEHANDLE建立一个窗口,并设置APPLICATIONWNDPROC为回调函数这里使用了MAKEOBJECTINSTANCE方法,后面再谈。APPLICATIONWNDPROC主要处理一些应用程序级别的消息。我第一次跟踪应用程序的执行时没有发现APPLICATION对象的创建过程,原来在SYSINIT_INITEXE中被隐含调用了。如果你想跟踪这个过程,不要设置断点,直接按F7就发现了。然后才到了PROJECT1的第1句APPLICATIONINITIALIZE这个函数只有一句代码IFINITPROCNILTHENTPROCEDUREINITPROC也就是说如果用户想在应用程序的执行前运行一个特定的过程,可以设置INITPROC指向该过程。为什么用户不在APPLICATIONINITIALIZE之前或在单元的INITLIAZATION段中直接运行这个特定的过程呢一个可能的答案是如果元件设计者希望在应用程序的代码执行之前执行一个过程,并且这个过程必须在其他单元的INITIALIZATION执行完成之后执行比如说APPLICATION对象必须创建,则只能使用这个过程指针来实现。然后是PROJECT1的第2句APPLICATIONCREATEFORMTFORM1,FORM1这句的主要作用是创建TFORM1对象,然后把APPLICATIONMAINFORM设置为TFORM1。最后是PROJECT1的第3句APPLICATIONRUNTAPPLICATIONRUN调用TAPPLICATIONHANDLEMESSAGE处理消息。APPLICATIONHANDLEMESSAGE的代码也只有一行IFNOTPROCESSMESSAGEMSGTHENIDLEMSGTAPPLICATIONPROCESSMESSAGE才真正开始建立消息循环。PROCESSMESSAGE使用PEEKMESSAGEAPI代替GETMESSAGE获取消息队列中的消息。使用PEEKMESSAGE的好处是PEEKMESSAGE发现消息队列中没有消息时会立即返回,这样就为HANDLEMESSAGE函数执行IDLEMSG提供了依据。PROCESSMESSAGE在处理消息循环的时候还特别处理了HINTMSG、MDIMSG、KEYMSG、DLGMSG等特殊消息,所以在DELPHI中很少再看到纯WIN32SDK编程中的要区分DIALOGWINDOW、MDIWINDOW的处理,这些都被封装到TFORM中去了其实WIN32SDK中的DIALOG也是只是MICROSOFT专门写了一个窗口过程和一组函数方便用户界面的设计,其内部运作过程与一个普通窗口无异。FUNCTIONTAPPLICATIONPROCESSMESSAGEVARMSGTMSGBOOLEANVARHANDLEDBOOLEANBEGINRESULTFALSE4IFPEEKMESSAGEMSG,0,0,0,PM_REMOVETHEN/从消息队列获取消息BEGINRESULTTRUEIFMSGMESSAGEWM_QUITTHENBEGINHANDLEDFALSE/HANDLED表示APPLICATIONONMESSAGE是否已经处理过/当前消息。/如果用户设置了APPLICATIONONMESSAGE事件句柄,/则先调用APPLICATIONONMESSAGEIFASSIGNEDFONMESSAGETHENFONMESSAGEMSG,HANDLEDIFNOTISHINTMSGMSGANDNOTHANDLEDANDNOTISMDIMSGMSGANDNOTISKEYMSGMSGANDNOTISDLGMSGMSGTHEN/思考NOTHANDLED为什么不放在最前BEGINTRANSLATEMESSAGEMSG/处理字符转换DISPATCHMESSAGEMSG/调用WNDCLASSLPFNWNDPROCENDENDELSEFTERMINATETRUE/收到WM_QUIT时应用程序终止/这里只是设置一个终止标记ENDEND从上面的代码来看,DELPHI应用程序的消息循环机制与标准WIN32C语言应用程序差不多。只是DELPHI为了方便用户的使用设置了很多扩展空间,其副作用是消息处理会比纯CWIN32API调用效率要低一些。TWINCONTROLCREATE、注册窗口过程和创建窗口上面简单讨论了一个APPLICATION的建立到形成消息循环的过程,现在的问题是DELPHI控件是如何封装创建窗口这一过程的。因为只有建立了窗口,消息循环才有意义。让我们先回顾DELPHIVCL中几个主要类的继承架框TOBJECT所有对象的基类TPERSISTENT所有具有流特性对象的基类TCOMPONENT所有能放在DELPHIFORMDESIGNER上的对象的基类TCONTROL所有可视的对象的基类TWINCONTROL所有具有窗口句柄的对象基类DELPHI是从TWINCONTROL开始实现窗口相关的元件。所谓窗口,对于程序设计者来说,就是一个窗口句柄HWND。TWINCONTROL有一个FHANDLE私有成员代表当前对象的窗口句柄,通过TWINCONTROLHANDLE属性来访问。我第一次跟踪TWINCONTROLCREATE过程时,竟然没有发现CREATEWINDOWAPI被调用,说明TWINCONTROL并不是在对象创建时就建立WINDOWS窗口。如果用户使用TWINCONTROLCREATEAPPLICATION以后,立即使用HANDLE访问窗口会出现什么5情况呢答案在TWINCONTROLGETHANDLE中,HANDLE是一个只读的窗口句柄PROPERTYTWINCONTROLHANDLEHWNDREADGETHANDLETWINCONTROLGETHANDLE代码的内容是一旦用户要访问FHANDLE成员,TWINCONTROLHANDLENEEDED就会被调用。HANDLENEEDED首先判断TWINCONTROLFHANDLE是否是等于0还记得吗任何对象调用构造函数以后所有对象成员的内存都被清零。如果FHANDLE不等于0,则直接返回FHANDLE;如果FHANDLE等于0,则说明窗口还没有被创建,这时HANDLENEEDED自动调用TWINCONTROLCREATEHANDLE来创建一个HANDLE。但CREATEHANDLE只是个包装函数,它首先调用TWINCONTROLCREATEWND来创建窗口,然后生成一些维护VCLCONTROL运行的参数我还没细看。CREATEWND是一个重要的过程,它先调用TWINCONTROLCREATEPARAMS设置创建窗口的参数。CREATEPARAMS是个虚方法,也就是说程序员可以重载这个函数,定义待建窗口的属性。CREATEWND然后调用TWINCONTROLCREATEWINDOWHANDLE。CREATEWINDOWHANDLE才是真正调用CREATEWINDOWEXAPI创建窗口的函数。够麻烦吧,我们可以抱怨BORLAND为什么把事情弄得这么复杂,但最终希望BORLAND这样设计自有它的道理。上面的讨论可以总结为TWINCONTROL为了为了减少系统资源的占用尽量推迟建立窗口,只在某个方法需要调用到控件的窗口句柄时才真正创建窗口。这通常发生在窗口需要显示的时候。一个窗口是否需要显示常常发生在对PARENT属性在TCONTROL中定义赋值的时候。设置PARENT属性时,TCONTROLSETPARENT方法会调用TWINCONTROLREMOVECONTROL和TWINCONTROLINSERTCONTROL方法。INSERTCONTROL调用TWINCONTROLUPDATECONTROLSTATE。UPDATECONTROLSTATE检查TWINCONTROLSHOWING属性来判断是否要调用TWINCONTROLUPDATESHOWING。UPDATESHOWING必须要有一个窗口句柄,因此调用TWINCONTROLCREATEHANDLE来创建窗口。不过上面说的这些,只是繁杂而不艰深,还有很多关键的代码没有谈到呢。你可能发现有一个关键的东西被遗漏了,对,那就是窗口的回调函数。由于DELPHI建立一个窗口的回调过程太复杂了并且是非常精巧的设计,只好单独拿出来讨论。CHEKA的VCL窗口函数注册机制研究手记,兼与MFC比较一文中对VCL的窗口回调实现进行了深入的分析,请参考HTTP/WWWDELPHIBBSCOM/DELPHIBBS/DISPQASPLID584889我在此简单介绍回调函数在VCL中的实现TWINCONTROLCREATE的代码中,第一句是INHERITED,第二句是FOBJECTINSTANCECLASSESMAKEOBJECTINSTANCEMAINWNDPROC我想这段代码可能吓倒过很多人,如果没有CHEKA的分析,很多人难以理解。但是你不一定真的要阅读MAKEOBJECTINSTANCE的实现过程,你只要知道MAKEOBJECTINSTANCE在内存中生成了一小段汇编代码,这段代码的内容就是一个标准的窗口过程。这段汇编代码中同时存储了两个参数,一个是MAINWNDPROC的地址,一个是SELF对象的地址。这段汇编代码的功能就是使用SELF参数调用6TWINCONTROLMAINWNDPROC函数。MAKEOBJECTINSTANCE返回后,这段代码的地址存入了TWINCONTROLFOBJECTINSTANCE私有成员中。这样,TWINCONTROLFOBJECTINSTANCE就可以当作标准的窗口过程来用。你可能认为TWINCONTROL会直接把TWINCONTROLFOBJECTINSTANCE注册为窗口类的回调函数使用REGISTERCLASSAPI,但这样做是不对的。因为一个FOBJECTINSTANCE的汇编代码内置了对象相关的参数对象的地址SELF,所以不能用它作为公共的回调函数注册。TWINCONTROLCREATEWND调用CREATEPARAMS获得要注册的窗口类的资料,然后使用CONTROLSPAS中的静态函数INITWNDPROC作为窗口回调函数进行窗口类的注册。INITWNDPROC的参数符合WINDOWS回调函数的标准。INITWNDPROC第一次被回调时就把新建窗口注意不是窗口类的回调函数替换为对象的TWINCONTROLFOBJECTINSTANCE这是一种WINDOWSSUBCLASSING技术,并且使用SETPROP把对象的地址保存在新建窗口的属性表中,供DELPHI的辅助函数读取比如CONTROLSPAS中的FINDCONTROL函数。总之,TWINCONTROLFOBJECTINSTANCE最终是被注册为窗口回调函数了。这样,如果TWINCONTROL对象所创建的窗口收到消息后形象的说法,会被WINDOWS回调TWINCONTROLFOBJECTINSTANCE,而FOBJECTINSTANCE会呼叫该对象的TWINCONTROLMAINWNDPROC函数。就这样VCL完成了对象的消息处理过程与WINDOWS要求的回调函数格式差异的转换。注意,在转换过程中,WINDOWS回调时传递进来的第一个参数HWND被抛弃了。因此DELPHI的组件必须使用TWINCONTROLHANDLE或PROTECTED中的WINDOWHANDLE来得到这个参数。WINDOWS回调函数需要传回的返回值也被替换为TMESSAGE结构中的最后一个字段RESULT。为了使大家更清楚窗口被回调的过程,我把从DISPATCHMESSAGE开始到TWINCONTROLMAINWNDPROC被调用的汇编代码你可以把从FOBJECTINSTANCECODE开始至最后一行的代码看成是一个标准的窗口回调函数DISPATCHMESSAGE把WINDOWS回调前下一条语句的地址保存在堆栈中JMPFOBJECTINSTANCECODE调用TWINCONTROLFOBJECTINSTANCEFOBJECTINSTANCECODE只有一句CALL指令CALLOBJECTINSTANCEOFFSETPUSHEIPNEXTJMPINSTANCEBLOCKCODE调用INSTANCEBLOCKCODEINSTANCEBLOCKCODEPOPECX将EIPNEXT的值存入ECX,用于取MAINWNDPROC和SELF7JMPSTDWNDPROC跳转至STDWNDPROCSTDWNDPROC的汇编代码FUNCTIONSTDWNDPROCWINDOWHWNDMESSAGE,WPARAMLONGINTLPARAMLONGINTLONGINTSTDCALLASSEMBLERASMPUSHEBPMOVEBP,ESPXOREAX,EAXXOREAX,EAXPUSHEAXPUSHEAX设置MESSAGERESULT0PUSHLPARAM为什么BORLAND不从上面的堆栈中直接PUSHDWORDPTREBP14获取这些参数而要重新PUSH一遍PUSHWPARAM因为TMESSAGE的RESULT是PUSHDWORDPTREBP10记录的最后一个字段,而回调函数的HWNDPUSHMESSAGE是第一个参数,没有办法兼容。PUSHDWORDPTREBP0CMOVEDX,ESPMOVEDX,ESP设置MESSAGE在堆栈中的地址为MAINWNDPROC的参数MOVEAX,ECXLONGINT4MOVEAX,ECX04设置SELF为MAINWNDPROC的隐含参数CALLECXPOINTERCALLDWORDPTRECX呼叫TWINCONTROLMAINWNDPROCSELF,MESSAGEADDESP,12ADDESP,0CPOPEAXPOPEAXENDPOPEBPRET0010MOVEAX,EAX看不懂上面的汇编代码,不影响对下文讨论的理解。补充知识TWNDMETHOD概述写这段基础知识是因为我在阅读MAKEOBJECTINSTANCEMAINWNDPROC这句时不知道究竟传递了什么东西给MAKEOBJECTINSTANCE。弄清楚了TWNDMETHOD类型的含义还可以理解后面VCL消息系统中的一个小技巧。TWNDMETHODPROCEDUREVARMESSAGETMESSAGEOFOBJECT8这句类型声明的意思是TWNDMETHOD是一种过程类型,它指向一个接收TMESSAGE类型参数的过程,但它不是一般的静态过程,它是对象相关OBJECTRELATED的。TWNDMETHOD在内存中存储为一个指向过程的指针和一个对象的指针,所以占用8个字节。TWNDMETHOD类型的变量必须使用已实例化的对象来赋值。举个例子VARSOMEMETHODTWNDMETHODBEGINSOMEMETHODFORM1MAINWNDPROC/正确。这时SOMEMETHOD包含MAINWNDPROC/和FORM1的指针,可以用SOMEMETHODMSG/来执行。SOMEMETHODTFORMMAINWNDPROC/错误不能用类引用。END如果把TWNDMETHOD变量赋值给虚方法会怎样举例VARSOMEMETHODTWNDMETHODBEGINSOMEMETHODFORM1WNDPROC/TFORMWNDPROC是虚方法END这时,编译器实现为SOMEMETHOD指向FORM1对象虚方法表中的WNDPROC过程的地址和FORM1对象的地址。也就是说编译器正确地处理了虚方法的赋值。调用SOMEMETHODMESSAGE就等于调用FORM1WNDPROCMESSAGE。在可能被赋值的情况下,对象方法最好不要设计为有返回值的函数FUNCTION,而要设计为过程PROCEDURE。原因很简单,把一个有返回值的对象方法赋值给TWNDMETHOD变量,会造成编译时的二义性。VCL的消息处理从TWINCONTROLMAINWNDPROC开始通过对APPLICATIONRUN、TWINCONTROLCREATE、TWINCONTROLHANDLE和TWINCONTROLCREATEWND的讨论,我们现在可以把焦点转向VCL内部的消息处理过程。VCL控件的消息源头就是TWINCONTROLMAINWNDPROC函数。如果不能理解这一点,请重新阅读上面的讨论。让我们先看一下MAINWNDPROC函数的代码异常处理的语句被我删除PROCEDURETWINCONTROLMAINWNDPROCVARMESSAGETMESSAGEBEGINWINDOWPROCMESSAGEENDTWINCONTROLMAINWNDPROC以引用也就是隐含传地址的方式接受一个TMESSAGE类型的参数,TMESSAGE的定义如下其中的WPARAM、LPARAM、RESULT各有HIWORD和LOWORD的联合字段,被我删除了,免得代码太长9TMESSAGEPACKEDRECORDMSGCARDINALWPARAMLONGINTLPARAMLONGINTRESULTLONGINTENDTMESSAGE中并没有窗口句柄,因为这个句柄已经在窗口创建之后保存在TWINCONTROLHANDLE之中。TMESSAGEMSG是消息的ID号,这个消息可以是WINDOWS标准消息、用户定义的消息或VCL定义的CONTROL消息等。WPARAM和LPARAM与标准WINDOWS回调函数中WPARAM和LPARAM的意义相同,RESULT相当于标准WINDOWS回调函数的返回值。注意MAINWNDPROC不是虚函数,所以它不能被TWINCONTROL的继承类重载。思考为什么BORLAND不将MAINWNDPROC设计为虚函数呢MAINWNDPROC中建立两层异常处理,用于释放消息处理过程中发生异常时的资源泄漏,并调用默认的异常处理过程。被异常处理包围着的是WINDOWPROCMESSAGE。WINDOWPROC是TCONTROL而不是TWINCONTROL的一个属性PROPERTYPROPERTYWINDOWPROCTWNDMETHODREADFWINDOWPROCWRITEFWINDOWPROCWINDOWPROC的类型是TWNDMETHOD,所以它是一个对象相关的消息处理函数指针请参考前面TWNDMETHOD的介绍。在TCONTROLCREATE中FWINDOWPROC被赋值为WNDPROC。WNDPROC是TCONTROL的一个函数,参数与TWINCONTROLMAINWNDPROC相同PROCEDURETCONTROLWNDPROCVARMESSAGETMESSAGEVIRTUAL原来MAINWNDPROC只是个代理函数,最终处理消息的是TCONTROLWNDPROC函数。那么BORLAND为什么要用一个FWINDOWPROC来存储这个WNDPROC函数,而不直接调用WNDPROC呢我猜想可能是基于效率的考虑。还记得上面TWNDMETHOD的讨论吗一个TWNDMETHOD变量可以被赋值为一个虚函数,编译器对此操作的实现是通过对象指针访问到了对象的虚函数表,并把虚函数表项中的函数地址传回。由于WNDPROC是一个调用频率非常高的函数可能要用“百次/秒”或“千次/秒”来计算,所以如果每次调用WNDPROC都要访问虚函数表将会浪费大量时间,因此在TCONTROL的构造函数中就把WNDPROC的真正地址存储在WINDOWPROC中,以后调用WINDOWPROC将就转换为静态函数的调用,以加快处理速度。TWINCONTROLWNDPROC转了层层弯,到现在我们才刚进入VCL消息系统处理开始的地方WNDPROC函数。如前所述,TWINCONTROLMAINWNDPROC接收到消息后并没有处理消息,而是把消息传递给WINDOWPROC处理。由于WINDOWPROC总是指向当前对象的WNDPROC函数的地址,我们可以简单地认为WNDPROC函数是VCL中第一个处理消息的函数,调用WINDOWPROC只是效率问题。WNDPROC函数是个虚函数,在TCONTROL中开始定义,在TWINCONTROL中被重载。10BORLAND将WNDPROC设计为虚函数就是为了各继承类能够接管消息处理,并把未处理的消息或加工过的消息传递到上一层类中处理。这里将消息处理的传递过程和对象的构造函数稍加对比对象的构造函数通常会在第一行代码中使用INHERITED语句调用父类的构造函数以初始化父类定义的成员变量,父类也会在构造函数开头调用祖父类的构造函数,如此递归,因此一个TWINCONTROL对象的创建过程是TCOMPONENTCREATETCONTROLCREATETWINCONTROLCREATE。而消息处理函数WNDPROC则是先处理自己想要的消息,然后看情况是否要递交到父类的WNDPROC中处理。所以消息的处理过程是TWINCONTROLWNDPROCTCONTROLWNDPROC。因此,如果要分析消息的处理过程,应该从子类的WNDPROC过程开始,然后才是父类的WNDPROC过程。由于TWINCONTROL是第一个支持窗口创建的类,所以它的WNDPROC是很重要的,它实现了最基本的VCL消息处理。TWINCONTROLWNDPROC主要是预处理一些键盘、鼠标、窗口焦点消息,对于不必响应的消息,TWINCONTROLWNDPROC直接返回,否则把消息传递至TCONTROLWNDPROC处理。从TWINCONTROLWNDPROC摘抄一段看看WM_KEYFIRSTWM_KEYLASTIFDRAGGINGTHENEXIT/注意使用EXIT直接返回这段代码的意思是如果当前组件正处于拖放状态,则丢弃所有键盘消息。再看一段WM_MOUSEFIRSTWM_MOUSELASTIFISCONTROLMOUSEMSGTWMMOUSEMESSAGETHENBEGINCHECKHANDLEALLOCATEDBECAUSEISCONTROLMOUSEMSGMIGHTHAVEFREEDTHEWINDOWIFUSERCODEEXECUTEDSOMETHINGLIKEPARENTNILIFMESSAGERESULT0ANDHANDLEALLOCATEDTHENDEFWINDOWPROCHANDLE,MESSAGEMSG,MESSAGEWPARAM,MESSAGELPARAM/DEFWINDOWPROC是WIN32API中缺省处理消息的函数EXITEND这里的ISCONTROLMOUSEMSG很关键。让我们回忆一下TCONTROL类的对象并没有创建WINDOWS窗口,它是怎样接收到鼠标和重绘等消息的呢原来这些消息就是由它的PARENT窗口发送的。在上面的代码中,TWINCONTROLISCONTROLMOUSEMSG判断鼠标地址是否落在TCONTROL类控件上,如果不是就返回否值。TWINCONTROL再调用TCONTROLWNDPROC,TCONTROLWNDPROC又调用了TOBJECTDISPATCH方法,这是后话。如果当前鼠标地址落在窗口上的TCONTROL类控件上,则根据TCONTROL对象的相对位置重新生成了鼠标消息,再调用TCONTROLPERFORM方法把加工过的鼠标消息11直接发到TCONTROLWNDPROC处理。TCONTROLPERFORM方法以后再谈。如果TWINCONTROL的继承类重载WNDPROC处鼠标消息,但不使用INHERITED把消息传递给父类处理,则会使从TCONTROL继承下来的对象不能收到鼠标消息。现在我们来做个试验,下面FORM1上的TSPEEDBUTTON等非窗口控件不会发生ONCLICK等鼠标事件。PROCEDURETFORM1WNDPROCVARMESSAGETMESSAGEOVERRIDEBEGINCASEMESSAGEMSGOFWM_MOUSEFIRSTWM_MOUSELASTBEGINDEFWINDOWPROCHANDLE,MESSAGEMSG,MESSAGEWPARAM,MESSAGELPARAMEXIT/直接退出ENDELSEINHERITEDENDENDTWINCONTROLWNDPROC的最后一行代码是INHERITEDWNDPROCMESSAGE也就是调用TCONTROLWNDPROC。让我们来看看TCONTROLWNDPROC做了些什么。TCONTROLWNDPROCTCONTROLWNDPROC主要实现的操作是响应与FORMDESIGNER的交互在设计期间在控件不支持双击的情况下把鼠标双击事件转换成单击判断鼠标移动时是否需要显示提示窗口HINTWINDOW判断控件是否设置为AUTODRAG,如果是则执行控件的拖放处理调用TCONTROLMOUSEWHEELHANDLER实现鼠标滚轮消息使用TOBJECTDISPATCH调用DMT消息处理方法TCONTROLWNDPROC相对比较简单,在此只随便谈谈第二条。你是否有过这样的使用经验在你快速双击某个软件的BUTTON时,只形成一次CLICK事件。所以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数CLICK事件的按钮时,就需要参考TCONTROLWNDPROC处理鼠标消息的过程了。TCONTROLWNDPROC最后一行代码是DISPATCHMESSAGE,也就是说如果某个消息没有被TCONTROL以后的任何类处理,消息会被DISPATCH处理。TOBJECTDISPATCH是DELPHIVCL消息体系中非常关键的方法。TOBJECTDISPATCHTOBJECTDISPATCH是个虚函数,它的声明如下12PROCEDURETOBJECTDISPATCHVARMESSAGEVIRTUAL请注意它的参数虽然与MAINWNDPROC和WNDPROC的参数相似,但它没有规定参数的类型。这就是说,DISPATCH可以接受任何形式的参数。DELPHI的文档指出MESSAGE参数的前2个字节是MESSAGE的ID下文简称为MSGID,通过MSGID搜索对象的消息处理方法。这段话并没有为我们理解DISPATCH方法提供更多的帮助,看来我们必须通过阅读源代码来分析这个函数的运作过程。TOBJECTDISPATCH虽然是个虚方法,但却没有被TPERSISTENT、TCOMPONENT、TCONTROL、TWINCONTROL、TFORM等后续类重载TCOMMONDIALOG调用了TOBJECTDISPATCH,但对于整个VCL消息系统并不重要,并且只由TCONTROLWNDPROC调用过。所以可以简单地认为如果消息没有在WNDPROC中被处理,则被TOBJECTDISPATCH处理。我们很容易查觉到一个很重要的问题MSGID是2个字节,而TMESSAGEMSG是4个字节,如果TCONTROLWNDPROC把TMESSAGE消息传递给DISPATCH方法,是不是会形成错误的消息呢要解释这个问题,必须先了解WINDOWS消息的规则。由于WINDOWS操作系统的所有窗口都使用消息传递事件和信息,MICROSOFT必须制定窗口消息的格式。如果每个程序员都随意定义消息ID值肯定会产生混乱。MICROSOFT把窗口消息分为五个区段0X00000000至WM_USER1标准视窗消息,以WM_为前缀WM_USER至WM_APP1用户自定义窗口类的消息WM_APP至0X0000BFFF应用程序级的消息0X0000C000至0X0000FFFFREGISTERWINDOWMESSAGE生成的消息范围0X00010000至0XFFFFFFFFMICROSOFT保留的消息,只由系统使用WM_USER0X00000400,WM_APP0X00008000发现问题的答案了吗原来应用程序真正可用的消息只有0X00000000至0X0000FFFF,也就是消息ID只有低位2字节是有效的。BORLAND真是牛啊,连这也能想出来。由于INTELCPU的内存存放规则是高位字节存放在高地址,低位字节存放在低地址,所以DISPATCH的MESSAGE参数的第一个内存字节就是LOWORDMESSAGEMSG。下图是MESSAGE参数的内存存放方式描述|MEMORY|HIWORD|LOWORD|C000,调用DEFAULTHANDLER注意这里PUSHEAX保存对象的指针MOVEAX,EAX找到对象的VMT指针CALLGETDYNAMETHOD调用对象的动态方法如果找到了动态方法ZF0,没找到ZF1注GETDYNAMETHOD是SYSTEMPAS中的获得动态方法地址的汇编函数POPEAX恢复EAX为对象的指针JEDEFAULT如果没找到相关的动态方法,调用DEFAULTHANDLERMOVECX,ESI把找到的动态方法指针存入ECXPOPESI恢复ESIJMPECX调用对象的动态方法DEFAULTPOPESI恢复ESIMOVECX,EAX把对象的VMT指针存入ECX,以调用DEFAULTHANDLERJMPDWORDPTRECXVMTOFFSETTOBJECTDEFAULTHANDLERENDTOBJECTDISPATCH的执行过程是把MSGID存入SI,作为动态方法的索引值如果SIC000,则调用DEFAULTHANDLER也就是所有14REGISTERWINDOWMESSAGE生成的消息ID会直接被发送到DEFAULTHANDLER中,后面会讲一个实例检查是否有相对应的动态方法找到了动态方法,则执行该方法没找到动态方法,则调用DEFAULTHANDLER原来以MESSAGE关键字定义的对象方法就是动态方法,随便从TWINCONTROL中抓几个消息处理函数出来PROCEDUREWMSIZEVARMESSAGETWMSIZEMESSAGEWM_SIZEPROCEDUREWMMOVEVARMESSAGETWMMOVEMESSAGEWM_MOVE到现在终于明白WM_SIZE、WM_PAINT方法的处理过程了吧。不但是WINDOWS消息,连DELPHI自己定义的消息也是以同样的方式处理的PROCEDURECMENABLEDCHANGEDVARMESSAGETMESSAGEMESSAGECM_ENABLEDCHANGEDPROCEDURECMFONTCHANGEDVARMESSAGETMESSAGEMESSAGECM_FONTCHANGED所以如果你自己针对某个控件定义了一个消息,你也可以用MESSAGE关键字定义处理该方法的函数,VCL的消息系统会自动调用到你定义的函数。由于DISPATCH的参数只以最前2个字节为索引,并且自MAINWNDPROC到WNDPROC到DISPATCH都是以引用传递地址的方式来传递消息内容,你可以将消息的结构设置为任何结构,甚至可以只有MSGID只要你在处理消息的函数中正确地访问这些参数就行。最关键的DISPATCH方法告一段落,现在让我们看看DEFAULTHANDLER做了些什么TWINCONTROLDEFAULTHANDLERDISPATCHHANDLER是从TOBJECT就开始存在的,它的声明如下PROCEDURETOBJECTDEFAULTHANDLERVARMESSAGEVIRTUAL从名字也可以看出该函数的大概目的最终的消息处理函数。在TOBJECT的定义中DEFAULTHANDLER并没有代码,DEFAULTHANDLER是在需要处理消息的类TCONTROL之后被重载的。从上面的讨论中已经知道DEFAULTHANDLER是由TOBJECTDISPATCH调用的,所以DEFAULTHANDLER和DISPATCH的参数类型一样都是无类型的VARMESSAGE。由于DEFAULTHANDLER是个虚方法,所以执行流程是从子类到父类。在TWINCONTROL和TCONTROL的DEFAULTHANDLER中,仍然遵从WNDPROC的执行规则,也就是TWINCONTROL没处理的消息,再使用INHERITED调用TCONTROLDEFAULTHANDLER来处理。在TWINCONTROLDEFAULTHANDLER中先是处理了一些不太重要的WINDOWS消息,如WM_CONTEXTMENU、WM_CTLCOLORMSGBOX等。然后做了两件比较重要的工作1、处理RM_GETOBJECTINSTANCE消息;2、对所有未处理的窗口消息调用TWINCONTROLFDEFWNDPROC。15下面分别讨论。RM_GETOBJECTINSTANCE是应用程序启动时自动使用REGISTERWINDOWMESSAGEAPI注册的WINDOWS系统级消息ID,也就是说这个消息到达DISPATCH后会无条件地传递给DEFAULTHANDLER见DISPATCH的分析。TWINCONTROLDEFAULTHANDLER发现这个消息就把SELF指针设置为返回值。在CONTROLSPAS中有个函数OBJECTFROMHWND使用窗口句柄获得TWINCONTROL的句柄,就是使用这个消息实现的。不过这个消息是由DELPHI内部使用,不能被应用程序使用。思考每次应用程序启动都会调用REGISTERWINDOWMESSAGE,如果电脑长期不停机,那么0XC0000XFFFF之间的消息ID是否会被耗尽另外,TWINCONTROLDEFAULTHANDLER在TWINCONTROLFHANDLE不为0的情况下,使用CALLWINDOWPROCAPI调用TWNDCONTROLFDEFWNDPROC窗口过程。FDEFWNDPROC是个指针,它是从哪里初始化的呢跟踪一下,发现它是在TWINCONTROLCREATEWND中被设置为如下值FDEFWNDPROCPARAMSWINDOWCLASSLPFNWNDPROC还记得前面讨论的窗口创建过程吗TWINCONTROLCREATEWND函数首先调用TWINCONTROLCREATEPARAMS获得待创建的窗口类的参数。CREATEPARAMS把WNDCLASSLPFNWNDPROC设置为WINDOWS的默认回调函数DEFWINDOWPROCAPI。但CREATEPARAMS是个虚函数,可以被TWINCONTROL的继承类重载,因此程序员可以指定一个自己设计的窗口过程。所以TWINCONTROLDEFAULTHANDLER中调用FDEFWNDPROC的意图很明显,就是可以在WIN32API的层次上支持消息的处理比如可以从C语言写的DLL中导入窗口过程给VCL控件,给程序员提供充足的弹性空间。TWINCONTROLDEFAULTHANDLER最后一行调用了INHERITED,把消息传递给TCONTROL来处理。TCONTROLDEFAULTHANDLER只处理了三个消息WM_GETTEXT、WM_GETTEXTLENGTH、WM_SETTEXT。为什么要处理这个几个看似不重要的消息呢原因是WINDOWS系统中每个窗口都有一个WINDOWTEXT属性,而VCL的TCONTROL为了模拟成窗口也存储了一份保存在FTEXT成员中,所以TCONTROL在此接管这几个消息。TCONTROLDEFAULTHANDLER并没有调用INHERITED,其实也没有必要调用,因为TCONTROL的祖先类都没有实现DEFAULTHANDLER函数。可以认为DEFAULTHANDLER的执行到此为止。VCL的消息流程至此为止。TCONTROLPERFORM和TWINCONTROLBROADCAST现在介绍VCL消息系统中两个十分简单但调用频率很高的函数。TCONTROLPERFORM用于直接把消息送往控件的消息处理函数WNDPROC。PERFORM方法不是虚方法,它把参数重新组装成一个TMESSAGE类型,然后调用WINDOWPROC还记得WINDOWPROC的作用吗,并返回MESSAGERESULT给用户。它的调用格式如下FUNCTIONTCONTROLPERFORMMSGCARDINALWPARAM,LPARAMLONGINTLONGINTPERFORM经常用于通知控件某些事件发生,或得到消息处理的结果,如下例16PERFORMCM_ENABLEDCHANGED,0,0TEXTPERFORMWM_GETTEXTLENGTH,0,0TWINCONTROLBROADCAST用于把消息广播给每一个子控件。它调用TWINCONTROLCONTROLS数组中的所有对象的WINDOWSPROC过程。PROCEDURETWINCONTROLBROADCASTVARMESSAGE注意BROADCAST的参数是无类型的。虽然如此,在BROADCAST函数体中会把消息转换为TMESSAGE类型,也就是说BROADCAST的参数必须是TMESSAGE类型。那么为什么要设计为无类型的消息呢原因是TMESSAGE有很多变体MSG和RESULT字段不会变,WPARAM和LPARAM可设计为其它数据类型,将BROADCAST设计为无类型参数可以使程序员不用在调用前强制转换参数,但调用时必须知道这一点。比如以下字符消息的变体,是和TMESSAGE兼容的TWMKEYPACKEDRECORDMSGCARDINALCHARCODEWORDUNUSEDWORDKEYDATALONGINTRESULTLONGINTENDTWINCONTROLWMPAINT上面在讨论TWINCONTROLWNDPROC时提到,TCONTROL类控件的鼠标和重绘消息是从PARENTTWINCONTROL中产生的。但我们只发现了鼠标消息的产生,那么重绘消息是从哪里产生出来的呢答案是TWINCONTROLWMPAINTPROCEDURETWINCONTROLWMPAINTVARMESSAGETWMPAINTMESSAGEWM_PAINT在TWINCONTROLWMPAINT中建立了双缓冲重绘机制,但我们目前不关心这个,只看最关键的代码IFNOTCSCUSTOMPAINTINCONTROLSTATEANDCONTROLCOUNT0THENINHERITED/注意INHERITED的实现ELSEPA
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025党政领导干部拟任县处级资格考试题及答案
- 2025临床执业医师资格考试模拟试题及答案解析
- 建筑施工合同签订前的合同主体资格审查与合规性审查
- 个人租房合同模板:带租赁保险条款的租赁协议
- 环保自主验收意见模板编制与审核流程合同
- 离婚协议书翻译与跨国离婚法律程序指导合同
- 离婚房产分割与共同财产分割及子女抚养权协议
- 公务员考试题及答案(逻辑推理)
- 知识产权保护与科技成果转化交流合作协议
- 个人股份转让协议书4篇
- 输液并发症静脉炎课件
- 综艺脱口秀节目创意策划及实施方案
- 浪浪山小妖怪-2025~2026学年美术开学第一课《浪浪山小妖怪》
- (2025年标准)盆景购销协议书
- 设计合同结算协议书范本
- 2025广东湛江市廉江市政协办公室等7个单位招聘政府雇员9人笔试参考题库附答案解析
- (2025年标准)婚后债务分离协议书
- 2025广东河源紫金县殡仪馆招聘编外人员2人笔试参考题库附答案解析
- 2025四川南充营山县医疗卫生辅助岗招募39人考试参考题库附答案解析
- 看守所巡控岗位课件
- AIGC艺术设计 课件全套 第1-8章 艺术设计的新语境:AI的介入 -AIGC艺术设计的思考与展望
评论
0/150
提交评论