深入解析MFC消息相应和消息路由

1.MFC中的消息分为三种

(1)标准消息,也叫窗口消息(例:WM_PAINT,WM_CREATE,WM_LBUTTONDOWN,WM_CHAR)

(2)命令消息,来自菜单,工具栏和加速键,都以WM_COMMAND表示

(3)控件消息,控件消息又分为三小类,第一类和标准消息格式一样,第二类和命令消息格式一样(不过多了一个控件窗口的句柄),第三类是WM_NOTIFY.其具体细节不是本文叙述的重点.

2.为什么要消息路由?什么叫消息路由?

  如果像SDK那样,我们的程序只有一个窗口,一个窗口函数,那哪还有消息路由呢?所有的消息都有一个窗口函数来处理了。至所以要消息路由,是因为MFC程序中有CMyView,CMyDoc,CMyFrameWnd,CMyApp等,MFC框架要做的工作是给用户提供一个机会,让用户可以选择这些类当中的任意一个来处理我们的命令消息。

  注意,消息路由主要是针对上述的第二类消息(命令消息)。对于第一类窗口消息,其消息的接收者是确定的,不需要路由。比如:对于WM_CREATE消息,处理这个消息的类就是产生这个消息的窗口,你不可能让CMyDoc,CMyFrame,CMyApp去处理CView的WM_CREATE消息,那根本不符合逻辑,MFC框架当然也不会让你那么做。而对于来自菜单,工具栏的命令消息,用户是有选择权的,用户可以选择其接收者为View,Doc,App,Frame等当中的任意一个。下面就详细说一下命令消息的路由过程,主要通过分析MFC源代码。

3.首先说一下函数的调用顺序.所有的三类消息初始都被送入一个全局函数AfxWndProc,

之后是pWnd->WindowProc,pWnd->OnWndMsg,在OnWndMsg()中这三类消息分道扬镳了,其中第一类消息由OnWndMsg自己处理,第二类交给了OnCommand(),第三类交给了OnNotify(),下面主要说第二类的处理过程:

 AfxWndProc()

AfxCallWndProc

pWnd->WindowProc(注意,这里的pWnd指向的是产生消息的那个窗口,可能是CMyView,CMyFrameWnd等)

pWnd->OnWndMsg()

pWnd->OnCommand()

....

注意:WindowProc和OnWndMsg这两个函数实际只有CWnd类中才有,在其它类中并没有重写这两个虚函数,所以我们调用的是CWnd::WindowProc()和CWnd::OnWndMsg(),但要注意:它们的this指针是指向我们的程序中的具体类对象(这是下面运用多态的前提)。

下面以多文档为例,解析其对第二类消息的处理过程:

对于多文档来说,命令消息都来自我们自己的主框架窗口(CMyFrameWnd),因为菜单是属于主框架窗口。

AfxWndProc-->CWnd::WindProc()-->CWnd::OnWndMsg()(此处三类消息分道扬镳)-->CMyFrameWnd::OnCommand(不存在)-->CMDIFrameWnd::OnCommand(),

下面是CMDIFrameWnd::OnCommand()的源代码:

{

CMDIChildWnd* pActiveChild = MDIGetActive();

if (pActiveChild != NULL && AfxCallWndProc(pActiveChild,          (1)

  pActiveChild->m_hWnd, WM_COMMAND, wParam, lParam) != 0)

return TRUE; // handled by child

if (CFrameWnd::OnCommand(wParam, lParam))(2)

return TRUE; // handled through normal mechanism (MDI child or frame)

.....}

父框架窗口是先认子框架窗口来处理,如果其不处理,再自己处理

  那子框架窗口又是如何处理的呢?由于子框架窗口没有重写上面的WindowProc,OnWndMsg,OnCommand,所以最终调用的是其父类CFrameWnd::OnCommand().注意:这里虽然调用了两次CFrameWnd::OnCommand,

但其this指针却不同,一个this指针指向的是CMyChildWnd,一个指向的是CFrameWnd.下面是CFrameWnd::OnCommand()源代码:

{...

CMyView* pView=GetActiveView()

pView->OnCmdMsg() (1.1)

CWnd::OnCmdMsg(); (1.2)

CMyApp* pApp=(CMyApp*)AfxGetApp(); (1.3)

pApp->OnCmdMsg();

.....}

子框架窗口是先让CMyView处理,如果其不处理,再自己处理(CWnd::OnCmdMsg),否则,再 App处理.

  那CMyView又是如何处理的呢,下面是其CView::OnCmdMsg()源代码

{

CWnd::OnCmdMsg(); (1.1.1)

m_pDocument->OnCmdMsg(); (1.1.2)

}

CMyView是先让自己处理,如果其不处理,再让其对应的Document处理,而Document是先自己处理,如果自己不处理,现让CDocTemplate处理(此处这一代码就不在写出,因为通常不会让文档模板处理)

  所谓自己处理,就是调用CCmdTarget::OnCmdMsg(),CWnd并没有重写这个函数,调用CWnd::OnCmdMsg就是调用CCmdTarget::OnCmdMsg(),这个函数的主要干的事情就是:调用GetMessageMap()这个虚函数,获得我们的子类的消息映射表,然后把该消息和此消息映射表对照,看其是否有对应的消息响应函数。如果没有,再看其父类是否有,父类也没有,就返回false,让其它的类来处理了。

通过以上函数调用和返回顺序,可以总结出其消息处理顺序:首先命令消息来自主框架窗口,它把消息交给了子框架窗口,子框架窗口又交给了View,View自己处理,否则就交给文档,所以最终的顺序是:

CMyView--->CMyDoc-->CMultiDocTemplate(它通常不会处理)-->CMyChildFrame-->CMyApp->CMyFrameWnd

注意:这里App先于MainFrame,在有些书上说成是App最后(特纠正)

对于单文档,那就比这简单了,

CMyView-->CMyDoc-->CSingleDocTemplate-->CMyFrame-->CMyApp

注意:在单文档中CMyApp是最后,因为这里的主框架实际占据的是MDI中子框架的位置。




 

