用MFC写了个贪吃蛇

二月 3rd, 2010 No Comments »

这几天看侯SIR的《深入浅出MFC》,看完了觉得如果就这么放下可能效果不好,就做了个贪吃蛇,本来打算做另一个东西呢,结果发现现在知识储备还不够,还得看看书,再说吧!

贪吃蛇实现得比较简单,SDI,单线程,没用位图,因为我没搞明白怎么编辑和使用位图,而且在网上看到一个也没用位图的贪吃蛇范例,很好看,就用简单地矩形填充做了。

做的过程中遇到的问题大概记录一下:

首先是定时器,贪吃蛇肯定要定时器,用以推进游戏进度。MFC中定时器的用法是:

SetTimer()用以设置定时器,指定定时间隔和消息处理函数,改变时间间隔可以改变游戏难度,如果不指定消息处理函数则由framework调用OnTimer(),这也是一般的做法;
OnTimer()用以响应,在此中完成该做的工作;
KillTimer()使用结束杀掉定时器。

需要注意的是,只有CWnd的派生类才能接受定时器消息,所以只能在CMyView和CMainFrm中使用。SetTimer()必须在窗口完全产生之后调用,KillTimer()必须在窗口销毁之前调用,因此要掌握好时机,否则会导致一个ASSERT FAILED。

有了定时器,蛇就可以走了,由于贪吃蛇每次前进时全身所有点都向前挪一位,就相当于尾部节点接到头结点前面而其它节点不动,而这样做移动的效率就很高,为了适应这种操作,选用了CList<CPoint>作为描述Snake的数据结构。

改变蛇的运动方向要靠响应键盘信息,响应ON_COMMAND的WM_KEY_DOWN,如果响应WM_KEY_UP的话灵敏度不太够。

对于设置背景颜色、改变窗口风格也都轻车熟路了,多亏了侯SIR的书。snake

由于刚看完《深入浅出MFC》,对MFC的框架比较熟悉,对Document/View的配合比较了解,所以写起来比上次写魔方顺手的多,有些改动也知其然并知其所以然了,感觉还是不错的。

不过写个破贪吃蛇真是没啥意思,本来我想把它写成多线程,当练手,不过又觉得太勉强了,就算了。

再好好看看书,写个复杂点的东西吧。

MFC的Serialization

一月 31st, 2010 No Comments »

所谓serialization,即序列化,说白了就是将程序用到的资料(Document)存储在文件中以及从文件中提取回来,目的是让对象有永久的生命力,不会因为程序的退出而丢失。比如做一个文字录入工具,肯定不希望程序退出后录入的文字就消失,而是希望将它保存起来,等再用程序打开文件时又能显示出来。
CArchive类是MFC负责serialization的类别。

由于Serialize()是CObject的虚函数,因此任何一个派生于此的类都有了该函数但也必须改写,而且所有CObject的直接派生类应该在改写的Serialize()中首先调用CObject::Serialize()。

任何派生自CObject的类都继承了虚函数Serialize(),该函数以一个CArchive对象为参数,并利用此对象进行序列化,即最终调用的是CArchive类的成员函数。

对serialization的管理应该呈层次化,即由上到下做好分内的事,然后让成员自己负责自己的序列化,即先用>>和<<将本层需负责的对象序列化好,再调用成员的Serialize()函数,在下一层中,成员仍然是利用<<和>>与CArchive对象交互。由于CArchive重载了<<和>>操作符,因此具体的序列化工作在CArchive的这两个操作符定义中完成。

《Serialization的写档动作》

