友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
深入浅出MFC第2版(PDF格式)-第55部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
为什么经过这样的宏之后,消息就会自动流往指定的函数去呢?谜底在于Message Map
的结构设计。如果你把第3章的Message Map 仿真程序好好研究过,现在应该已是成竹
在胸。我将在第9章再讨论MFC 的Message Map 。
好奇心摆两旁,还是先把实用上的问题放中间吧。如果某个消息在Message Map 中找不
到对映记录,消息何去何从?答案是它会往基础类别流窜,这个消息流窜动作称为
「Message Routing」。如果一直窜到最基础的类别仍找不到对映的处理例程,自会有预
设函数来处理,就像SDK 中的DefWindowProc 一样。
MFC 的CCmdTarget 所衍生下来的每一个类别都可以设定自己的Message Map ,因为
它们都可能(可以)收到消息。
396
…………………………………………………………Page 459……………………………………………………………
第6章 MFC 程式的生死因果
消息流动是个颇为复杂的机制,它和Document/View 、动态生成(Dynamic Creation ),
文件读写(Serialization)一样,都是需要特别留心的地方。
来龙去脉总整理
前面各节的目的就是如何将表面上看来不知所以然的MFC 程序对映到我们在SDK 程序
设计中学习到的消息流动观念,从而清楚地掌握MFC 程序的诞生与死亡。让我对MFC
程序的来龙去脉再做一次总整理。
程序的诞生:
■ Application object 产生,内存于是获得配置,初值亦设立了。
■ Afx WinMain 执行AfxWinInit,后者又调用AfxInitThread ,把消息队列尽量加大到
96。
■ Afx WinMain 执行InitApplication 。这是CWinApp 的虚拟函数,但我们通常不改
写它。
■ AfxWinMain 执行InitInstance 。这是CWinApp 的虚拟函数,我们必须改写它。
■ CMyWinApp ::InitInstance 'new' 了一个CMyFrameWnd 对象。
■ CMyFrameWnd 构造式调用Create,产生主窗口。我们在Create 参数中指定的
窗口类别是NULL , 于是MFC 根据窗口种类, 自行为我们注册一个名为
〃AfxFrameOrView42d〃 的窗口类别。
■ 回到InitInstance 中继续执行ShowWindow ,显示窗口。
■ 执行UpdateWindow ,于是发出WM_PAIN T。
■ 回到AfxWinMain,执行Run ,进入消息循环。
程序开始运作:
■ 程序获得WM_PAINT 消息(藉由CWinApp::Run 中的::GetMessage 循环)。
■ WM_PAINT 经由::DispatchMessage 送到窗口函数CWnd::DefWindowProc 中。
397
…………………………………………………………Page 460……………………………………………………………
第篇 湷觥 FC 程式設計
■ CWnd::DefWindowProc 将消息绕行过消息映射表格(Message Map )。
■ 绕行过程中发现有吻合项目,于是调用项目中对应的函数。此函数是应用程序
利用BEGIN_MESSAGE_MAP 和END_MESSAGE_MAP 之间的宏设立起来的。
■ 标准消息的处理例程亦有标准命名,例如WM_PAINT 必然由OnPaint 处理。
以下是程序的死亡:
■ 使用者选按【File/Close】,于是发出WM_CLOSE 。
■ CMyFrameWnd 并没有设置WM_CLOSE 处理例程,于是交给预设之处理例程。
■ 预设函数对于WM_CLOSE 的处理方式是调用::DestroyWindow , 并因而发出
WM_DESTRO Y。
■ 预设之WM_DESTROY 处理方式是调用::PostQuitMessage,因此发出WM_QUIT 。
■ CWinApp::Run 收到WM_QUIT 后会结束其内部之消息循环, 然后调用
ExitInstance,这是CWinApp 的一个虚拟函数。
■ 如果CMyWinApp 改写了ExitInstance , 那么CWinApp::Run 所调用的就是
CMyWinApp ::ExitInstance,否则就是CWinApp::ExitInstance 。
■ 最后回到AfxWinMain,执行AfxWinTerm,结束程序。
Callback 函数
Hello 的OnPaint 在程序收到WM_PAINT 之后开始运作。为了让〃Hello; MFC〃 字样从
天而降并有动画效果,程序采用LineDDA API 函数。我的目的一方面是为了示范消息的
处理,一方面也为了示范MFC 程序如何调用Windows API 函数。许多人可能不熟悉
LineDDA,所以我也一并介绍这个有趣的函数。
398
…………………………………………………………Page 461……………………………………………………………
第6章 MFC 程式的生死因果
首先介绍LineDDA :
void WINAPI LineDDA(int; int; int; int; LINEDDAPROC; LPARAM);
这个函数用来做动画十分方便,你可以利用前四个参数指定屏幕上任意两点的(x;y)
座
标,此函数将以Bresenham 算法(注) 计算出通过两点之直线中的每一个屏幕图素座
标;每计算出一个坐标,就通知由LineDDA 第五个参数所指定的callback 函数。这个
callback 函数的型式必须是:
typedef void (CALLBACK* LINEDDAPROC)(int; int; LPARAM);
通常我们在这个callback 函数中设计绘图动作。玩过Windows 的接龙游戏吗?接龙成
功后扑克牌的跳动效果就可以利用LineDDA 完成。虽然扑克牌的跳动路径是一条曲
线,但将曲线拆成数条直线并不困难。LineDDA 的第六个(最后一个)参数可以视应用
程序的需要传递一个32 位指针,本例中Hello 传的是一个Device Context 。
Bresenham 算法是计算机图学中为了「显示器(屏幕或打印机)系由图素构成」的这个
特性而设计出来的算法,使得求直线各点的过程中全部以整数来运算,因而大幅提升
计算速度。
{ (x1; y1) LineDDACallback(int; int; PLARAM)
{
。。。
。。。
。。。
}
}
(x2; y2)
LineDDA Bresenham 算法计算出通过两点之直线中每一个
你可以指定两个坐标点, 将以
屏幕图素的坐标。每计算出一个坐标,就以该坐标为参数,调用你所指定的callback 函数。
图6…6 LineDDA 函数说明
399
…………………………………………………………Page 462……………………………………………………………
第篇 湷觥 FC 程式設計
LineDDA 并不属于任何一个MFC 类别,因此调用它必须使用C++ 的〃scope operator〃
(也就是::):
void CMyFrameWnd::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect(rect);
dc。SetTextAlign(TA_BOTTOM | TA_CENTER);
::LineDDA(rect。right/2; 0; rect。right/2; rect。bottom/2;
(LINEDDAPROC) LineDDACallback; (LPARAM) (LPVOID) &dc);
}
其中LineDDACallback 是我们准备的callback 函数,必须在类别中先有声明:
class CMyFrameWnd : public CFrameWnd
{
。。。
private:
static VOID CALLBACK LineDDACallback(int;int;LPARAM);
};
请注意,如果类别的成员函数是一个callback 函数, 你必须声明它为〃static〃,才能把
C++ 编译器加诸于函数的一个隐藏参数this 去掉(请看方块批注) 。
以类别的成员函数作为 Windows callback 函数
虽然现在来讲这个题目,对初学者而言恐怕是过于艰深,但我想毕竟还是个好机会
……我可以在介绍如何使用callback 函数的场合,顺便介绍一些C++ 的重要观念。
首先我要很快地解释一下什么是callback 函数。凡是由你设计而却由Windows 系
统调用的函数,统称为callback 函数。这些函数都有一定的类型,以配合Windows
的调用动作。
某些Windows API 函数会要求以callback 函数作为其参数之一,这些API 例如
400
…………………………………………………………Page 463……………………………………………………………
第6章 MFC 程式的生死因果
SetTimer 、LineDDA、EnumObjects 。通常这种API 会在进行某种行为之后或满足某种
状态之时调用该callback 函数。图6…6 已解释过LineDDA调用callback 函数的时机;
下面即将示范的EnumObjects 则是在发现某个Device Context 的GDI object 符合我们
的指定类型时,调用callback 函数。
好,现在我们要讨论的是,什么函数有资格在C++ 程序中做为callback 函数?这个
问题的背后是:C++ 程序中的callback 函数有什么特别的吗?为什么要特别提出讨论?
是的,特别之处在于,C++ 编译器为类别成员函数多准备了一个隐藏参数(程序代码
中看不到),这使得函数类型与Windows callback 函数的预设类型不符。
假设我们有一个CMyclass 如下:
class CMyclass {
private :
int nCount;
int CALLBACK _export
EnumObjectsProc(LPSTR lpLogObject; LPSTR lpData);
public :
void enumIt(CDC& dc);
}
void CMyclass::enumIt(CDC& dc)
{
// 注册callback 函数
dc。EnumObjects(OBJ_BRUSH; EnumObjectsProc; NULL);
}
C++ 编译器针对CMyclass::enumIt 实际做出来的码相当于:
void CMyclass::enumIt(CDC& dc)
{
// 注册callback 函数
CDC::EnumObjects(OBJ_BRUSH; EnumObjectsProc;
NULL; (CDC *)&dc);
}
你所看到的最后一个参数,(CDC *)&dc,其实就是this 指针。类别成员函数靠着this
401
…………………………………………………………Page 464……………………………………………………………
第篇 湷觥 FC 程式設計
指针才得以抓到正确对象的资料。你要知道,内存中只会有一份类别成员函数,
但却可能有许多份类别成员变量……每个对象拥有一份。
C++ 以隐晦的this 指针指出正确的对象。当你这么做:
nCount = 0;
其实是:
this…》nCount = 0;
基于相同的道理,上例中的EnumObjectsProc 既然是一个成员函数,C++ 编译器也
会为它多准备一个隐藏参数。
好,问题就出在这个隐藏参数。callback 函数是给Windows 调用用的,Windows 并
不经由任何对象调用这个函数,也就无由传递this 指针给callback 函数,于是导至
堆栈中有一个随机变量会成为this 指针,而其结果当然是程序的崩溃了。
要把某个函数用作callback 函数,就必须告诉C++ 编译器,不要放this 指针作为
该函数的最后一个参数。两个方法可以做到这一点:
1。 不要使用类别的成员函数(也就是说,要使用全域函数)做为callback 函数。
2。 使用static 成员函数。也就是在函数前面加上static 修饰词。
第一种作法相当于在C 语言中使用callback 函数。第二种作法比较接近OO 的精神。
我想更进一步提醒你的是,C++ 中的static 成员函数特性是,即使对象还没有产生,
static 成员也已经存在(函数或变量都如此) 。换句话说对象还没有产生之前你已经
可以调用类别的static 函数或使用类别的static 变量了。请参阅第二章。
也就是说,凡声明为static 的东西(不管函数或变量)都并不和对象结合在一起,
它们是类别的一部份,不属于对象。
402
…………………………………………………………Page 465……………………………………………………………
第6章 MFC 程式的生死因果
空闲时间(idle time)的处理:OnIdle
为了让Hello 程序更具体而微地表现一个MFC 应用程序的水准,我打算为它加上空闲
时间(idle time )的处理。
我已经在第1章介绍过了空闲时间,也简介了Win32 程序如何以PeekMessage 「偷闲」。
Microsoft 业已把这个观念落实到CWinApp (不,应该是CWinThread)中。请你回头看
看本章的稍早的「CWinApp::Run 程序生命的活水源头」一节,那一节已经揭露了MFC
消息循环的秘密:
int CWinThread::Run()
{
。。。
for (;;)
{
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
}
。。。 // msg loop
}
}
CThread::OnIdle 做些什么事情呢?CWinApp 改写了OnIdle 函数,CWinApp::OnIdle 又
做些什么事情呢?你可以从THRDCORE。CPP 和APPCORE。CPP 中找到这两个函数的
源代码,源代码可以说明一切。当然基本上我们可以猜测OnIdle 函数中大概是做一些
系统(指的是MFC 本身)的维护工作。这一部份的功能可以说日趋式微,因为低优先
权的执行线程可以替代其角色。
如果你的MFC 程序也想处理idle time,只要改写CWinApp 衍生类别的OnIdle 函数即
可。这个函数的类型如下:
virtual BOOL OnIdle(LONG lCount);
403
…………………………………………………………Page 466……………………………………………………………
第篇 湷觥 FC 程式設計
lCount 是系统传进来的一个值,表示自从上次有消息进来,到现在,OnIdle 已经被调用
了多少次。稍后我将改写Hello 程序,把这个值输出到窗口上,你就可以知道空闲时间
是多么地频繁。lCount 会持续累增,直到CWinThread::Run 的消息循环又获得了一个讯
息,此值才重置为0 。
注意:Jeff Prosise 在他的Programming Windows 95 with MFC 一书第7章谈到OnIdle
函数时,曾经说过有几个消息并不会重置lCount 为0,包括鼠标消息、WM_SYSTIMER 、
WM_PAINT 。不过根据我实测的结果,至少鼠标消息是会的。稍后你可在新版的Hello 程
序移动鼠标,看看lCount 会不会重设为0 。
我如何改写Hello 呢?下面是几个步骤:
1。 在CMyWinApp 中增加OnIdle 函数的声明:
class CMyWinApp : public CWinApp
{
public:
virtual BOOL InitInstance(); // 每一个应用程序都应该改写此函数
virtual BOOL OnIdle(LONG lCount); // OnIdle 用来处理空闲时间(idle time)
};
2。 在CMyFrameWnd 中增加一个IdleTimeHandler 函数声明。这么做是因为我希
望在窗口中显示lCount 值, 所以最好的作法就是在OnIdle 中调用
CMyFrameWnd 成员函数,这样才容易获得绘图所需的DC 。
class CMyFrameWnd : public CFrameWnd
{
public:
CMyFrameWnd(); // constructor
afx_msg void OnPaint(); // for WM_PAINT
afx_msg void OnAbout(); // for
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!