GetMessage和PeekMessage的内部机制  2010-12-15 19:18:02|  分类: C/C++/VC编程 |  标签: |字号大中小 订阅 .

译者的话
该文重点讲述了Windows处理事件、消息的具体过程和步骤。尤其是在系系处理鼠标键盘事件的过程上做了详解。通过这篇文章,你将对Windows的消息处理机制有一个较全面的了解。

概念
这篇文章解释了GetMessage和PeekMessage的内部运作方式,同时也是一类与“消息及消息在16位 MS-DOS®/Microsoft® Windows™环境之下的影响”相关文章的基础。我们将讨论下面这些主题:

?系统和应用程序队列(译者注:以下简称为“程序队列”)
?GetMessage和PeekMessage函数
?消息过滤
?WM_QUIT消息
?让步和休眠
?让步的问题
?WaitMessage
    16位MS-DOS/Windows环境和32位Win32™/Windows NT™环境有些很重要的不同之处。虽然这些不同之处在这儿无法被忽视,但我们还是把它们做为遗留问题,由以后的文章去解释吧。


队列
    要理解GetMessage和PeekMessage的运作,必须首先明白Microsoft® Windows™操作系统是如何储存事件和消息的。在Windows中有两种类型的队列为此目的工作,它们分别是系统队列和消息队列。

硬件输入:系统队列
    Windows有一些驱动程序,它们负责响应来自于键盘和鼠标等硬件的中断服务。在中断时间中,键盘和鼠标驱动程序会调用USER.EXE中指定的一些入口点去报告一个事件的发生。在Windows中服务于光笔计算的光笔驱动程序,同样会在原始的光笔事件中调用这些入口点。 
    在Windows3.1中,系统队列是一个有着120个入口空间的定长的队列。在一般情形下这些“小房间”是足够了,但如果应用程序挂起了或者在一段长的时间里没有及时处理任何消息就可能导致系统队列被填满。如果真的发生了,任何尝试添加到系统队列的新事件都将会引起系统蜂鸣。(译者注:在DOS中,如果一个程序在一段时间内占用了所有的系统资源,使机器无法响应,这时如果你按住一个键不放,你就会听到机箱喇叭嘀嘀作响)

发送的消息和程序队列
    当一个应用程序开始时,一个队列将会因此而被创建。程序队列(有时会称为任务队列)常常用于储存“正在被发往应用程序的一个窗口” 的消息。唯一常驻程序队列的消息是那些由PostMessage或PostAppMessage明确发送的消息。(SendMessage从不使用系统队列)PostQuitMessage函数不会发送一个消息到程序队列。(WM_QUIT消息将在下文中论讨)
    默认的,每个程序队列可以保持八个消息。一般情况下这是相当足够的,因为PostMessage极少被使用。但是如果一个应用程序试图强制调用很多的PostMessage到某个应用程序时,那么这类应用程序将会用使用SetMessageQueue函数来增加消息队列的长度。你必须小心的使用SetMessageQueue函数,因为它无论何时都会先删掉当前的程序队列,并创建一个预期大小的新队列,此时任何在旧队列中的消息都会被销毁。因此,它必须在你的WinMain例程中在所有其它的应用程序编程接口(API)之前调用或在应用程序用PeekMessage明确的清除队列之后调用。

GetMessage和PeekMessage是怎样工作的
    在Windows的内部,GetMessage和PeekMessage执行着相同的代码。而两者最大的不同之处则体现在没有任何消息返回到应用程序的情况下。在此种情况下,PeekMessage会返回一个空值到应用程序,GetMessage会在此时让应用程序休眠。在它们之间还有一些其它的不同,我们将会在下面讨论,但它们相当次要。

GetMessage和PeekMessage逻辑

下面一步步的讲述了在Windows3.1版的GetMessage和PeekMessage公用代码。

    提示:下面所示步骤按照消息类型的优先权进行排序。举个例子,发送的消息总在键盘和鼠标消息之前被返回,而键盘和鼠标的消息又会在绘图(paint)消息之前反回,以此类推。

1. 检视在为“活动中任务”服务的程序队列中是否有消息的存在。如果是,首先在队首删除此消息并将其返回到应用程序。然后,应用程序中的GetMessage和PeekMessage会调用一些代码,用以从程序队列中接收此消息,这些代码是由该应用程序调用的动态链接库(DLL)生成的。记住,只有由PostMessage发送的消息会常驻于此队列中。

2. 与所有消息和窗体句柄过滤器进行对照,核查此消息。如果此消息不匹配指定的过滤器,就会把此消息留在程序队列中。如果队列中在此消息的后面还有其它消息,则会转向对下一个消息的处理。

3. 如果在程序队列中没有消息了,就扫描系统队列中的事件。这个过程相当复杂,并且我们将在下面的“扫描系统队列”小节中XX。一般来讲,在系统队列首部的事件是供这个应用程序所使用的,系统会将其转化为消息,并将消息返回到这个应用程序中(它不会首先被置于应用队列中)。注意,这个扫描系统队列的过程可能导致当前活动的应用程序将控制权让给其它的应用程序。

4. 如果在系统队列中没有等待处理的事件,则核查所有与当前应用程序(任务)相关的窗体以确定更新区域。当一个窗体的一部分需要被重绘时,一个更新区域就被创建在那个窗体部分之上。这个区域将与此窗体中现存的所有更新区域相结合,并储存在内部窗体结构体中。如果GetMessage或PeekMessage在这个任务中发现某些窗体有一些未处理的更新区域,将产生一个WM_PAINT消息,并为那个窗体返回到应用程序中。WM_PAINT从不驻留在任何队列中。此时,一个应用程序将为某个窗体不断的接收WM_PATIN消息,直到更新区域由BeginPaint/EndPaint,ValidateRect,或ValidateRgn所清除。

5. 如果这个任务中没有任何窗体需要被更新,GetMessage和PeekMessage就会在这一点让出控制权,除非PeekMessage调用被设置为PM_NOYIELD属性。

