用VC进行COM编程所必须掌握的理论知识

用VC进行COM编程所必须掌握的理论知识
用VC进行COM编程所必须掌握的理论知识

用VC进行COM编程所必须掌握的理论知识

这篇文章是给初学者看的,尽量写得比较通俗易懂,并且尽量避免编程细节。完全是根据我自己的学习体会写的,其中若有技术上的错误之处,请大家多多指正。

一、为什么要用COM

软件工程发展到今天,从一开始的结构化编程,到面向对象编程,再到现在的COM编程,目标只有一个,就是希望软件能象积方块一样是累起来的,是组装起来的,而不是一点点编出来的。结构化编程是函数块的形式,通过把一个软件划分成许多模块,每个模块完成各自不同的功能,尽量做到高内聚低藕合,这已经是一个很好的开始,我们可以把不同的模块分给不同的人去做,然后合到一块,这已经有了组装的概念了。软件工程的核心就是要模块化,最理想的情况就是100%内聚0%藕合。整个软件的发展也都是朝着这个方向走的。结构化编程方式只是一个开始。下一步就出现了面向对象编程,它相对于面向功能的结构化方式是一个巨大的进步。我们知道整个自然界都是由各种各样不同的事物组成的,事物之间存在着复杂的千丝万缕的关系,而正是靠着事物之间的联系、交互作用,我们的世界才是有生命力的才是活动的。我们可以认为在自然界中事物做为一个概念,它是稳定的不变的,而事物之间的联系是多变的、运动的。事物应该是这个世界的本质所在。面向对象的着眼点就是事物,就是这种稳定的概念。每个事物都有其固有的属性,都有其固有的行为,这些都是事物本身所固有的东西,而面向对象的方法就是描述出这种稳定的东西。而面向功能的模块化方法它的着眼点是事物之间的联系,它眼中看不到事物的概念它只注重功能,我们平常在划分模块的时侯有没有想过这个函数与哪些对象有关呢?很少有人这么想,一个函数它实现一种功能,这个功能必定与某些事物想联系,我们没有去掌握事物本身而只考虑事物之间是怎么相互作用而完成一个功能的。说白了,这叫本末倒置,也叫急功近利,因为不是我们智慧不够,只是因为我们没有多想一步。面向功能的结构化方法因为它注意的只是事物之间的联系,而联系是多变的,事物本身可能不会发生大的变化,而联系则是很有可能发生改变的,联系一变,那就是另一个世界了,那就是另一种功能了。如果我们用面向对象的方法,我们就可以以不变应万变,只要事先把事物用类描述好,我们要改变的只是把这些类联系起来的方法,只是重新使用我们的类库,而面向过程的方法因为它构造的是一个不稳定的世界,所以一点小小的变化也可能导致整个系统都要改变。然而面向对象方法仍然有问题,问题在于重用的方法。搭积木式的软件构造方法的基础是有许许多多各种各样的可重用的部件、模块。我们首先想到的是类库,因为我们用面向对象的方法产生的直接结果就是许多的类。但类库的重用是基于源码的方式,这是它的重大缺陷。首先它限制了编程语言,你的类库总是用一种语言写的吧,那你就不能拿到别的语言里用了。其次你每次都必须重新编译,只有编译了才能与你自己的代码结合在一起生成可执行文件。在开发时这倒没什么,关键在于开发完成后,你的EXE都已经生成好了,如果这时侯你的类库提供厂商告诉你他们又做好了一个新的类库,功能更强大速度更快,而你为之心动又想把这新版的类库用到你自己的程序中,那你就必须重新编译、重新调试!这离我们理想的积木式软件构造方法还有一定差距,在我们的设想里希望把一个模块拿出来再换一个新的模块是非常方便的事,可是现在不但要重新编译,还要冒着很大的风险,因为你可能要重新改变你自己的代码。另一种重用方式很自然地就想到了是DLL的方式。Windows里到处是DLL,它是Windows 的基础,但DLL也有它自己的缺点。总结一下它至少有四点不足。(1)函数重名问题。DLL里是一个一个的函数,我们通过函数名来调用函数,那如果两个DLL里有重名的函数怎么办?(2)各编译器对C++函数的名称修饰不兼容问题。对于C++函数,编译器要根据函数的参数信息为它生成修饰名,DLL库里存的就是这个修饰名,但是不同的编译器产生修饰的方法不一样,所以你在VC 里编写的DLL在BC里就可以用不了。不过也可以用extern "C";来强调使用标准的C函数特性,关闭修饰功能,但这样也丧失了C++的重载多态性功能。(3)路径问题。放在自己的目录下面,别人的程序就找不到,放在系统目录下,就可能有重名的问题。而真正的组件应该可以放在任何地方甚至可以不在本机,用户根本不需考虑这个问题。(4)DLL与EXE的依赖问题。我们一般都是用隐式连接的方式,就是编程的时侯指明用什么DLL,这种方式很简单,它在编译时就把EXE与DLL绑在一起了。如果DLL发行了一个新版本,我们很有必要重新链接一次,因为DLL里面函数的地址可能已经发生了改变。DLL的缺点就是COM的优点。

