深入浅出MFC学习笔记:(第一章:win32基本概念,第二章:C++的重要性质)

长平狐 发布于 2012/10/08 15:16
阅读 152
收藏 0

深入浅出MFC阅读笔记:

 

写在开始的话:

刚结束《C++primer》的第二次阅读,决定趁热打铁,学习《深入浅出MFC》。当然,学习框架不是目的,而是通过学习MFC底层框架实现原理,对C++面向对象思想以及各种特性的进一步巩固。编程是一门实践课,需要不断的练习,才能得到提高。而深入学习MFC框架,就是我选择提高C++编程水平的方式之一。按部就班、不浮躁是我对学习本书的要求。废话少说,直入主题!!!

第一章:win32基本程序概念

Windows的运行本质:基于消息,事件驱动。

Windows程序分为:程序代码和UI资源,两大部分。UI资源借助各种工具产生,以二进制形式存储,并以各种扩展名文件存在。使用.rc文件描述它们 RC编译器读取rc文件后将所有的UI资源文件制作成.RES文件。链接器将obj目标文件和.DEF.RES链接在一起,形成完整的windows可执行程序。

应用程序调用的windowsAPI是在执行时链接的,但是,在链接期间,需要为调用者提供必要的信息,表示在执行时需要动态调用哪些函数,它们在那个dll中。这些适当的信息放在.lib文件中。

所有的windows程序都学要包含windows.h头文件,但是它只包含三大模块所提供的API函数。如果需要其他模块中的API就需要包含相应的头文件。

Windows程序依靠外部事件驱动,程序不断等待任何可能的输入,一旦捕捉,就将消息放在系统队列或者程序队列中。GetMessage用于从消息队列中取出消息,然后交由程序处理。因此程序就是靠GetMessage函数推动的。

硬件产生的消息放在系统队列中。由windows系统或其他程序产生的消息放在程序队列。

接受并处理消息的主角是窗口,每个窗口都有一个处理消息的函数,被称为消息处理函数。

Win32callback被定义为_stdcall,它声明函数调用习惯:参数进栈顺序,处理堆栈的责任归属。

一般情况下,将注册窗口的代码封装在InitApplication内,将创建窗口的代码封装在InitInstance内。这种方式很普遍。MFC把这两个函数封装成CwinApp的两个虚成员函数。

TranslateMessage用于转换键盘消息。DispatchMessage将消息发送给函数处理。消息中包含所属窗口的句柄,而窗口类又注册了窗口处理函数。窗口处理函数通过switch/case判断消息的种类,然后决定处理方式。

MFC采用消息映射表的形式,来将消息和它所对应的消息处理函数一一对应。消息映射表为一个数组,每一项存储一个结构体类型:

Struct MSGMAP_ENTRY

{

   UINT nMessage;

   LONG (*pfn)(HWND,UINT,WPARAM,LPARAM);

};

nMessage为消息IDpfn是函数指针,表示该消息对应的消息处理函数。

有了这个结构,当需要相应一个新消息时,只需向数组里添加一项就可以了。这正是面向对象观念中,将数据和处理数据的方法封装起来的一种具体实现。

MFC定义了一个宏,用于计算数组的长度:

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

接下来将会定义两个MSGMAP_ENTRY类型的数组,并向数组添加元素,将消息与消息处理函数关联起来。

第一个数组处理非COMMAND消息。

struct MSGMAP_ENTRY _messageEntries[]=

{

   WM_CREATE,OnCreate,

   WM_COMMAND, OnCommand,//所有command消息都由此函数处理。

   …….

};

第二个处理WM_COMMAND消息:

struct MSGMAP_ENTRY _commandEntryis=

{

    IDM_ABOUT,   OnAbout,

    IDM_SAVEAS,  OnSaveAs

………..

};

定义好之后,窗口函数就可以以一种通用的形式设计,如:

LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)

{

    for(int i=0;i<dim(_messageEntries);i++)

{

   if(message==_messageEntries[i].nMessage)

{

   (_messageEntries[i].pfn)(hwnd,message,wParam,lParam);

}

}

return (DefWindowProc(hwnd,message,wParam,lParam));

}

LRESULT CALLBACK OnCommand(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)

{

    for(int i=0;i<dim(_commandEntries);i++)

{

   if(message==_ commandEntries [i].nMessage)

{

   (_commandEntries [i].pfn)(hwnd,message,wParam,lParam);

}

}

return (DefWindowProc(hwnd,message,wParam,lParam));

}

   如此一来,wndProcOnCommand可以永远不变,每当有新的要处理的消息,就在_messageEntries_commandEntries两个数组添加新元素, 并针对新消息编写消息处理函数即可。

以上就是对MFC消息映射的一个简单模拟,当然MFC做的更好更精致。

常用的对话框分为模态的和非模态的对话框。

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

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

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

DialogBoxEndDialog两个函数,用于模态对话框的激活与结束。DialogBox指明该对话框所使用的模板,所属父窗口以及窗口函数名称。对话框内部有一个系统维护的消息循环。对话框处理过消息后,应该返回TRUE,因为对话框函数上层还有一个系统提供的默认对话框函数,如果返回FALSE则默认对话框函数就会接受处理。

Windows程序需要一个模块定义文件,将模块名称、程序段和数据段的内存特性、模块堆大小、堆栈大小、所有的callback函数名称等登记下来。在vc中,不再需要特别准备.DEF文件,因为所有这些都有默认值。但是在写dll库时,好像需要自己提供.def文件。

操作系统和应用程序职责不同,二者是互相合作的关系,各做各的分内事,并互相以消息通知对方。