6. 当让步返回时,检视在当前任务中是否有计时器到期。如果是,创建一个WM_TIMER消息并返回。它不但发生在“返回一个WM_TIMER消息到窗体”的计时器上,同样也发生在“调用一个计时器处理过程”的计时器上。如要了解更多信息,请看在微软开发者网络(MSDN)光盘(包括技术文章、Windows文章、核心和驱动程序文章)中的文章“Timers and Timing in Microsoft Windows”(译者注:如果读者能够认可我的工作,我会不遗余力地翻译这篇关于计时器的文章)。

7. 如果这个应用程序没有计时器事件服务,并且一个应用程序正在被终止,代码将尝试去缩小图形设备界面(GDI)的本地内存堆。一些应用程序,比如绘图应用程序(像Paintbrush™),为GDI分配了大量的堆内存。当应用程序终止时释放这些对象时,会使GDI本地内存堆被空闲空间填满而膨胀。为了恢复这些空闲的空间, 在GetMessage/PeekMessage处理中,LocalShrink将在这一点被调用于GDI的内存堆。这个被完成一次,(每次)一个应用程序将终止。

8. 在这一时刻,代码将分叉为两条路,一是代码任意的返回一个有效的消息,另一个是完全没有这个应用程序去处理的消息、事件,而代码最终会走哪条路决定于PeekMessage和GetMessage中的哪一个被调用。


PeekMessage. 
    如果PeekMessage被调用,并设置了PM_NOYIELD标记,PeekMessage在此刻返回一个空值,这个空返回值指出已经没有要处理的消息了。如果没有设置PM_NOYIELD标记,PeekMessage就在此刻让出控制权。它不会休眠,但会单一的交给其它已准备好的应用程序一个执行的机会。(请参阅下面的“让步与休眠的不同)当让步返回,PeekMessage直接将控制权返回到应用程序,并返回一个空值,它指出这个应用程序没有要处理的消息了。

GetMessage. 
    在此刻,GetMessage会让应用程序休眠、等待,直到一些事件发生需要唤醒应用程序。控制权不会返回到调用GetMessage的应用程序,直到有应用程序必须去处理的消息出现。一旦这个应用程序从被置入休眠状态中醍来,GetMessage内部的循环将回到最开始(步骤1)。

WH_GETMESSAGE钩子
    在GetMessage和PeekMessage将一个消息返回到调用的应用程序之前,会做一个验证是否存在一个WH_GETMESSAGE钩子的测试。如果有一个已经被安装了,那这个钩子会被调用。如果PeekMessage没有发现可用的消息并返回一个空值时,这个钩子将不会被调用。在钩子处理过程中,你不可能得知是到底是GetMessage被调用还是PeekMessage被调用。

扫描系统队列
    综上所述,在系统队列中的事件仅仅是硬件事件的记录。那些代码扫描系统队列的主要任务是,从这些事件中创建消息,并确定哪一个窗体将接收这个消息。
    代码第一次在系统队列首部找到事件时,并不会马上将其删除。因为鼠标和键盘事件只是队列中的两种事件,而代码会分枝(译者注:类似于C语言中的switch语句)并单独处理每一种类型的事件。

处理系统队列中的鼠标事件
下面是处理鼠标事件的步骤。

1. 首先,将计算该事件屏幕坐标的相应窗体。此计算(调用窗体点击测试)以桌面窗体开始,从头至尾的扫描细统中的每一个窗体(包括子窗体),直到找到一个包含这个鼠标坐标点的窗体,并且这个窗体没有任何同样包含这个坐标点的子窗体。 
例如:如果图2中的箭头代表当前的鼠标位置,任何的鼠标行为,像单击鼠标键,将生成一个会在B窗体中产生消息的事件。

2. 如果一个窗体使用SetCapture捕获鼠标,那么“系统队列扫描”代码将通过普通的点击测试,并将所有的鼠标消息返回到捕获的窗体。例如:如果在图2 中的A窗体调用了SetCapture,则在箭头所指位置的所有鼠标行为,将产生窗体A中的消息,而不是窗体B。

3. 如果这个被处理的事件是一个“鼠标键按下”事件(任何一个鼠标键),代码会检测这个事件是否会转化为双击事件。你可以在微软开发者网络(译者注:MSDN)CD(技术文章,Ask Dr. GUI)中的“Ask Dr. GUI #5”中找到关于双击转化的描述。实质上,如果在两次鼠标键按下事件中,时间和距离的增量在允许的范围之中,该事件将会生成一个双击消息,否则它将生成一个标准的“按下”事件。所有的鼠标事件都将生成标准的鼠标消息,而双击测试只在鼠标事件指定的,包含CS_DBLCLKS类型的窗体中进行。

4. 一个消息从鼠标事件中构造出来。

5. 如果鼠标点击测试确定该事件发生在一个窗体的非客户区,如边框或标题栏,那么该构造出的消息映射到它相应的非客户区消息中。例如:一个WM_MOUSEMOVE事件会被映谢为WM_NCMOUSEMOVE消息。

6. 与所有指定的消息过滤器进行对照,核查此消息。(请参阅下面的“消息范围过滤和窗体句柄过滤”)如果该消息不匹配过滤器,则重新从头开始“系统队列扫描”代码,查看队列中的下一个消息。

7. 如果鼠标消息需要前往与当前任务不同的另一个任务的相关窗体,事件会被留在系统队列中,并且如果那个将会处理这个消息的任务在休眠之中,会被唤醒。这个新近被唤醒的任务不会在此刻立即运行,只会标记为准备运行。如果消息前往了其它任务,并且在系统队列中没有要处理的事件被发现,“系统队列扫描”会代码返回到GetMessage/PeekMessage主代码。请参阅下面的“让步与休眠的不同”以获得更多的信息。

8. 如果安装了鼠标钩子,它将在此刻被调用。如果鼠标钩子返回了一个非零值,那么该鼠标事件被忽略,并从系统队列中被删除,然后重新从头开始“系统队列扫描”代码。如果钩子返回零,则继续处理。