首先我们要先把握住一点,COM和DLL一样都是基于二进制的代码重用,所以它不存在类库重用时的问题。另一个关键点是,COM本身也是DLL,既使是ActiveX控件.ocx它实际上也是DLL,所以说DLL在还是有重用上有很大的优势,只不过我们通过制订复杂的COM协议,通COM本身的机制改变了重用的方法,以一种新的方法来利用DLL,来克服DLL本身所固有的缺陷,从而实现更高一级的重用方法。COM没有重名问题,因为根本不是通过函数名来调用函数,而是通过虚函数表,自然也不会有函数名修饰的问题。路径问题也不复存在,因为是通过查注册表来找组件的,放在什么地方都可以,即使在别的机器上也可以。也不用考虑和EXE的依赖关系了,它们二者之间是松散的结合在一起,可以轻松的换上组件的一个新版本,而应用程序混然不觉。

二、用VC进行COM编程,必须要掌握哪些COM理论知识

我见过很多人学COM,看完一本书后觉得对COM的原理比较了解了,COM也不过如此,可是就是不知道该怎么编程序,我自己也有这种情况,我也是经历了这样的阶段走过来的。要学COM的基本原理,我推荐的书是《COM技术内幕》。但仅看这样的书是远远不够的,我们最终的目的是要学会怎么用COM去编程序,而不是拼命的研究COM本身的机制。所以我个人觉得对COM的基本原理不需要花大量的时间去追根问底,没有必要,是吃力不讨好的事。其实我们只需要掌握几个关键概念就够了。这里我列出了一些我自己认为是用VC编程所必需掌握的几个关键概念。(这里所说的均是用C++语言条件下的COM编程方式)

(1) COM组件实际上是一个C++类,而接口都是纯虚类。组件从接口派生而来。我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:

class IObject

{

public:

virtual Function1(...) = 0;

virtual Function2(...) = 0;

....

};

class MyObject : public IObject

{

public:

virtual Function1(...){...}

virtual Function2(...){...}

....

};

看清楚了吗?IObject就是我们常说的接口,MyObject就是所谓的COM组件。切记切记接口都是纯虚类,它所包含的函数都是纯虚函数,而且它没有成员变量。而COM组件就是从这些纯虚类继承下来的派生类,它实现了这些虚函数,仅此而已。从上面也可以看出,COM组件是以C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。

(2) COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatch。

COM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,

一个组件也不是只给你一个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各样的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。

IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平常是怎么实例化一个类对象的?是用‘new’命令!很简单吧,COM组件也一样如此。但是谁来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果客户知道组件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表着组件的128位的数字串而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一下,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是类厂。每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操作都交给类厂对象去做了。IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则是太少太少了。

IDispatch叫做调度接口。它的作用何在呢?这个世上除了C++还有很多别的语言,比如VB、VJ、VBScript、JavaScript等等。可以这么说,如果这世上没有这么多乱七八糟的语言,那就不会有IDispatch。:-) 我们知道COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如VBScript、JavaScript。不行的原因在于它们并不支持指针,连指针都不能用还怎么用多态性啊,还怎么调这些虚函数啊。唉,没办法,也不能置这些脚本语言于不顾吧,现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,我们只能另寻他法了。时势造英雄,IDispatch应运而生。:-) 调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,仅给一个编号就能让别人知道怎么调用一个函数那不是天方夜潭吗,你总得让别人知道你要调用的函数要带什么参数,参数类型什么以及返回什么东西吧,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看一看MS的类库里实现Invoke的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事,而且可能永远也没这样的机会。:-)

(3) dispinterface接口、Dual接口以及Custom接口

这一小节放在这里似乎不太合适,因为这是在ATL编程时用到的术语。我在这里主要是想谈一下自动化接口的好处及缺点,用这三个术语来解释可能会更好一些,而且以后迟早会遇上它们,我将以一种通俗的方式来解释它们,可能并非那么精确,就好象用伪代码来描述算法一样。-:)

所谓的自动化接口就是用IDispatch实现的接口。我们已经讲解过IDispatch的作用了,它的好处就是脚本语言象VBScript、JavaScript也能用COM组件了,从而基本上做到了与语言无关它的缺点主要有两个,第一个就是速度慢效率低。这是显而易见的,通过虚函数表一下子就可以调用函数了,而通过Invoke 则等于中间转了道手续,尤其是需要把函数参数转换成一种规范的格式才去调用函数,耽误了很多时间。所以一般若非是迫不得已我们都想用VTable的方式调用函数以获得高效率。第二个缺点就是只能使用规定好的所谓的自动化数据类型。如果不用IDispatch我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度代码。而用自动化接口就不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预料到我们要用到的所有类型,它只能根据一些常用的数据类型来写它的处理代码,而且它也要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成的调度代码只适用于它所规定好的那些数据类型,当然这些数据类型已经足够丰富了,但不能满足自定义数据结构的要求。你也可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。考虑到IDispatch的种种缺点(它还有一个缺点,就是使用麻烦,:-) )现在一般都推荐写双接口组件,称为dual接口,实际上就是从IDispatch继承的接口。我们知道任何接口都必须从IUnknown继承,IDispatch接口也不例外。那从IDispatch继承的接口实际上就等于有两个基类,一个是IUnknown,一个是IDispatch,所以它可以以两种方式来调用组件,可以通过IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用。这就有了很大的灵活性,这个组件既可以用于C++的环境也可以用于脚本语言中,同时满足了各方面的需要。

