写在前面
本文是我在复习Windows程序设计以及MFC的时候整理的笔记,修改了一下发布出来,希望对大家有所帮助。
win32程序开发流程
我们先给出一张图,图中展示了一个win32程序完整的开发流程。对于初学者,我先快速讲解一下图中的内容。
在传统的console应用程序开发中,我们一般只会用到源文件,但在win32应用程序开发中,我们还需要.rc文件,也就是资源文件,资源文件中包括了字体,对话框,图像等资源。通过使用.rc文件,我们可以很方便地使用资源。
在win32开发中,.c和.h文件是由c编译器编译的,而资源文件是交由资源编译器编译的。两个编译器生成的目标文件最终由链接器生成.exe文件。
函数库
在开发win32程序的过程中,我们经常需要函数库中的函数,并不是延伸文件名为 .dll 者才是动态链接函数库(DLL,Dynamic LinkLibrary),事实上 .exe、.dll、.fon、.mod、.drv、.ocx 都是所谓的动态链接函数库。
Windows应用程序调用的函数分为C Runtimes以及Windows API两大部分。其中,C Runtime库又分为两个版本:
- LIBC.LIB,c运行时库的静态链接版本
- MSVCRT.LIB,C Runtime的动态链接版本
对于动态链接来说,在链接时期,链接器会为调用者做一些准备,这样在运行时才可以正确地进行链接。
头文件
在win32程序的开发中,如果需要用到Windows API,通常是需要包含Windows.h头文件的。
消息和事件
Windows是一个由消息驱动的系统,不同的消息有不同的处理方式(函数)。消息处理机制是Windows编程中十分重要的内容,在后续的文章中会着重介绍。
消息又可以分为三种:
- 标准消息
- 命令消息
- 通告消息
如果没有MFC应用程序开发的基础,遇到这三种消息是一脸懵逼的。我会在后续的MFC应用程序开发教程中更加详细地解释这三种消息,现在先知道有这么一回事就好了。
消息是源源不断地产生的,为了方便处理消息,Windows系统维护了两个队列:
- 系统消息队列
- 应用程序消息队列
先记住有这么一回事即可。我们还需要注意,硬件中断放在系统队列;Windows系统产生的消息和应用程序产生的消息放在程序队列。
消息产生后,是由user.dll模块帮忙进行消息路由,从而找到正确的消息处理函数。
简单的win32程序示例
这张图十分重要,它为我们展示一个win32程序的基本架构,接下我们对该图进行解释。
WinMain
WinMain
函数是程序入口点。加载器加载程序,然后调用C Startup code,后者调用WinMain
。WinMain的参数由作业系统传递进来。
窗口类注册与创建
在创建窗口前,必须设定好窗口的样式和属性,随后使用RegisterClass
注册窗口类。随后调用CreateWindow
创建窗口。
CreateWindow只是产生窗口,并不显式窗口,我们调用ShowWindow
将窗口显式在屏幕上。随后调用UpdateWindow
传递WM_PAINT消息驱动窗口绘制。
在程序源代码中,我们把注册窗口的过程封装到了InitApplication
中,把创建并显示窗口的过程封装到了InitInstance
中。这是十分普遍的操作,在后续的MFC应用程序设计中,我们还会再提及这两个函数。
窗口类只需注册一次,即可供同一程序的后续每一个执行个体(instance)使用(之所以能够如此,是因为所有进程共在一个地址空间中),所以我们把RegisterClass这个动作安排在“只有第一个执行个体才会进入” 的InitApplication数中。
产生窗口,是每一个执行个体(instance)都得做的动作,所以我们把CreateWindow这个动作安排在「任何执行个体都会进入」的InitInstance 函数中。InitApplication和InitInstance只不过是两个自定函数,但是,在MFC中,,两个函数包被装成了CWinApp类的两个虚成员函数。
消息循环
消息发生之时,操作系统已根据当时状态,为它标明了所属窗口,而窗口所属之窗口类又已经明白标示了窗口函数(也就是wc.lpfnWndProc 所指定的函数),以DispatchMessage(派发消息的函数)可找到正确的窗口过程。
窗口函数
发送给窗口函数的消息,经USER模块协助发送到指定的窗口函数中。窗口函数是CALLBAKC修饰的,这代表该函数应该永远被Windows系统所调用。
由于它是被Windows系统所调用的(我们并没有在应用程序任何地方调用此函数),所以这是一种callback函数,意思是指「在你的程序中,被 Windows 系统调用」的函数。
注意,不论什么消息,都必须被处理,所以switch/case 指令中的default: 处必须调用DefWindowProc,这是Windows内部预设的消息处理函数。
MFC消息映射初探
有了上面的基础之后,我们可以稍微介绍一下MFC的内容。我们把消息及其对应的处理函数封装进一个结构体中,并使用一个函数遍历结构体,找到对应消息的处理函数
struct MSGMAP_ENTRY {UINT nMessage;LONG (*pfn)(HWND, UINT, WPARAM, LPARAM);
};
#define dim(x) (sizeof(x) / sizeof(x[0]))// 消息与处理例程之对照表
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命令项 这是命令处理例程// 窗口函数
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));
}
// OnCommand——专门处理WM_COMMAND
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));
}
//----------------------------------------------------------------------
LONG OnCreate(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{//...
}
//----------------------------------------------------------------------
LONG OnAbout(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{//...
}
这么一来,WndProc和OnCommand永远不必改变,每有新要处理的消息,只要在_messageEntries[]和_commandEntries[]两个数组中加上新元素,并针对新消息撰写新的处理例程即可。
这种观念以及作法就是MFC的Message Map的雏形。MFC把其中的动作包装得更好更精致(当然因此也就更复杂得多),成为一张庞大的消息地图;程序一旦获得消息,就可以按图上溯,直到被处理为止。
对话框的运作
对话框分为模态和非模态对话框。模态对话框是指在子对话框关闭之前,不可对父对话框进程操作的对话框,非模态对话框则可以。
对话框需要.rc文件中的对话框模板,还需要指定对话框函数。对话框处理过消息之后,应该返回TRUE,否则返回false。
模块定义文件
Windows 程序需要一个模块定义文件,将模块名称、程序节区和资料节区的内存特性、模块堆积(heap)大小、堆栈(stack)大小、所有callback 函数名称…等等登记下来。
.def文件。在集成开发环境中不在需要特别准备。
资源描述文档
RC文件是以文字描述资源的地方。经过RC编译器编译,产生可用的二进制代码。
Windows程序的生与死
- 程序初始化过程中调用CreateWindow,为程序建立了一个窗口,做为程序的萤
幕舞台。CreateWindow 产生窗口之后会送出WM_CREATE 直接给窗口函数,
后者于是可以在此时机做些初始化动作(例如配置内存、开文件、读初始资
料…)。 - 程序活着的过程中,不断以GetMessage 从消息贮列中抓取消息。如果这个消
息是WM_QUIT,GetMessage 会传回0 而结束while 循环,进而结束整个程序。 - DispatchMessage 透过Windows USER 模块的协助与监督,把消息分派至窗口
函数。消息将在该处被判别并处理。 - 程序不断进行2. 和3. 的动作。
- 当使用者按下系统菜单中的Close 命令项,系统送出WM_CLOSE。通常程序
的窗口函数不栏截此消息,于是DefWindowProc 处理它。 - DefWindowProc 收到WM_CLOSE 后, 调用DestroyWindow 把窗口清除。
DestroyWindow 本身又会送出WM_DESTROY。 - 程序对WM_DESTROY 的标准反应是调用PostQuitMessage。
- PostQuitMessage 没什么其它动作,就只送出WM_QUIT 消息,准备让消息循
环中的GetMessage 取得,如步骤2,结束消息循环。
空闲时间的处理:Onldle
所谓空闲时间(idle time),是指「系统中没有任何消息等待处理」的时间。
它们都是到消息队列中抓消息,如果抓不到,程序的主执行线程(primary thread,是一个UI 执行线程)会被操作系统悬住。当操作系统再次回来照顾此一执行线程,而发现消息队列中仍然是空的,这时候两个API函数的行为就有不同了:
- GetMessage 会过门不入,于是操作系统再去照顾其它人。
- PeekMessage 会取回控制权,使程序得以执行一段时间。于是上述消息循环进入OnIdle 函数中。
Console程序
指定子系统为console。
console程序设计
指定入口点为main
内核对象
内核对象是系统的一种资源,系统对象一旦产生,任何应用程序都可以开启并使用该对象,系统会对内核对象计数,便于其管理。内核对象有以下几种:
- event
- mutex
- semaphore
- file
- file-mapping
- process
- thread
产生不同内核对象所使用的函数是不同的。但都产生一个handle作为内核对象的识别。每被使用一次,其对应的数值就加一。使用CloseHandle函数关闭内核对象。
内核对象也是Windows程序设计中的一个十分重要的内容,后续会出文章进行专门的讲解,有兴趣的读者可以阅读《Windows核心编程》一书。
关于进程的更多内容,会在后续的文章中讲解,有兴趣的读者可以先看下面的两篇文章:
的两篇文章:
- 进程的基本概念
- 创建进程