9. 如果消息是一个“鼠标键按下”消息,“系统队列扫描”则会在返回此消息之前,按照下面的方法激活窗体。
它沿着父链一直向上寻找该窗体的“最终顶层父窗体”,直到相遇。
它用SendMessage向该窗体的“最终顶层父窗体”发送一个WM_MOUSEACTIVATE消息。
从WM_MOUSEACTVATE返回的值将在下面被测试:
a)       如果返回的值为空、MA_ACTIVATE或者MA_ACTIVATEANDEAT,ActivateWindow函数将被调用去激活那个“最终顶层父窗体”。
b)      如果返回的值是MA_NOACTIVATE或者MA_NOACTIVATEANDEAT,窗体则不被激活。
注意:MA_ACTIVATEANDEAT和MA_NOACTIVATEANDEAT会导致“鼠标键按下”事件从系统队列中被删除,而不会生成一个鼠标按下消息。
c)      最终,一个WM_SETCURSOR消息被发送到窗体,充许窗体设置指针的轮廓。

10. 如果鼠标钩子被调用,并且当前的鼠标事件从系统队列中被删除了,则检查“基于计算机训练”(CBT)的钩子。如果安装有有一个CBT钩子,将会携带HCBT_CLICKSKIPPED钩子码代调用它。

11. 按键状态表包含了三个用于跟踪鼠标按键状态的入口。这些按键被分配予虚拟键代码(VK_LBUTTON,VK_RUTTON和VC_MBUTTON),它们和GetKeyState一起始用去确事实上鼠标键是弹起还是按下。在返回鼠标消息之前,“系统队列扫描”代码会(为弹起或按下消息)设置按键状态表并且从系统队列中删除消息。如果PeekMessage被调用时携带PM_NOREMOVE,则按键状态表不会被修改。

处理系统队列中的键盘事件
1. 检查是否Ctrl键被按下和当前的事件是否ESC键按。如果是,该用户――直接窗体――会显示任务管理器窗体。一个WM_SYSCOMMAND消息将被发送到激活的窗体,并且参数wParam为SC_TASKLIT。然后键盘按下事件从系统队列中被删除,“系统队列扫描”代码又将重新从头开始。如果此激活的窗体是一个系统模块或者是一个被显示出来的“硬”系统模块消息框(比如一个“INT”24小时系统错误消息框,或一个使用MB_ICONHAND和MB_SYSTEMMODAL参数的MessageBox函数)的事件,将会被抛弃。

2. 下一步,试着去查看当前的事件是不是一个Print Screen键的按下事件。如果是,任意一个激活的窗体或整个桌面将被做为一个位图快照,保存到剪贴板中。如果Alt键被按下,一幅激活窗体的图像被复制到剪贴板中;如果没有,则是整个桌面被复制。然后Print Screen键按下事件从系统队列中被删除,“系统队列扫描”代码又将重新从头开始。如果显示了一个“硬”系统模块消息框,则此操作被忽略。

3. 下一步检测热键。使用程序管理器,用户可以定义用来运行一个应用程序的击键事件。这些击键被称为热键。如果当前的事件是一个按键事件,将会被测试是否与定义过的热键匹配。如果发现匹配,一个WM_SYSCOMMAND消息将被发送到激活的窗体,并且参数wParam为SC_HOTKEY。然后键盘按下事件从系统队列中被删除,“系统队列扫描”代码又将重新从头开始。如果此激活的窗体是一个系统模块或者是一个被显示出来的“硬”系统模块消息框,该测试被跳过。

4. 一般情况下,所有的键盘消息(如WM_KEYDOWN、WM_CHAR等等)前往具有输入焦点的窗体。如果这个具有输入焦点的窗体与另一个当前执行的任务相关联,那么该事件会被留在系统队列中,并且那个拥有“有焦点的窗体”的任务会被唤醒(如果休眠了)。“系统队列扫描”代码会像没要发现任何要处理的事件一样,返回到主GetMessage/PeekMessage代码。请参阅下面的“让步与休眠的不同”和“应用程序如何被唤醒”以获得更多的信息。

5. 如果遇到了没有任何一个窗体具有输入焦点的情形,键盘消息会直接前往当前激活的窗体,而不会被翻译成为系统键消息(如WM_SYSKEYDOW,WM_SYSCHAR,等等)。

6. 与所有指定的消息过滤器进行对照,核查此消息。(请参阅下面的“消息范围过滤和窗体句柄过滤”)如果该消息不匹配过滤器,则重新从头开始“系统队列扫描”代码,查看队列中的下一个消息。

7. 如果事件被返到了当前的任务,它将从系统队列中被删除掉,除非PeekMessage被指定为PM_NOREMOVE标记。请参阅下面的“PeekMessage的PM_NOREMOVE标记”以了解更多的关于不从队列中删除事件的信息。

8. 如果安装有键盘钩子,将在此刻被调用。如果事件从系统队列中被删除了,钩子的调用将伴随HC_ACTION属性;如果事件未被从系统队列中删除,钩子的调用将具有HC_NOREM属性。

9. 如果键盘钩子被调用,并且当前的按键事件从系统队列中被删除了,则检查现存的CBT钩子。如果安装有CBT钩子,将调用它并携带HCBT_KEYSKIPPED钩子码。

10. 最后,消息被返加到主GetMessage/PeekMessage代码。

PeekMessage与PM_NOREMOVE
    默认情况下,每一个消息被返回到应用程序后,PeekMessage和 GetMessage都会把消息和事件从系统队列中删除。然而有些时候,某个应用程序可能需要扫描队列中现存的消息而并不删除它们。例如,某个应用程序在做一些处理过程,这些处理过程期望“一但发现有可用的消息,就尽快终止”。


深入探讨MFC消息循环和消息泵