相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口(虽然它实际上不是的),这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。

Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了

(4) COM组件有三种,进程内、本地、远程。对于后两者情况必须调度接口指针及函数参数。

COM是一个DLL,它有三种运行模式。它可以是进程内的,即和调用者在同一个进程内,也可以和调用者在同一个机器上但在不同的进程内,还可以根本就和调用者在两台机器上。这里有一个根本点需要牢记,就是COM组件它只是一个DLL,它自己是运行不起来的,必须有一个进程象父亲般照顾它才行,即COM组件必须在一个进程内.那谁充当看护人的责任呢?先说说调度的问题。调度是个复杂的问题,以我的知识还讲不清楚这个问题,我只是一般性的谈谈几个最基本的概念。我们知道对于WIN32程序,每个进程都拥有4GB的虚拟地址空间,每个进程都有其各自的编址,同一个数据块在不同的进程里的编址很可能就是不一样的,所以存在着进程间的地址转换问题。这就是调度问题。对于本地和远程进程来说,DLL 和客户程序在不同的编址空间,所以要传递接口指针到客户程序必须要经过调度。Windows 已经提供了现成的调度函数,就不需要我们自己来做这个复杂的事情了。对远程组件来说函数的参数传递是另外一种调度。DCOM是以RPC为基础的,要在网络间传递数据必须遵守标准的网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调度,这个过程很复杂,不过Windows已经把一切都给我们做好了,一般情况下我们不需要自己来编写调度DLL。

我们刚说过一个COM组件必须在一个进程内。对于本地模式的组件一般是以EXE的形式出现,所以它本身就已经是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了调度代码以实现基本的调度。这个进程就是dllhost.exe。这是COM默认的DLL代理。实际上在分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着很强大的功能,是专门的用于管理分布式DLL组件的工具。

调度离我们很近又似乎很远,我们编程时很少关注到它,这也是COM的一个优点之一,既平台无关性,

无论你是远程的、本地的还是进程内的,编程是一样的,一切细节都由COM自己处理好了,所以我们也不用深究这个问题,只要有个概念就可以了,当然如果你对调度有自己特殊的要求就需要深入了解调度的整个过程了,这里推荐一本《COM+技术内幕》,这绝对是一本讲调度的好书。

(5) COM组件的核心是IDL。

我们希望软件是一块块拼装出来的,但不可能是没有规定的胡乱拼接,总是要遵守一定的标准,各个模块之间如何才能亲密无间的合作,必须要事先共同制订好它们之间交互的规范,这个规范就是接口。我们知道接口实际上都是纯虚类,它里面定义好了很多的纯虚函数,等着某个组件去实现它,这个接口就是两个完全不相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,我们没有时间自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们需要的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂商已经在自己的组件中实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组件,我们不关心组件从哪来,它有什么其它的功能,我们只关心它是否很好的实现了我们制订好的接口。这种接口可能是业界的标准,也可能只是你和几个厂商之间内部制订的协议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,是COM组件通信的标准。

COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL开始,先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,有的人可能善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你把最终的DLL 拿给我。这是一种多么好的开发模式,可以用任何语言来开发,也可以用任何语言来欣赏你的开发成果。

(6) COM组件的运行机制,即COM是怎么跑起来的。

这部分我们将构造一个创建COM组件的最小框架结构,然后看一看其内部处理流程是怎样的

IUnknown *pUnk=NULL;

IObject *pObject=NULL;

CoInitialize(NULL);

CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);

pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);

pUnk->Release();

pObject->Func();

pObject->Release();

CoUninitialize();

这就是一个典型的创建COM组件的框架,不过我的兴趣在CoCreateInstance身上,让我们来看看它内部做了一些什么事情。以下是它内部实现的一个伪代码:

CoCreateInstance(....)

{

.......

IClassFactory *pClassFactory=NULL;

CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);

pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);

pClassFactory->Release();

........

}

这段话的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。继续深入一步,看看CoGetClassObject的内部伪码:

CoGetClassObject(.....)

{

//通过查注册表CLSID_Object,得知组件DLL的位置、文件名

//装入DLL库

//使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。

//调用DllGetClassObject

}

DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.

下面是DllGetClassObject的伪码:

DllGetClassObject(...)

{

......

CFactory* pFactory= new CFactory; //类厂对象

pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);

//查询IClassFactory指针

pFactory->Release();

......

}

CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码:

CFactory::CreateInstance(.....)

{

...........

CObject *pObject = new CObject; //组件对象

pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);

pObject->Release();

...........

}