当用户选择SaveAs时,程序执行的流程如下:
ID_FILE_SAVE_AS的消息在CDocument中被映射到CDocument::OnFileSaveAs(),该函数调用CDocument::DoSave(NULL),由于以NULL为参数,因此会调用CDocument::DoPromptFileName()从用户处接受输入的文件名,再调用CDocument::OnSaveDocument(),在此函数中,会定义一个CArchive对象,借之作为参数调用Serialize(),由于CMyDoc改写了该虚函数,因此调用的是CMyDoc::Serialize(),此CMyDoc::Serialize()就是程序员需要修改的一个主要函数。也就是Serialization层层分工的开始,从这开始,每一层做好自己的Serialization工作,将剩余工作交给下一层,具体可以调用CArchive类的各个成员函数和<<、>>操作符。

如果在某一层调用了CObList::Serialize()的话,有一些内容还值得研究,CObList::Serialize()的内容说复杂也并不复杂:无论存取,都是先针对count值,即该CObList对象所含的元素个数,然后再在一个循环里依次存取各个元素。

CArchive的<<操作符调用CArchive::WriteObject(),该函数是CArchive类实现序列化的根本所在,负责对一个对象进行序列化。CArchive的复杂动作(之所以能高效地实现序列化)可在此函数处找到玄机。首先调用CArchive::WriteClass()写入该对象的RuntimeClass信息,CArchive::WriteClass()会判别此类别是不是新类别,如果是则存储其版本号、类名长度、类名,如果不是则进行相应标记。继而CArchive::WriteObject()会调用所序列化对象自己的Serialize(),由它完成具体的该对象序列化任务,该函数可能进行分层部署,如果此对象有其它类别成员的话。

《Serialization的读档动作》
读档动作是从资料到对象,所以涉及到RTTI和动态生成。简要回顾一下RTTI和动态生成:
想要支持RTTI和动态生成的每个类在声明和给出定义时要加上如下宏:
DECLARE_DYNAMIC()/IMPLEMENT_DYNAMIC() for RTTI
DECLARE_DYNCREATE()/IMPLEMENT_DYNCREATE() for Dynamic Creation
这两个宏负责将类在动态识别或生成时需要的信息(类名、类对象大小、生成类对象所需函数的指针等――都是CRuntimeClass对象的成员)加入一个CRuntimeClass对象的链表中。在读档时读出要生成的对象的类名,然后到CRuntimeClass链表中比对从而得到相应信息进行对象的动态生成,此为Serialization和Dynamic Creation的结合之处。

当用户点击File/Open时,ID_FILE_OPEN消息会被CWinApp::OnFileOpen()拦截,该函数又调用CDocManager::OnFileOpen(),此函数通过DoPromptFileName()取得用户输入的文件名,然后以之为参数调用CWinApp::OpenDocumentFile(),中间有很绕的调用关系,简单说来就是创建Document\Window\View三件组,然后进行serialization,Serialization的过程和写档就很相似了,又会调用到ReadObject()、ReadClass()等函数,还会调用CreateObject()用于动态生成来承接读档的结果。

Serialization由“<<”和“>>”操作符调用CArchive::ReadObject()和CArchive::WriteObject(),这也不是最底层,这两个函数先利用类似RTTI的技术取得所序列化的类别信息,也就得到了其Serialize()函数指针,再调用该函数进行具体的读写。而这个Serialize()函数,正是需要程序员具体实现的,即对于特定的类还是只有编写它程序员知道具体的读写方式,才能制定具体的读写方法。但是这个CMyObject::Serialize()有可能还会调用CArchive的“>>”和“<<”操作符,如果该对象还有其它类型对象作为其成员的话,这样又开始上述过程,整个过程呈递归螺旋上升状,这个递归过程直到所有的“<<”和“>>”操作都转换为CArchive所重载的那些针对C++基础类别和MFC基础类别的“<<”和“>>”为止,进行递归返回,直到最外层的CMyObject::Serialize()调用结束。

关于一个程序相关文件的内容如何解释,必须由程序员负责,所谓负责就是程序员在CMyObject::Serialize()中定义将对象按何种格式写入文件,并按照相同格式读取之回到对象。

关于MFC的document/view结构以及document template的学习笔记

一月 31st, 2010 No Comments »

