您正在查看: MFC 分类下的文章

第14章MFC多线程程序设计

第 14 章 mfc 多线程程序设计

Multi-threaded Programming in mfc

线程(thread),是线程(thread of execution)的简单称呼。"Thread" 这个字的原意是「线」。中文字里头的「线程」也有「线」的意思,所以我采用「线程」、「线程」这样的中文名称。如果你曾经看过「多线」这个名词,其实就是本章所谓的「多线程」。

我曾经在第1章以三两个小节介绍 win32 环境下的进程与线程观念,并且以程序直接调用 CreateThread 的形式,示范了几个 win32 小例子。现在我要更进一步从操作系统的层面谈谈线程的学理基础,然后带引各位看看 MFC 对于「线程」支持了什么样的类。然后,实际写个MFC多线程程序。

从操作系统层面看线程

书籍推荐:如果要从操作系统层面来了解线程,Matt Pietrek 的 Windows 95 System Programming SECRETS(Windows 95 系统程序设计大奥秘/侯俊杰译/旗标出版)无疑是最佳知识来源。Matt 把操作系统核心模块(KERNEL32.DLL)中用来维护线程生存的数据结构都挖掘出来,非常详尽。这是对线程的最基础认识,直达其灵魂深处。

你已经知道,CreateThread 可以产生一个线程,而「线程」的本体就是 CreateThread 第3个参数所指定的一个函数(一般我们称之为「线程函数」)。这个函数将与目前的「执行事实」同时并行,成为另一个「执行事实」。线程函数的执行期,也就是该线程的生命期。

操作系统如何造成这种多任务并行的现象?线程对于操作系统的意义到底是什么?系统如何维护许多个线程?线程与其父亲大人(进程)的关系如何维持?CPU 只有一个,线程却有好几个,如何摆平优先权与排程问题?这些疑问都可以在下面各节中获得答案。

三个观念:模块、进程、线程

试着回答这个问题:进程(process)是什么?给你一分钟时间。

z z z z z...

你的回答可能是:『一个可执行文件执行起来,就是一个进程』。唔,也不能算错。但能不能够有更具体的答案?再问你一个问题:模块(module)是什么?可能你的回答还是:『一个可执行文件执行起来,就是一个模块』。这也不能够算错。但是你明明知道,模块不等于进程。KERNEL32 DLL 是一个模块,但不是一个进程;Scribble EXE 是一个模块,也是一个进程。

我们需要更具体的数据,更精准的答案。

如果我们能够知道操作系统如何看待模块和进程,就能够给出具体的答案了。一段可执行的程序(包括 EXE 和 DLL),其程序代码、数据、资源被加载到内存中,由系统建置一个数据结构来管理它,就是一个模块。这里所说的数据结构,名为 Module Database(MDB),其实就是PE格式中的PE表头,你可以从 WINNT.H 檔中找到一个IMAGE_NT_HEADER 结构,就是它。

好,解释了模块,那么进程是什么?这就比较抽象一点了。这样说,进程就是一大堆拥有权(ownership)的集合。进程拥有地址空间(由 memory context 决定)、动态配置而来的内存、文件、线程、一系列的模块。操作系统使用一个所谓的 Process Database(PDB)数据结构,来记录(管理)它所拥有的一切。

线程呢?线程是什么?进程主要表达「拥有权」的观念,线程则主要表达模块中的程序代码的「执行事实」。系统也是以一个特定的数据结构(Thread Database,TDB)记录线程的所有相关数据,包括线程局部储存空间(Thread Local Storage,TLS)、消息队列、handle 表格、地址空间(Memory Context)等等等。

最初,进程是以一个线程(称为主线程,primary thread)做为开始。如果需要,进程可以产生更多的线程(利用 CreateThread),让 CPU 在同一时间执行不同段落的代码。当然,我们都知道,在只有一颗 CPU 的情况下,不可能真正有多任务的情况发生,「多个线程同时工作」的幻觉主要是靠排程器来完成 -- 它以一个硬件定时器和一组复杂的游戏规则,在不同的线程之间做快速切换动作。以Windows 95 和Windows NT而言,在非特殊的情况下,每个线程被CPU照顾的时间(所谓的timeslice)是20个milliseconds。

如果你有一部多 CPU 计算机,又使用一套支持多 CPU 的操作系统(如 Windows NT),那么一个 CPU 就可以分配到一个线程,真正做到实实在在的多任务。这种操作系统特性称为symmetric multiprocessing(SMP)。Windows 95 没有SMP性质,所以即使在多CPU计算机上跑,也无法发挥其应有的高效能。

图 14-1 表现出一个进程(PDB)如何透过「MODREF 串列」连接到其所使用的所有模组。图 14-2 表现出一个模块数据结构(MDB)的细部内容,最后的 DataDirectory[16] 记录着16个特定节区(sections)的地址,这些sections 包括程序代码、数据、资源。图14-3 表现出一个线程数据结构(PDB)的细部内容。

当Windows加载器将程序加载内存中,KERNEL32 挖出一些内存,构造出一个PDB、一个TDB、一个以上的MDBs(视此程序使用到多少DLL 而定)。针对TDB,操作系统又要产生出memory context(就是在操作系统书籍中提到的那些所谓 page tables)、消息队列、handle 表格、环境数据结构(EDB)...。当这些系统内部数据结构都构造完毕,指令指位器(Instruction Pointer)移到程序的进入点,才开始程序的执行。

图14-1 进程(PDB)透过「MODREF串列」连接到其所使用的所有模块

线程优先权(Priority)

我想我们现在已经能够用很具体的形象去看所谓的进程、模块、线程了。「执行事实」发生在线程身上,而不在进程身上。也就是说,CPU 排程单位是线程而非进程。排程器据以排序的,是每个线程的优先权。

优先权的设定分为两个阶段。我已经在第1章介绍过。线程的「父亲大人」(进程)拥有所谓的优先权等级(priority class,图 1-7),可以在 CreateProcess 的参数中设定。线程基本上继承自其「父亲大人」的优先权等级,然后再加上 CreateThread 参数中的微调差额(-2~+2)。获得的结果(图 1-8)便是线程的所谓base priority,范围从0~31,数值愈高优先权愈高。::SetThreadPriority 是调整优先权的工具,它所指定的也是微调差额(-2~+2)。

图14-2 模块数据结构 MDB 的细部内容(数据整理自Windows 95 System Programming SECRETS, Matt Pietrek, IDG Books)

线程排程(Scheduling)

排程器挑选「下一个获得 CPU 时间的线程」的唯一依据就是:线程优先权。如果所有等待被执行的线程中,有一个是优先权16,其它所有线程都是优先权15(或更低),那么优先权16者便是下一个夺标者。如果线程A和B同为优先权16,排程器会挑选等待比较久的那个(假设为线程A)。当A的时间切片(timeslice)终了,如果B以外的其它线程的优先权仍维持在 15(以下),线程B就会获得执行权。

「如果B以外的其它线程的优先权仍维持在 15(以下)...」,唔,这听起来彷佛优先权会变动似的。的确是。为了避免朱门酒肉臭、路有冻死骨的不公平情况发生,排程器会弹性调整线程优先权,以强化系统的反应能力,并且避免任何一个线程一直未能接受 CPU 的润泽。一般的线程优先权是 7,如果它被切换到前景,排程系统可能暂时地把它调升到8 或9 或更高。对于那些有着输入消息等待被处理的线程,排程系统也会暂时调高其优先权。

对于那些优先权本来就高的线程,也并不是有永久的保障权利。别忘了 Windows 毕竟是个消息驱动系统,如果某个线程调用 ::GetMessage 而其消息队列却是空的,这个线程便被冻结,直到再有消息进来为止。冻结的意思就是不管你的优先权有多高,暂时退出排班行列。线程也可能被以 ::SuspendThread 强制冻结住(::ResumeThread 可以解除冻结)。

图14-3 线程数据结构( PDB)的细部内容(数据整理自Windows 95 System Programming SECRETS,Matt Pietrek,IDG Books)

会被冻结,表示这个线程「要去抓取消息,而线程所附带的消息队列中却没有消息」。如果一个线程完全和 UI 无关呢?是否它就没有消息队列?倒不是,但它的程序代码中没有消息循环倒是事实。是的,这种线程称为 worker thread。正因它不可能会被冻结,所以它绝对不受 Win16Mutex 或其它因素而影响其强制性多任务性质,及其优先权。

Thread Context

Context 一词,我不知道有没有什么好译名,姑且就用原文吧。它的直接意思是「前后关系、脉络;环境、背景」。所以我们可以说Thread Context 是构成线程的「背景」。

那是指什么呢?狭义来讲是指一组缓存器值(包括指令指位器 IP)。因为线程常常会被暂停,被要求把CPU拥有权让出来,所以它必须将暂停之前一刻的状态统统记录下来,以备将来还可以恢复。

你可以在WINNT.H中找到一个CONTEXT资料结构 , 它可以用来储存Thread Context 。::GetThreadContext 和::SetThreadContext 可 以 取 得 和 设 定 某个线程的context,因而改变该线程的状态。这已经是非常低阶的行为了。Matt Pietrek在其Windows 95 System Programming SECRETS 一书第10 章,写了一个Win32 API Spy程序,就充份运用了这两个函数。

我想我们在操作系统层面上的线程学理基础已经足够了,现在让我们看看比较实际一点的东西。

从程序设计层面看线程

书籍推荐:如果要从程序设计层面来了解线程,Jim Beveridge 和 Robert Wiener 合着的 Multithreading Applications in Win32 (Win32 多线程程序设计/侯俊杰译/碁峰出版)是很值得推荐的一份知识来源。这本书介绍线程的学理观念、程序方法、同步控制、资料一致性的保持、C runtime library 的多线程版本、C++ 的多线程程序方法、MFC 中的多线程程序方法、除错、进程通讯(IPC)、DLLs...,以及约 50 页的实际应用。

书籍推荐:Jeffrey Richter 的 Advanced Windows 在进程与线程的介绍上(第2章和第3章),也有非常好的表现。他的切入方式是详细而深入地叙述相关 Win32 API 的规格与用法。并举实例左证。

如何产生线程?我想各位都知道了,::CreateThread 可以办到。图 14-4 是与线程有关的 Win32 API。

与线程有关的Win32 API 功能

AttachThreadInput 将某个线程的输入导向另一个线程

CreateThread 产生一个线程

ExitThread 结束一个线程

GetCurrentThread 取得目前线程的 handle

GetCurrentThreadId 取得目前线程的 ID

GetExitCodeThread 取得某一线程的结束代码(可用以决定线程是否已结束)

GetPriorityClass 取得某一进程的优先权等级

GetQueueStatus 传回某一线程的消息队列状态

GetThreadContext 取得某一线程的 context

GetThreadDesktop 取得某一线程的 desktop 对象

GetThreadPriority 取得某一线程的优先权

GetThreadSelectorEntry 除错器专用,传回指定之线程的某个selector 的LDT 记录项

ResumeThread 将某个冻结的线程恢复执行

SetPriorityClass 设定优先权等级

SetThreadPriority 设定线程的优先权

Sleep 将某个线程暂时冻结。其它线程将获得执行权。

SuspendThread 冻结某个线程

TerminateThread 结束某个线程

TlsAlloc 配置一个TLS(Thread Local Storage)

TlsFree 释放一个TLS(Thread Local Storage)