(7) 一个典型的自注册的COM DLL所必有的四个函数

DllGetClassObject:用于获得类厂指针

DllRegisterServer:注册一些必要的信息到注册表中

DllUnregisterServer:卸载注册信息

DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载DLL

DLL还有一个函数是DllMain,这个函数在COM中并不要求一定要实现它,但是在VC生成的组件中自动都包含了它,它的作用主要是得到一个全局的实例对象。

(8) 注册表在COM中的重要作用

首先要知道GUID的概念,COM中所有的类、接口、类型库都用GUID来唯一标识,GUID是一个128位的字串,根据特制算法生成的GUID可以保证是全世界唯一的。COM组件的创建,查询接口都是通过注册表进行的。有了注册表,应用程序就不需要知道组件的DLL文件名、位置,只需要根据CLSID查就可以了。当版本升级的时侯,只要改一下注册表信息就可以神不知鬼不觉的转到新版本的DLL。

本文是本人一时兴起的涂鸭之作,讲得并不是很全面,还有很多有用的体会没写出来,以后如果有时间有兴趣再写出来。希望这篇文章能给大家带来一点用处,那我一晚上的辛苦就没有白费了。-:)

C/C++头文件一览

C、传统C++

#include //设定插入点

#include //字符处理

#include //定义错误码

#include //浮点数处理

#include //文件输入/输出

#include //参数化输入/输出

#include //数据流输入/输出

#include //定义各种数据类型最值常量

#include //定义本地化函数

#include //定义数学函数

#include //定义输入/输出函数

#include //定义杂项函数及内存分配函数

#include //字符串处理

#include //基于数组的输入/输出

#include //定义关于时间的函数

#include //宽字符处理及输入/输出

#include //宽字符分类

////////////////////////////////////////////////////////////////////////// 标准C++(同上的不再注释)

#include //STL 通用算法

#include //STL 位集容器

#include

#include

#include

#include

#include //复数类

#include

#include

#include

#include

#include //STL 双端队列容器

#include //异常处理类

#include

#include //STL 定义运算函数(代替运算符)#include

#include //STL 线性列表容器

#include //STL 映射容器

#include

#include //基本输入/输出支持

#include //输入/输出系统使用的前置声明#include

#include //基本输入流

#include //基本输出流

#include //STL 队列容器

#include //STL 集合容器

#include //基于字符串的流

#include //STL 堆栈容器

#include //标准异常类

#include //底层输入/输出支持

#include //字符串类

#include //STL 通用模板类

#include //STL 动态数组容器

#include

#include

using namespace std;

////////////////////////////////////////////////////////////////////////// C99 增加

#include //复数处理

#include //浮点环境

#include //整数格式转换

#include //布尔环境

#include //整型环境

#include //通用类型数学宏

Visual C++编程的若干技巧

Visual C++是一种面向对象的可视化编程工具,它提供的AppWizard能自动生成应用程序的标准框架,大大减轻了编程的工作量。本文主要介绍如下的编程技巧:修改主窗口风格、创建不规则形状窗口、用鼠标单击窗口标题条以外区域移动窗口、使用上下文菜单、使应用程序只能运行一个实例、使应用程序显示为任务条通知区中的图标和显示旋转文本等。

1. 修改主窗口风格

AppWizard生成的应用程序框架的主窗口具有缺省的窗口风格,比如在窗口标题条中自动添加文档名、

窗口是叠加型的、可改变窗口大小等。要修改窗口的缺省风格,需要重载CWnd:reCreateWindow (CREATESTRUCT& cs)函数,并在其中修改CREATESTRUCT型参数cs。

CWnd:reCreateWindow 函数先于窗口创建函数执行。如果该函数被重载,则窗口创建函数将使用CWnd:reCreateWindow 函数返回的CREATESTRUCT cs参数所定义的窗口风格来创建窗口;否则使用

预定义的窗口风格。

CREATESTRUCT结构定义了创建函数创建窗口所用的初始参数,其定义如下:

typedef struct tagCREATESTRUCT {

LPVOID lpCreateParams; // 创建窗口的基本参数

HANDLE hInstance; // 拥有将创建的窗口的模块实例句柄

HMENU hMenu; // 新窗口的菜单句柄

HWND hwndParent; // 新窗口的父窗口句柄

int cy; // 新窗口的高度

int cx; // 新窗口的宽度

int y; // 新窗口的左上角Y坐标

int x; // 新窗口的左上角X坐标

LONG style; // 新窗口的风格

LPCSTR lpszName; // 新窗口的名称

LPCSTR lpszClass; // 新窗口的窗口类名

DWORD dwExStyle; // 新窗口的扩展参数

} CREATESTRUCT;