首先,应该清楚MFC的消息循环(::GetMessage,::PeekMessage),消息泵(CWinThread::PumpMessage)和 MFC的消息在窗口之间的路由是两件不同的事情。在MFC的应用程序中(应用程序类基于CWinThread继承),必须要有一个消息循环,他的作用是从应用程序的消息队列中读取消息,并把它派送出去(::DispatchMessage)。而消息路由是指消息派送出去之后,系统(USER32.DLL) 把消息投递到哪个窗口,以及以后消息在窗口之间的传递是怎样的。

消息分为队列消息(进入线程的消息队列) 和非队列消息(不进入线程的消息队列)。对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息;还有例如:WM_PAINT、WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由Windows系统负责把消息加入到相应线程的消息队列中,于是就有了消息循环(从消息队列中读取并派送消息)。还有一种是非队列消息,他绕过系统队列和消息队列,直接将消息发送到窗口过程。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。创建窗口时发送WM_CREATE消息。在后面你将看到,MS这么设计是很有道理的,以及他的整套实现机制。

这里讲述MFC的消息循环,消息泵。先看看程序启动时,怎么进入消息循环的:
_tWinMain ->AfxWinMain ->AfxWinInit ->CWinThread::InitApplication ->CWinThread::InitInstance ->CWinThread::Run

非对话框程序的消息循环的事情都从这CWinThread的一Run开始...

第一部分:非对话框程序的消息循环机制。

//thrdcore.cpp
// main running routine until thread exits
int CWinThread::Run()
{
ASSERT_VALID(this);

// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;

// acquire and dispatch messages until a WM_QUIT message is received.
for (;;)
{
// phase1: check to see if we can do idle work
while (bIdle &&
   !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
{
   // call OnIdle while in bIdle state
   if (!OnIdle(lIdleCount++))
    bIdle = FALSE; // assume "no idle" state
}

// phase2: pump messages while available
do
{
   // pump message, but quit on WM_QUIT
   if (!PumpMessage())
    return ExitInstance();

   // reset "no idle" state after pumping "normal" message
   if (IsIdleMessage(&m_msgCur))
   {
    bIdle = TRUE;
    lIdleCount = 0;
   }

} while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
}    //无限循环,退出条件是收到WM_QUIT消息。

ASSERT(FALSE); // not reachable
}

这是一个无限循环,他的退出条件是收到WM_QUIT消息:

if (!PumpMessage())
    return ExitInstance();

在PumpMessage中,如果收到WM_QUIT消息,那么返回FALSE,所以ExitInstance()函数执行,跳出循环,返回程序的退出代码。所以,一个程序要退出,只用在代码中调用函数

VOID PostQuitMessage( int nExitCode )。指定退出代码nExitCode就可以退出程序。

下面讨论一下这个函数Run的流程,分两步:

1, 第一个内循环phase1。bIdle代表程序是否空闲。他的意思就是,如果程序是空闲并且消息队列中没有要处理的消息,那么调用虚函数OnIdle进行空闲处理。在这个处理中将更新UI界面(比如工具栏按钮的enable和disable状态),删除临时对象(比如用FromHandle得到的对象指针。由于这个原因,在函数之间传递由FromHandle得到的对象指针是不安全的,因为他没有持久性)。OnIdle是可以重载的,你可以重载他并返回 TRUE使消息循环继续处于空闲状态。

NOTE:MS用临时对象是出于效率上的考虑,使内存有效利用,并能够在空闲时自动撤销资源。关于由句柄转换成对象,可以有若干种方法。一般是先申明一个对象obj,然后使用obj.Attatch来和一个句柄绑定。这样产生的对象是永久的,你必须用obj.Detach来释放对象。

2,第二个内循环phase2。在这个循环内先启动消息泵(PumpMessage),如果不是WM_QUIT消息,消息泵将消息发送出去(::DispatchMessage)。消息的目的地是消息结构中的hwnd字段所对应的窗口。
//thrdcore.cpp
BOOL CWinThread::PumpMessage()
{
ASSERT_VALID(this);

//如果是WM_QUIT就退出函数(return FALSE),这将导致程序结束.
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
#ifdef _DEBUG
   if (afxTraceFlags & traceAppMsg)
    TRACE0("CWinThread::PumpMessage - Received WM_QUIT.\n");
   m_nDisablePumpCount++; // application must die
    // Note: prevents calling message loop things in 'ExitInstance'
    // will never be decremented
#endif
   return FALSE;
}

#ifdef _DEBUG
if (m_nDisablePumpCount != 0)
{
   TRACE0("Error: CWinThread::PumpMessage called when not permitted.\n");
   ASSERT(FALSE);
}
#endif

#ifdef _DEBUG
if (afxTraceFlags & traceAppMsg)
   _AfxTraceMsg(_T("PumpMessage"), &m_msgCur);
#endif

// process this message

if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
   ::TranslateMessage(&m_msgCur); //键转换
   ::DispatchMessage(&m_msgCur); //派送消息
}
return TRUE;
}

在这一步有一个特别重要的函数大家一定认识:PreTranslateMessage。这个函数在::DispatchMessage发送消息到窗口之前,进行对消息的预处理。PreTranslateMessage函数是CWinThread的成员函数,大家重载的时候都是在View类或者主窗口类中,那么,它是怎么进入别的类的呢?代码如下:
//thrdcore.cpp
BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
ASSERT_VALID(this);

// 如果是线程消息,那么将会调用线程消息的处理函数
if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg))
   return TRUE;

// walk from target to main window
CWnd* pMainWnd = AfxGetMainWnd();
if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg))
   return TRUE;

// in case of modeless dialogs, last chance route through main
//   window's accelerator table
if (pMainWnd != NULL)
{
   CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd);
   if (pWnd->GetTopLevelParent() != pMainWnd)
    return pMainWnd->PreTranslateMessage(pMsg);
}

return FALSE;   // no special processing
}

由上面这个函数可以看出:

第一,如果(pMsg->hwnd == NULL),说明这是一个线程消息。调用CWinThread::DispatchThreadMessageEx到消息映射表找到消息入口,然后调用消息处理函数。