TlsGetValue 取得某个TLS(Thread Local Storage)的内容

TlsSetValue 设定某个TLS(Thread Local Storage)的内容

WaitForInputIdle 等待,直到不再有输入消息进入某个线程中

图14-4 与线程有关的 Win32 API函数

注意,多线程并不能让程序执行得比较快(除非是在多 CPU 机器上,并且使用支持symmetric multiprocessing 的操作系统),只是能够让程序比较「有反应」。试想某个程序在某个选单项目被按下后要做一个小时的运算工作,如果这份工作在主线程中做,而且没有利用 PeekMessage 的技巧时时观看消息队列的内容并处理之,那么这一个小时内这个程序的使用者接口可以说是被冻结住了,将毫无反应。但如果沉重的运算工作是由另一个线程来负责,使用者接口将依然灵活,不受影响。

Worker Threads 和 UI Threads

从Windows 操作系统的角度来看,线程就是线程,并未再有什么分类。但从MFC的角度看,则把线程划分为和使用者接口无关的worker threads,以及和使用者接口(UI)有关的UI threads。

基本上,当我们以 ::CreateThread 产生一个线程,并指定一个线程函数,它就是一个 worker thread,除非在它的生命中接触到了输入消息—— 这时候它应该有一个消息循环,以抓取消息,于是该线程摇身一变而为 UI thread。

注意,线程本来就带有消息队列,请看图 14-3 的 TDB 结构。而如果线程程序代码中带有一个消息循环,就称为 UI thread。

错误观念

我记得曾经在微软的技术文件中,也曾经在微软的范例程序中,看到他们鼓励这样的作法:为程序中的每一个窗口产生一个线程,负责窗口行为。这种错误的示范尤其存在于 MDI 程序中。是的,早期我也沾沾自喜地为 MDI 程序的每一个子窗口设计一个线程。基本上这是错误的行为,要付出昂贵的代价。因为子窗口一切换,上述作法会导至线程也切换,而这却要花费大量的系统资源。比较好的作法是把所有UI(User Interface)动作都集中在主线程中,其它的「纯种运算工作」才考虑交给worker threads去做。

正确态度

什么是使用多线程的好时机呢?如果你的程序有许多事要忙,但是你还要随时保持注意某些外部事件(可能来自硬件或来自使用者),这时就适合使用多线程来帮忙。

以通讯程序为例。你可以让主线程负责使用者接口,并保持中枢的地位。而以一个分离的线程处理通讯端口。

MFC 多线程程序设计

我已经在第1章以一个小节介绍了 Win32 多线程程序的写法,并给了一个小范例MltiThrd。这一节,我要介绍 MFC 多线程程序的写法。

探索 CWinThread

就像CWinApp 对象代表一个程序本身一样,CWinThread 对象代表一个线程本身。这个 MFC 类我们曾经看过,第6章讲「MFC 程序的生死因果」时,讲到「CWinApp::Run——程序生命的活水源头」,曾经追踪过CWinApp::Run 的源头 CWinThread::Run(里面有一个消息循环)。可见程序的「执行事实」系发生在 CWinThread 对象身上,而 CWinThread 对象必须要(必然会)产生一个线程。

我希望「CWinThread 对象必须要(必然会)产生一个线程」这句话不会引起你的误会,以为程序在application object(CWinApp 对象)的构造函数必然有个动作最终调用到CreateThread 或 _beginthreadex。不,不是这样。想想看,当你的 Win32 程序执行起来,你的程序并没有调用 CreateProcess 为自己做出代表自己的那个进程,也没有调用CreateThread 为自己做出代表自己的主线程(primary thread)的那个线程。为你的程序产生第一个进程和线程,是系统加载器以及核心模块(KERNEL32)合作的结果。

所以,再次循着第6章一步步剖析的步骤,MFC 程序的第一个动作是 CWinApp::CWinApp(比 WinMain 还早),在那里没有「产生线程」的动作,而是已经开始在收集线程的相关信息了:

// in MFC 4.2 APPCORE.CPP

CWinApp::CWinApp(LPCTSTR lpszAppName)

{

...

// initialize CWinThread state

AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();

AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;

ASSERT(AfxGetThread() == NULL);

pThreadState->m_pCurrentWinThread = this;

ASSERT(AfxGetThread() == this);

m_hThread = ::GetCurrentThread();

m_nThreadID = ::GetCurrentThreadId();

...

}

虽然MFC程序只会有一个CWinApp对象,而CWinApp派生自CWinThread,但并不是说一个MFC程序只能有一个CWinThread 对象。每当你需要一个额外的线程,不应该在MFC程序中直接调用::CreateThread 或 _beginthreadex,应该先产生一个CWinThread 对象,再调用其成员函数 CreateThread 或全局函数 AfxBeginThread 将线程产生出来。当然,现在 你必然已经可以推测到,CWinThread::CreateThread或AfxBeginThread内部呼 叫了::CreateThread或_beginthreadex(事实上答案是_beginthreadex)。

这看起来颇有值得商议之处:为什么CWinThread 构造函数不帮我们调用 AfxBeginThread 呢?似乎CWinThread 为德不卒。

图 14-5 就是CWinThread 的相关原始代码。

图14-5 CwinThread的相关原始代码

产生线程,为什么不直接用::CreateThread 或_beginthreadex?为什 么要透过CWinThread对象 ?我想你可以轻易从MFC原始代码中看出,因为CWinThread::CreateThread 和AfxBeginThread 不只是::CreateThread 的一层包装,更做了一些application framework 所需的内部数据初始化工作,并确保使用正确的C runtime library 版本。原始代码中有:

#ifndef _MT

... // 做些设定工作,不产生线程,回返。

#else

... // 真正产生线程,回返。

#endif //!_MT)

的动作,只是被我删去未列出而已。

接下来我要把worker thread和UI thread 的产生步骤做个整理。它们都需要调用AfxBeginThread 以产生一个CWinThread 对象,但如果要产生一个 UI thread,你还必须先定义一个 CWinThread 派生类。

产生一个Worker Thread

Worker thread 不牵扯使用者接口。你应该为它准备一个线程函数,然后调用AfxBeginThread:

CWinThread* pThread = AfxBeginThread(ThreadFunc, &Param);

...

UINT ThreadFunc (LPVOID pParam)

{

...

}

AfxBeginThread 事实上一共可以接受六个参数,分别是:

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc,

LPVOID pParam,

int nPriority = THREAD_PRIORITY_NORMAL,

UINT nStackSize = 0,

DWORD dwCreateFlags = 0,

LPSECURITY_ATTRIBUTES>

参数一pfnThreadProc 表示线程函数。参数二pParam 表示要传给线程函数的参数。参数三nPriority表示优先权的微调值,预设为 THREAD_PRIORITY_NORMAL,也就是没有微调。参数四 nStackSize 表示堆栈的大小,默认值 0 则表示堆栈最大容量为1MB。参数五 dwCreateFlags 如果为默认值 0,就表示线程产生后立刻开始执行;如果其值为 CREATE_SUSPENDED,就表示线程产生后先暂停执行。之后你可以使用CWinThread::ResumeThread 重新执行它。参数六 lpSecurityAttrs 代表新线程的安全防护属性。默认值 NULL 表示此一属性与其产生者(也是个线程)的属性相同。

在这里我们遭遇到一个困扰。线程函数是由系统调用的,也就是个 callback 函数,不容许有 this 指针参数。所以任何一般的 C++ 类成员函数都不能够拿来当做线程函数。它必须是个全局函数,或是个 C++ 类的 static 成员函数。其原因我已经在第6章的「Callback 函数」一节中描述过了,而采用全局函数或是C++ static 成员函数,其间的优劣因素我也已经在该节讨论过。

线程函数的类型AFX_THREADPROC定义于AFXWIN.H 之中:

// in AFXWIN.H

typedef UINT (AFX_CDECL *AFX_THREADPROC)(LPVOID);

所以你应该把本身的线程函数声明如下(其中的 pParam 是个指针,在实用上可以指向程序员自定的数据结构):

UINT ThreadFunc (LPVOID pParam);

否则,编译时会获得这样的错误消息:

error C2665: 'AfxBeginThread' : none of the 2 overloads can convert

parameter 1 from type 'void (unsigned long *)'

有时候我们会让不同的线程使用相同的线程函数,这时候你就得特别注意到线程函数使用全局变量或静态变量时,数据共享所引发的严重性(有好有坏)。至于放置在堆栈中的变量或对象,都不会有问题,因为每一个线程自有一个堆栈。

产生一个UI Thread

UI thread 可不能够光由一个线程函数来代表,因为它要处理消息,它需要一个消息循环。好得很,CWinThread::Run 里头就有一个消息循环。所以,我们应该先从CWinThread派生一个自己的类,再调用 AfxBeginThread 产生一个 CWinThread 对象:

class CMyThread : public CWinThread

{

DECLARE_DYNCREATE(CMyThread)

public:

void BOOL InitInstance();

};

IMPLEMENT_DYNCREATE(CMyThread, CWinThread)

BOOL CMyThread::InitInstance()

{

...

}

CWinThread *pThread = AfxBeginThread(RUNTIME_CLASS(CMyThread));

我想你对RUNTIME_CLASS 宏已经不陌生了,第3章和第8章都有这个宏的原始代码展现以及意义解释。AfxBeginThread 是上一小节同名函数的一个 overloaded 函数,一共可以接受五个参数,分别是:

CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass,

int nPriority = THREAD_PRIORITY_NORMAL,

UINT nStackSize = 0,

DWORD dwCreateFlags = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

最后四个参数的意义和默认值比上一节同名函数相同,但是少接受一个 LPVOID pParam 参数。

你可以在AFXWIN.H 中找到CWinThread的定义:

class CWinThread : public CCmdTarget