CREATESTRUCT结构的style域定义了窗口的风格。比如,缺省的MDI主窗口的风格中就包括FWS_ADDTOTITLE(在标题条中显示当前的工作文档名)、FWS_PREFIXTITLE(把文档名放在程序标题的前面)、WS_THICKFRAME(窗口具有可缩放的边框)等风格。由于多种风格参数由逻辑或(“|”)组合在一起的,因此添加某种风格,就只需用“|”把对应的参数加到CREATESTRUCT结构的style域中;删除已有的风格,则需用“&”连接CREATESTRUCT结构的style域与该风格的逻辑非值。CREATESTRUCT结构的x、y、cx、cy域分别定义了窗口的初始位置和大小,因此,在CWnd::PreCreateWindow 函数中给它们赋值,将能定义窗口的初始显示位置和大小。

下例中的代码将主框窗口的大小将固定为1/4屏幕,标题条中仅显示窗口名,不显示文档名。

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)

{

// TODO: Modify the Window class or styles here by modifying

// the CREATESTRUCT cs

// 修改主窗风格

cs.style &= ~FWS_ADDTOTITLE; //去除标题条中的文档名

cs.style &= ~WS_THICKFRAME; //去除可改变大小的边框

cs.style |= WS_DLGFRAME; //增加不能改变大小的边框

// 确定主窗的大小和初始位置

int cxScreen = ::GetSystemMetrics(SM_CXSCREEN);//获得屏幕宽

int cyScreen = ::GetSystemMetrics(SM_CYSCREEN); //获得屏幕高

cs.x = 0; // 主窗位于左上角

cs.y = 0;

cs.cx = cxScreen/2; // 主窗宽为1/2屏幕宽

cs.cy = cxScreen/2; // 主窗高为1/2屏幕高

return CMDIFrameWnd::PreCreateWindow(cs);

}

2. 创建不规则形状窗口

标准的Windows窗口是矩形的,但在有些时候我们需要非矩形的窗口,比如圆形的、甚至是不规则的。借助CWnd类的SetWindowRgn函数可以创建不规则形状窗口。

CWnd::SetWindowRgn的函数原型如下:

int SetWindowRgn( HRGN hRgn, // 窗口区域句柄

BOOL bRedraw ); // 是否重画窗口

CRgn类封装了关于区域的数据和操作。通过(HRGN)强制操作可以从CRgn类中取得其HRGN值。

CRgn提供了CreateRectRgn、CreateEllipticRgn和CreatePolygonRgn成员函数,分别用以创建矩形、(椭)圆形和多边形区域。

创建非矩形窗口的方法如下:首先,在窗口类中定义区域类成员数据(如CRgn m_rgnWnd);其次,在窗口的OnCreate函数或对话框的OnInitDialog函数中调用CRgn类的CreateRectRgn、CreateEllipticRgn或CreatePolygonRgn函数创建所需的区域,并调用SetWindowRgn函数。

下例将生成一个椭圆窗口。

1. 在Developer Studio中选取File菜单中的New命令,在出现的New对话框中选择创建MFC AppWizard (exe)框架应用程序,并输入项目名为EllipseWnd。设定应用程序类型为基于对话框(Dialog based),其它选项按缺省值创建项目源文件。

2. 使用资源编辑器从主对话框(ID为IDD_ELLIPSEWND_DIALOG)删除其中的所有控制,并从其属性对话框(Dialog Properties)中设定其风格为Popup、无标题条和边框。

3. 在EllipseWndDlg.h源文件中给主对话框类CEllipseWndDlg增加一个CRgn类保护型数据成员m_rgnWnd,它将定义窗口的区域。

4. 在EllipseWndDlg.cpp源文件中修改主对话框类CEllipseWndDlg的OnInitDialog()函数,增加m_rgnWnd 的创建,并将其定义为窗口区域。粗体语句为新增部分。

BOOL CEllipseWndDlg::OnInitDialog()

{

CDialog::OnInitDialog();

// Add "About..." menu item to system menu.

// IDM_ABOUTBOX must be in the system command range.

ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);

ASSERT(IDM_ABOUTBOX < 0xF000);

CMenu* pSysMenu = GetSystemMenu(FALSE);

if (pSysMenu != NULL)

{

CString strAboutMenu;

strAboutMenu.LoadString(IDS_ABOUTBOX);

if (!strAboutMenu.IsEmpty())

{

pSysMenu->AppendMenu(MF_SEPARATOR);

pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);

}

}

// Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog

SetIcon(m_hIcon, TRUE); // Set big icon

SetIcon(m_hIcon, FALSE); // Set small icon

// 设置窗口标题为“椭圆窗口”,虽然对话框没有标题条,

// 但在任务条的按钮中仍需要标题

SetWindowText(_T("椭圆窗口"));

// 取得屏幕宽、高

int cxScreen = ::GetSystemMetrics(SM_CXSCREEN);

int cyScreen = ::GetSystemMetrics(SM_CYSCREEN);

// 设置椭圆X、Y方向的半径

int nEllipseWidth = cxScreen/8;

int nEllipseHeight = cyScreen/8;

// 将窗口大小设为宽nEllipseWidth,高nEllipseHeight

// 并移至左上角

MoveWindow(0, 0, nEllipseWidth, nEllipseHeight);

// 创建椭圆区域m_rgnWnd

m_rgnWnd.CreateEllipticRgn(0, 0, nEllipseWidth, nEllipseHeight);