NOTE: 一般用PostThreadMessage函数发送线程之间的消息,他和窗口消息不同,需要指定线程id,消息激被系统放入到目标线程的消息队列中;用 ON_THREAD_MESSAGE( message, memberFxn )宏可以映射线程消息和他的处理函数。这个宏必须在应用程序类(从CWinThread继承)中,因为只有应用程序类才处理线程消息。如果你在别的类(比如视图类)中用这个宏,线程消息的消息处理函数将得不到线程消息。

第二,消息的目标窗口的 PreTranslateMessage函数首先得到消息处理权,如果函数返回FALSE,那么他的父窗口将得到消息的处理权,直到主窗口;如果函数返回 TRUE(表示消息已经被处理了),那么就不需要调用父类的PreTranslateMessage函数。这样,保证了消息的目标窗口以及他的父窗口都可以有机会调用PreTranslateMessage--在消息发送到窗口之前进行预处理(如果自己处理完然后返回FALSE的话 -_-b),如果你想要消息不传递给父类进行处理的话,返回TRUE就行了。

第三,如果消息的目标窗口和主窗口没有父子关系,那么再调用主窗口的PreTranslateMessage函数。为什么这样?由第二步知道,一个窗口的父窗口不是主窗口的话,尽管它的 PreTranslateMessage返回FALSE,主窗口也没有机会调用PreTranslateMessage函数。我们知道,加速键的转换一般在框架窗口的PreTranslateMessage函数中。我找遍了MFC中关于加速键转换的处理,只有CFrameWnd, CMDIFrameWnd,CMDIChildWnd等窗口类有。所以,第三步的意思是,如果消息的目标窗口(他的父窗口不是主窗口,比如一个这样的非模式对话框)使消息的预处理继续漫游的话(他的PreTranslateMessage返回FALSE),那么给一次机会给主窗口调用 PreTranslateMessage(万一他是某个加速键消息呢?),这样能够保证在有非模式对话框的情况下还能保证主窗口的加速键好使。
我做了一个小例子,在对话框类的PreTranslateMessage中,返回FALSE。在主窗口显示这个非模式对话框,在对话框拥有焦点的时候,仍然能够激活主窗口的快捷键。

总之,整个框架就是让每个消息的目标窗口(包括他的父窗口)都有机会参与消息到来之前的处理。呵呵~

至此,非对话框的消息循环和消息泵的机制就差不多了。这个机制在一个无限循环中,不断地从消息队列中获取消息,并且保证了程序的线程消息能够得到机会处理,窗口消息在预处理之后被发送到相应的窗口处理过程。那么,还有一点疑问,为什么要一会儿调用::PeekMessage,一会儿调用:: GetMessage呢,他们有什么区别?

NOTE:一般来说,GetMessage被设计用来高效地从消息队列获取消息。如果队列中没有消息,那么函数GetMessage将导致线程休眠(让出CPU时间)。而PeekMessage是判断消息队列中如果没有消息,它马上返回0,不会导致线程处于睡眠状态。

在上面的phase1第一个内循环中用到了PeekMessage,它的参数PM_NOREMOVE表示并不从消息队列中移走消息,而是一个检测查询,如果消息队列中没有消息他立刻返回0,如果这时线程空闲的话将会引起消息循环调用OnIdle处理过程(上面讲到了这个函数的重要性)。如果将:: PeekMessage改成::GetMessage(***),那么如果消息队列中没有消息,线程将休眠,直到线程下一次获得CPU时间并且有消息出现才可能继续执行,这样,消息循环的空闲时间没有得到应用,OnIdle也将得不到执行。这就是为什么既要用::PeekMessage(查询),又要用::GetMessage(做实际的工作)的缘故。

 

 

 

第二部分: 对话框程序的消息循环机制

基于对话框的MFC工程和上面的消息循环机制不一样。实际上MFC的对话框工程程序就是模式对话框。他和上面讲到的非对话框程序的不同之处,主要在于应用程序对象的InitInstance()不一样。

//dlg_5Dlg.cpp
BOOL CDlg_5App::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls();    // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif

CDlg_5Dlg dlg; //定义一个对话框对象
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal(); //对话框的消息循环在这里面开始
if (nResponse == IDOK)
{
   // TODO: Place code here to handle when the dialog is
   // dismissed with OK
}
else if (nResponse == IDCANCEL)
{
   // TODO: Place code here to handle when the dialog is
   // dismissed with Cancel
}

// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}

NOTE: InitInstance函数返回FALSE,由最上面程序启动流程可以看出,CWinThread::Run是不会得到执行的。也就是说,上面第一部分说的消息循环在对话框中是不能执行的。实际上,对话框也有消息循环,她的消息循环在CDialog::DoModal()虚函数中的一个 RunModalLoop函数中。

