《深入浅出MFC》– 消息循环、CreateThread/beginthreadex

1.所有Windows程序都必须载入windows.h。

 

2.每一个Windows程序都应该有一个如下的循环:

MSG msg;

while(GetMessage(&msg,NULL,NULL,NULL)){

     TranslateMessage(&msg);

     DispatchMessage(&msg);

}

 

3.Windows消息分为两类(按输入):由硬件装置所产生的消息(如鼠标按下),放在系统队列(System Queue)中,以及由Windows系统或其它Windows程序传送过来的消息,放在程序队列(application queue)中。以应用程序的眼光来看,消息就是消息,来自哪里或放在哪里其实没有太大的区别,反正程序通过调用GetMessage API来取得消息,程序的生命靠它来推动。接受并处理消息的主角是窗口,每一个窗口都应当有一个窗口函数来负责处理消息。

wps_clip_image-30472

4.一开始,Windows程序必须做些初始化工作,如产生窗口。CreateWindow可以做到,但是在窗口产生之前、其属性必须先设定好。所谓属性包括窗口的“外貌”和“行为”,一个窗口的边框、颜色、标题、位置等等就是外貌,而窗口接受消息后的反应就是其行为(具体地说就是指窗口函数本身)。程序必须在产生窗口之前先利用RegisterClass设定属性(注册窗口类)。RegisterClass需要一个大型数据结构WNDCLASS作为参数,CreateWindow则另需要11个参数。

image

其中wc.lpfnWndProc所指定的函数就是窗口的行为中枢,也就是所谓的窗口函数。CreateWindow只产生窗口,并不显示窗口,所以稍后我们必须再利用ShowWindow将它显示在屏幕上。我们又希望先传送一个WM_PAINT给窗口,以驱动窗口的绘图操作,所以调用UpdateWindow。

 

5.消息循环:初始化工作完成后,WinMain进入所谓的消息循环:

while(GetMessage(&msg,NULL,NULL,NULL)){

     TranslateMessage(&msg);          //转换键盘消息

     DispatchMessage(&msg);            //分派消息

}

 

6.消息映射(Message Map)的雏形

首先定义一个MSGMAP_ENTRY结构和一个dim宏:

struct MSGMAP_ENTRY{

     UINT   nMessage;

     LONG  (*pfn)(HWND, UINT, WPARAM, LPARAM);   //函数指针,指向函数处理nMessage消息

};

#define dim(x)  (sizeof(x) / sizeof(x[0]))

接下来,组织两个数组_messageEntries[]和_commandEntries[],把程序中欲处理的消息以及消息处理例程的关联性建立起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 消息与处理例程之对照表格
struct MSGMAP_ENTRY _messageEntries[] =
{
    WM_CREATE, OnCreate,
    WM_PAINT, OnPaint,
    WM_SIZE, OnSize,
    WM_COMMAND, OnCommand,
    WM_SETFOCUS, OnSetFocus,
    WM_CLOSE, OnClose,
    WM_DESTROY, OnDestroy,
};                       消息 , 消息处理例程
// Command-ID与处理例程之对照表格
struct MSGMAP_ENTRY _commandEntries =
{
    IDM_ABOUT, OnAbout,
    IDM_FILEOPEN, OnFileOpen,
    IDM_SAVEAS, OnSaveAs,
};        WM_COMMAND命令项 , 命令处理例程

于是窗口函数可以这么设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,WPARAM wParam, LPARAM lParam)
{
    int i;
    for(i=0; i < dim(_messageEntries); i++) {
        if (message == _messageEntries[i].nMessage)
        return((*_messageEntries[i].pfn)(hWnd, message, wParam, lParam));
    }
    return(DefWindowProc(hWnd, message, wParam, lParam));
}

LONG OnCommand(HWND hWnd, UINT message,WPARAM wParam, LPARAM lParam)
{
    int i;
    for(i=0; i < dim(_commandEntries); i++) {  
        if(LOWORD(wParam) == _commandEntries[i].nMessage)
            return((*_commandEntries[i].pfn)(hWnd, message, wParam, lParam));
        }
    return(DefWindowProc(hWnd, message, wParam, lParam));
}

这么一来,WinProc和OnCommand永远不必改变,每当有新的要处理的消息,只要在_messageEntries[]和_commandEntries[]两个数组中加上新元素,并针对新消息撰写新的处理例程即可。

 

7.对话框的运行

常用的对话框分为模态(令其父窗口无效,直到对话框结束)的和非模态(父窗口与对话框共同运行)的对话框。

为了创建一个对话框,需要两样东西:

1:对话框模板,它是在RC文件中定义的,用以表明对话框的外观。

2:对话框函数。很类似与窗口函数,但是只处理WM_INITDIALOG和WM_COMMAND两个消息。对话框中的每个控件都有自己的窗口函数,它们以消息的方式与父窗口沟通。所有的控件传来的消息都是WM_COMMAND消息,有所属控件ID区分。