// 将m_rgnWnd设置为窗口区域

SetWindowRgn((HRGN)m_rgnWnd, TRUE);

return TRUE; // return TRUE unless you set the focus to a control

}

3. 用鼠标单击窗口标题条以外区域移动窗口

移动标准窗口是通过用鼠标单击窗口标题条来实现的,但对于没有标题条的窗口,就需要用鼠标单击窗口标题条以外区域来移动窗口。有两种方法可以达到这一目标。

方法一:当窗口确定鼠标位置时,Windows向窗口发送WM_NCHITTEST消息,可以处理该消息,使得只要鼠标在窗口内,Windows便认为鼠标在标题条上。这需要重载CWnd类处理WM_NCHITTEST消息的OnNcHitTest函数,在函数中调用父类的该函数,如果返回HTCLIENT,说明鼠标在窗口客户区内,使重载函数返回HTCAPTION,使Windows误认为鼠标处于标题条上。

下例是使用该方法的实际代码:

UINT CEllipseWndDlg::OnNcHitTest(CPoint point)

{

// 取得鼠标所在的窗口区域

UINT nHitTest = CDialog::OnNcHitTest(point);

// 如果鼠标在窗口客户区,则返回标题条代号给Windows

// 使Windows按鼠标在标题条上类进行处理,即可单击移动窗口

return (nHitTest==HTCLIENT) ? HTCAPTION : nHitTest;

}

方法二:当用户在窗口客户区按下鼠标左键时,使Windows认为鼠标是在标题条上,即在处理WM_LBUTTONDOWN消息的处理函数OnLButtonDown中发送一个wParam参数为HTCAPTION,lParam 为当前坐标的WM_NCLBUTTONDOWN消息。

下面是使用该方法的实际代码:

void CEllipseWndDlg::OnLButtonDown(UINT nFlags, CPoint point)

{

// 调用父类处理函数完成基本操作

CDialog::OnLButtonDown(nFlags, point);

// 发送WM_NCLBUTTONDOWN消息

// 使Windows认为鼠标在标题条上

PostMessage(WM_NCLBUTTONDOWN,

HTCAPTION,

MAKELPARAM(point.x, point.y));

}

4. 使用上下文菜单

Windows 95应用程序支持单击鼠标右键弹出上下文菜单的功能,这可通过处理WM_CONTEXTMENU消息来实现。

当在窗口内单击鼠标右键时,窗口将接收到WM_CONTEXTMENU消息,在该消息的处理函数内装载上下文菜单,并调用CMenu::TrackPopupMenu函数便可显示上下文菜单。CMenu::TrackPopupMenu函数的原型如下:

BOOL TrackPopupMenu( UINT nFlags, // 显示和选取方式标志

int x, int y, // 显示菜单的左上角坐标

CWnd* pWnd, // 接收菜单操作的窗口对象

LPCRECT lpRect = NULL ); // 敏感区域

为了使用上下文菜单,首先应在资源编辑器中编制好上下文菜单,假设上下文菜单名为IDR_MENU_CONTEXT;其次,用ClassWizard给窗口增加处理消息WM_CONTEXTMENU的函数

OnContextMenu,以及各菜单命令的处理函数;然后编写相应的代码。

下面的是OnContextMenu函数的代码实例:

void CEllipseWndDlg::OnContextMenu(CWnd* pWnd, CPoint point)

{

CMenu menu;

// 装入菜单

menu.LoadMenu(IDR_MENU_CONTEXT);

// 显示菜单

menu.GetSubMenu(0)->TrackPopupMenu(

TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RIGHTBUTTON,

point.x, point.y, this);

}

5. 使应用程序只能运行一个实例

Windows是多进程操作系统,框架生成的应用程序可以多次运行,形成多个运行实例。但在有些情况下为保证应用程序的安全运行,要求程序只能运行一个实例,比如程序要使用只能被一个进程单独使用的特殊硬件(例如调制解调器)时,必须限制程序只运行一个实例。

这里涉及两个基本的问题,一是在程序的第二个实例启动时,如何发现该程序已有一个实例在运行,而是如何将第一个实例激活,而第二个实例退出。

对于第一个问题,可以通过给应用程序设置信号量,实例启动时首先检测该信号量,如已存在,则说明程序已运行一个实例。

第二个问题的难点是获取第一个实例的主窗对象指针或句柄,然后便可用SetForegroundWindow来激活。虽然FindWindow函数能寻找正运行着的窗口,但该函数要求指明所寻找窗口的标题或窗口类名,不是实现通用方法的途径。我们可以用Win 32 SDK函数SetProp来给应用程序主窗设置一个特有的标记。用GetDesktopWindow可以获取Windows系统主控窗口对象指针或句柄,所有应用程序主窗都可看成该窗口的子窗口,即可用GetWindow函数来获得它们的对象指针或句柄。用Win 32 SDK函数GetProp查找每一应用程序主窗是否包含有我们设置的特定标记便可确定它是否我们要寻找的第一个实例主窗。使第二个实例退出很简单,只要让其应用程序对象的InitInstance函数返回FALSE即可。此外,当主窗口退出时,应用RemoveProp函数删除我们为其设置的标记。