当没有任何程序使用定时器,使用者也没有触碰键盘和鼠标或任何外围设备,那么系统就进入了空闲时间 。空闲时间是指:系统中没有任何消息等待处理。

PeekMessageGetMessage都是从消息队列中获取消息,有消息时将消息分发出去。不同点是:当消息队列中没有消息时,GetMessage会一直等待(挂起),直到出现下一个消息时返回。而PeekMessage会在没有取得消息后,立即返回,这使得程序得以继续执行。因此,传统的SDK程序一般使用PeekMessage代替GetMessage来运行后台工作。

C runtime函数库,有两个版本,一个为单线程程序使用,另一个视为多线程程序使用。而vc提供了六个版本的C runtime函数库。可以使用vc编译器提供的选项,确定使用哪个版本的C  runtime函数库。

进程是资源分配和保护的基本单位。而线程是cpu调度的基本单位

内核对象是系统的一种资源。如进程对象、事件对象、线程对象、文件对象、、、。《windows核心编程》第三章对内核对象有个比较详细的介绍,可以参考下。

这些内核对象通过API产生,通过对应类型句柄进行引用。调用CloseHandle进行关闭。

系统通过调用CreateProcess执行进程。本来通过这种方式执行起来的所有windows程序都是shell的子进程。但是,shell在调用时CreateProcess时已经把关系切断了。它们都是独立的个体。调用CreateProcess产生一个进程对象和一个主线程对象,它们存储在传给CreatProcess的一个PROCESS_INFORMATION结构内。可以从此结构中获得句柄,然后在父进程中使用。

觉得《深入浅出MFC》在介绍进程内核对象的计数时有错误。系统调用CreateProcess成功后,进行内核对象的计数应该是2,而不是1。否则在调用成功后,再CloseHandle切断与父进程关系时,进程对象就被释放了。线程也类似。《windows核心编程》进程这一章,也有详细的介绍。看来大师也有犯浑的时候。不知理解是否正确,有明白的请不吝赐教。

为了保证多线程安全,使用C runtime函数库时,必须为每一个线程做一些登记工作,有了这些记录,C runtime 函数库就可以为每一个线程配置一块内存作为线程的局部空间使用。_beginthreadex就负责这个任务。它和CreateThread参数完全相同,只是类型不同,因为C runtime函数库不应该依赖于具体的平台。同样C runtime 也提供了替代ExitThread的函数:_endthreadex

优先级是线程调度的主要依据。优先级高的线程,首先获得cpu的调度。操作系统会视情况调整各线程的优先级,以确保优先级高的不会长时间占用cpu,而优先级低的线程却长时间饥饿。

优先级是由两方面决定的:1,优先级等级。2,相对优先级。

所有这些关于windows内核对象、进程、线程以及优先级调度,我会在博文《谈谈windows核心编程系列》有更详细的介绍。

     

第二章:C++的重要性质

       虽然对C++的很多特定都接触过,也刚看完《C++primer》,但我仍觉得学习这一章也很有必要。一是因为侯绍的都是在MFC中需要的一些技术,在介绍C++的同时也穿插着MFC的一些原理。二是为了阅读的连贯性,跟着作者一步一步的走,慢慢的与作者形成默契,后面的东西也更容易接受。开始!!

      面向对象的观念是描绘现实用的,可以用生活中的经验去思考程序设计的逻辑。C++仅仅是其中一种面向对象的语言,而各种面向对象的思想是相通的。

      高速行驶的汽车刹车时不能踩离合器。这个真不知道。

      MFC有两个十分重要的虚函数:与docunment有关的Serilize函数,和与view有关的OnDraw函数。应该在自己的CMyDocCMyView改写这两个函数。

      每一个内含虚函数的类,编译器都会为它生成一个虚函数表。表中的每一项都指向一个虚函数的地址。编译器会在类中添加一项成员:虚函数表地址。因此,任何含有虚函数的类,除了其本身数据成员占有的空间外,还要有一个指向虚函数表的指针所占的空间。32位机器为4字节。

      C++类的成员函数,在编译时被编译器改过名称,并添加一个参数:this指针,通过传递的对象的地址不同,作用在不同的对象上。在类的对象上的内存区块上,看不到任何与成员函数有关的东西。它们都变成了一般的函数了。

      当我们调用每个对象的虚函数时,事实上是通过指向虚函数表指针,找到虚函数表,间接找出虚函数的真正地址。虚函数表中各虚函数的地址是依据虚函数的声明次序填入的。

      派生类会继承基类的虚函数表。当我们在派生类中改写虚函数时,虚函数表就受到影响:表中元素所指的地址将不再是基类的虚函数地址,而是在派生类定义的函数地址。

上面介绍的不就是《深入探索C++对象模型》中的内容吗!!既然如此添加一条书上没有的,《深入探索C++对象模型》中说:内存对齐产生的结果,一定是类中数据成员中,占最大字节数的变量的整数倍。

      static成员函数,没有this函数.MFC应用程序中,这正是被调用callback函数所需要的。另外:线程函数如果在类中的话,也必须是static类型的。

      在P75页,介绍RTTI时,举了一个例子:基类有个name成员,但是派生类对此成员的初始化都是在派生类的构造函数进行的。这一点很不好,《C++primer》中说,基类的成员一定要通过基类的构造函数初始化,不要在派生类中直接初始化,如果以后其他人又派生了其他类,有可能就忘记初始化了。

      MFC支持两种exception版本:一种是C++提供的,另一种是MFC定义的宏定义。个人倾向于使用C++的东西,一是觉得看着这么多宏头疼,二是为了可移植性的原因。


原文链接:http://blog.csdn.net/ithzhang/article/details/7622830
加载中
返回顶部
顶部