MFC之所以能成为application framework,很大的原因就在于其Document/View结构对于快速开发的支持。Document/View很好地划分了程序代码的前台后台,让程序员可以专心于设计数据结构和UI。
Document即为“资料”,按我理解就是饭店的厨师;而View就是饭店的服务员。View负责点菜和上菜(对用户请求做出直接响应),而Document负责烹饪,即处理用户的要求。
除了Document和View,还有一个Frame,因为View要放在Frame内部,Frame就是承载View的框架。而三者之间的关系是由Document Template来管理的,一份Document Template管理一个document\frame\view三件组,而一个程序可以有多个document template,多个document template由一个CDocManager对象管理。

document template

一个MDI(多文档接口)应用程序使用主框架窗口(main frame window)作为工作区,在工作区里用户可以打开多个文档框架窗口,每一个文档框架窗口用以显示一份文档。

Document template是用来定义以下三种类之间关系的模板:

Document(文档)类,从CDocument派生而来,用于处理数据,即所谓数据之体。

View(视图)类,用于将来自Document类的数据显示出来,可以从CView、CScrollView、CFormView和CEditView类派生,也可以直接使用CEditView类。

框架窗口(frame window)类,用以包含View。对于MDI程序,可以从CMDIChildWnd派生,也可以直接使用该类。

MDI应用程序可以支持不止一种文档,而且不同种类的文档可以同时打开(比如一个text和一个bitmap)。对于每一种所支持的文档,应用程序都应该有一份对应的document template进行管理。也就是说你的应用程序支持几种文档,就应该有几个Document template。

当用户创建新文档的时候,应用程序就会使用document template。如果程序支持的文档种类在一种以上,那么程序框架就会从document templates处取得所有的文档类型名字,显示在File New对话框里。一旦用户选择了文档类型,应用程序就会创建一个document对象,一个frame window对象和一个view对象,并且将它们联系在一起(通过document template)。

通常程序员不需要使用CMultiDocTemplate的任何成员函数(除了构造函数外)。框架会在内部自动处理CMultiDocTemplate对象。

为了管理通过相关view对象和frame window对象来构建document的复杂过程,framework使用两种document template类:
CSingleDocTemplate类用于SDI程序;CMultiDocTemplate类用于MDI程序。一个CSingleDocTemplate在同一时刻只能创建并储存单一种类的一个文档;一个CMultiDocTemplate在同一时刻可以管理单一种类的多个文档。

有些应用程序支持不止一种文档类型,比如同时支持文本和图形。这种应用程序为每个支持的类型使用单独的document template对象,见下图:

这个应用程序支持两种文档类型,因此具备两个document template对象。对于每一种文档类型可以打开多个文档,每打开一个文档应用程序就为之创建三个对象:CMyDocument对象用于处理数据,CMyView对象用于显示,CMyFrameWnd用于装载view,但是不管打开多少个同类型文档,负责管理该类型的document template对象只有一个,它负责管理的是上述三个类之间的关系,负责在这三个类的对象创建之时指定它们之间的关系。

上面说到每打开一个document,会随之一起创建一个view和一个frame window,而这三者的创建工作就是由document template完成的,当用户点击“File/New”或者“File/Open”后,消息发出,被theApp的OnFileNew()接到,但它经过一系列的调用(比较绕)最终调用的是CMultiDocTemplate::OpenDocumentFile(),该函数完成此三对象的创建,其中view的创建又是非常的绕,最终经过一系列的调用由CFrameWnd::CreateView()完成,另外还会调用CView从CWnd继承来的函数Create()用于产生与该view对应的真实窗口。而创建什么种类的document、window、view是在创建document template时由document template的构造函数的参数指定的。下面显示了创建一个CMultiDocTemplate(用以管理MDI的document template)的过程:

CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(IDR_CMyDocTypeTYPE,
?? RUNTIME_CLASS(CMyDoc),
?? RUNTIME_CLASS(CChildFrame), // custom MDI child frame
?? RUNTIME_CLASS(CMyView));
if (!pDocTemplate)
?? return FALSE;
AddDocTemplate(pDocTemplate);

传给构造函数的第一个参数是一个资源ID,该资源用于提供该文档类型的菜单、快捷键、按钮等。剩余三个参数用RUN_CLASS()宏提供CMultiDocTemplate创建document\window\view时所需要的类型信息(即对应的RuntimeClass对象,当用户打开一个文件时,document template就可以据此动态创建出document\window\view,这就很好体现了MFC动态创建的用途,关于动态创建是由DECLARE_DYNCREATE()\IMPLEMENT_DYNCREATE()宏实现的),最后用AddDocTemplate()加载此document template,AddDocTemplate()实际上是将document template加到由theApp的一个指针CDocManager* m_pDocManager所维护的document template链表中CDocTemplate有三个成员变量分别持有document\window\view的RuntimeClass对象的指针,另外还有一个资源ID成员。

Document template对象是被theApp创建的。在theApp的InitInstance()中的一个关键任务就是创建一个或多个适当种类的document template。theApp会在template list中保存指向每一个document template的指针并提供一个接口用于增加document template(AddDocTemplate())。如果你想要支持两个或以上的文档类型,你必须为每个文档类型显式地调用AddDocTemplate()。

多个Document template是由一个CDocManager对象管理的,很多原本由CWinApp做的关于document template的工作如:AddDocTemplate()、OpenDocumentFile()、NewDocumentFile(),在MFC4.0后都由CDocManager来做了。

CDocTemplate\CDocment\CFrameWnd\CView之间的指针互指关系
列出:

CDocTemplate有指向其余三者RuntimeClass对象的指针:
?CRuntimeClass* m_pDocClass;
?CRuntimeClass* m_pFrameClass;
?CRuntimeClass* m_pViewClass;
还有指向Document列表的指针:CPtrList m_pDocList;表示一个CDocTemplate可以维护多个同类型文档。

CDocument有CDocTemplate* m_pDocTemplate回指CDocTemplate;另有CPtrList m_pViewList指向一个view的链表,表示一个Document可以对应多个View。

CFrameWnd有CView* m_pViewActive指向当前活动在其中的view。

CView有CDocument* m_pDocument指向对应的Document。

CDocument\CFrameWnd\CView之间互相操作的函数

CDocument::UpdateAllViews()—————>CView:OnUpdate()
CView::GetDocument();
CView::GetParentFrame();
CFrameWnd::GetActiveView();
CFrameWnd::GetActiveDocument();

View和Document的通信
程序员通过改写CMyView的如下函数达到View和Document通信的目的:
CView::OnInitialUpdate():负责view的初始化。
CView::OnUpdate():Frameword在Document发生变化时调用此函数,此为预留给程序员的“用Document的变化指导View”的接口。
CView::OnDraw():该函数作为WM_PAINT的间接响应,负责View的更新。
CDocument::UpdateAllViews()/CView::OnUpdate()这一对函数是命令与执行的关系,调用UpdateAllViews()就会通知所有的View,通知方法就是调用其OnUpdate()。

一个MFC程序的运行过程

一月 28th, 2010 No Comments »

1.由于theApp是全局对象,因此会在进入WinMain()之前完成构造,theApp的构造动作会导致一系列的对theApp的初始化动作:
pModuleState->m_pCurrentWinApp = this;

2.接着进入WinMain(),WinMain()(_tWinMain(),这是为了UniCode而准备的一个宏)直接调用AfxWinMain(),AfxWinMain()发挥真正应该由WinMain()发挥的作用,它取得theApp,然后必须先调用AfxWinInit(),再用取得的指向theApp的指针pApp调用InitApplication()、InitInstance()和Run(),最后调用AfxWinTerm()终止程序。
即AfxWinMain()中的动作相当于:

