本人现在在学习C++编程,经过一段时间也积累收藏了很多自己认为不错的文章,其中也有自己写的一些,现整理出来方便大家使用!并希望大家指正和建议和交流!希望对大家有帮助!咱们一起努力进步,因为我还是编程的一般水平! 首先是C/C++忠告(转载)估计大家都看过: 1.把C++当成一门新的语言学习(和C没啥关系!真的。); 2.看《Thinking In C++》,不要看《C++变成死相》; 3.看《The C++ Programming Language》和《Inside The C++ Object Model》,不要因为他们很难而我们自己是初学者所以就不看; 4.不要被VC、BCB、BC、MC、TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言; 5.不要放过任何一个看上去很简单的小编程问题——他们往往并不那么简单,或者可以引伸出很多知识点; 6.会用Visual C++,并不说明你会C++; 7.学class并不难,template、STL、generic programming也不过如此——难的是长期坚持实践和不遗余力的博览群书; 8.如果不是天才的话,想学编程就不要想玩游戏——你以为你做到了,其实你的C++水平并没有和你通关的能力一起变高——其实可以时刻记住:学C++是为了编游戏的; 9.看Visual C++的书,是学不了C++语言的; 10.浮躁的人容易说:XX语言不行了,应该学YY;——是你自己不行了吧!? 11.浮躁的人容易问:我到底该学什么;——别问,学就对了; 12.浮躁的人容易问:XX有钱途吗;——建议你去抢银行; 13.浮躁的人容易说:我要中文版!我英文不行!——不行?学呀! 14.浮躁的人容易问:XX和YY哪个好;——告诉你吧,都好——只要你学就行; 15.浮躁的人分两种:a)只观望而不学的人;b)只学而不坚持的人; 16.把时髦的技术挂在嘴边,还不如把过时的技术记在心里; 17.C++不仅仅是支持面向对象的程序设计语言; 18.学习编程最好的方法之一就是阅读源代码; 19.在任何时刻都不要认为自己手中的书已经足够了; 20.请阅读《The Standard C++ Bible》(中文版:标准C++宝典),掌握C++标准; 21.看得懂的书,请仔细看;看不懂的书,请硬着头皮看; 22.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍; 23.请看《Effective C++》和《More Effective C++》以及《Exceptional C++》; 24.不要停留在集成开发环境的摇篮上,要学会控制集成开发环境,还要学会用命令行方式处理程序; 25.和别人一起讨论有意义的C++知识点,而不是争吵XX行不行或者YY与ZZ哪个好; 26.请看《程序设计实践》,并严格的按照其要求去做; 27.不要因为C和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样; 28.C++绝不是所谓的C的“扩充”——如果C++一开始就起名叫Z语言,你一定不会把C和Z语言联系得那么紧密; 29.请不要认为学过XX语言再改学C++会有什么问题——你只不过又在学一门全新的语言而已; 30.读完了《Inside The C++ Object Model》以后再来认定自己是不是已经学会了C++; 31.学习编程的秘诀是:编程,编程,再编程; 32.请留意下列书籍:《C++面向对象高效编程(C++ Effective Object-Oriented Software Construction)》《面向对象软件构造(Object-Oriented Software Construction)》《设计模式(Design Patterns)》《The Art of Computer Programming》; 33.记住:面向对象技术不只是C++专有的; 34.请把书上的程序例子亲手输入到电脑上实践,即使配套光盘中有源代码; 35.把在书中看到的有意义的例子扩充; 36.请重视C++中的异常处理技术,并将其切实的运用到自己的程序中; 37.经常回顾自己以前写过的程序,并尝试重写,把自己学到的新知识运用进去; 38.不要漏掉书中任何一个练习题——请全部做完并记录下解题思路; 39.C++语言和C++的集成开发环境要同时学习和掌握; 40.既然决定了学C++,就请坚持学下去,因为学习程序设计语言的目的是掌握程序设计技术,而程序设计技术是跨语言的; 41.就让C++语言的各种平台和开发环境去激烈的竞争吧,我们要以学习C++语言本身为主; 42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成以保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43); 43.别心急,设计C++的class确实不容易;自己程序中的class和自己的class设计水平是在不断的编程实践中完善和发展的; 44.决不要因为程序“很小”就不遵循某些你不熟练的规则——好习惯是培养出来的,而不是一次记住的; 45.每学到一个C++难点的时候,尝试着对别人讲解这个知识点并让他理解——你能讲清楚才说明你真的理解了; 46.记录下在和别人交流时发现的自己忽视或不理解的知识点; 47.请不断地对自己写的程序提出更高的要求,哪怕你的程序版本号会变成Version 100.XX; 48.保存好你写过的所有的程序——那是你最好的积累之一; 49.请不要做浮躁的人; 50.请热爱C++! |
C++的效率浅析
自从七十年代C语言诞生以来,一直以其灵活性、高效率和可移植性为软件开发人员所钟爱,成为系统软件开发的首选工具。而 C++作为C语言的继承和发展,不仅保留了C语言的高度灵活、高效率和易于理解等诸多优点,还包含了几乎所有面向对象的特征,成为新一代软件系统构建的利器。 相对来说,C语言是一种简洁的语言,所涉及的概念和元素比较少,主要是:宏(macro)、指针(pointer)、结构(struct)、函数 (function)和数组(array),比较容易掌握和理解。而C++不仅包含了上面所提到的元素,还提供了私有成员(private members)、公有成员(public members)、函数重载(function overloading)、缺省参数(default parameters)、构造函数、析构函数、对象的引用(references)、操作符重载(operator overloading)、友元(friends)、模板(templates)、异常处理(exceptions)等诸多的要素,给程序员提供了更大的设计空间,同时也增加了软件设计的难度。 C语言之所以能被广泛的应用,其高效率是一个不可忽略的原因,C语言的效率能达到汇编语言的80%以上,对于一种高级语言来说,C语言的高效率就不言而喻了。那么,C++相对于C来说,其效率如何呢?实际上,C++的设计者stroustrup要求C++效率必须至少维持在与C相差5%以内,所以,经过精心设计和实现的C++同样有很高的效率,但并非所有C++程序具有当然的高效率,由于C++的特殊性,一些不好的设计和实现习惯依然会对系统的效率造成较大的影响。同时,也由于有一部分程序员对C++的一些底层实现机制不够了解,就不能从原理上理解如何提高软件系统的效率。 本文主要讨论两个方面的问题:第一,对比C++的函数调用和C函数调用,解析C++的函数调用机制;第二,例举一些C++程序员不太注意的技术细节,解释如何提高C++的效率。为方便起见,本文的讨论以下面所描述的单一继承为例(多重继承有其特殊性,另作讨论)。 class X { public: virtual ~X(); //析构函数 virtual void VirtualFunc(); //虚函数 inline int InlineFunc() { return m_iMember}; //内联函数 void NormalFunc(); //普通成员函数 static void StaticFunc(); //静态函数 private: int m_iMember; }; class XX: public X { public: XX(); virtual ~XX(); virtual void VirtualFunc(); private: String m_strName; int m_iMember2; }; C++的的函数分为四种:内联函数(inline member function)、静态成员函数(static member function)、虚函数(virtual member function)和普通成员函数。 内联函数类似于C语言中的宏定义函数调用,C++编译器将内联函数的函数体扩展在函数调用的位置,使内联函数看起来象函数,却不需要承受函数调用的开销,对于一些函数体比较简单的内联函数来说,可以大大提高内联函数的调用效率。但内联函数并非没有代价,如果内联函数体比较大,内联函数的扩展将大大增加目标文件和可运行文件的大小;另外,inline关键字对编译器只是一种提示,并非一个强制指令,也就是说,编译器可能会忽略某些inline关键字,如果被忽略,内联函数将被当作普通的函数调用,编译器一般会忽略一些复杂的内联函数,如函数体中有复杂语句,包括循环语句、递归调用等。所以,内联函数的函数体定义要简单,否则在效率上会得不偿失。 静态函数的调用,如下面的几种方式: X obj; X* ptr = &obj; obj.StaticFunc(); ptr->StaticFunc(); X::StaticFunc(); 将被编译器转化为一般的C函数调用形式,如同这样: mangled_name_of_X_StaticFunc(); //obj.StaticFunc(); mangled_name_of_X_StaticFunc(); // ptr->StaticFunc(); mangled_name_of_X_StaticFunc(); // X::StaticFunc(); mangled_name_of_X_StaticFunc()是指编译器将X::StaticFunc()函数经过变形(mangled)后的内部名称 (C++编译器保证每个函数将被mangled为独一无二的名称,不同的编译器有不同的算法,C++标准并没有规定统一的算法,所以mangled之后的名称也可能不同)。可以看出,静态函数的调用同普通的C函数调用有完全相同的效率,并没有额外的开销。 普通成员函数的调用,如下列方式: X obj; X* ptr = &obj; obj.NormalFunc(); ptr->NormalFunc(); 将被被编译器转化为如下的C函数调用形式,如同这样。 mangled_name_of_X_NormalFunc(&obj); //obj.NormalFunc(); mangled_name_of_X_NormalFunc(ptr); // ptr->NormalFunc(); 可以看出普通成员函数的调用同普通的C调用没有大的区别,效率与静态函数也相同。编译器将重新改写函数的定义,增加一个const X* this参数将调用对象的地址传送进函数。 虚函数的调用稍微复杂一些,为了支持多态性,实现运行时刻绑定,编译器需要在每个对象上增加一个字段也就是vptr以指向类的虚函数表vtbl,虚函数的多态性只能通过对象指针或对象的引用调用来实现,如下的调用: X obj; X* ptr = &obj; X& ref = obj; ptr->VirtualFunc(); ref.VirtualFunc(); 将被C++编译器转换为如下的形式。 ( *ptr->vptr[2] )(ptr); ( *ptr->vptr[2] )(&ref); 其中的2表示VirtualFunc在类虚函数表的第2个槽位。可以看出,虚函数的调用相当于一个C的函数指针调用,其效率也并未降低。 由以上的四个例子可以看出,C++的函数调用效率依然很高。但C++还是有其特殊性,为了保证面向对象语义的正确性,C++编译器会在程序员所编写的程序基础上,做大量的扩展,如果程序员不了解编译器背后所做的这些工作,就可能写出效率不高的程序。对于一些继承层次很深的派生类或在成员变量中包含了很多其它类对象(如XX中的m_strName变量)的类来说,对象的创建和销毁的开销是相当大的,比如XX类的缺省构造函数,即使程序员没有定义任何语句,编译器依然会给其构造函数扩充以下代码来保证对象语义的正确性: XX::XX() { // 编译器扩充代码所要做的工作 1、 调用父类X的缺省构造函数 2、 设定vptr指向XX类虚函数表 3、 调用String类的缺省构造函数构造m_strName }; 所以为了提高效率,减少不必要的临时对象的产生、拖延暂时不必要的对象定义、用初始化代替赋值、使用构造函数初始化列表代替在构造函数中赋值等方法都能有效提高程序的运行效率。以下举例说明: 1、 减少临时对象的生成。如以传送对象引用的方式代替传值方式来定义函数的参数,如下例所示,传值方式将导致一个XX临时对象的产生 效率不高的做法 高效率做法 void Function( XX xx ) void Function( const XX& xx ) { { //函数体 //函数体 } } 2、 拖延暂时不必要的对象定义。在C中要将所有的局部变量定义在函数体头部,考虑到C++中对象创建的开销,这不是一个好习惯。如下例,如果大部分情况下bCache为"真",则拖延xx的定义可以大大提高函数的效率。 效率不高的做法 高效率做法 void Function( bool bCache ) void Function( bool bCache ) { { //函数体 //函数体 XX xx; if( bCache ) if( bCache ) {// do something without xx { return; // do something without xx } return; } //对xx进行操作 XX xx; //对xx进行操作 … return; return; } } 3、 可能情况下,以初始化代替先定义后赋值。如下例,高效率的做法会比效率不高的做法省去了cache变量的缺省构造函数调用开销。 效率不高的做法 高效率做法 void Function( const XX& xx ) void Function( const XX& xx ) { { XX cache; XX cache = xx; cache = xx ; } } 4、在构造函数中使用成员变量的初始化列表代替在构造函数中赋值。如下例,在效率不高的做法中,XX的构造函数会首先调用m_strName的缺省构造函数,再产生一个临时的String object,用空串""初始化临时对象,再以临时对象赋值(assign)给m_strName,然后销毁临时对象。而高效的做法只需要调用一次 m_strName的构造函数。 效率不高的做法 高效率做法 XX::XX() XX::XX() : m_strName( "" ) { { m_strName = ""; … … } } 类似的例子还很多,如何写出高效的C++程序需要实践和积累,但理解C++的底层运行机制是一个不可缺少的步骤,只要平时多学习和思考,编写高效的C++程序是完全可行的。 |
软件编码规范
通过建立代码编写规范,形成BCB 开发小组编码约定,提高程序的可靠性、可读性、可修改性、可维护性、一致性,保证程序代码的质量,继承软件开发成果,充分利用资源。提高程序的可继承性,使开发人员之间的工作成果可以共享。 软件编码要遵循以下原则: 1.遵循开发流程,在设计的指导下进行代码编写。 2.代码的编写以实现设计的功能和性能为目标,要求正确完成设计要求的功能,达到设计的性能。 3.程序具有良好的程序结构,提高程序的封装性好,减低程序的耦合程度。 4.程序可读性强,易于理解;方便调试和测试,可测试性好。 5.易于使用和维护;良好的修改性、扩充性;可重用性强/移植性好。 6.占用资源少,以低代价完成任务。 7.在不降低程序的可读性的情况下,尽量提高代码的执行效率。 本规范的描述主要以 Borland C++ Builder 语言为例 一、 规范:以下对本规范作详细说明。 1:源程序的文件管理: a) 组织:每个程序文件单元通常都应由 .cpp、.dfm和 .h 等文件组成,并将单元的公共声明部分放在 .h 文件中。划分单元主要是以类为依据,原则上每个较大的类都应为一个单独的单元,但在类较小且多个小类关系密切等情况下也可几个类共一个单元(建议仅对已经详细测试的较为通用的类采用)。 b)命名:原程序文件命名采用有意义的格式。例如:对登陆程序来说三个文件的命名应该是这样,.cpp的是 Login.cpp .dfm的是Login.dfm .h的是Login.h c)文件结构:每个程序文件由标题、内容和附加说明三部分组成。 (A)标题:文件最前面的注释说明,其内容主要包括:程序名,作者,版本信息,简要说明等,必要时应有更详尽的说明(将以此部分以空行隔开单独注释)。 (B) 内容:为文件源代码部分基本上按预处理语句、类型定义、变量定义、函数原型、函数实现(仅对 .cpp 文件)的顺序。 main 、 winmain ,控件注册等函数应放在内容部分的最后,类的定义按 private 、 protected 、 pubilic 、 __pubished 的顺序,并尽量保持每一部分只有一个,各部分中按数据、函数、属性、事件的顺序。 (C)附加说明:文件末尾的补充说明,如参考资料等,若内容不多也可放在标题部分的最后。 举例说明: /************************************************************* 类:class TimageManipulation 设计者:lunhongjun (2001/05/09) 用途:用于图象处理,实现图象亮度、对比度、反白、色彩平衡等处理 版本: 1.0 2001/05/09 完成基本的图象处理功能设计 2001/05/10 修改完成一个小Bug. *************************************************************/ class TImageManipulation { private://define variant Graphics::TBitmap * pSourceBitmap;//用于存放未经处理的原始图像 Graphics::TBitmap * pManipulatedImage;//用处存放经过处理后的图象 //图像处理过程中的相关参数 int iBrightness; //色彩亮度 int iContrast; //色彩对比度 int iRedColorBalance; //红色色彩平衡度 int iBlueColorBalance; //蓝色色彩平衡度 int iGreenColorBalance; //绿色色彩平衡度 bool bRotate; //字体旋转度数 bool bMonochrome; //是否反白显示 private: void __fastcall BrightnessImage(void);//调整图象亮度 void __fastcall ContrastImage(void);//调整图象对比度 void __fastcall DoManipulationImage(void);//图象处理 void __fastcall MonochromeImage(void);//图象反白 void __fastcall DoColor(void); void __fastcall DoFilter(int * flt, int Div); void __fastcall RotateImage(void);//调整图象色彩平衡 public://define property ,method,event,function __fastcall TImageManipulation(); __fastcall ~TImageManipulation(); void __fastcall DoBrightness(int BrightnessIncrement); void __fastcall DoContrast(int ContrastIncrement); void __fastcall DoMonochrome(void); void __fastcall DoChangeColorBalance(int RedBalance, int BlueBalance, int GreenBalance); void __fastcall SetSourceImage(Classes::TPersistent* Source); Graphics::TBitmap * __fastcall GetManipulationImage(void); void __fastcall DoBlur(void); void __fastcall DoSharp(void); void __fastcall DoEmboss(void); void __fastcall LoadImageFromFile(AnsiString FileName); void __fastcall SaveManipulatedImageAsFile(AnsiString FileName); TImageManipulation& operator=(const TImageManipulation & imSource); void __fastcall DoRotate(void); }; #endif 2.编辑风格: (1)缩进 缩进以4个空格为单位。建议在Tools/Editor Options中设置General页面的Block ident为4,Tab Stop为4,不要选中Use tab character。预处理语句、全局数据、函数原型、标题、附加说明、函数说明、标号等均顶格书写。语句块的“{”“}”配对对齐,并与其前一行对齐,语句块类的语句缩进建议每个“{”“}”单独占一行。 (2)空格 变量、类、常量数据和函数在其类型,修饰(如 __fastcall 等)名称之间适当空格并据情况对齐。关键字原则上空一格,如: if ( ... ) 等,运算符的空格规定如下:“::”、“->”、“[”、“]”、“++”、“--”、“~”、“!”、“+”、“-”(指正负号), “&”(取址或引用)、“*”(指使用指针时)等几个运算符两边不加空格(其中单目运算符系指与操作数相连的一边),其它运算符(包括大多数二目运算符和三目运算符“?:”两边均加一空格,“(”、“)”运算符在其内侧空一格,在作函数定义时还可据情况多空或不空格来对齐,但在函数实现时可以不用。“,”运算符只在其后空一格,需对齐时也可不空或多空格,“sizeof”运算符建议也在其后空一格,不论是否有括号,对语句行后加的注释应用适当空格与语句隔开并尽可能对齐。 (3)对齐 原则上关系密切的行应对齐,对齐包括类型、修饰、名称、参数等各部分对齐。另每一行的长度不应超过屏幕太多,必要时适当换行,换行时尽可能在“,”处或运算符处,换行后最好以运算符打头,并且以下各行均以该语句首行缩进,但该语句仍以首行的缩进为准,即如其下一行为“{”应与首行对齐。 变量定义最好通过添加空格形成对齐,同一类型的变量最好放在一起。如下例所示: int Value; int Result; int Length; DWORD Size; DWORD BufSize; char * pBuf; void * pOutputBuf; LPCSTR * pPath; (4)空行 程序文件结构各部分之间空两行,若不必要也可只空一行,各函数实现之间一般空两行,由于BCB会自动产生一行“//------”做分隔,另因每个函数还要有函数说明注释,故通常只需空一行或不空,但对于没有函数说明的情况至少应再空一行。对自己写的函数,建议也加上“//------”做分隔。函数内部数据与代码之间应空至少一行,代码中适当处应以空行空开,建议在代码中出现变量声明时,在其前空一行。类中四个“p”之间至少空一行,在其中的数据与函数之间也应空行。 (5)注释 对注释有以下三点要求: A.必须是有意义。 B.必须正确的描述了程序。 C.必须是最新的。 注释必不可少,但也不应过多,以下是四种必要的注释: A.标题、附加说明。 B.函数说明。对几乎每个函数都应有适当的说明,通常加在函数实现之前,在没有函数实现部分的情况下则加在函数原型前,其内容主要是函数的功能、目的、算法等说明,参数说明、返回值说明等,必要时还要有一些如特别的软硬件要求等说明。 C.在代码不明晰或不可移植处必须有一定的说明。 D.及少量的其它注释。 注释有块注释和行注释两种,分别是指:“/**/”和“//”建议对A用块注释,D用行注释,B、C则视情况而定,但应统一,至少在一个单元中B类注释形式应统一。 举例如下: /*************************************************************************** 函数名称:ResultType MyFunction(ParamType1 Param1, ParamTyp2,Param2) 功能:该函数主要是完成如下的功能 设计目的:设计该函数主要是为了解决。。。 设计原理:该函数是这样设计的。。。 实现方法/过程:为了实现函数的目的,这个函数是这样实现的。。。 出入口参数: ParamType1 Param1:类型ParamType1,这个参数是。。。 。。。 返回值描述: 设计修改日志: 2001/4/16 第一次设计 2001/4/17 修改了。。。 2001/4/18 添加了。。。删除了。。。 相关函数: 其他补充说明: **************************************************************************/ ResultType MyFunction(ParamType1 Param1, ParamTyp2,Param2) { int Value; int Result; DWORD Size; char * pBuf; 。。。。 } (6)代码长度: 对于每一个函数建议尽可能控制其代码长度为53行左右,超过53行的代码要重新考虑将其拆分为两个或两个以上的函数。函数拆分规则应该一不破坏原有算法为基础,同时拆分出来的部分应该是可以重复利用的。对于在多个模块或者窗体中都要用到的重复性代码,完全可以将起独立成为一个具备公用性质的函数,放置于一个公用模块中(Common.cpp/Common.h) 3.符号名的命名(包括变量、函数、标号、模块名等) 选用有实际意义的英文标识符号或缩写符号,名称中尽可能不使用阿拉伯数字,如这样的名称是不提倡的:Value1,Value2,Value3,Value4…..。 例如: file(文件),code(编号),data(数据),pagepoint(页面指针), faxcode(传真号) ,address(地址),bank(开户银行),…… 变量名称:由(前缀+修饰语)构成。 (1)生存期修饰:用l(local)表示局域变量,p(public)表示全局变量,s(send)表示参数变量 (2)类型修饰:用s(AnsiString)表示字符串,c(Char)表示字符,n(number)数值,i(intger)表示整数,d(double)表示双精度,f(float)浮点型,b(bool)布尔型,d(date)表示日期型. 例如: li_length表示整形的局域变量,是用来标识长度的.ls_code表示字符型的局域变量,用来标识代码. 控件名称:由(前缀+修饰语)构成。前缀即为控件的名称。 按钮变量 Button+Xxxxxxx 例如:ButtonSave,ButtonExit,ButtonPrint等 题标变量 Label+Xxxxxxxx 例如:LabelName,LabelSex等 数据表变量 Table+Xxxxxx 例如:TableFile,TableCount等 查询变量 Query+Xxxxxx 例如:QueryFile,QueryCeneter等 数据源变量 DataSource+Xxx 例如:DataSourceFile,DataSourceCenter等 。。。。。。。。。。。。。。。。 (注:对于与表有关的控件“修饰语”部分最好直接用表名。) 4:输入输出 输入和输出方式和格式尽可能方便用户,避免因设计不当给使用带来麻烦。应根据不同用户的类型、特点和不同的要求来制定方案。格式力求简单,并应有完备的出错检查和出错恢复措施。 界面布局主要考虑各区域在屏幕的放置,使用户能以最快的速度找到操作对象、发现目标,屏幕的布局还要考虑界面的表现形式,使界面美观一致,协调合理。界面设计要求满足以下要求: ●界面上只包含必要的信息。 ●界面上包含所有必要的信息。 ●界面布局从左上角开始。 ●制订格式标准,所有的屏幕设计都遵守这些标准,保持一致性。 ●根据逻辑关系将相关的信息放在一起。 ●屏幕设计要保持对称的平衡。 ●避免过多使用前调信息。//例如OnEnter事件请尽量少用。 ●区分标题和内容。 ●提示信息要简洁。 ●设计与用户知识和经验一致。 ●按照使用顺序显示信息。 ●遵照流行软件的事实标准。 ●选择合理的显示方式。 ●尽可能不让用户切换画面即可完成一次完整的操作。 5:其它程序技巧 程序中增加合理适量的注释。程序的注释作为评测考核的一个重要指标。 在程序设计时,应该全面考虑出现可能存在的例外情况的处理。应该有一个良好的错误处理和例外处理机制,在处错误的时候能保证程序能正常运行/退出,不会造成用户的数据丢失/损失。对于发生意外错误或例外是要能记录当时的运行情况并且用户可将这个信息返回给开发人员。 避免使用相似的变量名,变量中尽量不含数字。 同一变量名不要有多种意义。 显示说明所有变量。 注意浮点运算的误差,少用浮点数比较。 注意整数运算的特点。 少用或不用GOTO语句,即使用GOTO不要相互交叉。 尽量少用全局和静态变量。 提高程序的封装性,降低程序各模块的耦合性。 提高程序的可继承性,建立通用的函数库、控件库,使开发人员之间的工作成果可以共享。 说明: 该文档设计人:luhongjun(过江项羽)。 文档版权归BCB开发团队所有,欢迎大家转载引用,但请注明文档所有者和设计人。未经同意请勿任意修改,不可用于商业目的。 |
如何在C语言中巧用正则表达式
如果用户熟悉Linux下的sed、awk、grep或vi,那么对正则表达式这一概念肯定不会陌生。由于它可以极大地简化处理字符串时的复杂度,因此现在已经在许多Linux实用工具中得到了应用。千万不要以为正则表达式只是Perl、Python、Bash等脚本语言的专利,作为C语言程序员,用户同样可以在自己的程序中运用正则表达式。 标准的C和C++都不支持正则表达式,但有一些函数库可以辅助C/C++程序员完成这一功能,其中最著名的当数Philip Hazel的Perl-Compatible Regular Expression库,许多Linux发行版本都带有这个函数库。 编译正则表达式 为了提高效率,在将一个字符串与正则表达式进行比较之前,首先要用regcomp()函数对它进行编译,将其转化为regex_t结构: int regcomp(regex_t *preg, const char *regex, int cflags); 参数regex是一个字符串,它代表将要被编译的正则表达式;参数preg指向一个声明为regex_t的数据结构,用来保存编译结果;参数cflags决定了正则表达式该如何被处理的细节。 如果函数regcomp()执行成功,并且编译结果被正确填充到preg中后,函数将返回0,任何其它的返回结果都代表有某种错误产生。 匹配正则表达式 一旦用regcomp()函数成功地编译了正则表达式,接下来就可以调用regexec()函数完成模式匹配: int regexec(const regex_t *preg, const char *string, size_t nmatch,regmatch_t pmatch[], int eflags); typedef struct { regoff_t rm_so; regoff_t rm_eo; } regmatch_t; 参数preg指向编译后的正则表达式,参数string是将要进行匹配的字符串,而参数nmatch和pmatch则用于把匹配结果返回给调用程序,最后一个参数eflags决定了匹配的细节。 在调用函数regexec()进行模式匹配的过程中,可能在字符串string中会有多处与给定的正则表达式相匹配,参数pmatch就是用来保存这些匹配位置的,而参数nmatch则告诉函数regexec()最多可以把多少个匹配结果填充到pmatch数组中。当regexec()函数成功返回时,从string+pmatch[0].rm_so到string+pmatch[0].rm_eo是第一个匹配的字符串,而从string+pmatch[1].rm_so到string+pmatch[1].rm_eo,则是第二个匹配的字符串,依此类推。 释放正则表达式 无论什么时候,当不再需要已经编译过的正则表达式时,都应该调用函数regfree()将其释放,以免产生内存泄漏。 void regfree(regex_t *preg); 函数regfree()不会返回任何结果,它仅接收一个指向regex_t数据类型的指针,这是之前调用regcomp()函数所得到的编译结果。 如果在程序中针对同一个regex_t结构调用了多次regcomp()函数,POSIX标准并没有规定是否每次都必须调用regfree()函数进行释放,但建议每次调用regcomp()函数对正则表达式进行编译后都调用一次regfree()函数,以尽早释放占用的存储空间。 报告错误信息 如果调用函数regcomp()或regexec()得到的是一个非0的返回值,则表明在对正则表达式的处理过程中出现了某种错误,此时可以通过调用函数regerror()得到详细的错误信息。 size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size); 参数errcode是来自函数regcomp()或regexec()的错误代码,而参数preg则是由函数regcomp()得到的编译结果,其目的是把格式化消息所必须的上下文提供给regerror()函数。在执行函数regerror()时,将按照参数errbuf_size指明的最大字节数,在errbuf缓冲区中填入格式化后的错误信息,同时返回错误信息的长度。 应用正则表达式 最后给出一个具体的实例,介绍如何在C语言程序中处理正则表达式。 #include #include #include /* 取子串的函数 */ static char* substr(const char*str, unsigned start, unsigned end) { unsigned n = end - start; static char stbuf[256]; strncpy(stbuf, str + start, n); stbuf[n] = 0; return stbuf; } /* 主程序 */ int main(int argc, char** argv) { char * pattern; int x, z, lno = 0, cflags = 0; char ebuf[128], lbuf[256]; regex_t reg; regmatch_t pm[10]; const size_t nmatch = 10; /* 编译正则表达式*/ pattern = argv[1]; z = regcomp(®, pattern, cflags); if (z != 0){ regerror(z, ®, ebuf, sizeof(ebuf)); fprintf(stderr, "%s: pattern '%s' \n", ebuf, pattern); return 1; } /* 逐行处理输入的数据 */ while(fgets(lbuf, sizeof(lbuf), stdin)) { ++lno; if ((z = strlen(lbuf)) > 0 && lbuf[z-1] == '\n') lbuf[z - 1] = 0; /* 对每一行应用正则表达式进行匹配 */ z = regexec(®, lbuf, nmatch, pm, 0); if (z == REG_NOMATCH) continue; else if (z != 0) { regerror(z, ®, ebuf, sizeof(ebuf)); fprintf(stderr, "%s: regcom('%s')\n", ebuf, lbuf); return 2; } /* 输出处理结果 */ for (x = 0; x if (!x) printf("%04d: %s\n", lno, lbuf); printf(" $%d='%s'\n", x, substr(lbuf, pm[x].rm_so, pm[x].rm_eo)); } } /* 释放正则表达式 */ regfree(®); return 0; } 上述程序负责从命令行获取正则表达式,然后将其运用于从标准输入得到的每行数据,并打印出匹配结果。执行下面的命令可以编译并执行该程序: # gcc regexp.c -o regexp # ./regexp 'regex[a-z]*' 0003: #include $0='regex' 0027: regex_t reg; $0='regex' 0054: z = regexec(®, lbuf, nmatch, pm, 0); $0='regexec' 小结 对那些需要进行复杂数据处理的程序来说,正则表达式无疑是一个非常有用的工具。本文重点在于阐述如何在C语言中利用正则表达式来简化字符串处理,以便在数据处理方面能够获得与Perl语言类似的灵活性 |
C++运算符重载探讨
多态性是面向对象程序设计的重要特征之一。它与前面讲过的封装性和继承性构成了面向对象程序设计的三大特征。这三大特征是相互关联的。封装性是基础,继承性是关键,多态性是补充,而多态又必须存在于继承的环境之中。 所谓多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为。这里所说的消息主要是指对类的成员函数的调用,而不同的行为是指不同的实现。利用多态性,用户只需发送一般形式的消息,而将所有的实现留给接收消息的对象。对象根据所接收到的消息而做出相应的动作(即操作)。 函数重载和运算符重载是简单一类多态性。函数重载的概念及用法在《函数重载》一讲中已讨论过了,这里只作简单的补充,我们重点讨论的是运算符的重载。 所谓函数重载简单地说就是赋给同一个函数名多个含义。具体地讲,C++中允许在相同的作用域内以相同的名字定义几个不同实现的函数,可以是成员函数,也可以是非成员函数。但是,定义这种重载函数时要求函数的参数或者至少有一个类型不同,或者个数不同。而对于返回值的类型没有要求,可以相同,也可以不同。那种参数个数和类型都相同,仅仅返回值不同的重载函数是非法的。因为编译程序在选择相同名字的重载函数时仅考虑函数表,这就是说要靠函数的参数表中,参数个数或参数类型的差异进行选择。 由此可以看出,重载函数的意义在于它可以用相同的名字访问一组相互关联的函数,由编译程序来进行选择,因而这将有助于解决程序复杂性问题。如:在定义类时,构造函数重载给初始化带来了多种方式,为用户提供更大的灵活性。 下面我们重点讨论运算符重载。 运算符重载就是赋予已有的运算符多重含义。C++中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能,这便增强了C++语言的扩充能力。 运算符重载的几个问题 1. 运算符重载的作用是什么? 它允许你为类的用户提供一个直觉的接口。 运算符重载允许C/C++的运算符在用户定义类型(类)上拥有一个用户定义的意义。重载的运算符是函数调用的语法修饰: class Fred { public: // ... }; #if 0 // 没有算符重载: Fred add(Fred, Fred); Fred mul(Fred, Fred); Fred f(Fred a, Fred b, Fred c) { return add(add(mul(a,b), mul(b,c)), mul(c,a)); // 哈哈,多可笑... } #else // 有算符重载: Fred operator+ (Fred, Fred); Fred operator* (Fred, Fred); Fred f(Fred a, Fred b, Fred c) { return a*b + b*c + c*a; } #endif 2. 算符重载的好处是什么? 通过重载类上的标准算符,你可以发掘类的用户的直觉。使得用户程序所用的语言是面向问题的,而不是面向机器的。 最终目标是降低学习曲线并减少错误率。 3. 哪些运算符可以用作重载? 几乎所有的运算符都可用作重载。具体包含: 算术运算符:+,-,*,/,%,++,--; 位操作运算符:&,|,~,^,<<,>> 逻辑运算符:!,&&,||; 比较运算符:<,>,>=,<=,==,!=; 赋值运算符:=,+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=; 其他运算符:[],(),->,,(逗号运算符),new,delete,new[],delete[],->*。 下列运算符不允许重载: .,.*,::,?: 4. 运算符重载后,优先级和结合性怎么办? 用户重载新定义运算符,不改变原运算符的优先级和结合性。这就是说,对运算符重载不改变运算符的优先级和结合性,并且运算符重载后,也不改变运算符的语法结构,即单目运算符只能重载为单目运算符,双目运算符只能重载双目运算符。 5. 编译程序如何选用哪一个运算符函数? 运算符重载实际是一个函数,所以运算符的重载实际上是函数的重载。编译程序对运算符重载的选择,遵循着函数重载的选择原则。当遇到不很明显的运算时,编译程序将去寻找参数相匹配的运算符函数。 6. 重载运算符有哪些限制? (1) 不可臆造新的运算符。必须把重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中。 (2) 重载运算符坚持4个"不能改变"。 ·不能改变运算符操作数的个数; ·不能改变运算符原有的优先级; ·不能改变运算符原有的结合性; ·不能改变运算符原有的语法结构。 7. 运算符重载时必须遵循哪些原则? 运算符重载可以使程序更加简洁,使表达式更加直观,增加可读性。但是,运算符重载使用不宜过多,否则会带来一定的麻烦。 使用重载运算符时应遵循如下原则: (1) 重载运算符含义必须清楚。 (2) 重载运算符不能有二义性。 运算符重载函数的两种形式 运算符重载的函数一般地采用如下两种形式:成员函数形式和友元函数形式。这两种形式都可访问类中的私有成员。 1. 重载为类的成员函数 这里先举一个关于给复数运算重载复数的四则运算符的例子。复数由实部和虚部构造,可以定义一个复数类,然后再在类中重载复数四则运算的运算符。先看以下源代码: #include <iostream.h> class complex { public: complex() { real=imag=0; } complex(double r, double i) { real = r, imag = i; } complex operator +(const complex &c); complex operator -(const complex &c); complex operator *(const complex &c); complex operator /(const complex &c); friend void print(const complex &c); private: double real, imag; }; inline complex complex:perator +(const complex &c) { return complex(real + c.real, imag + c.imag); } inline complex complex:perator -(const complex &c) { return complex(real - c.real, imag - c.imag); } inline complex complex:perator *(const complex &c) { return complex(real * c.real - imag * c.imag, real * c.imag + imag * c.real); } inline complex complex::operator /(const complex &c) { return complex((real * c.real + imag + c.imag) / (c.real * c.real + c.imag * c.imag), (imag * c.real - real * c.imag) / (c.real * c.real + c.imag * c.imag)); } void print(const complex &c) { if(c.imag<0) cout<<c.real<<c.imag<<@#i@#; else cout<<c.real<<@#+@#<<c.imag<<@#i@#; } void main() { complex c1(2.0, 3.0), c2(4.0, -2.0), c3; c3 = c1 + c2; cout<<"\nc1+c2="; print(c3); c3 = c1 - c2; cout<<"\nc1-c2="; print(c3); c3 = c1 * c2; cout<<"\nc1*c2="; print(c3); c3 = c1 / c2; cout<<"\nc1/c2="; print(c3); c3 = (c1+c2) * (c1-c2) * c2/c1; cout<<"\n(c1+c2)*(c1-c2)*c2/c1="; print(c3); cout<<endl; } 该程序的运行结果为: c1+c2=6+1i c1-c2=-2+5i c1*c2=14+8i c1/c2=0.45+0.8i (c1+c2)*(c1-c2)*c2/c1=9.61538+25.2308i 在程序中,类complex定义了4个成员函数作为运算符重载函数。将运算符重载函数说明为类的成员函数格式如下: <类名> operator <运算符>(<参数表>) 其中,operator是定义运算符重载函数的关键字。 程序中出现的表达式: c1+c2 编译程序将给解释为: c1.operator+(c2) 其中,c1和c2是complex类的对象。operator+()是运算+的重载函数。 该运算符重载函数仅有一个参数c2。可见,当重载为成员函数时,双目运算符仅有一个参数。对单目运算符,重载为成员函数时,不能再显式说明参数。重载为成员函数时,总时隐含了一个参数,该参数是this指针。this指针是指向调用该成员函数对象的指针。 2. 重载为友元函数 运算符重载函数还可以为友元函数。当重载友元函数时,将没有隐含的参数this指针。这样,对双目运算符,友元函数有2个参数,对单目运算符,友元函数有一个参数。但是,有些运行符不能重载为友元函数,它们是:=,(),[]和->。 重载为友元函数的运算符重载函数的定义格式如下: friend <类型说明符> operator <运算符>(<参数表>) {……} 下面用友元函数代码成员函数,重载编写上述的例子,程序如下: #include <iostream.h> class complex { public: complex() { real=imag=0; } complex(double r, double i) { real = r, imag = i; } friend complex operator +(const complex &c1, const complex &c2); friend complex operator -(const complex &c1, const complex &c2); friend complex operator *(const complex &c1, const complex &c2); friend complex operator /(const complex &c1, const complex &c2); friend void print(const complex &c); private: double real, imag; }; complex operator +(const complex &c1, const complex &c2) { return complex(c1.real + c2.real, c1.imag + c2.imag); } complex operator -(const complex &c1, const complex &c2) { return complex(c1.real - c2.real, c1.imag - c2.imag); } complex operator *(const complex &c1, const complex &c2) { return complex(c1.real * c2.real - c1.imag * c2.imag, c1.real * c2.imag + c1.imag * c2.real); } complex operator /(const complex &c1, const complex &c2) { return complex((c1.real * c2.real + c1.imag + c2.imag) / (c2.real * c2.real + c2.imag * c2.imag), (c1.imag * c2.real - c1.real * c2.imag) / (c2.real * c2.real + c2.imag * c2.imag)); } void print(const complex &c) { if(c.imag<0) cout<<c.real<<c.imag<<@#i@#; else cout<<c.real<<@#+@#<<c.imag<<@#i@#; } void main() { complex c1(2.0, 3.0), c2(4.0, -2.0), c3; c3 = c1 + c2; cout<<"\nc1+c2="; print(c3); c3 = c1 - c2; cout<<"\nc1-c2="; print(c3); c3 = c1 * c2; cout<<"\nc1*c2="; print(c3); c3 = c1 / c2; cout<<"\nc1/c2="; print(c3); c3 = (c1+c2) * (c1-c2) * c2/c1; cout<<"\n(c1+c2)*(c1-c2)*c2/c1="; print(c3); cout<<endl; } 该程序的运行结果与上例相同。前面已讲过,对又目运算符,重载为成员函数时,仅一个参数,另一个被隐含;重载为友元函数时,有两个参数,没有隐含参数。因此,程序中出现的 c1+c2 编译程序解释为: operator+(c1, c2) 调用如下函数,进行求值, complex operator +(const coplex &c1, const complex &c2) 3. 两种重载形式的比较 一般说来,单目运算符最好被重载为成员;对双目运算符最好被重载为友元函数,双目运算符重载为友元函数比重载为成员函数更方便此,但是,有的双目运算符还是重载为成员函数为好,例如,赋值运算符。因为,它如果被重载为友元函数,将会出现与赋值语义不一致的地方。 其他运算符的重载举例 1).下标运算符重载 由于C语言的数组中并没有保存其大小,因此,不能对数组元素进行存取范围的检查,无法保证给数组动态赋值不会越界。利用C++的类可以定义一种更安全、功能强的数组类型。为此,为该类定义重载运算符[]。 下面先看看一个例子: #include <iostream.h> class CharArray { public: CharArray(int l) { Length = l; Buff = new char[Length]; } ~CharArray() { delete Buff; } int GetLength() { return Length; } char & operator [](int i); private: int Length; char * Buff; }; char & CharArray::operator [](int i) { static char ch = 0; if(i<Length&&i>=0) return Buff; else { cout<<"\nIndex out of range."; return ch; } } void main() { int cnt; CharArray string1(6); char * string2 = "string"; for(cnt=0; cnt<8; cnt++) string1[cnt] = string2[cnt]; cout<<"\n"; for(cnt=0; cnt<8; cnt++) cout<<string1[cnt]; cout<<"\n"; cout<<string1.GetLength()<<endl; } 该数组类的优点如下: (1) 其大小不心是一个常量。 (2) 运行时动态指定大小可以不用运算符new和delete。 (3) 当使用该类数组作函数参数时,不心分别传递数组变量本身及其大小,因为该对象中已经保存大小。 在重载下标运算符函数时应该注意: (1) 该函数只能带一个参数,不可带多个参数。 (2) 不得重载为友元函数,必须是非static类的成员函数。 2). 重载增1减1运算符 增1减1运算符是单目运算符。它们又有前缀和后缀运算两种。为了区分这两种运算,将后缀运算视为又目运算符。表达式 obj++或obj-- 被看作为: obj++0或obj--0 下面举一例子说明重载增1减1运算符的应用。 #include <iostream.h> class counter { public: counter() { v=0; } counter operator ++(); counter operator ++(int ); void print() { cout<<v<<endl; } private: unsigned v; }; counter counter::operator ++() { v++; return *this; } counter counter::operator ++(int) { counter t; t.v = v++; return t; } void main() { counter c; for(int i=0; i<8; i++) c++; c.print(); for(i=0; i<8; i++) ++c; c.print(); } 3). 重载函数调用运算符 可以将函数调用运算符()看成是下标运算[]的扩展。函数调用运算符可以带0个至多个参数。下面通过一个实例来熟悉函数调用运算符的重载。 #include <iostream.h> class F { public: double operator ()(double x, double y) const; }; double F::operator ()(double x, double y) const { return (x+5)*y; } void main() { F f; cout<<f(1.5, 2.2)<<endl; } |
键盘扫描码大全
对程序进行键盘控制时,往往要用到一些键的扫描码,以前每次用时都得先扫下试试,实在麻烦,今天又搞了个小程序,用到了扫描码,索性整了个测试程序,把所有键的扫描码全存入一个文件啦,以便以后编程时使用.在此跟大家分享一下,虽然得来非常容易,但至少可以免得大家都像我以前那样用时再查 扫描码 键 0x011b ESC 0x3b00 F1 0x3c00 F2 0x3d00 F3 0x3e00 F4 0x3f00 F5 0x4000 F6 0x4100 F7 0x4200 F8 0x4300 F9 0x4400 F10 主键盘区: 0x2960 ~ 0x0231 1 0x0332 2 0x0433 3 0x0534 4 0x0635 5 0x0736 6 0x0837 7 0x0938 8 0x0a39 9 0x0b30 0 0x0c2d - 0x0d3d = 0x2b5c \ 0x0e08 退格键 0x0f09 Tab 0x1071 q 0x1177 w 0x1265 e 0x1372 r 0x1474 t 0x1579 y 0x1675 u 0x1769 i 0x186f o 0x1970 p 0x1a5b [ 0x1b5d ] 0x1e61 a 0x1f73 s 0x2064 d 0x2166 f 0x2267 g 0x2368 h 0x246a j 0x256b k 0x266c l 0x273b ; 0x2827 ' 0x1c0d 回车 0x2c7a z 0x2d78 x 0x2e63 c 0x2f76 v 0x3062 b 0x316e n 0x326d m 0x332c , 0x342e . 0x352f / 0x3920 空格键 右边数字键盘: 0x5200 Insert 0x4700 Home 0x4900 Page UP 0x5300 Delete 0x4f00 End 0x5100 PageDown 0x4800 上箭头 0x4b00 左箭头 0x5000 下箭头 0x4d00 右箭头 0x352f / 0x372a * 0x4a2d - (注意,这是数字键盘的) 0x4737 7 0x4838 8 0x4939 9 0x4b34 4 0x4c35 5 0x4d36 6 0x4e2b + 0x4f31 1 0x5032 2 0x5133 3 0x5230 0 0x532e Del |
如何分析网页元素并进行相应处理
如何分析网页元素,然后进行相应处理,比如填表,递交等等 #include AnsiString __fastcall TForm3::Submit(void) { IHTMLDocument2 *pHTMLDoc = NULL; LPDISPATCH pDisp = NULL; pDisp =CppWebBrowser1->Document; if(pDisp) { if (SUCCEEDED(pDisp->QueryInterface(IID_IHTMLDocument2, (void**)&pHTMLDoc))) { pDisp->Release(); IHTMLElementCollection *pelement; if(pHTMLDoc->get_forms(&pelement)!=S_OK) { pHTMLDoc->Release(); pDisp->Release(); pelement->Release(); return "1发送失败"; } pDisp->Release(); tagVARIANT a,i; a.vt=VT_UI4; a.lVal=0;//这个值为你所要填写的表单在整个网页中的顺序,0为第一个表单 if(pelement->item(a,i,&pDisp)!=S_OK) { pelement->Release(); pDisp->Release(); return "2发送失败"; } pelement->Release(); IHTMLFormElement* pFormElem=NULL; if(pDisp) { if(!SUCCEEDED(pDisp->QueryInterface(IID_IHTMLFormElement,(LPVOID*)&pFormElem))) { pFormElem->Release(); pDisp->Release(); return "3发送失败"; } } else { return "31发送失败"; } pDisp->Release(); LPDISPATCH pDisp_msg = NULL; tagVARIANT phone,msg,empty; phone.vt=VT_UI4; phone.lVal=0;//这个值为Input框在表单中的顺序 msg.vt=VT_UI4; msg.lVal=1;//这个值为Text文本框在表单中的顺序 if((pFormElem->item(phone,empty,&pDisp)!=S_OK)||(pFormElem->item(msg,empty,&pDisp_msg)!=S_OK)) { pFormElem->Release(); if(pDisp) pDisp->Release(); if(pDisp_msg) pDisp_msg->Release(); return "4发送失败"; } IHTMLInputTextElement* phoneElem=NULL; IHTMLTextAreaElement* msgElem; if(!pDisp||!pDisp_msg) return "11失败"; if (!SUCCEEDED(pDisp->QueryInterface(IID_IHTMLInputTextElement, (void**)&phoneElem))||(!SUCCEEDED(pDisp_msg->QueryInterface(IID_IHTMLTextAreaElement, (void**)&msgElem)))) { if(phoneElem) phoneElem->Release(); if(msgElem) msgElem->Release(); if(pDisp) pDisp->Release(); if(pDisp_msg) pDisp_msg->Release(); return "5发送失败"; } WideString s_phone,s_msg; s_phone="888888" s_msg="你好"; phoneElem->put_value(s_phone.c_bstr());//填写 msgElem->put_value(s_msg.c_bstr());//填写 pFormElem->submit();//提交 phoneElem->Release(); msgElem->Release(); pDisp->Release(); pDisp_msg->Release(); pFormElem->Release(); return "发送成功"; } } return "发送失败"; } |
自定义消息获取消息
1. 自定义消息 (1) 手工定义消息,可以这么写 #define WM_MY_MESSAGE(WM_USER+100), MS 推荐的至 少是 WM_USER+100; (2)写消息处理函数,用 WPARAM,LPARAM返回LRESULT. LRESULT CMainFrame::OnMyMessage(WPARAM wparam,LPARAM lParam) { //加入你的处理函数 } (3) 在类的 AFX_MSG处进行声明,也就是常说的"宏映射" //----------------------------------------------------------------------- 2. 获取有关窗口正在处理的当前消息的信息 调用CWnd: : GetCurrentMessage 可以获取一个MSG指针。例如,可以使用ClassWizard 将几个菜单项处理程序映射到一个函数中,然后调用GetCurrentMessage 来确定所选中 的菜单项。 viod CMainFrame : : OnCommmonMenuHandler ( ) { //Display selected menu item in debug window . TRACE ("Menu item %u was selected . \n" , GetCruuentMessage ( ) ―> wParam ); } //----------------------------------------------------------------------- 3.窗口最大化、最小化及关闭的消息是什么 最大化、最小化将发送WM_SYSCOMMAND消息。要处理该消息,可以这么做: (1)、在Form的头文件中添加: void __fastcall RestrictMinimizeMaximize(TMessage &Msg); BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_SYSCOMMAND, TMessage, RestrictMinimizeMaximize) END_MESSAGE_MAP(TForm) (2)、在Form的单元文件中添加: void __fastcall TForm1::RestrictMinimizeMaximize(TMessage& Msg) { if (Msg.WParam == SC_MINIMIZE) { //catches minimize... } else if (Msg.WParam == SC_MAXIMIZE) { //catches maximize... } TForm:ispatch(&Msg); // or "else TForm:ispatch(&Msg)" to trap } 关闭窗口的消息为WM_CLOSE,C++Builder提供了OnClose事件 //----------------------------------------------------------------------- 消息映射的定义和实现 MFC处理的三类消息 根据处理函数和处理过程的不同, MFC主要处理三类消息: Windows 消息,前缀以“WM_”打头,WM_COMMAND例外。Windows消息直接送给MFC窗口过程处理,窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。 控制通知消息,是控制子窗口送给父窗口的 WM_COMMAND通知消息。窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。 需要指出的是,Win32使用新的WM_NOFITY来处理复杂的通知消息。WM_COMMAND类型的通知消息仅仅能传递一个控制窗口句柄(lparam)、控制窗ID和通知代码(wparam)。WM_NOTIFY能传递任意复杂的信息。 命 令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的WM_COMMAND通知消息,属于应用程序自己定义的消息。通过消息映射机制,MFC框架把 命令按一定的路径分发给多种类型的对象(具备消息处理能力)处理,如文档、窗口、应用程序、文档模板等对象。能处理消息映射的类必须从 CCmdTarget类派生。 在讨论了消息的分类之后,应该是讨论各类消息如何处理的时候了。但是,要知道怎么处理消息,首先要知道如何映射消息。 MFC消息映射的实现方法 MFC 使用ClassWizard帮助实现消息映射,它在源码中添加一些消息映射的内容,并声明和实现消息处理函数。现在来分析这些被添加的内容。 在类的定义(头文件)里,它增加了消息处理函数声明,并添加一行声明消息映射的宏 DECLARE_MESSAGE_MAP。 在类的实现(实现文件)里,实现消息处理函数,并使用 IMPLEMENT_MESSAGE_MAP宏实现消息映射。一般情况下,这些声明和实现是由MFC的ClassWizard自动来维护的。看一个例子: 在 AppWizard产生的应用程序类的源码中,应用程序类的定义(头文件)包含了类似如下的代码: //{{AFX_MSG(CTttApp) afx_msg void OnAppAbout(); //}}AFX_MSG DECLARE_MESSAGE_MAP() 应用程序类的实现文件中包含了类似如下的代码: BEGIN_MESSAGE_MAP(CTApp, CWinApp) //{{AFX_MSG_MAP(CTttApp) ON_COMMAND(ID_APP_ABOUT, OnAppAbout) //}}AFX_MSG_MAP END_MESSAGE_MAP() 头文件里是消息映射和消息处理函数的声明,实现文件里是消息映射的实现和消息处理函数的现。它表示让应用程序对象处理命令消息 ID_APP_ABOUT,消息处理函数是OnAppAbout。 为什么这样做之后就完成了一个消息映射?这些声明和实现到底作了些什么呢?接着,将讨论这些问题。 在声明与实现的内部 DECLARE_MESSAGE_MAP宏: 首先,看DECLARE_MESSAGE_MAP宏的内容: #ifdef _AFXDLL #define DECLARE_MESSAGE_MAP() \ private: \ static const AFX_MSGMAP_ENTRY _messageEntries[]; \ protected: \ static AFX_DATA const AFX_MSGMAP messageMap; \ static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); \ virtual const AFX_MSGMAP* GetMessageMap() const; \ #else #define DECLARE_MESSAGE_MAP() \ private: \ static const AFX_MSGMAP_ENTRY _messageEntries[]; \ protected: \ static AFX_DATA const AFX_MSGMAP messageMap; \ virtual const AFX_MSGMAP* GetMessageMap() const; \ #endif DECLARE_MESSAGE_MAP 定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。 BEGIN_MESSAE_MAP宏 然后,看BEGIN_MESSAE_MAP宏的内容: #ifdef _AFXDLL #define BEGIN_MESSAGE_MAP(theClass, baseClass) \ const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \ { return &baseClass::messageMap; } \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return &theClass::messageMap; } \ AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \ { &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] }; \ const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \ { \ #else #define BEGIN_MESSAGE_MAP(theClass, baseClass) \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return &theClass::messageMap; } \ AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \ { &baseClass::messageMap, &theClass::_messageEntries[0] }; \ const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \ { \ #endif #define END_MESSAGE_MAP() \ {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \ }; \ 对应地, BEGIN_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。END_MESSAGE_MAP相对简单,就只有一种定义。 ON_COMMAND宏 最后,看ON_COMMAND宏的内容: #define ON_COMMAND(id, memberFxn) \ {\ WM_COMMAND,\ CN_COMMAND,\ (WORD)id,\ (WORD)id,\ AfxSig_vv,\ (AFX_PMSG)memberFxn\ }; 消息映射声明的解释 在清楚了有关宏的定义之后,现在来分析它们的作用和功能。 消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。 |
钩子的类型和实现
Windows系统是建立在事件驱动的机制上的,说穿了就是整个系统都是通过消息的传递来实现的。而钩子是Windows系统中非常重要的系统接 口,用它可以截获并处理送给 其他应用程序的消息,来完成普通应用程序难以实现的功能。钩子可以监视系统或进程中的各种事件消息,截获发往目标窗口的消息并进行处理。这样,我们就可以在系统中安装自定义的钩子,监视系统中特定事件的发生,完成特定的功能,比如截获键盘、鼠标的输入,屏幕取词,日志监视等等。可见,利用钩子可以实现许多 特殊而有用的功能。因此,对于高级编程人员来说,掌握钩子的编程方法是很有必要的。 钩子的类型 一. 按事件分类,有如下的几种常用类型 (1) 键盘钩子和低级键盘钩子可以监视各种键盘消息。 (2) 鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。 (3) 外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。 (4) 日志钩子可以记录从系统消息队列中取出的各种事件消息。 (5) 窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。 此外,还有一些特定事件的钩子提供给我们使用,不一一列举。 下面描述常用的Hook类型: 1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。 WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。 2、WH_CBT Hook 在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括: 1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件; 2. 完成系统指令; 3. 来自系统消息队列中的移动鼠标,键盘事件; 4. 设置输入焦点事件; 5. 同步系统消息队列事件。 Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。 3、WH_DEBUG Hook 在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。 4、WH_FOREGROUNDIDLE Hook 当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。 5、WH_GETMESSAGE Hook 应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。 6、WH_JOURNALPLAYBACK Hook WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。 WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。 WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。 WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。(估计按键精灵是用这个hook做的) 7、WH_JOURNALRECORD Hook WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。 WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。 WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。 8、WH_KEYBOARD Hook 在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。 9、WH_KEYBOARD_LL Hook WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。 10、WH_MOUSE Hook WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。 11、WH_MOUSE_LL Hook WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。 12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。 WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。 WH_SYSMSGFILTER Hook监视所有应用程序消息。 WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。 13、WH_SHELL Hook 外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。 WH_SHELL 共有5钟情況: 1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁; 2. 当Taskbar需要重画某个按钮; 3. 当系统需要显示关于Taskbar的一个程序的最小化形式; 4. 当目前的键盘布局状态改变; 5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。 按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfo function注册它自己。 以上是13种常用的hook类型! 二. 按使用范围分类,主要有线程钩子和系统钩子 (1) 线程钩子监视指定线程的事件消息。 (2) 系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL) 中。这是系统钩子和线程钩子很大的不同之处。 几点需要说明的地方: (1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。 (2) 对同一事件消息可安装多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子信息传递给下一个钩子函数。而且最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。 (3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载。 编写钩子程序 编写钩子程序的步骤分为三步:定义钩子函数、安装钩子和卸载钩子。 1.定义钩子函数 钩子函数是一种特殊的回调函数。钩子监视的特定事件发生后,系统会调用钩子函数进行处理。不同事件的钩子函数的形式是各不相同的。 下面以鼠标钩子函数举例说明钩子函数的原型: LRESULT CALLBACK HookProc(int nCode ,WPARAM wParam,LPARAM lParam) 参数wParam和 lParam包含所钩消息的信息,比如鼠标位置、状态,键盘按键等。nCode包含有关消息本身的信息,比如是否从消息队列中移出。 我们先在钩子函数中实现自定义的功能,然后调用函数 CallNextHookEx.把钩子信息传递给钩子链的下一个钩子函数。 CallNextHookEx.的原型如下: LRESULT CallNextHookEx( HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam ) 参数 hhk是钩子句柄。nCode、wParam和lParam 是钩子函数。 当然也可以通过直接返回TRUE来丢弃该消息,就阻止了该消息的传递。2.安装钩子 在程序初始化的时候,调用函数SetWindowsHookEx安装钩子。 其函数原型为: HHOOK SetWindowsHookEx( int idHook,HOOKPROC lpfn, INSTANCE hMod,DWORD dwThreadId ) 参数idHook表示钩子类型,它是和钩子函数类型一一对应的。比如,WH_KEYBOARD表示安装的是键盘钩子,WH_MOUSE表示是鼠标钩子等等。 Lpfn是钩子函数的地址。 HMod是钩子函数所在的实例的句柄。对于线程钩子,该参数为NULL;对于系统钩子,该参数为钩子函数所在的DLL句柄。 dwThreadId 指定钩子所监视的线程的线程号。对于全局钩子,该参数为NULL。 SetWindowsHookEx返回所安装的钩子句柄。 3.卸载钩子 当不再使用钩子时,必须及时卸载。简单地调用函数 BOOL UnhookWindowsHookEx( HHOOK hhk)即可。 值得注意的是线程钩子和系统钩子的钩子函数的位置有很大的差别。线程钩子一般在当前线程或者当前线程派生的线程内,而系统钩子必须放在独立的动态链接库中,实现起来要麻烦一些。 线程钩子的编程实例: 按照上面介绍的方法实现一个线程级的鼠标钩子。钩子跟踪当前窗口鼠标移动的位置变化信息。并输出到窗口。 (1)在VC++6.0中利用MFC APPWizard(EXE)生成一个不使用文档/视结构的单文档应用mousehook。打开childview.cpp文件,加入全局变量: HHOOK hHook;//鼠标钩子句柄 CPoint point;//鼠标位置信息 CChildView *pView; // 鼠标钩子函数用到的输出窗口指针 在CChildView::OnPaint()添加如下代码: CPaintDC dc(this); char str[256]; sprintf(str,“x=%d,y=%d",point.x,point.y); //构造字符串 dc.TextOut(0,0,str); //显示字符串 (2)childview.cpp文件中定义全局的鼠标钩子函数。 LRESULT CALLBACK MouseProc (int nCode, WPARAM wParam, LPARAM lParam) {//是鼠标移动消息 if(wParam==WM_MOUSEMOVE||wParam ==WM_NCMOUSEMOVE) { point=((MOUSEHOOKSTRUCT *)lParam)->pt; //取鼠标信息 pView->Invalidate(); //窗口重画 } return CallNextHookEx(hHook,nCode,wParam,lParam); //传递钩子信息 } (3)CChildView类的构造函数中安装钩子。 CChildView::CChildView() { pView=this;//获得输出窗口指针 hHook=SetWindowsHookEx(WH_MOUSE,MouseProc,0,GetCurrentThreadId()); } (4)CChildView类的析构函数中卸载钩子。 CChildView::~CChildView() { if(hHook) UnhookWindowsHookEx(hHook); } 系统钩子的编程实例: 由于系统钩子要用到dll,所以先介绍下win32 dll的特点: Win32 DLL与 Win16 DLL有很大的区别,这主要是由操作系统的设计思想决定的。 一方面,在Win16 DLL中程序入口点函数和出口点函数(LibMain和WEP)是分别实现的;而在Win32 DLL中却由同一函数DLLMain来实现。 无论何时,当一个进程或线程载入和卸载DLL时,都要调用该函数,它的原型是 BOOL WINAPI DllMain (HINSTANCE hinstDLL,DWORD fdwReason, LPVOID lpvReserved); 其中,第一个参数表示DLL的实例句柄;第三个参数系统保留; 这里主要介绍一下第二个参数,它有四个可能的值: DLL_PROCESS_ATTACH(进程载入), DLL_THREAD_ATTACH(线程载入), DLL_THREAD_DETACH(线程卸 载), DLL_PROCESS_DETACH(进程卸载), 在DLLMain函数中可以对传递进来的这个参数的值进行判别,并根据不同的参数值对DLL进 行必要的初始化或清理工作。 举个例子来说,当有一个进程载入一个DLL时,系统分派给DLL的第二个参数DLL_PROCESS_ATTACH,这时, 你可以根据这个参数初始化特定的数据。另一方面,在Win16环境下,所有应用程序都在同一地址空间;而在Win32环境下,所有应用程序都有自己的私有空间,每个进程的空间都是相互独立的,这减少了应用程序间的相互影响,但同时也增加了编程的难度。 大家知道,在Win16环境中,DLL的全局数据对每个 载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,当进程在载入DLL时,系统自动把DLL地址映射到该进程的私有空间,而且也复制 该DLL的全局数据的一份拷贝到该进程空间,也就是说每个进程所拥有的相同的DLL的全局数据其值却并不一定是相同的。因此,在Win32环境下要想在多 个进程中共享数据,就必须进行必要的设置。亦即把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。 在VC6 中有三种形式的MFC DLL(在该DLL中可以使用和继承已有的MFC类)可供选择,即Regular statically linked to MFC DLL(标准静态链接MFC DLL)和Regular using the shared MFC DLL(标准动态链接MFC DLL)以及Extension MFC DLL(扩展MFC DLL)。 第一种DLL的特点是,在编译时把使用的MFC代码加入到DLL中,因此,在使用该程序时不需要其他MFC动态链接类库的存在,但占用磁盘空间 比较大; 第二种DLL的特点是,在运行时,动态链接到MFC类库,因此减少了空间的占用,但是在运行时却依赖于MFC动态链接类库;这两种DLL既可以被 MFC程序使用也可以被Win32程序使用。 第三种DLL的特点类似于第二种,做为MFC类库的扩展,只能被MFC程序使用。 VC6中全局共享数据的实现 在主文件中,用#pragma data_seg建立一个新的数据段并定义共享数据,其具体格式为: #pragma data_seg ("shareddata") HWND sharedwnd=NULL;//共享数据 #pragma data_seg() 仅定义一个数据段还不能达到共享数据的目的,还要告诉编译器该段的属性,有两种方法可以实现该目的(其效果是相同的),一种方法是在.DEF文件中加入如下语句: SETCTIONS shareddata READ WRITE SHARED 另一种方法是在项目设置链接选项中加入如下语句: /SECTION:shareddata,rws 好了,准备知识已经学完了,让我们开始编写个全局的钩子程序吧! 由于全局钩子函数必须包含在动态链接库中,所以本例由两个程序体来实现。 1.建立钩子Mousehook.DLL (1)选择MFC AppWizard(DLL)创建项目Mousehook; (2)选择MFC Extension DLL(共享MFC拷贝)类型; (3)由于VC5没有现成的钩子类,所以要在项目目录中创建Mousehook.h文件,在其中建立钩子类: class AFX_EXT_CLASS Cmousehook:public CObject { public: Cmousehook(); //钩子类的构造函数 ~Cmousehook(); //钩子类的析构函数 BOOL starthook(HWND hWnd); //安装钩子函数 BOOL stophook(); 卸载钩子函数 }; (4)在Mousehook.app文件的顶部加入#include"Mousehook.h"语句; (5)加入全局共享数据变量: #pragma data_seg("mydata") HWND glhPrevTarWnd=NULL; //上次鼠标所指的窗口句柄 HWND glhDisplayWnd=NULL; //显示目标窗口标题编辑框的句柄 HHOOK glhHook=NULL; //安装的鼠标钩子句柄 HINSTANCE glhInstance=NULL; //DLL实例句柄 #pragma data_seg() (6)在DEF文件中定义段属性: SECTIONS mydata READ WRITE SHARED (7)在主文件Mousehook.cpp的DllMain函数中加入保存DLL实例句柄的语句: DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { //如果使用lpReserved参数则删除下面这行 UNREFERENCED_PARAMETER(lpReserved); if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("MOUSEHOOK.DLL Initializing!\n"); //扩展DLL仅初始化一次 if (!AfxInitExtensionModule(MousehookDLL, hInstance)) return 0; new CDynLinkLibrary(MousehookDLL); //把DLL加入动态MFC类库中 glhInstance=hInstance; //插入保存DLL实例句柄 } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0("MOUSEHOOK.DLL Terminating!\n"); //终止这个链接库前调用它 AfxTermExtensionModule(MousehookDLL); } return 1; } (8)类Cmousehook的成员函数的具体实现: Cmousehook::Cmousehook() //类构造函数 { } Cmousehook::~Cmousehook() //类析构函数 { stophook(); } BOOL Cmousehook::starthook(HWND hWnd) //安装钩子并设定接收显示窗口句柄 { BOOL bResult=FALSE; glhHook=SetWindowsHookEx(WH_MOUSE,MouseProc,glhInstance,0); if(glhHook!=NULL) bResult=TRUE; glhDisplayWnd=hWnd; //设置显示目标窗口标题编辑框的句柄 return bResult; } BOOL Cmousehook::stophook() //卸载钩子 { BOOL bResult=FALSE; if(glhHook) { bResult= UnhookWindowsHookEx(glhHook); if(bResult) { glhPrevTarWnd=NULL; glhDisplayWnd=NULL;//清变量 glhHook=NULL; } } return bResult; } (9)钩子函数的实现: LRESULT WINAPI MouseProc(int nCode,WPARAM wparam,LPARAM lparam) { LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lparam; if (nCode>=0) { HWND glhTargetWnd=pMouseHook->hwnd; //取目标窗口句柄 HWND ParentWnd=glhTargetWnd; while (ParentWnd !=NULL) { glhTargetWnd=ParentWnd; ParentWnd=GetParent(glhTargetWnd); //取应用程序主窗口句柄 } if(glhTargetWnd!=glhPrevTarWnd) { char szCaption[100]; GetWindowText(glhTargetWnd,szCaption,100); //取目标窗口标题 if(IsWindow(glhDisplayWnd)) SendMessage(glhDisplayWnd,WM_SETTEXT,0,(LPARAM)(LPCTSTR)szCaption); glhPrevTarWnd=glhTargetWnd; //保存目标窗口 } } return CallNextHookEx(glhHook,nCode,wparam,lparam); //继续传递消息 } (10)编译项目生成mousehook.dll。 2.创建钩子可执行程序 (1)用MFC的AppWizard(EXE)创建项目Mouse; (2)选择“基于对话应用”并按下“完成”键; (3)编辑对话框,删除其中原有的两个按钮,加入静态文本框和编辑框,用鼠标右键点击静态文本框,在弹出的菜单中选择“属性”,设置其标题为“鼠标所在的窗口标题”; (4)在Mouse.h中加入对Mousehook.h的包含语句#Include"..\Mousehook\Mousehook.h"; (5)在CMouseDlg.h的CMouseDlg类定义中添加私有数据成员: CMouseHook m_hook;//加入钩子类作为数据成员 (6)修改CmouseDlg::OnInitDialog()函数: BOOL CMouseDlg::OnInitDialog() { CDialog::OnInitDialog(); ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX 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); } } SetIcon(m_hIcon, TRUE);//Set big icon SetIcon(m_hIcon, FALSE);//Set small icon //TOD Add extra initialization here CWnd * pwnd=GetDlgItem(IDC_EDIT1); //取得编辑框的类指针 m_hook.starthook(pwnd->GetSafeHwnd()); //取得编辑框的窗口句柄并安装钩子 return TRUE; //return TRUE unless you set the focus to a control } (7)链接DLL库,即把..\Mousehook\debug\Mousehook.lib加入到项目设置链接标签中; (8)编译项目生成可执行文件; (9)把Mousehook.DLL拷贝到..\mouse\debug目录中; (10)先运行几个可执行程序,然后运行Mouse.exe程序,把鼠标在不同窗口中移动,在Mouse.exe程序窗口中的编辑框内将显示出鼠标所在的应用程序主窗口的标题。 |
用C++控制DVD/CD驱动器的开关
在Windows资源浏览器中,可在DVD/CD光驱图标上单击鼠标右键,选择"弹出"来打开光驱仓门,你可能也发现了,菜单中并没有"关闭"命令来关闭光驱。下面,就让我们用程序来控制打开、关闭光驱。 程序的主要工作部分为CD_OpenClose(BOOL bOpen, TCHAR cDrive)函数: //cDrive是光驱盘符,或者0x01为默认驱动器。 //例如: //CD_OpenCloseDrive(TRUE, 'G'); //打开光驱G: //CD_OpenCloseDrive(FALSE, 'G'); //关闭光驱G: //CD_OpenCloseDrive(TRUE, 1); //打开第一个逻辑光驱 void CD_OpenCloseDrive(BOOL bOpenDrive, TCHAR cDrive) { MCI_OPEN_PARMS op; MCI_STATUS_PARMS st; DWORD flags; TCHAR szDriveName[4]; strcpy(szDriveName, "X:"); ::ZeroMemory(&op, sizeof(MCI_OPEN_PARMS)); op.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_CD_AUDIO; if(cDrive > 1) { szDriveName[0] = cDrive; op.lpstrElementName = szDriveName; flags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT| MCI_OPEN_SHAREABLE; } else flags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_SHAREABLE; if (!mciSendCommand(0,MCI_OPEN,flags,(unsigned long)&op)) { st.dwItem = MCI_STATUS_READY; if(bOpenDrive) mciSendCommand(op.wDeviceID,MCI_SET,MCI_SET_DOOR_OPEN,0); else mciSendCommand(op.wDeviceID,MCI_SET,MCI_SET_DOOR_CLOSED,0); mciSendCommand(op.wDeviceID,MCI_CLOSE,MCI_WAIT,0); } } 为了方便对多个光驱进行操作,添加了下面这个函数,其会调用上面的CD_OpenCloseDrive()函数: void CD_OpenCloseAllDrives(BOOL bOpenDrives) { //判定所有光驱,并逐个打开或关闭。 int nPos = 0; UINT nCount = 0; TCHAR szDrive[4]; strcpy(szDrive, "?:"); DWORD dwDriveList = ::GetLogicalDrives (); while (dwDriveList) { if (dwDriveList & 1) { szDrive[0] = 0x41 + nPos; if(::GetDriveType(szDrive) == DRIVE_CDROM) CD_OpenCloseDrive(bOpenDrives, szDrive[0]); } dwDriveList >>= 1; nPos++; } } 最后一点,别忘了在程序开头包括Mmsystem.h头文件,及在链接选项里写上Winmm.lib。 |
直方图变换图像处理技术
图像增强处理技术一直是图像处理领域一类非常重要的基本处理技术。通过采取适当的增强处理可以将原本模糊不清甚至根本无法分辨的原始图片处理成清楚、明晰的富含大量有用信息的可使用图像,因此此类图像处理技术在医学、遥感、微生物、刑侦以及军事等诸多领域得到广泛应用。本文从空间域的角度对图像的灰度直方图增强处理方法进行详细的介绍。 图像的灰度直方图处理技术 在空间域对图像进行增强处理的方式有许多种,如增强对比度和动态范围压缩等等,但这些处理方式都是针对原始图像的每一个像素直接对其灰度进行处理的,其处理过程主要是通过增强函数对像素的灰度级进行运算并将运算结果作为该像素的新灰度值来实现的。通过改变选用的增强函数的解析表达式就可以得到不同的处理效果,这类处理方法比较灵活方便,处理效果也不错,但对于某些灰度分布很密集或对比度很弱的图像,虽然也能起到一定的增强效果但并不明显。对于这种情况就可以采用本文提出的灰度直方图变换方法将原始图像密集的灰度分布变得比较疏散,从而拉大图像的对比度并在视觉上达到明显增强的效果,使一些原本不易观察到的细节能变得清晰可辨。 图像的灰度变换处理是通过改变原始图像各像素在各灰度级上的概率分布来实现的。通过对图像的灰度值进行统计可以得到一个一维离散的图像灰度统计直方图函数p(sk)=nk/n(这里k=0,1,2……L-1),该式表示在第k个灰度级上的像素个数nk占全部像素总数n的比例,p(sk)则给出了对sk出现概率的一个估计。因此该直方图函数实际是图像的各灰度级的分布情况的反映,换句话说也就是给出了该幅图像所有灰度值的整体描述。通过该函数可以清楚地了解到图像对应的动态范围情况,可以了解到图像灰度的主要集中范围。因此可以通过图像增强程序的干预来改变直方图的灰度分布状况,使灰度均匀地或是按预期目标分布于整个灰度范围空间,从而达到增强图像对比度的效果。这种方法是基于数理统计和概率论的,比直接在空间域对原始图像采取对比度增强效果要好得多。在实际应用中直方图的变换主要有均衡变换和规定变换两种,而后者又可根据灰度级映射规则的不同分单映射规则和组映射规则两种。 直方图均衡化处理 直方图均衡化处理的中心思想是把原始图像的灰度直方图从比较集中的某个灰度区间变成在全部灰度范围内的均匀分布。对图像空间域点的增强过程是通过增强函数t=EH(s)来完成的,t、s分别为目标图像和原始图像上的像素点(x,y),在进行均衡化处理时,增强函数EH需要满足两个条件:增强函数EH(s)在0≤s≤L-1的范围内是一个单调递增函数,这个条件保证了在增强处理时没有打乱原始图像的灰度排列次序; 另一个需要满足的条件是对于0≤s≤L-1应当有0≤EH(s)≤L-1,它保证了变换过程中灰度值的动态范围的一致性。同样的,对于反变换过程s=EH-1(t),在0≤t≤1时也必须满足上述两个条件。累计分布函数(cumulative distribution function,CDF)就是满足上述条件的一种,通过该函数可以完成s到t的均匀分布转换。此时的增强转换方程为: tk = EH(sk)=∑(ni/n)=∑ps(si),(k=0,1,2……L-1) 上述求和区间为0到k,根据该方程可以由原图像的各像素灰度值直接得到直方图均衡化后各像素的灰度值。在实际处理变换时,一般先对原始图像的灰度情况进行统计分析,并计算出原始直方图分布,然后根据计算出的累计直方图分布tk,按式tk=[(N-1)* tk+0.5]对其取整并得出源灰度sk到tk的灰度映射关系,其中N为灰度的级数。重复上述步骤,得到所有的源图像各灰度级到目标图像各灰度级的映射关系,再按照新的映射关系对源图像各点像素进行灰度转换,即可完成对源图的直方图均衡化。下面是按照上述算法实现的部分关键程序代码。 首先对原始图像的各像素点的灰度情况进行统计计算。对于24位BMP图像,图像阵列是从第54字节开始的,每像素按R、G、B的顺序占3个字节。 for (DWORD i=54; i ns_r[m_cpBuffer]++; //ns_r[k]为k灰度级像素数,m_cpBuffer为当前的灰度值 i++; ns_g[m_cpBuffer]++; //ns_g为G分量的统计计数 i++; ns_b[m_cpBuffer]++; //ns_b为B分量的统计计数 } for (i=0; i=0.0f) now_value=ps_r-pu[j]; else now_value=pu[j]-ps_r; if (now_value { m_r=j; min_value_r=now_value; } …… //对G和B分量处理的代码与R分量类似,在此省略 …… } //建立灰度映射关系 ns_r=nu[m_r]; ns_g=nu[m_g]; ns_b=nu[m_b]; } 在得到ps(si)到pu(uj)的映射关系后,按照该映射关系把原图的原始灰度值映射到经过均衡化的新灰度级上,以完成最后的处理。上图(图3)为实验得到的按单映射规则对直方图规定化后的效果,同直方图均衡化处理效果相比,可以看出高亮度部分得到了充分的增强。 组映射规则的直方图规定化处理 单映射规则虽然实现起来比较简单直观,但在实际处理时仍存在不可忽视的取整误差,因此在一定程度上还不能很好的实现规定直方图的意图。可以通过在规定化直方图时选取适当的对应规则来改善,一种比较好的对应规则是组映射规则(group mapping law,GML)。这种规则的约定如下: 存在一维离散整数函数I(a),(a=0,1,2……N-1),而且满足0≤I(0) ≤I(1) ≤……≤I(a) ≤……≤I(N-1) ≤M-1。寻找能使 |∑ps(si)-∑pu(uj)| 达到最小的I(a),其中ps(si)的求和区间为[0,I(a)],pu(uj)的求和区间仍为[0,a]。a=0时,将介于0和I(0)之间的ps(si)都映射到pu(u0)中;1≤a≤N-1时,将介于I(a-1)+1和I(a)之间的ps(si)都映射到pu(uj)中去。 由于同单映射规则相比只是对应规则做了变化,因此编码部分只需将对应规则部分的代码根据上面介绍的组映射规则做必要修改即可: for (i=0; i=0.0f) now_value=ps_r[j]-pu; else now_value=pu-ps_r[j]; if (now_value { A2_r=j; min_value_r=now_value; } for (int k=A1_r; k<=A2_r; k++) //建立R分量的映射规则 ns_r[k]=nu; A1_r=A2_r+1; //对于G、B分量的处理类似,在此省略 …… } 对原始图像应用本算法,实验得出的按组映射规则对原图做直方图规定化后的效果如图4所示。 该图同单映射规则处理图像相比虽无太大变化,但在直方图分布和图像细节上更能体现出规定直方图的意图,而且通过下面的分析也可以看出组映射规则的误差要小得多。 在ps(si)映射到pu(uj)时,采取SML规则的映射方法由于取整误差的影响可能产生的最大误差是pu(uj)/2,而采用GML规则的映射方法可能出现的误差为ps(si)/2,由于M≥N,所以一定有pu(uj)/2≥ps(si)/2成立,也就是说SML映射规则的期望误差一定不会小于GML映射规则的期望误差。而且从算法实现上也可以看出,SML映射规则是一种有偏的映射规则,某些范围的灰度级会被有偏地映射到接近开始计算的灰度级;而GML映射规则是统计无偏的,从根本上避免了上述问题的出现。通过分析可以看出GML映射规则总会比SML映射规则更能体现规定直方图的意图,而且通常产生的误差只有SML映射规则的十几分之一。 结论 本文从理论上讲述了直方图变换处理中常用的直方图均衡化、采取单映射和组映射规则的直方图规定化变换方法,通过程序算法实现了上述图像增强过程,并给出了通过三种算法实验得出的处理图像。实验表明,本文介绍的方法对于暗、弱信号的原始图像的目标识别和图像增强等有着良好的处理效果,尤其是通过组映射规则的直方图规定化变换方法结合设计良好的规定直方图,可以得到更佳的图像处理效果。本文给出的程序代码在Microsoft Visual C++ 6.0下编译通过 |
制作VC表格控件
我们在利用Visual C++开发基于数据库的软件时,经常要使用大量的表格,而Visual C++中并没有提供相应的表格控件,因此需要我们程序员自己制作,本文将介绍一个简单的表格控件的制作全过程。 其实,一张表格是由一系列的单元格排列在一个窗口中而构成的,所以,制作表格控件的最重要的一步是单元格类的设计。从最简单的角度出发,通过改造编辑框(Edit Box)来制作单元格类是最容易也是最适合的,所以范例程序的重点是,从CEdit类中派生一个CCell类并对这个类做适当的修改。在编写这个单元格类时,还要注意当用户操作单元格时,应当适当地改变单元格的外观,让用户感觉更直观。 我们利用AppWizard创建一个单文档程序项目,命名为CGridDemo。利用ClassWizard加入两个新类:基类为CEdit类的CCell类和基类为CWnd类的CGridWnd类。下面列出这两个类的主要代码: //下面是CCell类的主要代码 void CCell::OnSetfocus() //当用户操作单元格时,改变单元格的外观 { SetFocus=TRUE; //SetFocus为BOOL型的变量 CDC* pDC=this-〉GetWindowDC(); this-〉OnEraseBkgnd(pDC); //改变单元格外观 } BOOL CCell::OnEraseBkgnd(CDC* pDC) //改变单元格外观,重载OnEraseBkgnd函数 { RECT rect; CPen Pen; CBrush Brush; LOGBRUSH LogBrush; if(SetFocus==TRUE) //当用户操作单元格时,为单元格加上一个黑色边框 { Pen.CreatePen(PS_SOLID,2,RGB(0,0,0)); //设置线条宽度为两个像素 LogBrush.lbColor=RGB(0,0,0); LogBrush.lbStyle=BS_HOLLOW; Brush.CreateBrushIndirect(&&LogBrush); pDC-〉SelectObject(&&Pen); pDC-〉SelectObject(&&Brush); this-〉GetClientRect(&&rect); pDC-〉Rectangle(&&rect); //为单元格加黑色边框 } else //当用户操作另外的单元格,消除黑色边框 { Pen.CreatePen(PS_SOLID,2,RGB(255,255,255)); //创建白色的Pen以覆盖边框 LogBrush.lbColor=RGB(0,0,0); LogBrush.lbStyle=BS_HOLLOW; Brush.CreateBrushIndirect(&&LogBrush); pDC-〉SelectObject(&&Pen); pDC-〉SelectObject(&&Brush); this-〉GetClientRect(&&rect); pDC-〉Rectangle(&&rect); } return CEdit::OnEraseBkgnd(pDC); } void CCell::OnKillfocus() //用户焦点离开单元格,消除黑色边框 { SetFocus=FALSE; CDC* pDC=this-〉GetWindowDC(); this-〉OnEraseBkgnd(pDC); } //下面是CGridWnd类的主要代码 CGridWnd::CGridWnd(CWnd* pWnd,UINT nRow,UINT nCol) //CGridWnd类构造函数 { m_pParentWnd=pWnd; //父窗口句柄 Row=nRow;//行数 Col=nCol;//列数 } BOOL CGridWnd::Create() //重载Create函数,建立一个与父窗口同样大小的Grid窗口 { RECT rect; m_pParentWnd-〉GetClientRect(&&rect); return CWnd::Create(NULL, NULL, WS_CHILD|WS_VISIBLE, rect, m_pParentWnd, NULL,NULL); } BOOL CGridWnd::OnEraseBkgnd(CDC* pDC) //这个函数根据给出的行、列数画出网格 { for(int i=0;i〈Row+1;i++) //变量Row和Col是要创建的表格的行列数 { pDC-〉MoveTo(0,i*24); //网格大小为104×24 pDC-〉LineTo(Col*104,i*24); } for(int j=0;j〈Col+1;j++) { pDC-〉MoveTo(j*104,0); pDC-〉LineTo(j*104,Row*24); } return TRUE; } int CGridWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)//将单元格放到画好的网格中 { RECT rect; for(int i=0;i〈Row;i++) for(int j=0;j〈Col;j++) { Cell=new CCell(); //根据每行、列的网格数生成相应的单元格 rect.top=i*24+2; //设定每个单元格大小为100×20,并确定单元格所在位置 rect.left=j*104+2; rect.bottom=rect.top+20; rect.right=rect.left+100; Cell-〉Create(WS_CHILD|WS_VISIBLE,rect,this,0); //在网格中放入单元格 } return 0; } //下面的代码演示使用表格窗口的方法 void CGridDemoView::OnDraw(CDC* pDC) { CGridDemoDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!Flags) { Grid=new CGridWnd(this,5,5); //建立一个5行5列的表格 Grid-〉Create(); Flags=TRUE; } } 编译这个程序,便可以得到一个使用表格控件的演示程序。 |
VC++环境下浮动工具条的编程
许多程序员都希望自己的程序能更象商业化程序,特别是希望自己的程序工具 条能够象WINDOWS95下的OFFICES一样具有浮动效果。针对VISUALC++编程环境, 以下分别介绍了两种产生浮动工具条的方法。各有其优点和不足,可供程序员选择。 第一种方法 该方法最为简单,利用VC中的工具条的系统未公布的隐含参数 TBSTYLE_FLAT,不需更改原由程序只要在程序中添加4条语句即可实现浮动效果。 不过该方法只能产生简单的浮动工具条,而且没有象OFFICES中工具条右侧的拖 动条gripper。而且该方法在VC50下存在严重的缺陷,当拖动工具条时,系统不 能对工具条进行刷新,因此会产生移动工具条到新位置时,原处仍有工具条。主 要是负责工具条的动态连接库COMCTL32.DLL的BUG。该问题在VC60中由于更新了 该动态连接库而得以解决。程序员如果使用的是VC5版可以用新的版本COMCTL32.DLL (4.72版以上)替换该动态连接库来解决。 具体方法如下: int CMainFrame::OnCreate (LPCREATESTRUCT lpCreateStruct) { ... m_wndToolBar.SetBarStyle (m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC|TBSTYLE_FLAT); //添加以下4句语句,产生浮动效果 LONG lStyle; lStyle=GetWindowLong (m_wndToolBar,GWL_STYLE); lStyle=lStyle|TBSTYLE_FLAT; SetWindowLong(m_wndToolBar, GWL_STYLE,lStyle); m_wndToolBar.EnableDocking (CBRS_ALIGN_ANY); ... } 第二种方法 该方法是对MFC原来的CTOOLBAR类进行继承,主要是重载成员函数 OnWindowPosChanging(LPWINDOWPOS lpWndPos);OnPaint();OnNcPaint();OnNcCalcSize(); 实现浮动工具条。使用本类,只要将以下的程序代码FlatToolBar.h和FlatToolBar.Cpp 加入项目,并用INCLUDE将FlatToolBar.H包含到绘制主窗口的CMainFrame中,然后把你 的原先定义工具条变量的CToolBar类变成 CFlatToolBar类,并在建立工具条后调用 SetFlatLookStyle()函数设置浮动方式。为方便以后其它使用程序,可以在该类 上右击鼠标,选择Add to Gallery将CFlatToolBar类存入类库以便下次使用。 |
Visual C++中OpenGL编程入门
OpenGL作图非常方便,故日益流行,但对许多人来说,是在微机上进行的,首先碰到的问题是,如何适应微机环境。这往往是最关键的一步,虽然也是最初级的。一般的,我不建议使用glut 包.那样难以充分发挥 windows 的界面上的功能. 下面介绍如何在 VC++ 上进行 OpenGL 编程。 OpenGL 绘图的一般过程可以看作这样的,先用 OpenGL 语句在 OpenGL 的绘图环境 RenderContext (RC)中画好图, 然后再通过一个 Swap buffer 的过程把图传给操作系统的绘图环境 DeviceContext (DC)中,实实在在地画出到屏幕上. 下面以画一条 Bezier 曲线为例,详细介绍VC++ 上 OpenGL编程的方法。文中给出了详细注释,以便给初学者明确的指引。一步一步地按所述去做,你将顺利地画出第一个 OpenGL 平台上的图形来。 一、产生程序框架 Test.dsw New Project | MFC Application Wizard (EXE) | "Test" | OK *注* : 加“”者指要手工敲入的字串 二、导入 Bezier 曲线类的文件 用下面方法产生 BezierCurve.h BezierCurve.cpp 两个文件: WorkSpace | ClassView | Test Classes| <右击弹出> New Class | Generic Class(不用MFC类) | "CBezierCurve" | OK 三、编辑好 Bezier 曲线类的定义与实现 写好下面两个文件: BezierCurve.h BezierCurve.cpp 四、设置编译环境: 1. 在 BezierCurve.h 和 TestView.h 内各加上: #include <GL/gl.h> #include <GL/glu.h> #include <GL/glaux.h> 2. 在集成环境中 Project | Settings | Link | Object/library module | "opengl32.lib glu32.lib glaux.lib" | OK 五、设置 OpenGL 工作环境:(下面各个操作,均针对 TestView.cpp ) 1. 处理 PreCreateWindow(): 设置 OpenGL 绘图窗口的风格 cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | CS_OWNDC; 2. 处理 OnCreate():创建 OpenGL 的绘图设备。 OpenGL 绘图的机制是: 先用 OpenGL 的绘图上下文 Rendering Context (简称为 RC )把图画好,再把所绘结果通过 SwapBuffer() 函数传给 Window 的 绘图上下文 Device Context (简记为 DC).要注意的是,程序运行过程中,可以有多个 DC,但只能有一个 RC。因此当一个 DC 画完图后,要立即释放 RC,以便其它的 DC 也使用。在后面的代码中,将有详细注释。 int CTestView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; myInitOpenGL(); return 0; } void CTestView::myInitOpenGL() { m_pDC = new CClientDC(this); //创建 DC ASSERT(m_pDC != NULL); if (!mySetupPixelFormat()) //设定绘图的位图格式,函数下面列出 return; m_hRC = wglCreateContext(m_pDC->m_hDC);//创建 RC wglMakeCurrent(m_pDC->m_hDC, m_hRC); //RC 与当前 DC 相关联 } //CClient * m_pDC; HGLRC m_hRC; 是 CTestView 的成员变量 BOOL CTestView::mySetupPixelFormat() {//我们暂时不管格式的具体内容是什么,以后熟悉了再改变格式 static PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR), // size of this pfd 1, // version number PFD_DRAW_TO_WINDOW | // support window PFD_SUPPORT_OPENGL | // support OpenGL PFD_DOUBLEBUFFER, // double buffered PFD_TYPE_RGBA, // RGBA type 24, // 24-bit color depth 0, 0, 0, 0, 0, 0, // color bits ignored 0, // no alpha buffer 0, // shift bit ignored 0, // no accumulation buffer 0, 0, 0, 0, // accum bits ignored 32, // 32-bit z-buffer 0, // no stencil buffer 0, // no auxiliary buffer PFD_MAIN_PLANE, // main layer 0, // reserved 0, 0, 0 // layer masks ignored }; int pixelformat; if ( (pixelformat = ChoosePixelFormat(m_pDC->m_hDC, &pfd)) == 0 ) { MessageBox("ChoosePixelFormat failed"); return FALSE; } if (SetPixelFormat(m_pDC->m_hDC, pixelformat, &pfd) == FALSE) { MessageBox("SetPixelFormat failed"); return FALSE; } return TRUE; } 3. 处理 OnDestroy() void CTestView::OnDestroy() { wglMakeCurrent(m_pDC->m_hDC,NULL); //释放与m_hDC 对应的 RC wglDeleteContext(m_hRC); //删除 RC if (m_pDC) delete m_pDC; //删除当前 View 拥有的 DC CView::OnDestroy(); } 4. 处理 OnEraseBkgnd() BOOL CTestView::OnEraseBkgnd(CDC* pDC) { // TODO: Add your message handler code here and/or call default // return CView::OnEraseBkgnd(pDC); //把这句话注释掉,若不然,Window //会用白色北景来刷新,导致画面闪烁 return TRUE;//只要空返回即可。 } 5. 处理 OnDraw() void CTestView::OnDraw(CDC* pDC) { wglMakeCurrent(m_pDC->m_hDC,m_hRC);//使 RC 与当前 DC 相关联 myDrawScene( ); //具体的绘图函数,在 RC 中绘制 SwapBuffers(m_pDC->m_hDC);//把 RC 中所绘传到当前的 DC 上,从而 //在屏幕上显示 wglMakeCurrent(m_pDC->m_hDC,NULL);//释放 RC,以便其它 DC 进行绘图 } void CTestView::myDrawScene( ) { glClearColor(0.0f,0.0f,0.0f,1.0f);//设置背景颜色为黑色 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glPushMatrix(); glTranslated(0.0f,0.0f,-3.0f);//把物体沿(0,0,-1)方向平移 //以便投影时可见。因为缺省的视点在(0,0,0),只有移开 //物体才能可见。 //本例是为了演示平面 Bezier 曲线的,只要作一个旋转 //变换,可更清楚的看到其 3D 效果。 //下面画一条 Bezier 曲线 bezier_curve.myPolygon();//画Bezier曲线的控制多边形 bezier_curve.myDraw(); //CBezierCurve bezier_curve //是 CTestView 的成员变量 //具体的函数见附录 glPopMatrix(); glFlush(); //结束 RC 绘图 return; } 6. 处理 OnSize() void CTestView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); VERIFY(wglMakeCurrent(m_pDC->m_hDC,m_hRC));//确认RC与当前DC关联 w=cx; h=cy; VERIFY(wglMakeCurrent(NULL,NULL));//确认DC释放RC } 7 处理 OnLButtonDown() void CTestView::OnLButtonDown(UINT nFlags, CPoint point) { CView::OnLButtonDown(nFlags, point); if(bezier_curve.m_N>MAX-1) { MessageBox("顶点个数超过了最大数MAX=50"); return; } //以下为坐标变换作准备 GetClientRect(&m_ClientRect);//获取视口区域大小 w=m_ClientRect.right-m_ClientRect.left;//视口宽度 w h=m_ClientRect.bottom-m_ClientRect.top;//视口高度 h //w,h 是CTestView的成员变量 centerx=(m_ClientRect.left+m_ClientRect.right)/2;//中心位置, centery=(m_ClientRect.top+m_ClientRect.bottom)/2;//取之作原点 //centerx,centery 是 CTestView 的成员变量 GLdouble tmpx,tmpy; tmpx=scrx2glx(point.x);//屏幕上点坐标转化为OpenGL画图的规范坐标 tmpy=scry2gly(point.y); bezier_curve.m_Vertex[bezier_curve.m_N].x=tmpx;//加一个顶点 bezier_curve.m_Vertex[bezier_curve.m_N].y=tmpy; bezier_curve.m_N++;//顶点数加一 InvalidateRect(NULL,TRUE);//发送刷新重绘消息 } double CTestView::scrx2glx(int scrx) { return (double)(scrx-centerx)/double(h); } double CTestView::scry2gly(int scry) { } 附录: 1.CBezierCurve 的声明: (BezierCurve.h) class CBezierCurve { public: myPOINT2D m_Vertex[MAX];//控制顶点,以数组存储 //myPOINT2D 是一个存二维点的结构 //成员为Gldouble x,y int m_N; //控制顶点的个数 public: CBezierCurve(); virtual ~CBezierCurve(); void bezier_generation(myPOINT2D P[MAX],int level); //算法的具体实现 void myDraw();//画曲线函数 void myPolygon(); //画控制多边形 }; 2. CBezierCurve 的实现: (BezierCurve.cpp) CBezierCurve::CBezierCurve() { m_N=4; m_Vertex[0].x=-0.5f; m_Vertex[0].y=-0.5f; m_Vertex[1].x=-0.5f; m_Vertex[1].y=0.5f; m_Vertex[2].x=0.5f; m_Vertex[2].y=0.5f; m_Vertex[3].x=0.5f; m_Vertex[3].y=-0.5f; } CBezierCurve::~CBezierCurve() { } void CBezierCurve::myDraw() { bezier_generation(m_Vertex,LEVEL); } void CBezierCurve::bezier_generation(myPOINT2D P[MAX], int level) { //算法的具体描述,请参考相关书本 int i,j; level--; if(level<0)return; if(level==0) { glColor3f(1.0f,1.0f,1.0f); glBegin(GL_LINES); //画出线段 glVertex2d(P[0].x,P[0].y); glVertex2d(P[m_N-1].x,P[m_N-1].y); glEnd();//结束画线段 return; //递归到了最底层,跳出递归 } myPOINT2D Q[MAX],R[MAX]; for(i=0;i { Q.x=P.x; Q.y=P.y; } for(i=1;i<m_N;i++) { R[m_N-i].x=Q[m_N-1].x; R[m_N-i].y=Q[m_N-1].y; for(j=m_N-1;j>=i;j--) { Q[j].x=(Q[j-1].x+Q[j].x)/double(2); Q[j].y=(Q[j-1].y+Q[j].y)/double(2); } } R[0].x=Q[m_N-1].x; R[0].y=Q[m_N-1].y; bezier_generation(Q,level); bezier_generation(R,level); } void CBezierCurve::myPolygon() { glBegin(GL_LINE_STRIP); //画出连线段 glColor3f(0.2f,0.4f,0.4f); for(int i=0;i<m_N;i++) { glVertex2d(m_Vertex.x,m_Vertex.y); } glEnd();//结束画连线段 } |
贝赛尔曲线的拆分算法
贝赛尔曲线的拆分是指将贝赛尔曲线分解成逼近的多边形。可以用来判断贝赛尔曲线的选中,以及显示贝赛尔曲线的旋转效果等。 贝赛尔曲线简单介绍: 贝赛尔曲线的每一个顶点都有两个控制点,用于控制在该顶点两侧的曲线的弧度。所以本函数的顶点数组的记录方式是:控制点+顶点+控制点+控制点+顶点+控制点+……。所以两个顶点之间的曲线是由两个顶点以及两个顶点之间的控制点来决定的。 ==主函数PolyBezierToPolys== 【主要类型申明】 typedef CArray CPtArray;//点动态数组类型 【参数说明】 bezierPts[in]---贝赛尔曲线顶点和控制点数组 bClose[in]------是否封闭的贝赛尔曲线 polyPt[out]-----拆分后的多边形点数组 precision[in]---拆分精度 bool PolyBezierToPolys(CPtArray &bezierPts, bool bClose,CPtArray &polyPt,int precision) { polyPt.RemoveAll(); CPtArray apt; int i,count = bezierPts.GetSize(); //从1开始,是因为第一个是控制点,如果曲线不封闭,那么第一个控制点是没有用的。 //每一段贝赛尔曲线由相邻的两个顶点和之间的两个控制点决定,所以频率为3(后一个顶点在下一组中还要使用) for(i=1;iprecision){ if(!EndBezierCut(&polyPt, precision)){ bExit = false; InciseBezier(&polyPt, ptBuffer); polyPt.RemoveAt(i+1,2); polyPt.InsertAt(i+1,ptBuffer[1],5); for(j=0;jgap) gap=abs(p.x-p[i-1].x); if(abs(p.y-p[i-1].y)>gap) gap=abs(p.y-p[i-1].y); } return gap; } //判断是否可以终止更精细得拆分 bool EndBezierCut(CPoint *ptBezier, int nExtent) { double C,dx,dy,delt,delt1,delt2; if (nExtent delt || delt2 > delt) return FALSE; else return TRUE; }
|