这个函数的实现体在CWnd类中:
int CWnd::RunModalLoop(DWORD dwFlags)
{
ASSERT(::IsWindow(m_hWnd)); // window must be created
ASSERT(!(m_nFlags & WF_MODALLOOP)); // window must not already be in modal state

// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) && !(GetStyle() & WS_VISIBLE);
HWND hWndParent = ::GetParent(m_hWnd);
m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
MSG* pMsg = &AfxGetThread()->m_msgCur;

// acquire and dispatch messages until the modal state is done
for (;;)
{
   ASSERT(ContinueModal());

   // phase1: check to see if we can do idle work
   while (bIdle &&
    !::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
   {
    ASSERT(ContinueModal());

    // show the dialog when the message queue goes idle
    if (bShowIdle)
    {
     ShowWindow(SW_SHOWNORMAL);
     UpdateWindow();
     bShowIdle = FALSE;
    }

    // call OnIdle while in bIdle state
    if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != NULL && lIdleCount == 0)
    {
     // send WM_ENTERIDLE to the parent
     ::SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
    }
    if ((dwFlags & MLF_NOKICKIDLE) ||
     !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
    {
     // stop idle processing next time
     bIdle = FALSE;
    }
   }

// phase2: pump messages while available
   do
   {
    ASSERT(ContinueModal());

    // pump message, but quit on WM_QUIT
   //PumpMessage(消息泵)的实现和上面讲的差不多。都是派送消息到窗口。
    if (!AfxGetThread()->PumpMessage())
    {
     AfxPostQuitMessage(0);
     return -1;
    }

    // show the window when certain special messages rec'd
    if (bShowIdle &&
     (pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
    {
     ShowWindow(SW_SHOWNORMAL);
     UpdateWindow();
     bShowIdle = FALSE;
    }

    if (!ContinueModal())
     goto ExitModal;

    // reset "no idle" state after pumping "normal" message
    if (AfxGetThread()->IsIdleMessage(pMsg))
    {
     bIdle = TRUE;
     lIdleCount = 0;
    }

   } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
} //无限循环

ExitModal:
m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
return m_nModalResult;
}

先说说怎么退出这个无限循环,在代码中:
if (!ContinueModal())
goto ExitModal;
决定是否退出循环,消息循环函数返回也就是快要结束结束程序了。
BOOL CWnd::ContinueModal()
{
return m_nFlags & WF_CONTINUEMODAL;
}

NOTE: CWnd::ContinueModal()函数检查对话框是否继续模式。返回TRUE,表示现在是模式的;返回FALSE,表示对话框已经不是模式(将要结束)。

如 果要结束对话框,在内部最终会调用函数CWnd::EndModalLoop,它取消m_nFlags的模式标志(消息循环中的 ContinueModal函数将返回FALSE,消息循环将结束,程序将退出);然后激发消息循环读取消息。也就是说,结束模式对话框是一个标志,改变这个标志就可以了。他的代码是:

//wincore.cpp
void CWnd::EndModalLoop(int nResult)
{
ASSERT(::IsWindow(m_hWnd));

// this result will be returned from CWnd::RunModalLoop
m_nModalResult = nResult;

// make sure a message goes through to exit the modal loop
if (m_nFlags & WF_CONTINUEMODAL)
{
   m_nFlags &= ~WF_CONTINUEMODAL;
   PostMessage(WM_NULL);
}
}

NOTE: PostMessage(NULL)是有用的。如果消息队列中没有消息的话,可能消息循环中的ContinueModal()不会马上执行,发送一个空消息是激发消息循环马上工作。

下面说一下CWnd::RunModalLoop函数中的消息循环究竟干了些什么事情:
1, 第一个内循环。首先从消息队列中查询消息,如果对话框空闲,而且消息队列中没有消息,他做三件事情,大家应到都能从字面上明白什么意思。最重要的是发送 WM_KICKIDLE消息。为什么呢?第一部分讲到了,非对话框程序用OnIdle来更新用户界面(UI),比如工具栏,状态栏。那么,如果对话框中也有工具栏和状态栏呢,在哪里更新(网上有很多这样的程序)?可以处理WM_KICKIDLE消息:

LRESULT CDlg_5Dlg::OnKickIdle(WPARAM w,LPARAM l)
{
     //调用CWnd::UpdateDialogControls更新用户界面
     UpdateDialogControls(this, TRUE);
     return 0;
}

NOTE: CWnd::UpdateDialog函数发送CN_UPDATE_COMMAND_UI消息给所有的用户界面对话框控件。

2, 第二个内循环。最重要的还是PumpMessage派送消息到目标窗口。其他的,像第二个if语句,0x118消息好像是WM_SYSTIMER消息(系统用来通知光标跳动的一个消息)。也就是说,如果消息为WM_SYSTIMER或者WM_SYSKEYDOWN,并且空闲显示标志为真的话,就显示窗口并通知窗口立刻重绘。

总之,对话框的消息循环机制和非对话框(比如SDI,MDI)还是类似的,仅仅侧重点不同。模式对话框是模式显示,自然有他的特点。下面部分讨论一下模式对话框和非模式对话框的区别。因为模式对话框有自己的特殊消息循环;而非模式对话框,共用程序的消息循环,和普通的窗口已经没有什么大的区别了。

 


第三部分:模式对话框和非模式对话框的区别

这个话题已经有很多人讨论,我说说我所理解的意思。
在MFC 框架中,一个对话框对象DoModal一下就能产生一个模式对话框,Create一下就能产生一个非模式对话框。实际上,无论是模式对话框还是非模式对话框,在MFC内部都是调用::CreateDialogIndirect(***)函数来创建非模式对话框。只是模式对话框作了更多的工作,包括使父窗口无效,然后进入自己的消息循环等等。::CreateDialogIndirect(***)函数最终调用CreateWindowEx函数通知系统创建窗体并返回句柄,他内部没有实现自己的消息循环。
非模式对话框创建之后立即返回,并且和主程序共用一个消息循环。非模式对话框要等对话框结束之后才返回,自己有消息循环。比如下面的代码:
CMyDlg* pdlg = new CMyDlg;
pdlg ->Create(IDD_DIALOG1);
pdlg->ShowWindow(SW_SHOW);
MessageBox("abc");
非模式对话框和消息框MessageBox几乎是同时弹出来。而如果将Create改成DoModal,那么,只能弹出模式对话框,在关闭了对话框之后(模式对话框自己的消息循环结束),消息框才弹出来。

NOTE:可以在模式对话框中调用GetParent()->EnableWindow(true);这样,主窗口的菜单,工具栏又激活了,能用了。MFC使用非模式对话框来模拟模式对话框,而在win32 SDK程序中,模式对话框激发他的父窗口Enable操作是没有效果的。

关于消息循环总结:


1,我们站在一个什么高度看消息循环?消息循环其实没有什么深奥的道理。如果一个邮递员要不断在一个城市中送信,我们要求他做什么?要求他来回跑,但他一次只能在一个地方出现。如果我们的应用程序只有一个线程的话,我们要他不断地为窗口传递消息,我们怎么做?在一个循环中不断的检测消息,并将他发送到适当的窗口。窗口可以有很多个,但消息循环只有一个,而且每时每刻最多只有一个地方在执行代码。为什么? 看第二点。

2,因为是单线程的(程序进程启动的时候,只有而且有一个线程,我们称他为主线程),所以就像邮递员一样,每次只能在某一个地方干活。什么意思呢?举个例子,用:: DiapatchMessage派送消息,在窗口处理过程(WinProc,窗口函数)返回之前,他是阻塞的,不会立即返回,也就是消息循环此时不能再从消息队列中读取消息,直到::DispatchMessage返回。如果你在窗口函数中执行一个死循环操作,就算你用PostQuitMessage函数退出,程序也会down掉。
while(1)
{
    PostQuitMessage(0); //程序照样down.
}
所以,当窗口函数处理没有返回的时候,消息循环是不会从消息队列中读取消息的。这也是为什么在模式对话框中要自己用无限循环来继续消息循环,因为这个无限循环阻塞了原来的消息循环,所以,在这个无限循环中要用GetMessage,PeekMessage,DispatchMessage来从消息队列中读取消息并派送消息了。要不然程序就不会响应了,这不是我们所希望的。
所以说,消息循环放在程序的什么的地方都基本上是过的去的,比如放在DLL里面。但是,最好在任何时候,只有一个消息循环在工作(其他的都被阻塞了)。然后,我们要作好的一件事情,就是怎么从消息循环中退出!当然用WM_QUIT 是可以拉~(PostThreadMessage也是个好主意),这个消息循环退出后,可能程序退出,也可能会激活另外一个被阻塞的消息循环,程序继续运行。这要看你怎么想,怎么去做。最后一个消息循环结束的时候,也许就是程序快结束的时候,因为主线程的执行代码也快要完了(除非BT的再作个死循环)。

NOTE: 让windows系统知道创建一个线程的唯一方法是调用API CreatThread函数(__beginthreadex之类的都要在内部调用他创建新线程)。好像windows核心编程说,在win2000下,系统用CreateRemoteThread函数来创建线程,CreateThread在内部调用CreateRemoteThread。不过这不是争论的焦点,至少win98下CreateRemoteThread并不能正常工作,还是CreateThread主持大局。

3,在整个消息循环的机制中,还必须谈到窗口函数的可重入性。什么意思?就是窗口函数(他是个回调函数)的代码什么时候都可以被系统(调用者一般是user32模块)调用。比如在窗口过程中,向自己的窗口SendMessage(***);那么执行过程是怎样的?
我们知道,SendMessage是要等到消息发送并被目标窗口执行完之后才返回的。那么窗口在处理消息,然后又等待刚才发送到本窗口的消息被处理后之后(SendMessage返回)才继续往下执行,程序不就互相死锁了吗?
其实是不会的。windows设计一套适合SendMessage的算法,他判断如果发送的消息是属于本线程创建的窗口的,那么直接由user32模块调用窗口函数(可能就有窗口重入),并将消息的处理结果结果返回。这样做体现了窗口重入。上面的例子,我们调用SendMessage(***)发送消息到本窗口,那么窗口过程再次被调用,处理完消息之后将结果返回,然后SendMessage之后的程序接着执行。对于非队列消息,如果没有窗口重入,不知道会是什么样子。

NOTE: 由于窗口的可重入性。在win32 SDK程序中应尽量少用全局变量和静态变量,因为在窗口函数执行过程中可能窗口重入,如果重入后将这些变量改了,但你的程序在窗口重入返回之后继续执行,可能就是使用已经改变的全局或静态变量。在MFC中(所有窗口的窗口函数基本上都是AfxWndProc),按照类的思想进行了组织,一般变量都是类中的,好管理的多。

4,MFC中窗口类(比如C**View,CFrameWnd等)中的MessageBox函数,以及 AfxMessageBox函数都是阻塞原有的消息循环的。由消息框内部的一个消息循环来从消息队列中读取消息,并派送消息(和模式对话框类似)。实际上,这些消息函数最终调用的是::MessageBox,它在消息框内部实现了一个消息循环(原有的主程序消息循环被阻塞了)。论坛中碰到过几次关于计时器和消息框的问题,看下面的代码:
void CTest_recalclayoutView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
MessageBox("abc");
while(1); //设计一个死循环
CView::OnTimer(nIDEvent);
}
咱让OnTimer大约5秒钟弹出一个消息框。那么,消息框不断的被弹出来,只要消息框不被关闭,那么程序就不会进入死循环。实际上,每次弹出对话框,都是最上层的那个消息框掌握着消息循环,其他的消息循环被阻塞了。只要不关闭最上面的消息框,while(1);就得不到执行。如果点了关闭,程序就进入了死循环,只能用ctrl+alt+del来解决问题了。

5,消息循环在很多地方都有应用。比如应用在线程池中。一个线程的执行周期一般在线程函数返回之后结束,那么怎么延长线程的生命周期呢?一种方法就是按照消息循环的思想,在线程中加入消息循环,不断地从线程队列读取消息,并处理消息,线程的生命周期就保持着直到这个消息循环的退出。

NOTE:只要线程有界面元素或者调用GetMessage,或者有线程消息发送过来,系统就会为线程创建一个消息队列。

6, 在单线程程序中,如果要执行一个长时间的复杂操作而且界面要有相应的话,可以考虑用自己的消息泵。比如,可以将一个阻塞等待操作放在一个循环中,并将超时值设置得比较小,然后每个等待的片段中用消息泵继续消息循环,使界面能够响应用户操作。等等之类,都可以应用消息泵(调用一个类似这样的函数):
BOOL CChildView::PeekAndPump()
{
MSG msg;
while(::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
   if(!AfxGetApp()->PumpMessage())
   {
    ::PostQuitMessage(0);
    return false;
   }
}
return true;
}
其实,用多线程也能解决复杂运算时的界面问题,但是没有这么方便,而且一般要加入线程通信和同步,考虑的事情更多一点。

综上所述,MFC消息循环就那么回事,主要思想还是和SDK中差不多。这种思想主要的特点表现在迎合MFC整个框架上,为整个框架服务,为应用和功能服务。这是我的理解

 

 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值