wps_clip_image-8325

 

8.窗口的生命周期

image

1.程序初始化过程中调用CreateWindow,为程序建立了一个窗口,作为程序的屏幕舞台。CreateWindow产生窗口之后会送出WM_CREATE直接给窗口函数,后者于是可以在此时做些初始化操作(例如配置内存、打开文件、读初始数据)。

2.在程序活着的过程中,不断以GetMessage从消息队列中抓取消息。如果这个消息是WM_QUIT,GetMessage会传回0而结束while循环,进而结束整个程序。

3.DispatchMessage通过Windows User模块的协助与监督,把消息分派至窗口函数。消息将在该处被派别并处理。

4.程序不断进行2和3的操作。

5.当使用者按下系统菜单中的Close命令时,系统送出WM_CLOSE。通常程序的窗口函数不拦截此消息,于是DefWindowProc处理它。

6.DefWindowProc收到WM_CLOSE后,调用DestroyWindow把窗口清除。DestroyWindow本身又会送出WM_DESTROY。

7.程序对WM_DESTROY的标准反应是调用PostQuitMessage。

8.PostQuitMessage没什么其他操作,就只送出WM_QUIT消息,准备让消息循环中的GetMessage取得,如步骤2,结束消息循环。

 

9.GetMessage和PeekMessage

①:GetMessage将等到有合适的消息时才返回,而PeekMessage只是撇一下消息队列。

②:GetMessage会将消息从队列中删除,而PeekMessage可以设置最后一个参数wRemoveMsg来决定是否将消息保留在队列中。

当消息队列中没有消息时,GetMessage会一直等待(挂起),直到出现下一个消息时返回。而PeekMessage会在没有取得消息后,立即返回,这使得程序得以继续执行。

 

10.CreateThread、_beginthread和beginthreadex

CreateThread 是一个Win 32API 函数,_beginthread 是一个CRT(C Run-Time)函数,他们都是实现多线城的创建的函数。

CreateThread、_beginthread和_beginthreadex都是用来启动线程的,但大家看到oldworm没有提供_beginthread的方式,原因简单,_beginthread是_beginthreadex的功能子集,虽然_beginthread内部是调用_beginthreadex但他屏蔽了象安全特性这样的功能,所以_beginthread与CreateThread不是同等级别,_beginthreadex和CreateThread在功能上完全可替代,我们就来比较一下_beginthreadex与CreateThread!  

一般来说,从使用角度是没有多大的区别的,CRT函数中除了signal()函数不能在CreateThread创建的线城中使用外,其他的CRT函数都可一正常使用,但是如果在CreateThread创建的线城中使用CRT函数的话,会产生一些Memory Leak.

用CreateThread 创建的线城能否被CRT函数 _endthreadex() 关闭?

首先应当尽量避免使用CreateThread(),事实上,_beginthreadex()在内部先为线程创建一个线程特有的tiddata结构,然后调用CreateThread()。在某些非线程安全的CRT函数中会请求这个结构。如果直接使用CreateThread()的话,那些函数发现请求的tiddata为NULL,就会在现场为该线程创建该结构,此后调用EndThread()时会引起内存泄漏。_endthreadex()可以释放由CreateThread()创建的线程,实际上,在它的内部会先释放由_beginthreadex()创建的tiddata结构,然后调用EndThread()。

如果我使用_beginthreadex函数创建了线程,它会为我创建好CRT函数需要的一切,并且最后无需我操心,就可以把清除工作做得很好,可能唯一需要注意的就是,如果需要提前终止线程,最好是调用_endthreadex或者是返回,而不要调用ExitThread,因为这可能造成内存释放不完全。同时我们也可以看出,如果我们用CreateThread函数创建了线程,并且不对C运行库进行调用(包括任何间接调用),就不必担心什么问题了。

若要使多线程C和C++程序能够正确地运行,必须创建一个数据结构,并将它与使用C/C++运行期库函数的每个线程关联起来。当你调用C/C++运行期库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。  

  1.每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构。  

  2.传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。传递给该函数的参数也保存在该数据块中。  

  3._beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法。  

  4.当调用CreatetThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。   还有,传递给线程函数的参数是tiddata结构而不是pvParam的地址。  

  5.如果一切顺利,就会像CreateThread那样返回线程句柄。如果任何操作失败了,便返回NULL。  

_beginthreadex和_beginthread函数的区别。_beginthread函数的参数比较少,因此比特性全面的_beginthreadex函数受到更大的限制。

例如,如果使用_beginthread,就无法创建带有安全属性的新线程,无法创建暂停的线程,也无法获得线程的ID值。

 

转载请标注来源:http://www.alonemonkey.com 

By:AloneMonkey

本文链接:http://www.alonemonkey.com/mfc-one.html