{

DECLARE_DYNAMIC(CWinThread)

BOOL CreateThread(DWORD dwCreateFlags = 0, UINT nStackSize = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

...

int GetThreadPriority();

BOOL SetThreadPriority(int nPriority);

DWORD SuspendThread();

DWORD ResumeThread();

BOOL PostThreadMessage(UINT message, WPARAM wParam, LPARAM lParam);

...

};

其中有许多成员函数和图14-4中的Win32 API函数有关。在CWinThread的成员函数中,有五个函数只是非常单纯的 Win32 API 的包装而已,它们被定义于AFXWIN2.INL文件中:

// in AFXWIN2.INL

// CWinThread

_AFXWIN_INLINE BOOL CWinThread::SetThreadPriority(int nPriority)

{ ASSERT(m_hThread!=NULL);return ::SetThreadPriority(m_hThread, nPriority); }

_AFXWIN_INLINE int CWinThread::GetThreadPriority()

{ ASSERT(m_hThread != NULL); return ::GetThreadPriority(m_hThread); }

_AFXWIN_INLINE DWORD CWinThread::ResumeThread()

{ ASSERT(m_hThread != NULL); return ::ResumeThread(m_hThread); }

_AFXWIN_INLINE DWORD CWinThread::SuspendThread()

{ ASSERT(m_hThread != NULL); return ::SuspendThread(m_hThread); }

_AFXWIN_INLINE BOOL CWinThread::PostThreadMessage(UINT message, WPARAM wParam, LPARAM lParam)

{ ASSERT(m_hThread != NULL); return ::PostThreadMessage(m_nThreadID, message, wParam, lParam); }

线程的结束

既然worker thread 的生命就是线程函数本身,函数一旦 return,线程也就结束了,自然得很。或者线程函数也可以调用 AfxEndThread,结束一个线程。

UI 线程因为有消息循环的关系,必须在消息队列中放一个 WM_QUIT,才能结束线程。放置的方式和一般Win32程序一样,调用::PostQuitMessage 即可办到。亦或者,在线程的任何一个函数中调用AfxEndThread,也可以结束线程。

AfxEndThread 其实也是个外包装,其内部调用 _endthreadex,这个动作才真正把线程结束掉。

别忘了,不论worker thread或UI thread,都需要一个CWinThread 对象,当线程结束,记得把该对象释放掉(利用 delete)。

线程与同步控制

看起来线程的诞生与结束,以及对它的优先权设定、冻结、重新启动,都很容易。但是我必须警告你,多线程程序的设计成功关键并不在此。如果你的每一个线程都非常独立,彼此没有干联,也就罢了。但如果许多个线程互有关联呢?有经验的人说多线程程序设计有多复杂多困难,他们说的并不是线程本身,而是指线程与线程之间的同步控制。

原因在于,没有人能够预期线程的被执行。在一个合作型多任务系统中(例如 Windows 3.x),操作系统必须得到程序的允许才能够改变线程。但是在强制性多任务系统中(如Win95或WinNT),控制权被排程器强制移转,也因此两个线程之间的执行次序变得不可预期。这不可预期性造成了所谓的 race conditions。

假设你正在一个文件服务器中编辑一串电话号代码。文件打开来内容如下:

Charley 572-7993

Graffie 573-3976

Dennis 571-4219

现在你打算为Sue加上一笔新数据。正当你输入 Sue电话号代码的时候,另一个人也打开文件并输入另一笔有关于 Jason 的数据。最后你们两人也都做了存文件动作。谁的数据会留下来?答案是比较晚存盘的那个人,而前一个人的输入会被覆盖掉。这两个人面临的就是 race condition。

再举一个例子。你的程序产生两个线程,A和B。线程B的任务是设定全局变量X。线程A则要去读取X。假设线程B先完成其工作,设定了X,然后线程A才执行,读取X,这是一种好的情况,如图 14-6a。但如果线程A先执行起来并读取全局变量X,它会读到一个不适当的值,因为线程B还没有完成其工作并设定适当的X。如图14-6b。这也是race condition。

另一种线程所造成的可能问题是:死结(deadlock)。图14-7可以说明这种情况。

图14-6a race condition(good)

图14-6b race condition(bad)

图14-7 死结(deadlock)

要解决这些问题,必须有办法协调各个线程的执行次序,让某个线程等待某个线程。Windows 系统提供四种同步化机制,帮助程序进行这种工作:

1. Critical Section(关键局部)

2. Semaphore(号志)

3. Event(事件)

4. Mutex(Mutual Exclusive,互斥器)

MFC 也提供了四个对应的类:

MFC 多线程程序实例

我将在此示范如何把第1章最后的一个 Win32 多线程程序 MltiThrd 改装为 MFC 程序。我只示范主架构(与 CWinThread、AfxBeginThread、ThreadFunc 有关的部份),程序绘图部份留给您做练习。

首先我利用MFC AppWizard 产生一个Mltithrd 项目,放在书附盘片的 Mltithrd.14 子目录中,并接受 MFC AppWizard 的所有预设选项。

接下来我在resource.h 中加上一些定义,做为线程函数的参数,以便在绘图时能够把代表各线程的各个长方形涂上不同的颜色:

#define HIGHEST_THREAD 0x00

#define ABOVE_AVE_THREAD 0x3F

#define NORMAL_THREAD 0x7F

#define BELOW_AVE_THREAD 0xBF

#define LOWEST_THREAD 0xFF

然后我在Mltithrd.cpp 中加上一些全局变量(你也可以把它们放在 CMltithrdApp 之中。我只是为了图个方便):

然后在 CMltithrdApp::InitInstance 函数最后面加上一些代码:

这样一来我就完成了五个 worker threads 的产生,并且将其优先权做了 -2~+2 范围之间的微调。

接下来我应该设计线程函数。就如我在第1章已经说过,这个函数的五个线程可以使用同一个线程函数。本例中是设计为全局函数好呢?还是 static 成员函数好?如果是后者,应该成为哪一个类的成员函数好?

为了「要在线程函数做窗口绘图动作」的考虑,我把线程函数设计为 CMltithrdView的一个static 成员函数,并遵循应有的函数类型:

好,到此为止,编译链接,获得的程序将在执行后产生五个线程,并全部冻结。以 Process Viewer(Visual C++ 5.0 所附工具)观察之,证明它的确有六个线程(包括一个主线程以及我们所产生的另五个线程):

接下来,留给你的作业是:

1. 利用资源编辑器为程序加上各选单项目,如图 1-9。

2. 设计上述选单项目的命令处理例程。

3. 在线程函数ThreadFunc 内加上计算与绘图能力。并判断使用者选择何种延迟方式,做出适当反应。

第15章定制一个AppWizard

第15章 定制一个AppWizard

我们的Scribble 程序一路走来,大家可还记得它一开始并不是平地而起,而是由AppWizard 以「程序代码产生器」的身份,自动为我们做出一个我所谓的「骨干程序」来?

Developer's Studio 提供了一个开放的 AppWizard 接口。现在,我们可以轻易地扩充AppWizard:从小规模的扩充,到几乎改头换面成为一种全新类型的程序代码产生器。

Developer's Studio 提供了许多种不同的项目类型,供你选择。当你选按 Visual C++ 5.0 整合环境中的【File/New】命令项,并选择【Projects】附页,便得到这样的对话窗画面:

除了上述这些内建的程序类型,它还可以显示出任何自定程序类型(custom types)。Developer's Studio(整合环境)和 AppWizard 之间的接口借着一组类和一些组件表现出来,使我们能够轻易订制合乎自己需求的 AppWizard。制造出来的所谓custom AppWizard(一个扩展名为 .AWX 的动态链接函数库,注),必须被放置于磁盘目录\DevStudio\SharedIDE\Template 中,才能发挥效用。Developers Studio 和 AppWizard 和AWX 之间的基本架构如图 15-1。

注:我以 DUMPBIN(Visual C++ 附的一个观察文件类型的工具)观察 .AWX 檔,得到结果如下:

E:\DevStudio\SharedIDE\BIN\IDE>dumpbin addinwz.awx

Microsoft (R) COFF Binary File Dumper Version 5.00.7022

Copyright (C) Microsoft Corp 1992-1997. All rights reserved.

Dump of file addinwz.awx

File Type: DLL <--- 这证明 .AWX 的确是个动态链接函数库。

Summary

1000 .data

1000 .reloc

3A000 .rsrc

3000 .text

事实上AWX(Application Wizard eXtension)就是一个32位元的mfc extension DLL。

是不是Visual C++ 系统中早已存在有一些 .AWX 檔了呢?当然,它们是:

Directory of E:\DevStudio\SharedIDE\BIN\IDE

ADDINWZ AWX 255,872 03-29-97 16:43 ADDINWZ.AWX

ATLWIZ AWX 113,456 03-29-97 16:43 ATLWIZ.AWX

CUSTMWZ AWX 278,528 03-29-97 16:43 CUSTMWZ.AWX

INETAWZ AWX 91,408 03-29-97 16:43 INETAWZ.AWX

mfcTLWZ AWX 146,272 03-29-97 16:43 MFCTLWZ.AWX

5 file(s) 885,536 bytes

请放心,你只能够扩充(新增)项目类型,不会一不小心取代了某一个原已存在的项目类型。

图15-1 Developers Studio和AppWizard 和*.AWX 之间的基本架构

到底Wizard是什么?

所谓Wizard,就是一个扩展名为.AWX 的动态链接函数库。Visual C++ 的 "project manager" 会检查整合环境中的Template 子目录(\DevStudio\SharedIDE\Template),然后显示其图标于【New Project】对话窗中,供使用者选择。

Wizard 本身是一个所谓的「template 直译器」。这里所谓的"template" 是一些文字文件,内有许多特殊符号(也就是本章稍后要介绍的macros 和 directives)。Wizard 读取这些template,对于正常文字,就以正常的output stream 输出到另一个文件中;对于特殊符号或保留字,就解析它们然后再把结果以一般的 output stream 输出到文件中。Wizard 所显示给使用者看的「步骤对话窗」可以接受使用者的指定项目或文字输出,于是会影响 template 中的特殊符号的内容或解析,连带也就影响了Wizard 的stream 输出。这些stream 输出,最后就成为你的项目的源文件。

Custom AppWizard 的基本操作

Developers Studio提供了一个让我们制作custom AppWizard的Wizard,就叫作 Custom AppWizard。让我们先实地操作一下这个工具,再来谈程序技术问题。

注意:以下我以 Custom AppWizard 表示 Visual C++ 所附的工具,custom AppWizard 表示我们希望做出来的「订制型 AppWizard」。

选按【File/New】,在对话窗中选择 Custom AppWizard,然后在右边填写你的项目名称:

按下【OK】钮,进入步骤一画面:

你可以选择三种可能的扩充型式:

1. An existing project:根据一个原已存在的项目文件(*.dsp)来产生一个custom AppWizard。

2. Standard MFC AppWizard steps:根据某个原有的AppWizard,为它加上额外的几个步骤,成为一个新的 custom AppWizard。这是一般最被接受的一种方式。

3. Your own custom steps:有全新的步骤和全新的对话窗画面。这当然是最大弹性的展现啦,并同时也是最困难的一种作法,因为你要自行负责所有的工作。哪些工作呢?稍后我有一个例子使用第二种型式,将介绍所谓的macros和directievs,你可以从中推而想之这第三种型式的繁重负担。

我的目的是做出一个属于我个人研究室("Top Studio")专用的custom AppWizard,以原本的MFC AppWizard(exe)为基础(有六个步骤),再加上一页(一个步骤),让程序员填入姓名、简易说明,然后Top Studio AppWizard 能够把这些数据加到每一个原始代码文件最前端。所以,我应该选择上述三种情况的第二种:Standard MFC AppWizard steps,并在上图下方选择欲增加的步骤数量。本例为1。

接下来按【Next】进入 Custom AppWizard 的第二页:

既然刚刚选择的是 Standard MFC AppWizard steps,这第二页便问你要制造出MFC Exe或MFC Dll。我选择MFC Exe。并在对话窗下方选择使用的文字:英文。很可惜目前这里没有中文可供选择。

这样就完成了订制的程序。按下【Finish】钮,你获得一张清单:

再按下【OK】钮,开始产生程序代码。然后点选整合环境中的【Build/Top Studio.awx】。整合环境下方出现 "Making help file..." 字样。这时候你要注意了,上个厕所喝杯咖啡后它还是那样,一点动静都没有。原来,整合环境启动了 Microsoft Help Workshop,而且把它极小化;你得把它叫出来,让它动作才行。

如果你不想要那些占据很大磁盘空间的 HLP 文件和 HTM 檔,也可以把 Microsoft Help Workshop 关掉,控制权便会回到整合环境来,开始进行编译链接的工作。

建造过程完毕,我们获得了一个Top Studio.Awx文件。这个文件会被整合环境自动拷贝到\DevStudio\SharedIDE\Template 磁盘目录中:

Directory of E:\DevStudio\SharedIDE\Template

ATL <DIR> 03-29-97 14:12 ATL

MFC RCT 4,744 12-04-95 16:09 MFC.RCT

README TXT 115 10-30-96 17:54 README.TXT

TOPSTU~1 AWX 523,776 04-07-97 17:01 Top Studio.awx

TOPSTU~1 PDB 640,000 04-07-97 17:01 Top Studio.pdb

现在,再一次选按整合环境的【File/New】,在【Projects】对话窗中我们看到 Top Studio AppWizard 出现了:

试试它的作用。请像使用一般的MFC AppWizard 那样使用它(像第4章那样),你会发现它有7个步骤。前6个和MFC AppWizard 完全一样,第7个画面如下:

哇喔,怎么会这样?当然是这样,因为你还没有做任何程序动作嘛!目前 Top Studio AppWizard 产生出来的程序代码和第4章的Scribble step0 完全相同。

剖析 AppWizard Components

图15-2是AppWizard components 的架构图。所谓AppWizard components,就是架构出一个AppWizard 的所有「东西」,包括:

1. Dialog Templates(Dialog Resources)

2. Dialog Classes

3. Text Templates(Template 子目录中的所有 .H 檔和 .CPP 檔)

4. Macro Dictionary

5. Information Files

图15-2 用以产生一个custom AppWizard的各种components

Dialog Templates 和 Dialog Classes

以Top Studio AppWizard 为例,由于多出一个对话窗画面,我们势必需要产生一个对话窗面板(template),还要为这面板产生一个对应的C++ 类,并以 DDX/DDV(第10章)取得使用者的输入数据。这些技术我们已经在第10章中学习过。

获得的使用者输入数据如何放置到程序代码产生器所产生的项目原始代码中?

喔,到底谁是程序代码产生器?老实说我也没有办法明确指出是哪个模块,哪个文件(也许就是 AWX 本身)。但是我知道,程序代码产生器会读取 .AWX 文件,做出适当的原始代码来。而 .AWX 不正是前面才刚由Custom AppWizard 做出来吗?里面有些什么蹊跷呢?是的,有许多所谓的macros和directives存在于 Custom AppWizard 所产生的"text template"(也就是template子目录中的所有 .CPP 和 .H 檔)中。以Top Studio AppWizard 为例,我们获得这些文件:

H:\U004\PROG\TOP.15:

Top Studio.h

StdAfx.h

Top StudioAw.h

Debug.h

Resource.h

Chooser.h

Cstm1Dlg.h <---- 稍后要修改此文件内容

Top Studio.cpp

StdAfx.cpp

Top StudioAw.cpp

Debug.cpp

Chooser.cpp

Cstm1Dlg.cpp <---- 稍后要修改此文件内容

H:\U004\PROG\TOP.15\TEMPLATE:<---- 稍后要修改所有这些文件的内容

DlgRoot.h

Dialog.h

Root.h

StdAfx.h

Frame.h

ChildFrm.h

Doc.h

View.h

RecSet.h

SrvrItem.h

IpFrame.h

CntrItem.h

DlgRes.h

Resource.h

DlgRoot.cpp

Dialog.cpp

Root.cpp

StdAfx.cpp

Frame.cpp

ChildFrm.cpp

Doc.cpp

View.cpp

RecSet.cpp

SrvrItem.cpp

IpFrame.cpp

CntrItem.cpp

NewProj.inf

Confirm.inf

Macros

我们惯常所说的程序中的 macro,通常带有「动作」。这里的 macro 则是用来代表一个常数。前后以包夹起来的字符串即为一个macro 名称,例如:

class $$FRAME_CLASS$$ : public $$FRAME_BASE_CLASS$$

程序代码产生器看到这样的句子,如果发现$$FRAME_CLASS$$ 被定义为"CMDIFrameWnd",$$FRAME_BASE_CLASS$$ 被定义为 "CFrameWnd",就产生出这样的句子:

class CMDIFrameWnd : public CFrameWnd

Developer Studio 系统已经内建一组标准的 macros 如下,给 AppWizard 所产生的每一个项目使用:

宏名称 意义

APP 应用程序的CWinApp-driven class.

FRAME 应用程序的main frame class.

DOC 应用程序的document class.

VIEW 应用程序的view class.

CHILD_FRAME 应用程序的MDI child frame class(如果有的话)

DLG 应用程序的main dialog box class(在dialog-based 程序中)

RECSET 应用程序的recordset class(如果有的话)

SRVRITEM 应用程序的main server-item class(如果有的话)

CNTRITEM 应用程序的main container-item class(如果有的话)

IPFRAME 应用程序的in-place frame class(如果有的话)

另外还有一组macro,可以和前面那组搭配运用:

宏名称 意义

class 类名称(小写)

CLASS 类名称(大写)

base_class 基类的名称(小写)

BASE_CLASS 基类的名称(大写)

ifile 实现文件名称(.CPP 文件,不含扩展名)(小写)

IFILE 实现文件名称(.CPP 文件,不含扩展名)(大写)

Hfile 表头文件名称(.H 文件,不含扩展名)(小写)

hFILE 表头文件名称(.H 文件,不含扩展名)(大写)

ROOT 应用程序的项目名称(全部大写)

root 应用程序的项目名称(全部小写)

Root 应用程序的项目名称(可以引大小写)

图 15-3 列出项目名称为 Scribble 的某些个标准宏内容。

宏 实际内容

图15-3 项目名称为Scribble的数个标准宏内容

Directives

所谓directives,类似程序语言中的条件控制句(像是if、else 等等),用来控制text templates 中的流程。字符串前面如果以$$开头,就是一个 directive,例如:

$$IF(PROJTYPE_MDI)

...

$$ELSE

...

$$ENDIF

每一个 directive 必须出现在每一行的第一个字符。

系统提供了一组标准的 directives 如下:

$$IF

$$ELIF

$$ELSE

$$ENDIF

$$BEGINLOOP

$$ENDLOOP

$$SET_DEFAULT_LANG

$$//

$$INCLUDE

动手修改 Top Studio AppWizard

我的目的是做出一个属于我个人研究室专用的 Top Studio AppWizard,以原本的MFC AppWizard(exe)为基础,加上第7个步骤,让程序员填入姓名、简易说明,然后Top Studio AppWizard 就能够把这些数据加到每一个原始代码文件最前端。

看来我们已经找到出口了。我们应该先为Top Studio AppWizard 产生一个对话窗,当做步骤7的画面,再产生一个对应的C++ 类,于是DDX功能便能够取得对话窗所接收到的输入字符串(程序员姓名和程序主旨)。然后我们设计一些macros,再撰写一小段代码(其中用到那些macros),把这一小段代码加到每一个 .CPP 和 .H 檔的最前面。大功告成。

本例不需要我们动手写directives。

我想我遗漏了一个重要的东西。Macros 如何定义?放在什么地方?我曾经在本书第8章介绍Scribble 的数据结构时,谈到collection classes。其中有一种数据结构名为 Map(也就是Dictionary)。Macros 正是被定义并储存在一个Map之中,并以macro 名称做为键值(key)。

让我们一步一步来。

利用资源编辑器修改IDD_CUSTOM1对话窗画面

请参考第4章和第10章,修改IDD_CUSTOM1 对话窗画面如下:

两个edit控制组件的ID如图15-4所示。

利用ClassWizard修改IDD_CUSTOM1对话窗的对应类CCustom1Dlg

图15-4列出每一个控制组件的类型、识别代码及其对应的变量名称等数据。变量将做为DDX 所用。修改动作如图15-5。

control ID 名称 种类 变量类型

IDC_EDIT_AUTHOR m_szAuthor Value CString

IDC_EDIT_COMMENT m_szComment Value CString

图15-4 IDD_CUSTOM1对话窗控制组件的类型、ID、对应的变量名称

图15-5 利用ClassWizard 为IDD_CUSTOM1对话窗的两个edit

控制组件加上两个对应的变量m_szAuthor和m_szComment,以为DDX 所用

Custom AppWizard 为我们做出来的这个CCustom1Dlg必定派生自 CAppWizStepDlg。你不会在 MFC 类架构文件中发现 CAppWizStepDlg,它是 Visual C++ 的 mfcapwz.dll所提供的一个类。此类有一个虚函数 OnDismiss,当AppWizard 的使用者选按【Back】或【Next】或【Finish】钮时就会被唤起。如果它传回TRUE,AppWizard 就可以切换对话窗;如果传回的是FALSE,就不能。我们可以在这个函数中做数值检验的工作,更重要的是做 macros的设定工作。

改写OnDismiss虚函数,在其中定义macros

前面我已经说过,macros 的定义储存在一个Map 结构中。它在哪里?

整个Top Studio AppWizard(以及其它所有的 custom AppWizard)的主类系派生自系统提供的CCustomAppWiz:

// in Top StudioAw.h

class CTopStudioAppWiz : public CCustomAppWiz

{

....

};

// in "Top StudioAw.cpp"

CTopStudioAppWiz TopStudioaw; // 类似 application object。

// 对象命名规则是 "项目名称" + "aw"。

你不会在MFC类架构文件中发现CCustomAppWiz,它是Visual C++的 mfcapwz.dll所提供的一个类。此类拥有一个CMapStringToString 对象,名为m_Dictionary,所以TopStudioaw 自然就继承了m_Dictionary。这便是储存macros 定义的地方。我们可以利用TopStudioaw.m_Dictionary[xxx] = xxx 的方式来加入一个个的macros。

现在,改写OnDismiss 虚函数如下:

这么一来我们就定义了三个 macros:

macro名称 macro内容

PROJ_AUTHOR m_szAuthor

PROJ_DATE szDate

PROJ_COMMENT m_szComment

修改text template

现在,为Top Studio AppWizard 的template 子目录中的每一个 .H 檔和 .CPP 檔增加一小段代码,放在文件最前端:

/*

This project was created using the Top Studio AppWizard

$$PROJ_COMMENT$$

Project: $$Root$$

Author : $$PROJ_AUTHOR$$

Date : $$PROJ_DATE$$

*/

Top Studio AppWizard 执行结果

重新编译链接,然后使用 Top Studio AppWizard 产生一个项目。第7个步骤的画面如下:

由Top Studio AppWizard 产生出来的程序代码中,每一个 .CPP 和 .H 檔最前面果然有下面数行文字,大功告成。

更多的信息

我在本章中只是简单示范了一下「继承自原有之 Wizard,再添加新功能」的作法。这该算是半自助吧。全自助的作法就复杂许多。Walter Oney 有一篇 "Pay No Attention to the Man Behind the Curtain! Write Your Own C++ AppWizards" 文章,发表于Microsoft Systems Journal的1997 三月号,里面详细描述了全自助的作法。请注意,他是以Visual C++ 4.2 为演练对象。不过,除了画面不同,技术上完全适用于 Visual C++ 5.0。

Dino Esposito 有一篇文章"a new assistant",发表于Windows Tech Journal的1997 三月号,也值得参考。1997 年五月份的Dr. Dobb's Journal也有一篇名为"Extending Visual C++ : Custom AppWizards make it possible" 的文章,作者是John Roberts。

第16章站上众人的肩膀--使用Components&ampActiveXControls

第16章 站上众人的肩膀 -- 使用Components&ActiveX Controls

从Visual Basic开始,可以说一个以components(软件组件)为中心的程序设计时代,逐渐拉开了序幕。随后delphi和C++ Builder 陆续登场。Visual Basic使用VBX(Visual Basic eXtension)组件,delphi和C++ Builder 使用vcL(Visual Component Library)组件,Visual C++ 则使用OCX(OLE Control eXtension)组件。如今 OCX 又演化到所谓ActiveX 组件(其实和OCX 大同小异)。

Microsoft 的Visual Basic(使用Basic 语言),Borland 的Delphi(使用Pascal 语言),以及Borland 的 C++ Builder(使用 C++ 语言),都称得上是一种快速开发工具(RAD,Rapid Application Development)。它们所使用的组件都是 PME(Properties-Method-Event)架构。这使得它们的整合环境(IDE)能够做出非常可视化的开发工具,以拖放、填单的方式完成绝大部份的程序设计工作。它们的应用程序开发程序大约是这个样子:

1. 选择一些适当的软件组件(VBX或vcL)。

2. 打开一个form,把那些软件组件拖放到form 中适当的位置。

3. 在Properties 清单中填写适当的属性。例如精确位置、宽度高度、或是让A组件的某个属性连接到B组件...等等。

4. 撰写程序代码(method),做为某种event发生时的处理例程。

依我的看法,Visual C++ 还不能够算是RAD。虽然,mfc程序所能够使用的 OCX 也是PME(Properties-Method-Event)架构,但 Visual C++ 整合环境没有能够提供适当工具让我们以那么可视化的方式(像VB或Delphi或C++ Builder 那样拖放、填单)就几乎完成一个程序。

什么是 Component Gallery

Component Gallery 是自从Visual C++ 4.0 之后,整合环境中新增的一个东西。你可以把它想象成一个数据库,储存着ActiveX controls 和可重复使用的 C++ 类(也就是本章所谓的components)。VC++ 5.0的Component Gallery 的使用接口和 VC++ 4.x 有某种程度的不同,不过操控原则基本上是一致的。

当你安装了Visual C++ 5.0,Component Gallery 已 经内含了一些微软所提供的components 和ActiveX controls(注:以下我将把这两样东西统称为「组件」)。选按整合环境的【Project / Add To Project / Components and Controls...】选单项目,你就可以看到Component Gallery:

其中有Developer Studio Components 和 Registered ActiveX Controls 两个数据夹,打开任何一个,就会出现目前系统所拥有的「货色」:

如果你以为这些组件储存在两个地方(一个是它本来的位置,另一份拷贝放在 Component Gallery 之中),那你就错了。Component Gallery 只是存放那些组件的位置数据而已。你可以说,只是存放一个「链接」而已。

为什么组件在此分为Components和ActiveX controls 两种?有什么不同。简单地说,Components 是一些已写好的C++ 类。基本上C++ 类本来就具有重复使用性,Component Gallery 只是把它们多做一些必要的包装,连同其它资源放在一起成为一个包裹。当你需要某个component,Component Gallery 给你的是该components 的原始代码。

ActiveX controls 不一样。当你选用某个ActiveX controls,Component Gallery 当然也会为你填入一些代码,但它们不是组件的本体。那些代码只是使用组件时所必须的代码,组件本身在 .OCX 文件中(通常注册后的OCX 文件都放在 Windows\System 磁盘子目录)。

ActiveX controls 是很完整的一个有着PME(Proterties-Method-Event)架构的控制组件,但一般欲被重复使用的C++ 类却不会有那么完整的设计或包装。要把一个C++ 类做成完好的包装,放到Component Gallery 中,它必须变为一个单一文件,内含类资讯以及任何必须的资源。这在过去的Visual C++ 4.x中是很容易的事情,因为每次你使用ClassWizard 新增一个类,就有一个核示盒询问你要不要加到Component Gallery:

Visual C++ 4.x 的ClassWizard 新增类对话窗

但这一选项已在Visual C++ 5.0 中拿掉(你可以在第 10 章增加对话窗类时看到新的画面)。看来似乎要增加components 不再是那么方便了。这倒也不是坏事,我想许多人在设计程序时忽略了上图那个选项,于是每一个项目中的每一个类,都被包装到Component Gallery 去,而其中许多根本是没有价值的:

Visual C++ 4.x 的Component Gallery。常常因为程序员的疏忽,而产生了一大堆没有价值的components。

使用 Components

当你选择Component Gallery 中的Developer Studio Components 数据夹,出现许多的components。面对形形色色的「货」,你的心里一定嘀咕着:怎么用嘛?幸好画面上有一个【More Info】按钮,可以提供你比较多的信息。以下我挑三个最简单的components做示范。

Splash screen

所谓Splash Screen,你可以说它是一个「炫耀画面」。玩过微软的Office 吗?每一个Office 软件一出场,在它做初始化的那段时间里,都会出现一个画面,就是Splash screen。

Splash Screen 的【More Info】出现这样的画面:

选按上图下方的"Splash Screen Component - Specifics",你会获得一张使用规格说明,大意如下:

欲插入 splash Screen component,你必须:

1. 打开你希望安插Splash Screen component的那个项目。

2. 选择整合环境中的【Project/Add To Project/Components and Controls】选单项目。

3. 选择"Developer's Studio Components" 数据夹。

4. 选择数据夹中的 Splash Screen component 并按下【Insert】钮。

5. 设定必要的 Splash Screen 选项然后按下【OK】钮。

6. 重建(重新编译链接)项目。

如果要把 Splash Screen 加到一个以对话窗为主(dialog-based)的程序中,你必须在插入这个component之后做以下事情:

1. 找到你的 InitInstance 函数。

2. 在你调用:

int nResponse = dlg.DoModal();

之前,加上一行:

spl.ShowSplashScreen(FALSE);

增加这一行代码,可以确保 Splash Screen 在主对话窗被显示之前,会被清除掉。

看来很简单的样子

System Info for About Dlg

看过 WordPad 的【About】对话窗吗:

如果你也想让自己的对话窗有点系统信息的显示能力,可以采用 Component Gallery 提供的这个System Info for About Dlg component。它的规格说明文字如下:

SysInfo component可以为你的程序的About对话窗中加上一些系统信息(可用内存数量以及磁盘剩余空间)。你的程序必须以mfc AppWizard 完成。请参考 WordPad说明文件以获得更多信息。这份规格书不够详细。稍后我会在修改程序代码时加上我自己的说明。

Tip of the Day

看过这种画面吗(微软的Office软件就有):

这就是「每日小秘诀」。Component Gallery 提供的Tips for the Day component 让你很方便地为自己加上「每日小秘诀」。这个component的使用规格是:

小秘诀文字文件(TIPS.TXT):

拥有Tips for the Day component的程序将搜寻磁盘中的工作子目录,企图寻找TIPS.TXT 读取秘诀内容。如果你希望这个秘诀文字文件有不同的名称或是放在不同的位置,你可以修改CTIP.CPP中的CTIP 类构造函数。CTIP是预设的类名称。

(侯俊杰注:最后这句话是错误的。我使用这个 component,接受所有的预设项目,获得的类名称却是 CTIPDLG,文件则为 TIPDLG.CPP)

TIPS.TXT 的格式如下:

1. 文件必须是ASCII 文字,每一个秘诀以一行文字表示。

2. 如果某一行文字以分号(;)开头,表示这是一行说明文字,不生实效。说明文字必须有自己单独的一行。

3. 空白行会被忽略。

4. 每一个小秘诀最多 1000 个字符。

5. 每一行不能够以空白或定位符号(tab)开始。

小秘诀显示次序:

预设情况下,小秘诀的出现次序和它们在文件中的排列次序相同。如果全部都出现过了,就再循环一遍。如果文件被更改过了,显示次序就会从头开始。

错误情况:

这个组件希望在MFC程序中被使用。你的程序应该只有一个派生自 CWinApp的类。如果有许多个CWinApp 派生类,此组件会选择其中第一个做为实现的对象。其他的错误情况包括秘诀文字文件不存在,或格式不对等等。

在程序的【Help】选单中加上 Tip of The Day 项目:

这个组件会修改主框窗口的 OnInitMenu 函数,并且在你的【Help】选单下加挂一个Tip of The Day 项目。如果你的程序原本没有【Help】选单,此组件就自动为你产生一个。

Components 实际运用:ComTest 程序

现在,动手吧。首先利用MFC AppWizard 产生一个项目,就像第4章的 Scribble step0 那样。我把它命名为ComTest(放在书附光盘的ComTest.17 子目录中)。然后,不要离开这个项目,启动Component Gallery,进入 Developer Studio Components数据夹,分别选择Splash Screen 和System Info for About Dlg和Tips of the Day三个组件,分别按下【Insert】钮。Splash Screen 和 Tips of the Day 组件会要求我们再指定一些消息:

新增文件

这时候 ComTest 项目中的原始代码有了一些变动(被 Component Gallery 改变)。被改变的文件是:

STDAFX.H

RESOURCE H

COMTEST.H

COMTEST.CPP

COMTEST.RC

MAINFRM.H

MAINFRM.CPP

SPLASH.H

SPLASH.CPP

SPLSH16.BMP

TIPDLG.CPP

TIPDLG.H

选按整合环境的【Build/ Build ComTest.Exe】,把这个程序建造出来。建造完毕试执行之,你会发现在主窗口出现之前,一开始先有一张画面显现:

然后是每日小秘诀:

然后才是主窗口。至于About对话窗,画面如下(没啥变化):

看来,我们只要修改一下Splash Screen 画面,并增加一个TIPS.TXT 文字文件,再变化一下About 对话窗,就成了。程序编修动作的确很简单,不过我还是要把这三个组件加诸于你的程序的每一条痕印都揭发出来。

相关变化

让我们分析分析Component Gallery为我们做了些什么事情。

STDAFX.H(阴影部份为新增内容)

...

#include <afxwin.h> // MFC core and st

#include <afxext.h> // MFC extensions

#include <afxdisp.h> // MFC OLE automat

#ifndef _AFX_NO_AFXCMN_SUPPORT

#include <afxcmn.h> // MFC

#endif // _AFX_NO_AFXCMN_SUPPORT

#include <H:\u002p\prog\ComTest.16\TipDlg.h>

...

RESOURCE.H

下面是针对三个组件新增的一些常数定义。凡是稍后修改程序时会用到的常数,我都加上批注,提醒您特别注意。

...

#define IDB_SPLASH 102 //Splash screen 所加,代表一张16色bitmap 画面

#define CG_IDS_PHYSICAL_MEM 103

#define CG_IDS_DISK_SPACE 104

#define CG_IDS_DISK_SPACE_UNAVAIL 105

#define IDB_LIGHTBULB 106

#define IDD_TIP 107

#define CG_IDS_TIPOFTHEDAY 108//Tips 所加,一个字符串。稍后我要把它改为中文内容。

#define CG_IDS_TIPOFTHEDAYMENU 109

#define CG_IDS_DIDYOUKNOW 110//Tips 所加,一个字符串。稍后我要把它改为中文内容

#define CG_IDS_FILE_ABSENT 111

#define CG_IDP_FILE_CORRUPT 112

#define CG_IDS_TIPOFTHEDAYHELP 113

#define IDC_PHYSICAL_MEM 1000 //SysInfo 所加,代表「可用内存」这个static字段

#define IDC_BULB 1000

#define IDC_DISK_SPACE 1001 // SysInfo所加,代表「磁盘剩余空间」这个static字段

#define IDC_STARTUP 1001

#define IDC_NEXTTIP 1002

#define IDC_TIPSTRING 1004

...

COMTEST.H(阴影部份为新增内容)

class CComTestApp : public CWinApp

{

public:

virtual BOOL PreTranslateMessage(MSG* pMsg);

CComTestApp();

...

private:

void ShowTipAtStartup(void);

private:

void ShowTipOfTheDay(void);

}

COMTEST.CPP(阴影部份为新增内容)

COMTEST.RC(阴影部份为新增内容)

MAINFRM.H(阴影部份为新增内容)

MAINFRM.CPP(阴影部份为新增内容)

SPLASH.H(全新内容)

SPLASH.CPP(全新内容)

TIPDLG.H(全新内容)

TIPDLG.CPP(全新内容)

#0001 #include "stdafx.h"

修改 ComTest 程序内容

以下是对于上述新增文件的分析与修改。稍早我曾分析过,只要修改一下 Splash Screen画面,增加一个TIPS.TXT 文字文件,再变化一下About 对话窗,就成了。

COMTEST.RC

要把自己准备的图片做为「炫耀画面」,有两个还算方便的作法。其一是直接编修Splash Screen 组件带给我们的Splsh16.bmp 的内容,其二是修改RC檔中的IDB_SPLASH 所对应的文件名称。我选择后者。所以我修改RC檔中的一行:

IDB_SPLASH BITMAP DISCARDABLE "Dissect.bmp"

Dissect.bmp 图文件内容如下:

此外我也修改RC文件中的一些字符串,使它们呈现中文:

IDD_TIP DIALOG DISCARDABLE 0, 0, 231, 164

STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU

CAPTION "今日小秘诀"

FONT 8, "MS Sans Serif"

BEGIN

CONTROL "",-1,"Static",SS_BLACKFRAME,12,11,207,123

LTEXT "Some String",IDC_TIPSTRING,28,63,177,60

CONTROL "程序启动时显示小秘诀",IDC_STARTUP,"Button",

BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,13,146,85,10

PUSHBUTTON "下一个小秘诀",IDC_NEXTTIP,109,143,50,14,WS_GROUP

DEFPUSHBUTTON "关闭",IDOK,168,143,50,14,WS_GROUP

CONTROL "",IDC_BULB,"Static",SS_BITMAP,20,17,190,111

END

STRINGTABLE DISCARDABLE

BEGIN

...

// CG_IDS_DIDYOUKNOW "Did You Know..."

CG_IDS_DIDYOUKNOW "侯俊杰著作年表..."

END

增加一个TIPS.TXT

这很简单,使用任何一种文字编辑工具,遵循前面说过的 TIPS.TXT 文件格式,做出你的每日小秘诀。

修改RC檔中的About对话窗画面

我增加了四个static控制组件,其中两个做为标签使用,不必在乎其ID。另两个准备给ComTest程序在【About】对话窗出现时设定系统资讯使用,ID分 别设定为IDC_PHYSICAL_MEM和IDC_DISK_SPACE,配合System Info for About Dlg 组件的建议。

COMTEST.CPP

在CAboutDlg::OnInitDialog中利用SetDlgitemText 设定稍早我们为对话窗画面新增的两个static 控制组件的文字内容(Component Gallery 已经为我们做出这段程序代码,只是暂时把它标记为说明文字。我只要把标记符号//去除即可):

ComTest 修改结果

一切尽如人意。现在我们有了理想的 Splash Screen 画面如前所述,也有了 Tips of the Day 对话窗:

以及一个内含系统信息的 About 对话窗:

使用 ActiveX Controls

Microsoft的Visual Basic自1991年推出以来,已经成为Windows应用软件开发环境中的佼佼者。它的成功极大部份要归功于其开放性质:它所提供的 VBXs 被认为是一种极佳的面向对象程序设计架构。VBX 是一种动态链接函数库(DLL),类似Windows的订制型控制组件(custom control)。

VBX 不适用于32 位环境。于是Microsoft 再推出另一规格OCX。不论是 VBX 或OCX,或甚至Borland 的VCL,都提供Properties-Method-Event(PME)接口。Visual Basic 之于VBX,以及 Borland C++ Builder 和 Delphi 之于 VCL,都提供了整合开发环境(IDE)与 PME 接口之间的极密切结合,使得程序设计更进一步到达「以拖拉、填单等简易动作就能够完成」的可视化境界。也因此没有人会反对把Visual Basic 和Delphi和C++ Builder 归类为RAD(Rapid Application Development,快速软件开发工具)的行列。但是Visual C++ 之于OCX,还没能够有这么好的整合。

我怎么会谈到OCX 呢?本节不是ActiveX Control 吗?噢,OCX 就是 ActiveX Control!由于微软把它所有的Internet 技术都称为ActiveX,所以 OLE Controls 就变成了ActiveX Controls。

我不打算讨论ActiveX Control 的撰写,我打算把全部篇幅用到ActiveX Control 的使用上。

如果对ActiveX Control 的开发感兴趣,Adam Denning 的ActiveX Control Inside Out是一本很不错的书(ActiveX 控制组件彻底研究,侯俊杰译/松岗)

ActiveX Control 基础观念:Properties、Methods、Events

你必须了解ActiveX Control 三种接口的意义,并且充份了解你打算使用的某个 ActiveX Control 有些什么特殊的接口,然后才能够使用它。

基本上你可以拿你已经很熟悉的 C++ 类来比较 ActiveX control。类也是一个包装良好的组件,有它自己的成员变量,以及处理这些成员变量的所谓成员函数,是个自给自足的体系。ActiveX control 的三个接口也有类似性质:

property - 相当于 C++ 类的成员变量

method - 相当于 C++ 类的成员函数

event - 相当于 Windows 控制组件发出的 notification 消息

ActiveX Control规格中定有一些标准的(库存的)接口,例如BackColor和FontName等properties,AddItem和Move和Refresh 等methods,以及CLICK 和KEYDOWN 等events。也就是说,任何一个ActiveX Control 大致上都会有一些必备的、基础的性质和能力。

以下针对ActiveX Control 的三种接口与C++ 类做个比较。至于它们的具体展现以及如何使用,稍后在实例中可以看到。

methods

设计自己的C++ 类,你当然可以在其中设计成员函数。此一函数之调用者必须在编译时期知道这一函数的功能以及它的参数。搭配Windows内建之控制组件(如 Edit、Button)而设计的类(如CEdit、CButton),内部固定会设计一些成员函数。某些成员函数(如CEdit::GetLineCount)只适用于特定类,但某些根类的成员函数(例如CWnd::GetDlgitemText)则适用于所有的子类。

ActiveX Control 的 method 极类似 C++ 类中的成员函数。但它们被限制在一个有限的集合之中,集合内的名单包括 AddItem、RemoveItem、Move 和 Refresh 等等。并不是所有的 ActiveX Controls 都对每一个 method 产生反应,例如 Move 就不能够在每一个ActiveX Control 中运作自如。

properties

基本上properties用来表达 ActiveX Control 的属性或数据。一个名为 Date的组件可能会定义一个所谓的 DateValue,内放日期,这就表现了组件的数据。它还可能定义一个所谓的 DateFormat,允许使用者取得或设定日期表现形式,这就表现了组件的属性。

你可以说ActiveX Control的properties 相当于C++ 类的成员变量。每一个 ActiveX Control可以定义属于它自己的properties,可以是一个字符串,可以是一个长整数,也可以是一个浮点数。有一组所谓的properties 标准集合(被称为 stock properties),内含BackColor、FontName、Caption 等等 properties,是每个ActiveX control 都会拥有的。

一般而言 properties 可分为四种类型:

Ambient properties

Extended properties

Stock properties

Custom properties

events

Windows 控制组件以所谓的notification(通告)消息送给其父窗口(通常是对话窗),例如按钮组件可能传送出一个BN_CLICKED。ActiveX Control 使用完全相同的方法,不过现在notification 消息被称为event,用来表示某种状况发生了。Events 的发射可以使ActiveX Control 有能力通知其宿主(container,也就是VB 或VC 程序),于是对方有机会处理。大部份 ActiveX Controls 送出标准的 events,例如 CLICK、KEYDOWN、KEYUP等等,某些 ActiveX Controls 会送出独一无二的消息(例如 ROWCOLCHANGE)。

一般而言 events 可分为两种类型:

Stock events

Custom events

ActiveX Controls 的五大使用步骤

欲在程序中加上 ActiveX Controls,基本上需要五个步骤:

1. 建立新项目时,在 AppWizard 的步骤3中选择【ActiveX Controls】。 这会使程序代码多出一行:

BOOL COcxTestApp::InitInstance()

{

AfxEnableControlContainer();

...

}

2. 进入Component Gallery,把ActiveX Controls 安插到你的程序中。

3. 使用ActiveX Controls。通常我们在对话窗中使用它。我们可以把资源编辑器的工具箱里头的 ActiveX Controls 拖放到目标对话窗中。

4. 利用ClassWizard 产生对话窗类,并处理相关的Message Maps、消息处理例程、变量定义、对话框函数等等。

5. 编译链接。

我将以系统内建(已注册过)的 Grid ActiveX Control 做为示范的对象。Grid 具有小型电子表格能力,当然它远比不上 Excel(不然 Excel 怎么卖),不过你至少可以获得一个中规中矩的 7x14 电子表格,并且有基本的编辑和运算功能。

容我先解释我的目标。图16-1 是我期望的结果,这个电子表格完全为了家庭记账而量身设计,假设你有五种收入(真让人羡慕),这个表格可以让你登录每个月的每一种收入,并计算月总收入和年总收入,以及各分项总收入。

图16-1 在对话窗中使用Grid ActiveX control。

每一横列或纵行的最后一栏都是总和。

由于Grid 本身并不提供编辑能力,我们以电子表格右侧的一个edit字段做为编辑局部。使用者所选择的方格的内容会显示在这edit字段中,并且允许被编辑内容。数值填入后必须按下<Enter> 键,或是在【Update Value】钮上按一下,电子表格内容才会更新。如果要直接在电子表格字段上做编辑动作,并不是不可以,把edit不偏不倚贴到字段也就是了!

本书进行到这里,我想你对于工具的使用应该已经娴熟了,我将假设你对于像「利用ClassWizard 为CMainFrame 拦 截一个ID_GridTest命令,并指名其处理常式为OnGridTest」这样的叙述,知道该怎么去动手。

使用Grid ActiveX Control:OcxTest程序

首先利用MFC AppWizard 做出一个OcxTest 项目。记得在步骤3选择【 ActiveX Controls】:

然后进入Component Gallery,将Grid安插到项目中:

你必须回答一个对话窗:

对话窗的设计

产生一个崭新的对话窗。这个动作与你在第10章为Scribble加上"Pen Width" 对话窗的步骤完全一样。请把新对话窗的ID从IDD_DIALOG1改变为 IDD_GRID。

从工具箱中抓出控制组件来,把对话窗布置如下。

虽然你把Grid 拉大,它却总是只有2x2 个方格。你必须使用右键把它的 Control Properties 引出来(如下),进入 Control 附页,这时候会出现各个 properties:

Control附页在中文Windows中竟然变成「一般」。这是否也算是一只臭虫?

现在选择Rows,设定为14,再选择Cols,设定为7。你还可以设定行的宽度和列的高度,以及方格初值...。噢,记得给这个 Grid 组件一个ID,叫做 IDC_GRID 好了。

整个对话窗的设计规格如下:

对象 ID 文字内容

对话窗 IDD_GRID ActiveX Control (Grid) Testing

OK按钮 IDOK OK

Cancel按钮 IDCANCEL Cancel

Edit IDC_VALUE

Update Value按钮 IDC_UPDATEVALUE Update Value

Grid IDC_GRID

现在准备设计IDD_GRID 的对话窗类。这件事我们在第10章也做过。进入CLassWizard,填写【Add Class】对话窗如下,然后按下【OK】钮:

回到ClassWizard主画面,准备为组件们设计消息处理例程。步骤是先选择一个组件ID,再选择一个消息,然后按下【Add Function】钮。注意,如果你选择到一个ActiveX Control,"Messages" 清单中列出的就是该组件所能发出的events。

本例的消息处理例程的设计规格如下:

对象ID 消息 处理函数名称

到此为止,我们获得这些新文件:

RESOURCE.H

OCXTEST.RC

GRIDCTRL.H <-- 本例不处理这个文件

GRIDCTRL.CPP <-- 本例不处理这个文件

FONT.H <-- 本例不处理这个文件

FONT.CPP <-- 本例不处理这个文件

PICTURE.H <-- 本例不处理这个文件

PICTURE.CPP <-- 本例不处理这个文件

GRIDDLG.H <-- 本例主要的修改对象

GRIDDLG.CPP <-- 本例主要的修改对象

其中重要的相关程序代码我特别挑出来做个认识:

OCXTEST.RC

GRIDDLG.H

GRIDDLG.CPP

为对话框加上一些变量

进入ClassWizard,进入【Member Variables】附页,选按其中的【Add Variable】钮,为OcxTest 加上两笔成员变量。其中一笔用来储存目前被选中的电子表格方格内容,另一笔数据用来做为 Grid 对象,其变量类型是 CGridCtrl:

这两个动作为我们带来这样的程序代码:

GRIDDLG.H

class CGridDlg : public CDialog

{

// Dialog Data

//{{AFX_DATA(CGridDlg)

enum { IDD = IDD_GRID };

CGridCtrl m_OcxGrid;

CString m_cellValue;

//}}AFX_DATA

...

};

GRIDDLG.CPP

CGridDlg::CGridDlg(CWnd* pParent /*=NULL*/)

: CDialog(CGridDlg::IDD, pParent)

{

//{{AFX_DATA_INIT(CGridDlg)

m_cellValue = _T("");

//}}AFX_DATA_INIT

}

void CGridDlg::DoDataExchange(CDataExchange* pDX)

{

CDialog::DoDataExchange(pDX);

//{{AFX_DATA_MAP(CGridDlg)

DDX_Control(pDX, IDC_GRID, m_OcxGrid);

DDX_Text(pDX, IDC_VALUE, m_cellValue);

//}}AFX_DATA_MAP

}

新增一个选单项目

利用资源编辑器,将选单修改如下:

注意,我所改变的选单是IDR_MAINFRAME,这是在没有任何子窗口存在时才会出现的选单。所以如果你要执行 OcxTest 并看到 Grid 组件,你必须先将所有的子窗口关闭。

现在利用 ClassWizard 在主窗口的消息映射表中拦截它的命令消息:

获得对应的程序代码如下:

MAINFRM.H

MAINFRM.CPP

为了让这个新增选单命令真正发挥效用,将 Grid 对话窗唤起,我在 OnGridTest 函数加两行:

现在,将OcxTest编译连结一遍,得到一个可以顺利执行的程序,但Grid 之中全无内容。

Grid相关程序设计

现在我要开始设计Grid 相关函数。我的主要的工作是:

准备一个二维(7x14)的 DWORD 数组,用来储存 Grid 的方格内容。

程序初始化时就把二维数组的初值设定好(本例不进行文件读写),并产生 Grid对话框。

对话框一出现,程序立刻把电子表格的行、列、宽、高,以及字段名称都设定好,并且把二维数组的数值放到对应方格中。初值的总和也一并计算出来。

把计算每一列每一行总和的工作独立出来,成立一个ComputeSums 函数。为了放置电子表格内容,必须设计一个7x14 二维数组。虽然电子表格中某些方格(如列标题或行标题)不必有内容,不过为求简化,还是完全配合电子表格的大小来设计数值数组好了。注意,不能把这个变量放在 AFX_DATA 之内,因为我并非以 ClassWizard 加入此变量。

GRIDDLG.H

为了设定Grid 中的表头以及初值,我在OnInitDialog 中先以一个for loop设定横列表头再以一个for loop 设定纵行表头,最后再以巢状(两层)for loop 设定每一个方格内容,然后才调用ComputeSums 计算总和。

当使用者选择一个方格,其值就被 OnSelchangeGrid 拷贝一份到 edit 字段中,这时候就可以开始输入了。

OnUpdatevalue(【Update Value】按钮的处理例程)有两个主要任务,一是把edit字段内容转化为数值放到目前被选择的方格上,一是修正总和。

OnOk 必须能够把每一个方格内容(一个字符串)取出,利用atof转换为数值,然后储存到m_dArray二维数组中。

GRIDDLG.CPP

#0001

#0002 BOOL CGridDlg::OnInitDialog()

#0003 {

#0004 CString str;

#0005 int i, j;

#0143 }

#0144 }

下图是OcxTest 的执行画面。

第五篇 附录

附录A无责任书评

附录A 无责任书评

从摇篮到坟墓

Windows 的完全学习

侯捷/1996.08.12整理

侯俊杰先生邀请我为他呕心沥血的新作深入浅出mfc写点东西。我未写文章久矣,但是你知道,要拒绝一个和你住在同一个大脑同一个躯壳的人日日夜夜旦旦夕夕的请求,是很困难的。不,简直是不可能。于是,我只好重作冯妇!

事实上也不全然是因为躲不过日日夜夜的轰炸,一部份原因是,当初我还在杂志上主持无责任书评时,就有读者来信希望书评偶而变换口味,其中一个建议就是谈谈如何养成Windows 程序设计的全面性技术。说到全面性,那又是一个 impossible mission!真的,Windows 程序技术的领域实在是太广了,我们从来不会说游戏软件设计、多媒体程序设计、通讯软件设计... 是属于DOS程序技术的范畴,但,它们通常都被理所当然地归类属于Windows 程序设计领域。为什么?因为几乎所有的题目都拜倒在 Windows 作业系统的大伞之下,几乎每一种技术都被涵盖在千百计(并且以惊人速度继续增加中)的Windows API 之中。

我的才智实不足以涵盖这么大面积的学问,更遑论从中精挑细选经典之作介绍给你。那么,本文题目大剌剌的「完全学习」又怎么说?呃,我指的是 Windows 操作系统的核心观念以及程序设计的本质学能这一路,至于游戏、多媒体、通讯、Web Server、数据库、统统被我归类为「应用」领域。而Visual Basic、delphi、Java 虽也都可以开发 Windows程序,却又被我屏弃在C/C++ 的主流之外。

以下谨就我的视野,分门别类地把我心目中认为必备的相关好书介绍出来。你很容易就可以从我所列出的书名中看出我的浅薄:在操作系统方面,我只涉猎 Windows 3.1和Windows 95(Windows NT 4.0是我的下一波焦点),在 Application Framework 方面,我只涉猎 mfc(OWL 和 Java 是我的下一个猎物)。

Windows 操作系统

‹ Windows Internals / Matt Pietrek / Addison Wesley

最能够反应操作系统奥秘的,就是操作系统内部数据结构以及 API 的内部动作了。本书借着对这两部份所做的逆向工程,剖析Windows 的核心。

一个设计良好的应用程序接口(API)应该是一个不必让程序员担心的黑盒子。本书的主要立意并不在为了对API运作原理的讨论而获得更多程序写作方面的利益(虽然那其实是个必然的额外收获),而是藉由API伪代码,揭露出Windows 操作系统的运作原理。时光渐渐过去,程序员渐渐成长,我们开始对How感到不足而想知道Why了,这就是本书要给我们的东西。

本书不谈Windows官方手册上已有的信息,它谈「新信息」。如何才能获得手册上没有记载的信息?呵,原始代码说明一切。看原始代码当然是不错,问题是 Windows 的原始代码刻正锁在美国WA,Redmond(微软公司总部所在地)的保险库里,搞不好就在比尔.盖兹的桌下。我们唯一能够取得的Windows 原始代码大概只是SDK 磁盘上的defwnd.c 和defdlg.c(这是DefWindowProc 和DefDlgProc 的原始代码),以及 DDK 磁盘中的一大堆驱动程序原始代码。那么作者如何获得比你我更多的秘密呢?

Matt Pietrek 是软件反组译逆向工程的个中翘楚。本书藉由一个他自己开发的反组译工具,把获得的结果再以C虚拟代码表现出来。我们在书中看到许许多多的Windows API伪代码都是这么来的。Pietrek 还有一个很有名的产品叫做BoundsChecker,和SOFT- ICE/W(功能强大的 Windows Debugger,以企鹅为形象)搭配销售。

本书主要探讨Windows 3.1 386加强模式,必要时也会提及标准模式以及 Windows 3.0。书中并没有涵盖虚拟驱动程序、虚拟机器、网络 API、多媒体、DDE/OLE、dialog/control等主题,而是集中在 Windows 启动程序、内存管理系统、窗口管理系统、消息管理系统、排程管理系统、绘图系统身上。本书对读者有三大要求 :

对Intel CPU 的保护模式寻址方式、segmentation、selector 已有基本认识。

拥有Windows SDK 手册。

对操作系统有基础观念,例如什么是多任务,什么是虚拟内存...等等。

作者常借用面向对象的观念解释 Windows,如果你懂 C++ 语言,知道类与对象,知道成员函数和成员变量的意义与其精神,对他的比喻当能心领神会。

对系统感兴趣的人,本书一定让你如鱼得水。你唯一可能的抱怨就是:一大堆 API 函数的伪代码令人心烦气燥。文字瀚海图片沙漠的情形也一再考验读者的定力与耐力。然而小瑕不掩大瑜。我向来认为酿了一瓶好酒的人不必声嘶力竭地广告它,这本书就是一瓶好酒。作者Pietrek自1993/10 起已登上Microsoft Systems Journal 的Windows Q&A 主持人宝座,没两把刷子的人上这位子可是如坐针毡。现在他又主持同一本刊物的另一个专栏:Under The Hood。Dr. Dobb's Journal 的 Undocumented Corner 专栏也时有 Pietrek的踪影。

‹Undocumented Windows/Andrew Schulman,David Maxey,Matt Pietrek/ Addison Wesley

候捷-深入浅出MFC 目录

目 录

第0章 你一定要知道(导读)/1

这本书适合谁/1

你需要什么技术基础/1

你需要什么软硬件环境

让我们使用同一种语言

本书符号习惯/3

磁盘内容与安装

范例程序说明

第一篇 勿在浮砂筑高台 - 本书技术前提/3

第1章 win32 程序基本观念/3

win32程序开发流程/4

需要什么函数库(.LIB)/4

需要什么头文件(.H)/4

以消息为基础,以事件驱动之/5

一个具体而微的Win32 程序/5

程序进入点WinMain/10

窗口类之注册与窗口之诞生/11

消息循环/12

窗口的生命中枢—窗口函数/12

消息映射(Message Map)雏形/13

对话框的运作/14

模块定义文件(.DEF)/14

资源描述文件(.RC) /15

Windows 程序的生与死/15

闲置时间的处理:OnIdle /16

Console 程序/17

Console 程序与 DOS 程序的差别/17

Console 程序的编译链接/18

JBACKUP:Win32 Console 程序设计/19

mfcCON:mfc Console 程序设计/20

什么是C Runtime Library 的多线程版本/22

进程与线程(Process and Thread)/22

核心对象/22

一个进程的诞生与死亡/23

产生子进程/23

一个线程的诞生与死亡/24

以_beginthreadex取代CreateThread/25

线程优先权(Priority)/26

多线程程序设计实例/27

第2章 C++ 的重要性质 /29

类及其成员—谈封装(encapsulation)/29

基类与派生类—谈继承(Inheritance)/29

this 指针 /31

虚函数与多态(Polymorphism)/32

类与对象大解剖 /40

Object slicing 与虚函数 /42

静态成员(变量与函数) /44

C++程序的生与死:兼谈构造函数与析构函数 /46

四种不同的对象生存方式 /47

所谓“Unwinding”/48

运行时类型信息(RTTI) /48

动态生成(Dynamic Creation) /50

异常处理(Exception Handling) /50

Template /53

Template Functions /53

Template Classes /54

Templates 的编译与链接 /56

第3章 MFC六大关键技术之模拟/57

MFC类阶层/57

Frame1范例程序/57

MFC程序的初始化过程/59

Frame2范例程序/61

RTTI(运行时类型辨识)/65

CRuntimeClass与类型录网 /65

DECLARE_DYNAMIC / IMPLEMENT_DYNAMIC宏/66

Frame3 范例程序/71

IsKindOf(类型辨识)/77

Frame4 范例程序/77

Dynamic Creation(动态生成)/78

DECLARE_DYNCREATE / IMPLEMENT_DYNCREATE 宏 /79

Frame6 范例程序 /84

Persistence(永续生存)机制 /91

Serialize(数据读写) /91

DECLARE_SERIAL/IMPLEMENT_SERIAL 宏 /95

没有范例程序

Message Mapping(消息映射)/97

Frame7 范例程序/104

Command Routing(命令循环)/112

Frame8 范例程序/119

本章回顾/130

第二篇 欲善工事先利其器- Visual C++ 5.0 开发工具

第4章 Visual C++ - 整合性软件开发环境

安装与组成

四个重要的工具

内务府总管:Visual C++ 整合开发环境

关于project

关于工具设定

Source Browser

Online Help

除错工具

vc++ 除错器

Exception Handling

程序代码产生器—AppWizard

东圈西点完成 MFC 程序骨干

Scribble Step0

威力强大的资源编辑器

Icon 编辑器

Cursor编辑器

Bitmap编辑器

ToolBar编辑器

VERSIONINFO资源编辑器

String Table编辑器

Menu 编辑器

Accelerator 编辑器

Dialog 编辑器

Console 程序的项目管理

第三篇 浅出MFC程序设计

第5章 总观Application Framework /130

什么是Application Framework /130

侯捷怎么说 /130

我怎么说 /131

别人怎么说 /133

为什么使用 Application Framework /134

Microsoft Foundation Class(MFC) /136

白头宫女话天宝:Visual C++与MFC/137

纵览MFC /138

General Purpose classes /138

Windows API classes /139

Application framework classes /140

High level abstractions /140

Afx全局函数 /140

MFC宏(macros) /141

MFC数据类型(data type)/142

第6章 MFC程序设计导论——MFC程序的生死因果 /144

不二法门:熟记MFC类的阶层架构 /144

需要什么函数库(.LIB) /146

需要什么含入文件(.H) /146

简化的MFC程序架构—以Hello MFC为例 /148

Hello 程序原始代码 /148

MFC 程序的来龙去脉 /152

我只借用两个类:CWinApp和CFrameWnd /152

CWinApp—取代WinMain的地位 /152

CFrameWnd—取代WndProc的地位 /154

引爆器—Application object /155

隐晦不明的 WinMain /156

AfxWinInit - AFX 内部初始化动作 /158

CWinApp::InitApplication /160

CMyWinApp::InitInstance /160

CFrameWnd::Create 产生主窗口(并注册窗口类)/161

奇怪的窗口类名称 Afx:b:14ae:6:3e8f/168

窗口显示与更新 /170

CWinApp::Run - 程序生命的活水源头/170

把消息与处理函数串接在一起:Message Map机制/172

来龙去脉总整理/173

Callback 函数/174

闲置时间(idle time)的处理:OnIdle/176

Dialog 与 Control/178

通用对话框(Common Controls)/178

本章回顾/179

第7章 简单而完整:MFC 骨干程序/180

不二法门:熟记 MFC 类的阶层架构/180

MFC程序的UI新风貌/180

Document/View 支撑你的应用程序 /181

利用Visual C++工具完成Scribble step0 /183

骨干程序使用哪些MFC类?/183

Document Template的意义 /187

Scribble的Document/View 设计/190

主窗口的诞生/192

工具列和状态列的诞生(Toolbar & Status bar)/193

鼠标拖放(Drag and Drop)/195

消息映射(Message Map)/196

标准菜单File/Edit/View/Window/Help/196

对话框 /199

改用CEditView /199

第四篇 深入MFC程序设计 /199

第8章 Document-View 深入探讨 /200

为什么需要 Document-View(形而上)/200

Document /200

View /201

Document Frame(View Frame)/202

Document Template /202

CDocTemplate 管理 CDocument / CView / CFrameWnd /202

Scribble Step1 的 Document(数据结构设计) /207

MFC Collection Classes 的选用 /207

Template-Based Classes /208

Template-Based Classes 的使用方法 /209

CScribbleDoc 的修改 /209

SCRIBBLEDOC.H /211

SCRIBBLEDOC.CPP /212

文件:一连串的线条 /215

CScribbleDoc 的成员变量 /215

CObList /215

CScribbleDoc 的成员函数 /215

线条与坐标点 /217

CStroke 的成员变量 /217

CArray<CPoint, CPoint> /217

CStroke 的成员函数 /217

Scribble Step1 的 View:数据重绘与编辑 /218

CScribbleView 的修改 /218

SCRIBBLEVIEW.H /219

SCRIBBLEVIEW.CPP /220

View 的重绘动作—GetDocument和OnDraw /222

CScribbleView的成员变量 /222

CScribbleView的成员函数 /223

View 与使用者的交谈(鼠标消息处理实例)/223

ClassWizard 的辅佐 /224

WizardBar 的辅佐 /225

Serialize:对象的文件读写 /225

Serialization以外的文件读写动作 /226

台面上的Serialize动作 /227

台面下的Serialize写文件奥秘 /231

台面下的Serialize读文件奥秘 /233

DYNAMIC / DYNCREATE / SERIAL 三宏/240

Serializable 的必要条件/244

CObject 类/245

IsKindOf/245

IsSerializable/245

CObject::Serialize/245

CArchive类/246

operator<<和operator>>/246

效率考虑/250

自定 SERIAL宏给抽象类使用 /250

在CObList中加入 CStroke 以外的类 /250

Document与View 交流—为Scribble Step4做准备/254

第9章 消息映射与命令循环 /255

到底要解决什么 /255

消息分类 /256

万流归宗 Command Target(CCmdTarget)/256

三个奇怪的宏,一张巨大的网 /257

DECLARE_MESSAGE_MAP 宏/257

消息映射网的形成:BEGIN_/ON_/END_ 宏 /258

米诺托斯(Minotauros)与西修斯(Theseus)/261

两万五千里长征—消息的流窜 /265

直线上溯(一般 Windows 消息) /265

拐弯上溯(WM_COMMAND 命令消息) /268

罗塞达碑石:AfxSig_xx 的秘密/273

Scribble Step2:UI 对象的变化/277

改变菜单/277

改变工具列/278

利用ClassWizard连接命令项识别代码与命令处理函数/280

维护UI对象状态(UPDATE_COMMAND_UI)/282

本章回顾/285

第10章 MFC 与对话框/285

对话框编辑器/286

利用ClassWizard 连接对话框与其专属类/288

PENDLG.H /290

PENDLG.CPP /291

对话框的消息处理函数 /292

MFC中各式各样的MAP /294

对话框数据交换与查核(DDX & DDV)/294

MFC中各式各样的DDx_函数 /297

如何唤起对话框 /297

本章回顾 /299

第11章 View功能之加强与重绘效率之提升/299

同时修改多个 Views:UpdateAllViews 和 OnUpdate/300

在View中定义一个 hint/302

把hint传给 OnUpdate/304

利用hint增加重绘效率/305

可卷动的窗口:CScrollView /307

大窗口中的小窗口:Splitter /313

分裂窗口的功能 /313

分裂窗口的程序概念/314

分裂窗口之实现/315

本章回顾 /317

第12章 印表与预览/317

概观/317

打印动作的后台原理 /320

MFC预设的打印机制 /324

Scribble打印机制的补强 /333

打印机的页和文件的页 /333

配置GDI绘图工具 /334

尺寸与方向:关于映射模式(坐标系统)/334

分页/336

表头(Header)与表尾/338

动态计算页代码/338

打印预览(Print Preview)/339

本章回顾/339

第13章 多重文件与多重显示 /339

MDI 和 SDI /340

多重显像(Multiple Views) /340

窗口的动态分裂 /342

窗口的静态分裂 /343

CreateStatic 和 CreateView /343

窗口的静态三叉分裂 /345

Graph 范例程序 /346

静态分裂窗口之观念整理 /354

同源子窗口 /355

CMDIFrameWnd::OnWindowNew/355

Text 范例程序 /356

非制式作法的缺点 /361

多重文件 /361

新的Document类 /362

新的Document Template /363

新的UI系统 /364

新文件的文件读写动作 /365

第14章 MFC多线程程序设计(Multi-threaded Programming in MFC)/367

从操作系统层面看线程/367

三个观念:模块、进程、线程/367

线程优先权(Priority) /368

线程排程(Scheduling) /369

Thread Context /370

从程序设计层面看线程 /371

Worker Threads 和UI Threads /371

错误观念 /372

正确态度 /372

MFC多线程程序设计 /372

探索CwinThread/372

产生一个Worker Thread/374

产生一个UI Thread /375

线程的结束/376

线程与同步控制 /376

MFC多线程程序实例 /378

第15章 定制一个AppWizard /380

到底Wizard是什么? /381

Custom AppWizard 的基本操作 /381

剖析AppWizard Components /385

Dialog Templates 和Dialog Classes /385

Macros /386

Directives /387

动手修改Top Studio AppWizard/387

利用资源编辑器修改IDD_CUSTOM1对话窗画面/387

利用ClassWizard 修改CCustom1Dlg类 /388

改写OnDismiss 虚函数,在其中定义macros /389

修改text template/389

Top Studio AppWizard执行结果/390

更多的信息/390


第16章 站上众人的肩膀—使用Components和ActiveX Controls/391

什么是Component Gallery /391

使用 Components /393

Splash screen /393

System Info for About Dlg /394

Tips of the Day /394

Components实际运用:ComTest 程序/395

修改ComTest 程序内容 /409

使用ActiveX Controls /411

ActiveX Control 基础观念:Properties、Methods、Events/411

ActiveX Controls 的五大使用步骤/412

使用“Grid”ActiveX Control:OcxTest 程序/413