AfxWinInit();
CWinApp::InitApplication();
CMyWinApp::InitInstance();
CWinApp::Run();

3.AfxWinInit()做什么?
首先将WinMain()传进来的四个参数保留在theApp的成员变量里。
调用AfxInitThread(),此函数又调用::SetWindowHookEx(),不知干啥。另外此函数还将消息队列尽量加大到96。

4.CWinApp::InitApplication()做什么?
会做一些与CDocManager()和Document Template相关的工作,是MFC的内部管理范畴。

5.CMyWinApp::InitInstance()做什么?
终于来到了我们可以改写的InitInstance(),这个函数是CWinApp的虚函数,CMyWinApp通常要改写它,其实在CWinApp中InitInstance()是一个空函数。
InitInstance()的动作如下:

m_pMainWnd = new CMyFrameWnd();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();

InitInstance()首先会创建一个CMyFrameWnd对象,并将指针保留在m_pMainWnd中。这个CMyFrameWnd对象的创建过程就会导致主窗口的登记和建立,因为CFrameWnd的构造函数中会调用Create()函数,而这个函数负责创建窗口(通过调用CreateWindowEx),窗口风格使用最常见的WS_OVERLAPPEDWINDOW。但是这仅仅是创建窗口,窗口类注册的动作(必须先于窗口的创建)又发生在哪里呢?其实Create()的调用会导致CWnd::CreateEx()的调用,此函数才是真正调用CreateWindowEx()创建窗口者,在调用CreateWindowEx()之前会调用CFrameWnd::PreCreateWindow(),此函数判断传入的窗口类名lpszClassName是否为NULL,若是则通过一系列的调用(非常之绕)为其注册默认窗口类别。由于每个窗口创建过程中PreCreateWindow()都只在创建之前被调用,因此窗口的注册大多在创建之前发生。CWnd及其派生类的各个不同PreCreateWindow()函数为不同窗口指定了不同的默认窗口类别:
CWnd使用的窗口类别是_afxWnd
CFrameWnd和CMDIChildWnd使用的窗口类别是_afxWndFrameOrView
CMDIFrameWnd使用的窗口类别是_afxWndMDIFrame
综上,InitInstance()中的第一个动作――创建CMyFrameWnd对象――完成了窗口的创建。之后的动作就很直观了,ShowWindow()显示窗口,UpdateWindow()发送WM_PAINT消息更新窗口。

6.回到AfxWinMain()中,接下来执行的是CWinApp::Run()。Run()所做的正是维持程序运行的“消息循环”。CWinApp::Run()会调用CWinThread::Run(),此Run()中有消息循环,通过PumpMessage()得到并转发消息(调用::GetMessage()、::TranslateMessage()、::DispatchMessage(),一如SDK!)。但是转发到哪里呢?窗口函数呢?原来在MFC为我们注册默认窗口类的时候已经指定了窗口函数为DefWindowProc(),但是真正处理消息的又不是它,而是一个全局函数AfxWndProc(),这是MFC通过hook和subclassing技术做到的,暂且不提。

综上,MFC为我们的程序提供了WinMain()、注册了窗口类、创建了窗口甚至提供了窗口函数,留给我们做的只是写出响应消息的处理函数,而这些消息和处理函数如何对应起来还要靠MFC六大关键技术之一的Message Mapping(消息映射)。消息映射机制的目的是首先搭建起消息和消息处理函数对应的大框架,再通过宏的机制让程序员能够方便地添加消息和消息处理函数之间的对应关系。