下面的InitInstance、OnCreate和OnDestroy函数代码将实现上述的操作:

BOOL CEllipseWndApp::InitInstance()

{

// 用应用程序名创建信号量

HANDLE hSem = CreateSemaphore(NULL, 1, 1, m_pszExeName);

// 信号量已存在?

// 信号量存在,则程序已有一个实例运行

if (GetLastError() == ERROR_ALREADY_EXISTS)

{

// 关闭信号量句柄

CloseHandle(hSem);

// 寻找先前实例的主窗口

HWND hWndPrevious = ::GetWindow(::GetDesktopWindow(), GW_CHILD);

while (::IsWindow(hWndPrevious))

{

// 检查窗口是否有预设的标记?

// 有,则是我们寻找的主窗

if (::GetProp(hWndPrevious, m_pszExeName))

{

// 主窗口已最小化,则恢复其大小

if (::IsIconic(hWndPrevious))

::ShowWindow(hWndPrevious,

SW_RESTORE);

// 将主窗激活

::SetForegroundWindow(hWndPrevious);

// 将主窗的对话框激活

::SetForegroundWindow(

::GetLastActivePopup(hWndPrevious));

// 退出本实例

return FALSE;

}

// 继续寻找下一个窗口

hWndPrevious = ::GetWindow(hWndPrevious,

GW_HWNDNEXT);

}

// 前一实例已存在,但找不到其主窗

// 可能出错了

// 退出本实例

return FALSE;

}

AfxEnableControlContainer();

// Standard initialization

// If you are not using these features and wish to reduce the size // of your final executable, you should remove from the following // the specific initialization routines you do not need.

#ifdef _AFXDLL

Enable3dControls(); // Call this when using MFC in a shared DLL #else

Enable3dControlsStatic();// Call this when linking to MFC statically

#endif

CEllipseWndDlg dlg;

m_pMainWnd = &dlg;

int nResponse = dlg.DoModal();

if (nResponse == IDOK)

{

// TODO: Place code here to handle when the dialog is

// dismissed with OK

}

else if (nResponse == IDCANCEL)

{

// TODO: Place code here to handle when the dialog is

// dismissed with Cancel

}

// Since the dialog has been closed, return FALSE so that we exit the

// application, rather than start the application's message pump.

return FALSE;

}

int CEllipseWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

if (CDialog::OnCreate(lpCreateStruct) == -1)

return -1;

// 设置寻找标记

::SetProp(m_hWnd, AfxGetApp()->m_pszExeName, (HANDLE)1);

return 0;

}

void CEllipseWndDlg::OnDestroy()

{

CDialog::OnDestroy();

// 删除寻找标记

::RemoveProp(m_hWnd, AfxGetApp()->m_pszExeName);

}

6. 使应用程序显示为任务条通知区中的图标

在Windows 95任务条的右边有一个区域被称为通知区域,在其中可以显示一些应用程序的图标,用鼠标单击其中的图标一般能弹出应用程序的菜单,双击则能显示应用程序的完整窗口界面。时钟和音量控制是

任务条通知区最常见的图标。

任务条通知区编程可以通过Windows 95外壳编程接口函数Shell_NotifyIcon来实现,该函数在shellapi.h 头文件中声明,其原型如下:

WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage,

PNOTIFYICONDATA pnid);

dwMessage是对通知区图标进行操作的消息,主要有三中,如下表所示。

Shell_NotifyIcon使用的消息

消息

说明

NIM_ADD

在任务条通知区插入一个图标

NIM_ DELETE

在任务条通知区删除一个图标

NIM_ MODIFY

对任务条通知区的图标进行修改

pnid传入一个NOTIFYICONDATA结构的指针。NOTIFYICONDATA结构声明及各域的意义表示如下:typedef struct _NOTIFYICONDATA { // nid

DWORD cbSize; // NOTIFYICONDATA结构的字节数

HWND hWnd; // 处理通知区图标消息的窗口句柄

UINT uID; // 通知区图标的ID

UINT uFlags; // 表示下述三项是否有意义的标志

UINT uCallbackMessage; // 鼠标点击图标所发出消息的ID

HICON hIcon; // 图标句柄

char szTip[64]; // 当鼠标移到图标上时显示的提示信息

} NOTIFYICONDATA, *PNOTIFYICONDATA;

当用Shell_NotifyIcon在任务条通知区中放置一个图标时,同时也定义了一条回调消息,当用户用鼠标单击或双击图标时,NOTIFYICONDATA结构中指定的窗口句柄将接受到该消息。该消息的lParam参数将说明鼠标操作的方式。当应用程序退出时,应删除任务条中的图标。

下面的示例将说明如何使前述的椭圆窗口程序作为图标显示在任务条通知区中,当鼠标单击图标时,将弹出一个菜单,当双击时,椭圆窗口将完整显示。

1. 用资源编辑器在EllipseWnd项目的IDR_MENU_CONTEXT菜单中增加一个菜单项“在任务条中插入图标”(ID为IDM_INSERTICON)。