7.在程序运行的过程中,程序不断地由消息所驱动,直到用户动作发出了WM_CLOSE消息,程序即将关闭,过程是:由于CMyFrameWnd没有设立WM_CLOSE的处理函数,因此该消息被送往预设处理函数,预设函数对WM_CLOSE的处理是调用::DestroyWindow(),进而发送WM_DESTROY消息,WM_DESTROY消息同样会被送到预设处理函数,预设函数对WM_DESTROY的处理方法是调用::PostQuitMessage(),发送WM_QUIT,CWinApp::Run()在收到WM_QUIT后会结束自己的消息循环,并调用ExitInstance(),这是CWinApp的虚函数,可被CMyWinApp改写,调用过此函数后,回到AfxWinMain()中调用AfxWinTerm()结束程序。

理解“MFC的动态类型识别和动态创建”

一月 26th, 2010 No Comments »

我们写MFC程序的时候,要做的工作通常就是改写从几大基类继承来的派生类内容,加一些变量啦,加一些函数啦,改一些函数行为啦;或者再多派生一些类,再在新派生出来的类内部重复刚才提到的那些事。可是我们从来不会写

CMyDoc myDoc;

或者

CMyView myView;

之类的语句,也就是说我们从来没有把我们辛辛苦苦改好的类进行实例化。没有实例化又如何工作呢?当然这些类最终是要实例化成对象的,就是程序运行时候那一个个的窗口啊,编辑框啊等等。但是这些都是MFC帮我们做的,即不需要我们用代码写出来。那是不是MFC自动帮我们生成了这些用于实例化的代码呢?也不是,MFC就是想也没有这个能力,因为在代码编辑期它并不知道我们会怎样改写和怎么创建我们需要的类,也就是说MFC并没有这些类的信息,自然就无法创建。那怎么办?
当我们完成代码编辑之时,我们所需要的一切类的信息就确定了,而代码编辑之后的剩余步骤就是编译、链接和运行了。编译和链接都是为代码编写做后续工作。所以MFC真正有机会帮我们创建对象只能在运行期,就是所谓的动态创建。
可是运行期已经只剩下二进制代码了,MFC已经无法使用new这样的动作来执行创建了,所以MFC要想为我们动态创建对象,就必须具有运行期识别对象类型的能力,即所谓的RTTI(Runtime Type Information),如果在运行期能够知道要创建的对象的类型,稍作延伸,自然就可以进行该类型对象的创建了。
MFC采用这样的技术:定义一个叫做CRuntimeClass的类作为所有希望具有RTTI和动态创建能力的类的静态成员,当然包括从几大基类派生而来的类(CMyView啦,CMyDoc啦),然后维护一个CRuntimeClass对象的链表,这样就可以保存这些类的相关信息,并在运行时使用相关信息。比如CRuntimeClass类有如下成员:

m_lpszClassName //类名,通过对此成员的比较可以对RUNTIME_CLASS()宏和CObject::IsKindOf()函数提供支持
m_nObjectSize //类大小
m_pBaseClass //指向基类的CRuntimeClass对象,也是用于CObject::IsKindOf()的比对工作
m_pfnCreateObject //指向被描述类的创建函数(名为CreateObject(),非构造函数,但最终调用的仍然是构造函数),用于动态创建
CreateObject() //该函数内部利用m_pfnCreateObject()调用对应类的CreateObject()函数(同名,但不属于一个类,创建类中的CreateObject()单纯使用new创建对象)

还有其它成员没有列出。

有了这样的包含所有类的CRuntimeClass对象链表,就可以在运行时通过此链表得到类型信息和创建对象了,也就是可以RTTI和动态创建了――因为任何一个对象都有一个静态的CRuntimeClass成员。下面我们只要把修改过的类和新派生的类的信息加入这个链表就可以了,这项工作是通过几对宏来完成的,它们是:
DECLARE_DYNAMIC()/IMPLEMENT_DYNAMIC()
DECLARE_DYNCREATE()/IMPLEMENT_DYNCREATE()
宏做的工作就是为该类生成CRumtimeClass对象,完善该对象信息,并且将其加入链表。
MFC代码生成时,各个派生类自动带有这些宏,当程序员自己编码时如果想要具有RTTI和动态创建功能也应该写上这些宏。