2. 用资源编辑器在EllipseWnd项目中增加一个菜单资源IDR_MENU_ICON ,在其中设定三个菜单项:“激活椭圆窗口”(ID为IDM_ACTIVEWINDOW)

“关于...”(ID为IDM_ABOUTBOX)

“退出Alt+F4”(ID为IDM_EXIT)

3. 在CEllipseWndDlg.h源文件中定义一个消息UM_ICONNOTIFY用以响应图标操作,并在CEllipseWndDlg 类定义中增加响应该消息的处理函数OnIconNotify。用ClassWizard增加响应菜单命令IDM_INSERTICON 和IDM_ACTIVEWINDOW的函数定义和模板。CEllipseWndDlg.h中的修改如下:

// 定义响应图标操作的消息

#define UM_ICONNOTIFY WM_USER+100

class CEllipseWndDlg : public CDialog

{

// Construction

public:

CEllipseWndDlg(CWnd* pParent = NULL); // standard constructor

// Dialog Data

//{{AFX_DATA(CEllipseWndDlg)

enum { IDD = IDD_ELLIPSEWND_DIALOG };

// NOTE: the ClassWizard will add data members here

//}}AFX_DATA

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CEllipseWndDlg)

protected:

virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support //}}AFX_VIRTUAL

// Implementation

protected:

HICON m_hIcon;

CRgn m_rgnWnd;

// 处理图标的功能函数说明

BOOL AddIcon();

BOOL DeleteIcon();

// Generated message map functions

//{{AFX_MSG(CEllipseWndDlg)

virtual BOOL OnInitDialog();

afx_msg void OnSysCommand(UINT nID, LPARAM lParam);

afx_msg void OnPaint();

afx_msg HCURSOR OnQueryDragIcon();

afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);

afx_msg void OnAboutbox();

afx_msg void OnExit();

afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

afx_msg void OnDestroy();

afx_msg void OnInserticon();

afx_msg void OnActivewindow();

//}}AFX_MSG

// 图标消息的处理函数说明

afx_msg void OnIconNotify(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP()

};

4. 在CEllipseWndDlg.cpp中增加消息影射条目如下:

BEGIN_MESSAGE_MAP(CEllipseWndDlg, CDialog)

//{{AFX_MSG_MAP(CEllipseWndDlg)

ON_WM_SYSCOMMAND()

ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

ON_WM_LBUTTONDOWN()

ON_WM_CONTEXTMENU()

ON_COMMAND(IDM_ABOUTBOX, OnAboutbox)

ON_COMMAND(IDM_EXIT, OnExit)

ON_WM_CREATE()

ON_WM_DESTROY()

ON_COMMAND(IDM_INSERTICON, OnInserticon)

ON_COMMAND(IDM_ACTIVEWINDOW, OnActivewindow)

//}}AFX_MSG_MAP

ON_MESSAGE(UM_ICONNOTIFY, OnIconNotify)

END_MESSAGE_MAP()

5. 在CEllipseWndDlg.cpp中增加如下的函数或代码:

void CEllipseWndDlg::OnDestroy()

{

CDialog::OnDestroy();

// remove main window tag

::RemoveProp(m_hWnd, AfxGetApp()->m_pszExeName);

// 应用程序退出时,删除任务条中图标

DeleteIcon();

}

BOOL CEllipseWndDlg::AddIcon()

{

// 在任务条中增加图标

NOTIFYICONDATA nid;

nid.cbSize = sizeof(nid);

nid.hWnd = m_hWnd;

nid.uID = IDR_MAINFRAME;

nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;

nid.uCallbackMessage = UM_ICONNOTIFY;

nid.hIcon = m_hIcon;

CString str = "椭圆窗口";

lstrcpyn(nid.szTip, (LPCSTR)str,

sizeof(nid.szTip) / sizeof(nid.szTip[0]));

return Shell_NotifyIcon(NIM_ADD, &nid);

}

BOOL CEllipseWndDlg:eleteIcon()

{

// 删除任务条中的图标

NOTIFYICONDATA nid;

nid.cbSize = sizeof(nid);

nid.hWnd = m_hWnd;

nid.uID = IDR_MAINFRAME;

return Shell_NotifyIcon(NIM_DELETE, &nid);

}

// 响应图标消息处理函数

void CEllipseWndDlg::OnIconNotify(WPARAM wParam, LPARAM lParam)

{

switch ((UINT)lParam)

{

// 鼠标单击操作

case WM_LBUTTONDOWN:

case WM_RBUTTONDOWN:

{

// 装入图标操作菜单

CMenu menu;

menu.LoadMenu(IDR_MENU_ICON);

// 鼠标单击位置

CPoint point;

GetCursorPos(&point);

// 将背景窗口激活

SetForegroundWindow();

// 显示图标菜单

menu.GetSubMenu(0)->TrackPopupMenu(

TPM_LEFTBUTTON|TPM_RIGHTBUTTON,

point.x, point.y, this, NULL);

相关主题
相关文档
最新文档