另外,在实际的VC++教学中,发现很少有真正精通了C语言编程的学员,一般都有或多或少概念不是完全清楚的问题,特别是一些需要丰富的实战经验才能体会和明白的问题,如字符串,指针,类型转换,定义指向函数的指针类型,这也是导致学习VC++困难的一个原因。下面有几个简单测试将能发现你对C语言的掌握情况。
int x=35;
char str[10];
//问:strlen(str)和sizeof(str)的值分别是多少?
strcpy(str,"www.it315.org"/*共13个字母*/);
//问:此时x和strlen(str)的值分别是多少?
str="it315.org";//编译能通过吗?
char *pstr;
strcpy(pstr,"http://www.it315.org");
//上句编译能通过吗?运行时有问题吗?
const char *p1;
char * const p2;
//上面两句有什么区别吗?
p1=(const char *)str;
//如果是p1=str;编译能够通过吗?明白为什么要类型转换?类型转换的本质是什么?
strcpy(p1,"abc");//编译能够通过吗?
printf("%d",str);//有问题吗?
pstr=3000;//编译能过吗?如果不行,该如何修改以保证编译通过呢?
long y=(long)pstr;//可以这样做吗?
int *p=str;
*p=0x00313200;
printf("%s",str);//会是什么效果?提示0x31对应字符'1',0x32对应字符'2'。
p=3000;//p+1的结果会是多少?
char *pc=new char[100];//上述语句在内存中占据几个内存块,怎样的布局情况?
void test(char **p)
{
*p=new char[100];
}//这个编译函数有问题吗?外面要调用这个函数,该怎样传递参数?
//能明白typedef int(*PFUN)(int x,int y)及其作用吗?
对于许多类似的问题一般从书本上是看不到的,不通过大量的实践与调试是难以理解和令人困惑的,所以在本书中对于类似上述的C语言问题都将作出详细的解释和讲解。其实,本来也只有在实践中才能掌握和精通C语言的,学习VC++是可以反过来帮助提高C语言编程水平的。
这一课还没写完,完整版会在不久的将来与大家见面的。
我接触的大多数自学过VC++程序开发的人们都有一个共同的感慨,VC++入门太难了,没有一到两年的工夫,是学不会VC++的。但是,我们的教学实际证明,绝大多数有C语言基础和英语过四级的人们,都可以在半月内学好VC++的,并迅速进行运用的。
这是什么原因造成如此巨大的反差呢?原因如下:市面上很难找到一本结合初学者的实际学习疑惑,采用循序渐进的方式,将VC++编程的各种技术透彻地展现到读者面前的。一般的书籍都是简单地教导你怎样使用VC++的操作界面,怎样一步步地编写一个特定功能的小程序,没有分析为什么要这样做,也没总结这样做是一个什么样的机理,有哪些注意事项,能够怎样进行举一反三。这样的结果是导致初学者照着书能够编写该程序,但一撇开书本,就什么都不知道了。
再者就是书的章节安排不合理,经常是本末倒置,对于一个小学没上小学的学生,上来就教其学习大学的课程,只会导致初学者死记硬背,满脑子的问号,弄得一头雾水。
我们结合实际开发中总结出来的经验与心得,通过在长期的教学中收集到的学员的问题的总结,分析,以及最终如何找到有效的方式来说明讲清该问题。编写了本书。
一本总结了多年编程与教学经验的书。
使用简单的例子的好处是比较容易理解,将讨论的重点放在某一专题上,而无须读者费大力去搞清楚复杂例子中那些与讨论的重点不相关的细节与复杂性。
第一课 Windows程序内部运行原理及SDK编程实现
为了理解Visual C++应用程序开发过程,先要理解Windows程序的运行机制。因为 Visual C++是 Windows 开发语言,需要明白在 Windows 环境下编程和在其它环境下编程的一些根本性的差别。
Windows 的工作方式:
全面地讨论 Windows 的内部工作机制将需要整整一本书的容量,没有必要深入了解所有的技术细节。但是对于windows程序运行的一些根本性的概念,是一个Visual C++程序员所必须掌握的知识。
一、Windows应用程序,操作系统,计算机硬件之间的相互关系
我们这样解释上面的图例,向下的箭头1表示操作系统能够操纵输出设备,以执行特定的功能,如让声卡发出声音,让显卡画出图形。向上的箭头2表示操作系统能够感知输入设备状态的变化,如鼠标移动,键盘按下,并且能够知道鼠标移动的具体位置,键盘按下的哪个字符。这就是操作系统个计算机硬件之间的交互关系,应用程序开发者通常不需知道其具体实现细节。
1、关于API
向下的箭头3表示应用程序可以通知操作系统执行某个具体的动作,如操作系统能够控制声卡发出声音,但其并不知道何时发出何种声音,得由应用程序告诉操作系统该发出什么样的声音。这个关系好比有个机器人能够完成行走的功能,如果人们不告诉它往哪个方向上走,机器人是不会主动行走。这里的机器人就是操作系统,人们就是应用程序。应用程序是如何通知操作系统执行某个功能的呢?有编程知识的读者都应该知道,在应用程序中要完成某个功能,都是以函数调用的形式实现的,同样,应用程序也是以函数调用的方式来通知操作系统执行相应功能的,操作系统所能够完成的每一个特殊功能通常都有一个函数与其对应,也就是说,操作系统把它所能完成的功能以函数的形式提供给应用程序使用,应用程序对这些函数的调用叫系统调用,这些函数的集合是Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称Windows API。如CreateWindow就是一个API函数,应用程序中调用这个函数,操作系统就会按照该函数提供的参数信息产生一个相应的窗口。大家不妨看看lesson1中的源程序,体会一下在程序中是如何调用这个CreateWindow API函数的,关于这个函数的详细解释,请参阅MSDN(微软开发编程的开发系统)。
顺便提一下,对于一个真正的程序员来说,不可能死记硬背每一个API函数及其各参数的详细信息。通常都是只记住其英文拼写,有时甚至是凭着语意拼读出来的,如显示窗口用ShowWindow,退出Windows操作系统用ExitWindows等等,API函数的正确拼写格式及各参数的祥尽信息都是在MSDN迅速检索到的,没必要刻意去死记这些信息,等用的次数多了,这些信息也就在不知不觉中掌握了,但一定要具备在需要的时候能够从帮助系统中检索想要的信息的能力,这样就能做到事半功倍。学习VC++,一定要有一套真实的练习环境,学会查阅帮助系统,决不能纸上谈兵,照着书本亦步亦趋,否则就真的是没有一两年的时间,是学不好VC++的了。
注意:请不要将这里的API与java API以及其他API混淆。API正如其语义一样,已成为一种被广泛使用的专业术语。如果某个系统或某个设备提供给某种应用程序对其进行编程操作的函数,类,组件等的集合,就称作该系统的API。曾经有学员问我这样的问题,JavaAPI与windows API有何关系,是不是指java也可以调用windows里的API?读者现在应该明白这个问题了,不需我来回答了吧?
2、关于消息
向上的箭头4表示操作系统能够将输入设备的变化上传给应用程序。如用户在某个程序活动时按了一下键盘,操作系统马上能够感知到这一事件,并且能够知道用户按下的是哪一个键,操作系统并不决定对这一事件如何作出反应,而是将这一事件转交给应用程序,由应用程序决定如何对这一事件作出反应。好比有个蚊子叮了我们一口,我们的神经末梢(相当于操作系统)马上感知到这个事件,并传递给了我们的大脑(相当于应用程序),我们的大脑最终决定如何对这一事件作出反应,如将蚊子赶走,或是将蚊子拍死。对事件作出反应的过程就是消息响应,由水平箭头5表示。
操作系统是怎样将感知到的事件传递给应用程序的呢?这是通过消息机制来实现的。操作系统将每个事件都包装成一个称为消息的结构体MSG来传递给应用程序的,参看MSDN,MSG结构定义如下:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
看不懂这种定义的读者,请赶快复习C语言,其基本意义是定义一个structtagMSG的结构体,并在以后的应用中用MSG来代替struct tagMsg。该结构体中各成员变量的作用如下:
第一个成员变量hwnd即代表消息所属的窗口,一个消息一般都是与某个窗口相联系的,如在某个活动窗口中按下键盘,该键盘消息就是发给该窗口的,在VC中,用HWND变量类型来标识窗口。有关窗口的知识,在稍后有详细解释。
第二个成员变量message代表消息代号,无论是键盘按下,还是鼠标移动,都是用一个数字来表示的,不同的数值对应不同的消息。由于数值不便于记忆,在VC中将消息对应的数值定义为WM_xxx宏的形式,xxx对应某种消息的英文拼写的大写,如鼠标移动消息为WM_MOUSEMOVE,键盘按下消息为WM_KEYDOWN,输入一个字符消息为WM_CHAR等等。我们在程序中一般以WM_xxx宏的形式来使用消息。
提示:如果想知道WM_xxx消息对应的具体数值,请在程序中选中WM_xxx,单击右键,在弹出菜单中选择goto definition即可看到该宏的具体定义。跟踪,查看某个变量的定义,使用此方法非常有效。
第三个,四个成员变量分别为wParam,lParam,用于对消息进行补充说明,如message成员表示字符消息,但没有说明输入的是哪个字符,这就需要用其他变量对其进行补充说明。wParam,lParam代表的意义,随消息的不同而异。读者可用goto definition功能查看WPARAM,LPARAM的定义,发现它们分别为unsigned int和long,并不是什么神秘莫测的变量类型。VC++中之所以要这样做,是希望从变量定义的类型上,就能区分出变量的用途。对于同一种变量类型,可按其用途细分定义成多种其他的形式。这种概念在VC++中被广泛使用,也是导致初学者困惑的一个因素。
最后两个变量分别代表发出消息的时间和鼠标的当前位置,这里没有什么需要特殊解释的。
明白了消息,我们再来看看消息队列。如上面的图例所示,每个Windows程序都有一个消息队列。队列是一个先进先出的缓冲区,通常是一个某种变量类型的数组。消息队列里的每一个元素即一条消息,操作系统将生成的每个消息按先后顺序放进消息队列,第一条消息放入第一格,第二条消息放入第二格,依次类推...。应用程序总是取走队列里的第一条消息,消息取走后,第二条消息成为第一条,剩余的消息依次前移。应用程序取得消息后,便能够知道用户的操作和程序状态的变化。例如,应用程序从队列里取到了一条WM_CHAR消息,那一定是用户输入了一个字符,并且能够知道输入的是哪个字符。应用程序得到消息后,就要对消息进行处理,这即我们通常说的消息响应,消息响应是我们通过编码实现的,这也是Windows程序的主要代码区。在消息响应代码中,我们很可能又要调用操作系统提供的API函数,以便完成特定的功能。如果我们收到窗口的WM_CLOSE消息,我们可以调用DestroyWindow这个API函数来关闭该窗口,或是用MessageBox这个API函数来提示用户是否真的要关闭窗口。
通过上面的分析,我们可以想象到,要用VC++编写Windows程序,除了要具备良好的C语言功底外,还要求掌握掌握两点知识:1.不同的消息所代表的用户操作和程序状态,
2.要让操作系统执行某个功能所对应的API函数。
3.关于句柄
在Windows编程中我们时刻接触到一个称为句柄(HANDLE)的东西。可以这样去理解句柄,Windows程序中产生的任何资源(要占用某一块或大或小的内存),如图标,光标,窗口,应用程序的实例(已加载到内存运行中的程序)。操作系统每产生一个这样的资源时,都要将它们放入相应的内存,并为这些内存指定一个唯一的标识号,这个标识号即该资源的句柄。操作系统要管理和操作这些资源,都是通过句柄来找到对应的资源的。按资源的类型,又可将句柄细分成图标句柄(HICON),光标句柄(HCURSOR),窗口句柄(HWND),应用程序实例句柄(HINSTANCE),等等各种类型的句柄。操作系统给每一个窗口指定的一个唯一的标识号即窗口句柄。
4.WinMain函数
WinMain是Windows程序的入口点函数,同dos程序的入口点函数main的作用相同,当WinMain函数结束或返回时,Windows应用程序结束。WinMain函数的原型如下:
int WINAPI WinMain(
HINSTANCE hInstance, // handle tocurrent instance
HINSTANCE hPrevInstance, //handle to previous instance
LPSTR lpCmdLine, // pointerto command line
intnCmdShow // show state of window
);
该函数接受四个参数,这些参数都是系统调用WinMain函数时,传递给应用程序的。
第一个参数hInstance表示该程序的当前运行的实例句柄。同一应用程序在同一计算机上可运行多份实例,每启动一个这样的实例,操作系统都要给该实例分配一个标识号,即实例句柄,随后系统调用程序中的WinMain函数,并将该实例句柄传递给参数hInstance。
第二个参数hPrevInstance表示当前实例的上一个正在运行的,由同一个应用程序所产生的实例的句柄,即当前实例的"哥哥"的句柄。如果该值为NULL,则表示当前实例是该程序正在运行的第一份实例,是“长子”,是“老大”。如果该值不为NULL,只能表示当前实例不是该程序正在运行的第一份实例,不是“长子”,不是“老大”,但到底是“老几”,就无从得知了。这个参数到底有什么作用呢?如果想让我们的程序只能有一份实例运行,不能同时有多份实例运行,我们可以在WinMain函数的开始部分加上如下代码实现。
if(hPrevInstance) return 0;
大多数没有实际开发经验的读者都很难理解这段简单的代码。大家平时见到的if语句通常是if(x!=0)或if(x==0),其实if判断的是括号中的表达式的结果为“真”,还是为“假”。在C语言中,结果为0即“假”,非0即“真”。所以,我们也可以直接用if对某个变量的值进行判断,if(变量)代表“如果变量不等于0”,if(!变量)代表“如果变量等于0”。
我们再来看看if(hPrevInstance)return 0;的作用,如果hPrevInstance为NULL(即0),说明当前运行的实例是程序的第一个实例,WinMain函数将不返回,程序正常向下运行。只要hPrevInstance不为NULL,说明已经有同样程序的实例在运行,WinMain函数将返回,当前实例启动后立马结束,这样就保证了只有程序的一个实例可以运行。这个过程好比“计划生育”,只能要一个孩子,如果第二个孩子已经出世,当他发现自己不是老大,属于计划之外,便进行“自杀”,在程序中对应的是return 0语句。顺便说一下:“计划生育”的比喻只是为了方便大家理解问题,并不是我们的现实生活中真的要如此做法。
第三个参数lpCmdLine是一个字符串,里面包含有传递给应用程序的参数串,如:双击C盘下的1.txt文件方式启动notepad.exe程序,传递给notepad.exe程序的参数串即"c:\1.txt",不包含应用程序名本身。要在VC开发环境中给应用程序传递参数,请选择菜单Project->Settings...,在弹出的Project Settings对话框中选择Debug标签,在该标签页的Program arguments编辑框中输入你想传递给应用程序的参数。我们在WinMain函数的入口点设置一运行断点,以调试方式启动程序运行至该断点处,将鼠标移动到参数lpCmdLine上,在弹出的黄色小浮框中便能观察到该变量的值。在我们的程序调试中,经常要用到这种方法查看变量的值和状态。
第四个参数nCmdShow指定的程序的窗口应该如何显示,如最大化,最小化,隐藏等。
WinMain函数前的修饰符WINAPI的解释,请参看下面关于__stdcall的讲解,我们使用goto definition功能,发现WINAPI其实就是__stdcall。
Winmain函数的程序代码按功能划分主要有两部分:1.产生并显示程序的主窗口。窗口创建并显示后,用户便可以在窗口上进行各种操作了,用户的操作及程序状态的变化都以消息的形式放到了应用程序的消息队列中。2.从消息队列循环取走消息,并将消息派发到窗口过程函数中去处理。当消息循环取到一条WM_QUIT消息时,将结束循环,WinMain函数返回,结束整个程序的运行。
如果WinMain在消息循环之前返回,程序没有正常运行,返回值为0。如果在消息循环之后返回,返回值为WM_QIUT消息的wParam参数。
5.窗口及生成
不妨简单地将窗口看做带有边界的矩形区域。除文字处理程序中的文档窗口或者弹出提示有约会信息的对话框等这些最普通的窗口外,实际上还有许多其它类型的窗口。命令按钮、文本框、选项按钮都是窗口。
一个通常的Windows程序都有窗口,通过窗口,用户可以对应用程序进行各种操作。反之,应用程序可以通过窗口收集用户的操作信息,如在窗口上移动鼠标,按下键盘。可以说,窗口是应用程序和用户之间交互的界面,沟通的桥梁,联系的纽带。所以窗口的编写与管理在Windows程序中占有重要的地位。
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状),图标,菜单,背景色等。产生窗口的过程类似汽车的生产过程,在生产汽车前,必须先在图纸设计好该车型(选择搭配汽车的各个部件),并要为这种新设计好的车型起个名称,如“奔驰200”。以后,便可以生产“奔驰200”这款汽车了,可以按照这个型号生产若干辆汽车,同一型号的车,可以具有不同的颜色。
产生一个窗口前,也必须设计好窗口(指定窗口的那些特征)。窗口的特性是由一个WNDCLASS结构体进行定义的。参看MSDN,WNDCLASS定义如下:
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
style成员指定了这一类型窗口的样式。比较典型的取值有:
CS_NOCLOSE,这一类型的窗口没有关闭按钮,请实验体会。
CS_VREDRAW,当改变窗口的垂直方向上的高度时,将引发窗口重画。窗口的重画过程好比汽车重新喷漆一样,汽车车身上原有的文字与图案,如"http://www.it315.org"的字样将被擦除。同样,当窗口重画时,窗口上原有的文字和图形将被擦除。如果没有指定该值,当垂直方向上拉动窗口时,窗口不会重画,窗口上原有的文字和图形将被保留。
CS_HREDRAW,当改变窗口的水平方向上的宽度时,将引发窗口重画。
CS_DBLCLKS,设置该,可以接受到用户双击的消息。
其他的设置值请参阅MSDN,在一些特殊的场合可能要用到这些置。
提示:
在我们的程序中经常要用到一类变量,这个变量里的每一位(bit)都对应某一种特性。当该变量的某位为1时,表示有该位对应的那种特性,当该位为0时,即没有那位所对应的特性。当变量中的某几位同时为1时,就表示同时具有那几种特性的组合。一个变量中的哪一位代表哪种意义,不容易记忆,所以我们经常根据特征的英文拼写的大写去定义一些宏,该宏所对应的数值中仅有与该特征相对应的那一位(bit)为1,其余的bit都为0。我们再次使用goto definition就能发现CS_VREDRAW=0x0001,CS_HREDRAW=0x0002, CS_DBLCLKS=0x0008,CS_NOCLOSE=0x0200。他们的共同点就是只有一位为1,其余位都为0。如果我们希望某一变量的数值即有CS_VREDRAW特性,又有CS_HREDRAW特性,我们只需使用(|)操作符将他们相组合,如style=CS_VREDRAW|CS_HREDRAW | CS_NOCLOSE。如果我们希望在某一变量原有的几个特征上去掉其中一个特征,用(&~)就能够实现,如在刚才的style的基础上去掉CS_NOCLOSE特征,可以用style & ~CS_NOCLOSE实现。
lpfnWndProc成员指定了这一类型窗口的过程函数,也称回调函数。回调函数的原理是这样的,当应用程序收到给某一窗口的消息时(还记得前面讲过的消息通常与窗口相关的吗?),就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己来实施,由操作系统完成,但回调函数本身的代码由应用程序完成。对一条消息,操作系统到底调用应用程序中的哪个函数(回调函数)来处理呢?操作系统调用的就是接受消息的窗口所属的类型中的lpfnWndProc成员指定的函数。每一种不同类型的窗口都有自己专用的回调函数,该函数就是通过pfnWndProc成员指定的。汽车厂家生产汽车好比应用程序创建窗口,用户使用汽车好比操作系统管理窗口,某种汽车在销售前就指定好了修理站(类似回调函数),当用户的汽车出现故障后(类似窗口收到消息),汽车用户(类似操作系统)自己直接找到修理站去修理,不用厂家(类似应用程序)亲自将车送到修理站去修理,但修理站还得由厂家事先建造好。
提示:lpfnWndProc成员的变量类型为WNDPROC,我们使用goto definition将发现WNDPROC是被如下定义的:
typedef LRESULT (CALLBACK* WNDPROC)(HWND,UINT, WPARAM, LPARAM);读者不要被新的数据类型LRESULT,CAllBACK所吓倒,只要再次使用goto definition就知道他们的庐山真面目分别为long和__stdcall。顺便帮助大家复习一下C语言的知识。首先是关于用typedef定义指向函数的指针类型的问题,其次是__stdcall修饰符的问题。typedef int (*PFUN)(int x,inty);这样就定义了一个函数指针类型PFUN。以后便可以用PFUN定义变量。应用如下:int add(int x,int y);PFUNpfun=add;int sum=pfun(3,5);能够赋值给pfun的函数原型必须严格与PFUN的定义相同。WNDPROC定义了指向窗口回调函数的指针类型,回调函数的格式必须与WNDPROC相同。
__stdcall与__cdecl是两种不同的函数调用习惯,定义了参数的传递顺序、堆栈清除等。关于它们的详细信息请参看msdn。由于除了那些可变参数的API函数外,其余的API函数都是__stdcall习惯。由于VC++程序默认的编译选项是__cdecl,所以在VC++中调用这些__stdcall习惯的API函数,必须在声明这些函数的原型时加上__stdcall修饰符,以便对该函数的调用使用__stdcall习惯。我们曾有这样的经验,在Delphi(默认的编译选项是__stdcall)中编写的dll中的函数,在VC++中被调用时,总是造成程序崩溃,在函数的原型声明中加上__stdcall修饰符,便解决了这个问题。回调函数也必须是__stdcall调用习惯,在这里是用CALLBACK来标识的,否则,在NT4.0环境,程序将崩溃,但win98和win2000却没有这种现象。
cbClsExtra,cbWndExtra这两个成员变量一般都被初始化为0,如需进一步了解这两个参数更详细的信息,请参看msdn。
hInstance成员指定了提供回调函数的程序实例句柄。
hIcon成员指定了这一类型窗口的图标句柄,LoadIcon函数可以加载一个图标资源到内存中并返回系统分配给该图标的句柄。LoadIcon函数的详细信息请参阅msdn,但要注意的是,如果加载的是系统的标准图标,第一个参数必须为NULL,如果加载的是应用程序中自定义的图标,对于第二个参数要用MAKEINTRESOURCE宏转换。
在VC++中,对于自定义的菜单,图标,光标,对话框等都是以资源的形式进行管理的,它们的定义与描述存放在资源文件中(扩展名为.rc),资源文件是文本格式,读者可以用notepad.exe打开,阅读里面的信息。在VC++中是以“所见即所得”的方式打开资源文件的,在编辑窗口中看到的和编辑完后的结果即程序运行时的效果。对于每一个资源及资源中的子项都是用一个标识号来标识的,通常称为ID,同一个ID可以标识多个不同的资源。注意区别资源的ID号与句柄的区别,ID号是应用程序指定的,可以在资源还没在内存中产生前指定,也可在设计阶段就指定,基本上是固定的。而句柄则是资源在内存中产生时由操作系统临时安排的,每次产生的句柄可能都不一样,一个ID号标识的资源可在内存中产生多个实例句柄。资源文件中的ID标识符必须在"resouce.h"头文件中用宏定义成一个整数,这样程序中用到的一个ID号标识符实际上就是那个整数。
LoadIcon的第二个参数是LPCTSTR类型,用goto definition功能发现它实际被定义成CONST CHAR *,是字符串常量指针,而图标的ID是一个整数。参看msdn中的提示,对于这样的情况我们需用MAKEINTRESOURCE这个宏把资源ID标识符转换为需要的LPCTSTR类型。使用goto definition功能,或在MSDN中都可以看到MAKEINTRESOURCE的定义:
#define MAKEINTRESOURCE(i) (LPTSTR) ((DWORD) ((WORD) (i)))
之所以可以这样做,是因为字符串变量本身代表的就是一个字符数组的首地址,本身就是一个数字。所以字符串变量可以类型转换成整数,反之,一个整数也可以类型转换成字符串型。
hCursor成员指定了这一类型窗口的光标句柄,LoadCursor函数可以加载一个光标资源到内存中并返回系统分配给该光标的句柄。除了加载的是光标外,其特点与LoadIcon函数一样。
hbrBackground成员指定了这一类型窗口重画时所使用的刷子句柄。当窗口重画时会使用这里指定的刷子去刷新窗口背景。刷子是具有颜色和形状的,我们可以使用GetStockObject返回一个系统刷子,也可以直接使用msdn中提供的宏,如COLOR_WINDOWTEXT,还可以用CreateBrushIndirect函数产生具有一定形状的刷子。由于GetStockObject参数能够返回标准的刷子、笔、字体、调色板等图形设备对象,定义该函数时,是无法确定该函数到底返回的是刷子还是笔,所以该函数返回类型是HGDIOBJECT(图形设备对象的总称)。由于编译器的需要,在这里我们必须HGDIOBJECT转换成HBRUSH。
顺便提示:在VC++开发Windows程序中,类型转换的频率非常高,在这有必要重点介绍一下。比如有个函数为“去叫一个人来帮忙”,定义该函数时,其返回值只能是“人”,但实际来的“人”,要么是“男人”,要么是“女人”。即使叫来的是一个“男人”,如果将该函数的返回值直接赋给一个“男人”类型的变量,编译时是没法确定返回的是“男人”,还是“女人”,将不会通过。只有我们写程序的人才知道运行时返回的是“男人”,还是“女人”,我们可以对返回值进行类型转换,以便编译器通过。在类型转换时,程序员要对转换完的后果负责,要确保在内存中存在的对象本身确实可以被看成那种要转换成的类型,如果来的是“女人”,我们将其转换成“男人”后,编译能够通过,但程序运行时将会出错。作者在编码和调试时,总是用意境的方式,仿佛看到变量或对象在内存中的真实布局和状态,以及是如何进行转换的,这样编码时比较容易一气呵成,极少犯错。
lpszMenuName成员指定了这一类型窗口的菜单。可见菜单本身不是一个窗口,同图标、光标一样,是窗口的一个元素。不少的人和书都错以为菜单也是一个窗口,其实我们用Spy++实用工具的FindWindow功能就能够区分出桌面上的哪些元素为窗口,哪些不是。lpszMenuName是LPCTSTR类型,需用MAKEINTRESOURCE这个宏把资源ID标识符转换为lpszMenuName需要的LPCTSTR类型。
lpszClassName成员指定了这一类型窗口的名称,是字符串变量。与设计一辆新型汽车后,要为该汽车型号指定名称一样,设计了一种新型窗口后,也要为这种新型窗口起个名称。我们先将这里的名称指定成"http://www.it315.org",等会我们将看到如何使用这个名称。
设计完WNDCLASS后,需调用RegisterClass函数对其进行注册,以后便可以用CreateWindow函数产生这种类型的窗口窗口了。CreateWindow函数的定义如下:
HWNDCreateWindow(
LPCTSTR lpClassName, // pointerto registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // windowstyle
intx, // horizontal positionof window
inty, // vertical position ofwindow
intnWidth, // window width
intnHeight, // window height
HWND hWndParent, // handle toparent or owner window
HMENU hMenu, // handle tomenu or child-window identifier
HANDLE hInstance, // handle toapplication instance
LPVOID lpParam // pointerto window-creation data
);
参数lpClassName即我们刚才在WNDCLASS的lpszClassName成员指定的名称,在这里应该为"http://www.it315.org",表示要产生"http://www.it315.org"这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow函数之前,还没有用RegisterClass函数注册过名称为"http://www.it315.org"的窗口类型,操作系统无法得知这种窗口类型的配置信息,窗口产生过程失败。
参数lpWindowName指定产生的窗口实例上显示的标题文字。
参数dwStyle指定产生的窗口实例的样式,就象同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区别WNDCLASS中的style成员与参数dwStyle,前者是针对一个大类,后者是针对个别。
参数x,y,nWidth,nHeight指定了窗口左上角的x,y坐标,窗口的宽度,高度。如果x被设置成CW_USEDEFAULT,系统将窗口的左上角设置为确省值,参数y将被忽略。如果nWidth被设置成CW_USEDEFAULT,系统将窗口的大小设置为确省值,参数nHeight将被忽略。
参数lpWindowName指定了窗口的父窗口句柄。窗口之间可以组合成父子关系,子窗口必须具有WS_CHILD样式,当父窗口被破坏,隐藏,移动,显示时,也会破坏,隐藏,移动,显示子窗口。当lpWindowName为NULL时,桌面就成为当前窗口的父窗口。
参数lpWindowName指定了窗口的菜单或子窗口句柄。
参数hInstance指定了窗口所属的应用程序的句柄。
参数lpParam可以为窗口附加补充信息。
如果窗口创建成功,函数将返回系统为该窗口分配的句柄,否则,返回NULL。
6.消息循环
通常的消息循环代码如下:
MSGmsg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage函数从应用程序消息队列中取走一条消息,该函数的原型如下:
BOOL GetMessage(
LPMSG lpMsg, // address of structure with message
HWND hWnd, // handle of window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
参数lpMsg是接受消息的变量的指针。
参数hWnd指定了接收属于哪个窗口的消息。
参数wMsgFilterMin,wMsgFilterMax指定了接受某一范围内的消息。
如果队列中没有满足条件的消息,该函数将一直等待,不会返回。除了WM_QUIT消息外,该函数返回非零值,对WM_QUIT消息,该函数返回零。也就是说,只有收到WM_QUIT消息,上述代码才能退出while循环,程序才有可能结束运行。
TranslateMessage函数对取道的消息进行转换。用户按动一下某个键,系统将发出WM_KEYDOWN,WM_KEYUP,并且参数中提供的是该键的虚拟扫描码。但有时用户按动一下某个键,我们想得到一条表示用户输入了某个字符的消息,并在消息补充参数中提供字符的编码。TranslateMessage能将可行的WM_KEYDOWN, WM_KEYUP消息对转换成一条WM_CHAR消息,将可行的WM_SYSKEYDOWN, or WM_SYSKEYUP消息对转换成一条WM_SYSCHAR消息,并将转换后得到的新消息投递到程序的消息队列中。转换过程不会影响原来的消息,只在消息队列中增加新消息。
DispatchMessage函数将取道的消息传递到窗口的回调函数中去处理。可以理解成该函数通知操作系统,让操作系统去调用窗口的回调函数来处理收到的消息。
所有Windows程序的消息处理代码基本上都是相同的,如没特殊需要,可以照搬照抄上述代码。
顺便提示:从队列中取消息还有PeekMessage函数,PeekMessage函数有两种取消息的方式,第一种与GetMessage一样,从队列中直接取走消息,第二种是只取消息的一个副本,并不将消息从队列中取走。无论哪种方式,PeekMessage都不会因队列中没有满足条件的消息而阻塞,当取到满足条件的消息,该函数返回非零值反之,返回零。向队列中发送消息有PostMessage和SendMessage,PostMessage函数发送消息后立即返回,而SendMessage需等到发送的消息被处理完后才能返回。还有一个PostThreadMessage用于向线程发送消息,关于线程,请在以后的章节再学,但我们也因此想到消息不一定总是与窗口相关的,也就是说,对于某些消息,其对应的MSG结构中的hwnd可以为NULL。
7.回调函数
回调函数的原型为:
LRESULTCALLBACK WindowProc(
HWND hwnd, // handle towindow
UINT uMsg, // messageidentifier
WPARAM wParam, // first messageparameter
LPARAM lParam // second messageparameter
);
我们可以将函数名WindowProc改为我们喜欢的名称,如DefWndProc,该函数的四个参数对应消息的窗口句柄,消息码,消息码的两个补充参数。
在该函数内部是一个庞大的switch语句,用于对各种感兴趣的消息进行处理。我们分析程序中的代码。
LRESULT CALLBACK DefWndProc( HWND hWnd,UINTMsg,WPARAM wParam, LPARAM lParam)
{
HDChDC;
switch(Msg)
{
caseWM_CHAR:
charstr[20];
sprintf(str,"thechar code is %d",wParam);
MessageBox(hWnd,str,"www.it315.org",MB_OKCANCEL);
break;
caseWM_LBUTTONDOWN:
MessageBox(hWnd,"mouseclick","www.it315.org",MB_OK);
hDC=GetDC(hWnd);
TextOut(hDC,LOWORD(lParam),HIWORD(lParam),
"http://www.it315.org",strlen("http://www.it315.org"));
ReleaseDC(hWnd,hDC);
break;
caseWM_CLOSE:
if(IDOK==MessageBox(NULL,"真的要要退出吗?",
"http://www.it315.org",MB_OKCANCEL|MB_ICONQUESTION))
{
DestroyWindow(hWnd);
}
break;
caseWM_PAINT:
PAINTSTRUCTps;
hDC=BeginPaint(hWnd,&ps);//在WM_PAINT里必须用这个函数
TextOut(hDC,0,0,"http://www.it315.org",strlen("http://www.it315.org"));
EndPaint(hWnd,&ps);
break;
caseWM_DESTROY:
PostQuitMessage(0);
break;
default:
return(DefWindowProc(hWnd,Msg,wParam,lParam ));
}
return(0);
}
当用户在窗口上按下一个字符键,程序将得到一条WM_CHAR消息,参看msdn,在其wParam参数中含有字符的码值。使用程序中的代码,我们可以随时获得某个字符的码值,不用为此去专门查找书籍。MessageBox函数可以弹出一个显示信息的对话框,如果我们按下BackSpace键,在这里将弹出"the char code is 8"消息。
当用户在窗口上按下鼠标左按钮时,程序弹出一条消息框对收到的WM_LBUTTONDOWN进行响应,以证明按下鼠标左按钮动作与WM_LBUTTONDOWN消息的这种对应关系,另外程序还将在窗口上鼠标所按下的位置写上一串文字。当我们要在窗口上写字、绘图,并不直接在这些设备上操作,而是在一个称为设备描述表(Device Contexts,简称DC)的资源上操作的。使用DC,程序不用为图形的显示与打印输出分别单独处理了。无论是打印还是显示,我们都是在DC上操作,然后由DC映射到这些设备上。使用DC,我们不用担心程序在不同的硬件上执行的差异性,DC能够为我们装载合适的设备驱动程序,实现程序的图形操作与底层设备无关。
GetDC函数能够返回同窗口相关连的DC的句柄。
TextOut函数在当前DC上指定的位置输出一个字符串,当按下鼠标左按钮,消息WM_LBUTTONDOWN的补充参数lParam中含有鼠标位置的x,y坐标,其中的低16位含有x坐标,高16位含有y坐标,可以分别通过LOWORD,HIWORD宏获得。
对于每次成功调用GetDC返回的DC句柄,在执行完所有的图形操作后,必须调用ReleaseDC释放该句柄所占用的资源。否则,会造成内存泄露。我曾经帮助一个叫王健的学员调试他的On-job项目程序,他发现他的程序所占用的内存总是以4k的增量向上增长,每运行几小时后,程序便因内存不足而崩溃了。后来发现就是因为程序在定时器中反复使用GetDC而没有使用ReleaseDC释放造成的。读者可以将例子程序中的ReleaseDC一句注释掉,编译后运行,在NT4.0/win2000下启动任务管理器,切换到进程标签,查看你的程序所使用的内存量,在程序中不断点击鼠标左按钮,程序将不断调用GetDC函数,你将发现你的程序占用的内存量不断向上增长,我们通常使用这样的方式来检测程序的内存泄露的。
当用户点击窗口上的关闭按钮时,系统将给应用程序发送一条WM_CLOSE消息,如果程序不做进一步处理,窗口是不会关闭的。我们在程序中利用一个选择对话框,如果用户确认真的要退出,程序调用DestroyWindow函数将窗口关闭。
窗口关闭后,系统将给应用程序发送一条WM_DESTROY消息,需要注意的是,主窗口的关闭,不代表应用程序结束,在WinMain函数中的消息循环代码中,GetMessage函数必须取到一条WM_QUIT,消息循环才能结束。要让程序正常退出,必须在WM_DESTROY消息响应代码中,调用PostQuitMessage函数向程序的消息队列中发送一条WM_QUIT消息,PostQuitMessage函数的参数值传递给WM_QUIT消息的wParam参数,通常用作WinMain函数的返回值。
当窗口第一次产生,移动,改变大小,从其他窗口后面切换到前面等情况都会导致窗口的重画。重画时将使用设计窗口类时指定的刷子粉刷窗口的背景,窗口上原有的文字和图形都将被擦除掉。要想让图形和文字总显示在窗口的表面,只能是在这些图形和文字被擦除后,立即又将它们画上去。这个过程对用户来说,是感觉不到的,他们只能感觉到这些图形和文字永远都和窗口一并存在。当系统粉刷完窗口的背景后,都会发送一条WM_PAINT消息,以便通知应用程序原有的图形和文字已被擦除,如果还想保留哪些图形和文字,请在此处加入处理代码。也就是说,我们在WM_PAINT消息响应中作出的图形和文字是“永远”存在的。对于WM_PAINT消息响应代码中要获得窗口的DC,只能使用BeginPaint函数,除此之外的消息响应代码中必须用GetDC获得窗口的DC,BeginPaint获得的DC最后必须用EndPaint释放。提醒:水平或垂直改变窗口的大小时,窗口是否重画,取决于WNDCLASS结构中style成员的设置中是否包含CS_VREDRAW与CS_HREDRAW。
DefWindowProc函数提供了对所有消息的缺省处理方式,对于大多数不想特殊处理的消息,程序都可以调用这个函数来处理,所以程序在switch的default语句中调用此函数进行处理。
8.程序编写操作步骤与实验。
1) 首先启动VC++,在菜单中选择File->New,在弹出的窗体中选择Projects标签,然后在左侧选择Windows Application, 在右侧的Project name:文本框中为新建的工程起一个名字,VC++会为新建的工程在硬盘上建一个与工程同名的文件夹,这个文件夹放在Location:指定的路径下,你可以点击Location旁边的"..."按钮来改变路径。接下来一定要选择Create new workspace单选按钮,并勾选Platforms:中的Win32复选框。完成后的界面如图1-1所示:
2) 这一课对第一次接触Windows编程的读者来说,新的东西太多。但只要你把这些知识基本理解和掌握,学好VC++的日子离你就不太遥远了。惟有如此,你才可能精通VC++编程。
第二课:C++经典知识回顾
一、类的定义与应用
在C语言中,我们学过结构体,用于将描述某一对象的若干变量包装成一个整体使用,但没有但没有将与该对象相关的函数包含进来。C语言中的结构体只能描述一个对象的特征(属性),不能描述一个对象的动作(方法)。在C++中,我们是通过类的定义来解决这个问题的,在类的定义中,不仅可以包含变量,还可以包含函数。
我们通过一段程序来讲解类的使用。
#include"iostream.h"
class CPoint
{
public:
int x1;
int y1;
void Output();
CPoint();
CPoint(int x2,int y2)
~CPoint();
private:
int x2;
int y2;
int *pCount;
};
//注意类和结构定义完后,一定要用";"号结尾,忘记";"是许多人常犯的错误。
//在c++中,//......用于注释一行,/*......*/可以注释多行。
voidCPoint::Output()
{
if(pCount)
(*pCount)++;
else
{
pCount=new int;
*pCount=1;
}
cout<<"the first point is("<<x1<<','<<y1<<')'<<endl;
cout<<"the second point is("<<x2<<','<<y2<<')'<<endl;
}
CPoint::CPoint()
{
pCount=0;
cout<<"the first constructoris calling"<<endl;
}
CPoint::CPoint(intx2,int y2)
{
this->x2=x2;
this->y2=y2;
pCount=0;
cout<<"the second constructoris calling"<<endl;
}
CPoint::~CPoint()
{
if(pCount)
{
cout<<"你调用了Output成员函数共"<<*pCount<<"次"<<endl;
delete pCount;
}
else
cout<<"你还没有调用过Output成员函数"<<endl;
cout<<"the deconstructor iscalling"<<endl;
}
voidOutput(CPoint pt)
{
cout<<"the first point is("<<pt.x1<<','<<pt.y1<<')'<<endl;
//cout<<"the second point is("<<pt.x2<<','<<pt.y2<<')'<<endl;
//上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。
}
void main()
{
if(1==1)//限定pt变量的有效范围
{
CPiont pt;
cout<<"请输入两个整数";
cin>>pt.x1>>pt.y1;
//pt.x2=10;
//pt.y2=10;
//上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。
pt.Output();
pt.Output();
pt.Output();//故意演示Output被调用多次的情况。
Output(pt);
}
CPoint pt(10,10);
pt.Output();
}
上面的代码定义了一个类CPoint,其中包含有变量,称之为成员变量,也包含有函数的声明,称之为成员函数。
在类定义之外,我们必须对成员函数进行实现,成员函数的实现格式为:
返回类型 类名::函数名(参数列表)
{
函数体代码
}
上面的代码也编写了一个名为Output的全局函数,注意与类CPoint中的Output成员函数区别。
上面的代码还编写了一个main主函数,其中的代码演示了如何使用CPoint类。
C++中提供了一套输入输出流方法的对象,它们是cint和cout,cerr,对应c语言中的三个文件指针stdin,stdout,stderr,分别指向终端输入、终端输出和标准出错输出(也从终端输出)。cin与>>一起完成输入操作,cout,cerr与<<一起完成输出与标准错误输出。例如程序main函数中使用cin为pt.x1,pt.y1输入两个整数,Output函数中使用cout连续输出字符串、整数、字符、换行。在输出中使用endl(end of line)表示换行,相当与'\n'。利用cin和cout比scanf和printf要方便得多,cin和cout可以自动判别输入输出数据类型而自动调整输入输出格式,不必象scanf和printf那样一个个由用户指定。使用cin,cout不断方便,而且减少了出错的可能性。
从类CPoint的Output成员函数的实现中,我们可以看到类中的成员函数可以直接访问同类中的成员变量,如:x1,y1,x2,y2。说明:如果成员函数中的局部变量与成员变量同名,则在局部变量的作用范围内,成员变量不起作用。如果有全局变量与成员变量同名,则在成员变量的作用范围内(所有同类成员函数中),全局变量不起作用。main函数中的if(1==1)语句部分,主要是为了说明局部变量的有效范围。局部变量的有效范围位于定义它的复合语句之中,一对{}中所定义的语句即一个复合语句。也就是说,局部变量的有效范围并不是在定义它的函数体当中,而是在外层最靠近它定义的那对{}中,main()函数中定义的第一个CPoint对象pt在if语句的}处被系统释放。
在类中使用的private和public访问修饰符,它们限定成员被访问的范围。从一个修饰符的定义处,直到下一个修饰符定义之间的所有成员都属于第一个修饰符所定义的访问类型。
以public定义的修饰符,能够被同类中的成员函数及类定义之外的所有其他函数访问,如CPoint类中的x1,y1,Output等成员变量与函数。但要注意在类之外的函数中访问类成员,必须是对象.成员的格式。
以private定义的成员,只能被同类中的成员函数中访问,不能在其他函数中访问(即使是对象.成员的格式),如类CPoint中的成员变量x2,y2能被成员函数Output访问,但不能在main函数及全局Output函数中访问。说明:如果在类定义中的开始处没有使用任何修饰符,则在类定义的开始处使用private作为其默认修饰符。在C++中定义struct结构体也可以包含成员函数,除了开始处使用的默认修饰符为public外,其余之处与class类完全相同。
二、函数的重载:
在C语言中,如果同一程序中有两个函数名一样,但参数类型或个数不一样的函数定义,编译时将会出错。如果程序中有两个名为Add的函数定义,如:
int Add(intx,int y);
int Add(intx,int y,int z);
在C语言中编译,编译将提示函数名重复错误。在C++中上述定义是合法的,C++能够根据函数调用时所传递的参数个数及数据类型选择适当的函数。
三、构造函数与析构函数:
在类的定义中,有一种特殊的函数,函数名称与类的名称相同,我们称之为构造函数。构造函数不能有返回类型。因为C++支持函数的重载,所以一个类中可以有多个不同参数形式的构造函数。当用类去定义一个变量(后面可以附带参数),也就是在内存中产生一个类的实例(用类定义的实例变量通常也叫对象)时,程序将根据参数自动调用该类中对应的构造函数。如程序中CPoint pt;语句中调用函数CPoint(),CPoint pt(10,10)语句调用函数CPoint(int x2,int y2)。
如果类中有一个函数定义格式为~类名(),如~CPoint(),这个函数就称为析构函数,同样析构函数也不允许有返回值,析构函数不允许带参数,并且一个类中只能有一个析构函数。析构函数的作用正好与构造函数相反,对象超出其作用范围,对应的内存空间被系统收回或被程序用delete删除时,析构函数被调用。
根据构造函数的这种特点,可以在构造函数中初始化对象的某些成员变量,也就是初始化实例对象。在析构函数中释放对象运行期间所申请的资源,如动态申请的内存空间。通俗地讲,构造函数的作用是,在对象产生时,自动为其赋初值;析构函数的作用是,在对象消失时,为对象处理后事。提示:在类中定义成员变量时,不能给这些变量赋初值。如:
class A
{
int x=0;//错误,此处不能给变量x赋值。
};
在类中定义了一个指向整数的指针成员变量pCount,它所指向的内存地址中的数据(一个整数大小空间)用于统计成员函数Output被调用的次数。提示:在例子中的pCount的用法在实际应用中并不合理,我们这么使用主要是为了分析问题。在c++动态申请内存,是new操作符完成的,new操作符申请的内存是从堆中分配的。在程序中定义的变量所用内存是从栈中分配的,当变量超出其作用范围时,系统收回该变量所占用的内存,以后再分配给其他变量使用。new操作符申请的内存空间在程序运行期间是不会被系统收回的,除非程序调用delete操作符明确要求释放该内存空间。
我们对程序中关于pCount的语句进行分析。int*pCount定义了一个变量pCount,pCount变量自身是系统从栈中分配的,占四个字节。这个过程同定义一个整数变量的过程(int x;)没有什么两样,其中的数据没有被初始化,是一个不确定的数,一般不等于零。与定义一个整数变量不一样的是,pCount中的数据是用来表示某一内存块的地址的。尽管系统已为pCount自身分配了内存空间,但pCount中的那个不确定的数据所对应的地址空间却是没有分配的,不可使用的,如图1所示。*pCount=1;表示将pCount所指向的内存块中数据置为1,如果pCount中的数据所指向的内存块是不存在的或未被分配的,程序将会出错。同样(*pCount)++;表示将pCount所指向的内存块中数据加1,delete pCount;表示释放
pCount所指向的内存块,这些操作都要求pCount中的数据所指向的内存块是被分配过的,合法的。
pCount=new int;通过new操作符在堆中分配了一个整数变量空间大小的内存块,并将该内存块的首地址赋值给pCount,pCount中的数据便指向了这一段合法的地址空间,如图2所示。如果程序超出了pCount的定义范围,pCount变量自身的内存空间将被系统收回,但不影响pCount中的数据所指向的内存块,如果该内存块是通过new分配的,必须保证该内存块不再被使用时用delete删除掉,否则将会造成内存泄漏。
我们如何确定pCount中的数据所指向的内存块是否被正常分配过的呢?我们一般通过检查pCount中的数据是否为零来判断的,这就需要我们在pCount被分配之后将其初始化为0。由于pCount是在类中定义的成员变量,不能在定义变量时为其赋值,所以必须在构造函数中将其初始化为0。这样,一旦用CPoint定义一个对象,该对象中的pCount成员将立即被初始化为0,这下读者应该明白构造函数的作用了吧!在Output成员函数中,检查pCount是否为0,如果为零,则用new int;为其分配一个整数变量大小空间,并将其地址赋值给pCount,否则,直接引用原来已分配的空间。当CPoint定义的对象超出其有效范围时,为该对象分配的空间将被释放,pCount变量也将随该对象一并被释放,如果pCount已指向一个用new操作符分配的内存空间,该内存空间不会被释放,所以,我们必须在析构函数中用delete操作符释放该内存空间,保证pCount被释放前也释放掉pCount中的数据所指向的内存空间,这下读者也应该明白析构造函数的作用了!
this指针:
如果成员函数Output被调用,一定是产生了一个对象实例,在这假设对象名称为a,并以a.Output形式调用的,Output的操作一定是针对对象a的。有时,成员函数需要访问它所依赖的那个对象,而不仅仅是这个对象中的其他成员。在类的成员函数中,可以用this关键字代表成员函数所依赖的那个对象的地址,所以,在成员函数中可以用this->成员的方式访问其它的成员,如CPoint(int x2,int y2)函数中用this->x2访问成员变量x2。在成员函数中,我们通常可以省略this->,直接访问类中的成员变量。在CPoint(int x2,int y2)函数中,由于函数参数变量x2,y2与成员CPoint中的成员变量x2,y2同名,要在该函数中访问成员变量x2,y2,可用this->x2,this->y2与参数变量x2,y2区分。小技巧:在以后的MFC编程中,如果在成员函数中想调用同类中的某个成员,可以使用VC++提供的自动列出成员函数功能,使用this->,VC++将列出该类中的所有成员,我们可以从列表中选择我们想调用的成员。自动列出成员函数功能,可以提高编写速度,减少拼写错误。特别是我们不能完全记住某个函数的完整拼写,但却能够从列表中辨别出该函数时,自动列出成员函数功能更是有用。事实上,在各种IDE编程环境中,我们通常都没有完全记住某些函数的完整拼写,只是记住其大概写法和功能,要调用该函数时都是从自动列出成员函数中选取的。这样能够大大节省我们的学习时间,我们没有花大量的时间去死记硬背许多函数,利用自动列出成员函数功能和帮助系统,却也能够在编程使顺利使用这些函数,等用的次数多了,也就在不知不觉中完全掌握了这些函数。
注意比较Output全局函数与Output成员函数的差别。对Output全局函数的调用,可以理解成“输出某个pt点的坐标”,是一种谓宾关系,是面向过程(或函数)Output的。对Output成员函数的调用,可以理解成“pt这个点对象执行输出动作”,是面向对象pt的。希望通过这样的比较,能够有助于读者理解c++中关于面向对象的概念。
四、类的继承与protected访问修饰符:
类是可以继承的,如果类B继承了类A,我们称A为基类(也叫父类),B为派生类(也叫子类)。派生类不但拥有自己新的成员变量和成员函数,还可以拥有基类的成员变量和成员函数。派生类的定义方法是:
class 派生类名:访问权限基类名称
{
.....
};
要实现类B与类A的继承关系,我们在定义类B之前必须已定义了类A,并用如下的格式定义类B。
class B:public或private A
{
....
};
讲到类的继承后,我们再讲解另一种成员访问权限修饰符,protected。public,protected,private三种访问权限的比较:
public定义的成员可以被在任何地方访问。
protected定义的成员只能在该类及其子类中访问。
private定义的成员只能在该类自身中访问。
派生类可以用public和private两种访问权限继承基类中的成员,如果在定义派生类时没有指定如何继承访问权限,则默认为private。如果派生类以private继承基类的访问权限,基类中的成员在派生类中都变成private类型的访问权限。如果派生类以public继承基类的访问权限,基类中的成员在派生类中仍以原来的访问权限在派生类中出现。注意:基类中的private成员不能被子类访问,所以private成员不能被子类所继承。
我们分析如下代码:
class CAnimal
{
public:
void eat();
void breathe();
}
voidCAnimal::eat()
{
cout<<"eating"<<endl;
}
voidCAnimal::breathe()
{
cout<<"breathing"<<endl;
}
classCFish:public CAnimal
{
public:
void swim();
void breathe();
}
voidCFish::swim()
{
cout<<"swimming"<<endl;
}
voidCFish::breathe()
{
CAnimal::breathe();
cout<<"breathing"<<endl;
}
void main()
{
CFish f;
f.eat();
f.swim();
f.breathe();
//下面的代码演示虚拟函数的多态性
CAnimal *pA;
pA=&f;
pA->breathe();
}
关于类的继承及类的访问特性可以参照如下表:
基类的访问特性 | 类的继承特性 | 子类的访问特性 |
Public | Public | Public |
Public | Protected | Protected |
Public | Private | Private |
由于CFish继承了CAnimal,所以在main函数中用CFish定义的对象f可以将CAnimal中定义的eat()成员函数当作自己的成员函数调用。f还调用了CFish中新定义的成员函数swim()。
对象f还调用了breathe()函数,大家发现在基类CAnimal和派生类CFish中都定义了breathe函数,在这种情况下调用的到底是哪个类中定义的函数呢?在这里,调用的是子类CFish中定义的函数。如果在子类与父类中都定义了同样的函数,当用子类定义的对象调用这个函数时,调用的是子类定义的函数,这就是函数的覆盖。函数的覆盖,我们可以用生活中的例子来比喻,儿子继承了父亲的许多方法,包括“结婚”这一行为,但父亲“结婚”用的是花轿,而儿子“结婚”用的却是汽车,儿子不能使用父亲“结婚”的方式。如果儿子结婚时,即要花轿,也要汽车,也就是在子类的成员函数定义中,要调用父类中定义的那个被覆盖的成员函数,其语法为,父类名::函数名(参数)。如CFish定义的breathe函数中使用的CAnimal::breathe()语句,就是调用CAnimal中的breathe函数。
在程序中main函数的结尾处的代码:
CAnimal *pA;
pA=&f;
pA->breathe();
上述代码定义了一个CAnimal类型的指针pA,pA指向CFish定义的对象f的地址,用指针pA去调用breathe函数,在这种情况下调用的到底是哪个类中定义的函数呢?简单的死记硬背只能管一时,不能管一世。我们还是从类型转换的原理上寻找答案。将鱼CFish对象的首地址直接赋值给动物CAnimal类型的指针变量,是不用强制类型转换的,编译器能够自动完成这种转换,子类对象指针能够隐式转换成父类指针。这个过程好比现实生活中将一条鱼当作一个动物是没有什么问题的,但要将一个动物当作鱼来对待是存在问题的。如果某一动物确实是一条鱼,我们就可以将这个动物强制类型转换成鱼。也就是说,要将父类类型的对象转换成子类对象,在程序中必须强制类型转换,编译才能通过,但要保证内存中的对象确实是那种被转换成的类型,程序在运行时才不会有问题。我们可以这样想象类型转换,用目标类型的内存布局,去套取要类型转换的对象的首地址开始的那一段内存块(大小为目标类型的大小),套取的内容即为转换后的结果。见图x,&f转换成pA后,转换完后的内容包含的breathe是CAnimal中定义的那个。
五、虚函数与多态性。
如果我们在CAnimal中定义的void breathe()函数前增加virtual关键字,即改为如下定义:
class CAnimal
{
public:
void eat();
virtual void breathe();
};
则上面的代码
CAnimal *pA;
pA=&f;
pA->breathe();中breathe调用的是CFish中定义的那个,这就是编译器对虚函数调用的编译方式,这就是虚拟函数的多态性。如果在某个类的成员函数定义前加了virtual,这个函数就是虚函数,如果子类中有对该函数的覆盖定义,无论该覆盖定义是否有virtual关键字,都是虚拟函数。
五、类的书写规范与如何解决头文件重复引用问题。
操作符重载,匈牙利命名法。类继承中的构造函数调用顺序与指定父类中的构造函数。
关于完整的c++语法讲解,需要厚厚的一大本书,如果读者需要深入了解,请参看相关书籍。但只要掌握了本课中介绍的关于C++的知识,基本上就能够顺利学习以后的章节了,如有特殊需求,我们将在以后章节中用到时专门讲解。我们认为抱着问题学习的效果要比泛泛而学的效果好得多,并且学到一个新知识后马上便能看到其应用更能令人记忆深刻,举一反三。
实验步骤:
观察构造函数与析构函数的调用时机。
第三课:MFC思想
学完第一课的Windows程序运行原理及第二课经典C++知识回顾,这一课我们主要来看看MFC是如何用面向对象的方式对传统的面象过程的Windows程序运行原理代码进行封装的。
先看看第一课中的Winmain()代码
int PASCALWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,intnCmdShow)
{
WNDCLASS wndClass;
wndClass.style=CS_HREDRAW;
wndClass.lpfnWndProc=(WNDPROC)DefWndProc;
wndClass.cbClsExtra=0;
wndClass.cbWndExtra=0;
wndClass.hInstance=hInstance;
wndClass.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_ICON1));
wndClass.hCursor=LoadCursor(hInstance,MAKEINTRESOURCE(IDC_CURSOR1));
LOGBRUSH lgbr;
lgbr.lbStyle=BS_SOLID;
lgbr.lbColor=RGB(192,192,0);
lgbr.lbHatch=0;
wndClass.hbrBackground=CreateBrushIndirect(&lgbr);
wndClass.lpszMenuName=NULL;
wndClass.lpszClassName="xxxxx";
RegisterClass(&wndClass);
HWND hWnd;
hWnd=CreateWindow("xxxxx","http://www.it315.org",WS_OVERLAPPEDWINDOW& ~WS_MAXIMIZEBOX ,\
CW_USEDEFAULT,0,CW_USEDEFAULT,0,NULL,NULL,hInstance,NULL);
ShowWindow(hWnd,nCmdShow);
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return true;
}
在该段代码中,主要由三大部分组件,即设计窗口阶段(由WNDCLASS结构描述部分)、窗口的注册及创建显示过程、消息循环部分。以上的代码是标准的C语言代码,是面向过程的。在MFC中采用了面向对象的思想,即用面向对象的C++思想对以上代码进行了封装,也就是说将一些对窗口进行操作的API的函数封装到了一个类中,以下我将用简短的代码来演示一下这个过程:
class CIt315Wnd
{
public:
HWNDm_hWnd;
BOOLCreate();
BOOLShowWindow();
};
BOOL CIt315Wnd::Create()
{
WNDCLASSwndClass;
wndClass.style=CS_HREDRAW;
wndClass.lpfnWndProc=(WNDPROC)DefWndProc;
wndClass.cbClsExtra=0;
wndClass.cbWndExtra=0;
wndClass.hInstance=hInstance;
wndClass.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_ICON1));
wndClass.hCursor=LoadCursor(hInstance,MAKEINTRESOURCE(IDC_CURSOR1));
LOGBRUSHlgbr;
lgbr.lbStyle=BS_SOLID;
lgbr.lbColor=RGB(192,192,0);
lgbr.lbHatch=0;
wndClass.hbrBackground=CreateBrushIndirect(&lgbr);
wndClass.lpszMenuName=NULL;
wndClass.lpszClassName="xxxxx";
RegisterClass(&wndClass);
HWNDhWnd;
m_hWnd=CreateWindow("xxxxx","http://www.it315.org",WS_OVERLAPPEDWINDOW& ~WS_MAXIMIZEBOX ,\
CW_USEDEFAULT,0,CW_USEDEFAULT,0,NULL,NULL,hInstance,NULL);
if(m_hWnd!=NULL)
returntrue;
else
returnfalse;
}
BOOL CIt315Wnd::ShowWindow()
{
returnShowWindow(hWnd,nCmdShow);
}
为了保证代码和以前的执行方式一样,Winmain()函数可以写成如下形式:
int PASCAL WinMain(HINSTANCEhInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
CIt315Wndm_wnd;
m_wnd.Create();
m_wnd.ShowWindow();
MSGmsg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
returntrue;
}
此时,如果再写一个新的类来对剩下的代码进行封装,代码如下:
class CIt315App
{
public:
CIt315Wnd*m_pMainWnd;
BOOLInitInstance();
BOOLRun();
CIt315App();
};
CIt315App::CIt315App()
{
if(InitInstance())
Run();
}
BOOL CIt315App::InitInstance()
{
CIt315Wndm_wnd;
m_pMainWnd=&m_wnd;
m_pMainWnd->Create();
m_pMainWnd->ShowWindow();
returntrue;
}
BOOL CIt315App::Run()
{
MSGmsg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
returntrue;
}
此时,在Winmain()函数中仅仅只需要写如下几行代码了,而且对于每个程序来说都是一样,既然如此,那每个程序都这样,微软就把这部分代码为我们写好了,所以,在我们的MFC的工程里,看不到Winmain()的函数。
CIt315ApptheApp; //全局变量
int PASCAL WinMain(HINSTANCEm_hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
hInstance=m_hInstance;
}
因为theApp是一个全局变量,它的构造函数会自动的被调用,所以以上的代码就跟我们在第一课看到的代码的执行逻辑一样。加上Winmain()函数不用我们自己来写,所以要求我们写的类也就只有一个CIt315App类了,由于所有的应用程序的Run()函数都一样,也可以采用一个新的基类来实现Run()然后它的子类就自动的从它继承,也就是说其子类不用实现Run()函数了,也就是说 ,对我们的一般程序设计用户而言,在子类中仅仅只需要实现一个InitInstance()函数即可。而且此时,在我们一般的程序设计用户代码中,程序首先执行Initialize()函数。这也就是MFC程序的入口点。
此时代码演示如下:
class CMyWinApp
{
public:
CIt315Wnd* m_pMainWnd;
BOOL Run();
};
BOOLCMyWinApp::Run()
{
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return true;
}
class CIt315App:publicCMyWinApp
{
public:
CIt315App();
BOOL InitInstance();
};
CIt315App::CIt315App()
{
if(this->InitInstance())
Run();
}
BOOL CIt315App::InitInstance()
{
CIt315Wnd m_wnd;
m_pMainWnd=&m_wnd;
m_pMainWnd->Create();
m_pMainWnd->ShowWindow();
return true;
}
CIt315App theApp;
int PASCALWinMain(HINSTANCE m_hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,intnCmdShow)
{
hInstance=m_hInstance;
}
以上代码和变换仅仅只是一个代码游戏,但却反应了MFC的一种包装的思想,没有从本质上改变Windows程序的运行原理,但就是这种变换方便了我们的编程。在以上这些代码中,CIt315Wnd已经由MFC实现类名为CWnd,CMyWinApp在MFC中类名为CWnApp。
以上代码仅仅只是对Winmain()函数的包装,对于消息处理部分同样也有类似的包装,在此不多举例。
第四课:画线
一、 GDI、DC的概念
1. GDI:(Graphics Device Interfase)图形设备接口,是一个应用程序与输出设备之间的中介。一方面,GDI向应用程序提供一个与设备无关的编程环境,另一方面,它又以设备相关的格式和具体的设备打交道。
2. DC:(Device Context)设备描述表,是一种Windows数据结构。包括了与一个设备的绘制属性相关的信息。所有的绘制操作通过一个设备描述表进行,绘制线条、形状和文本的Windows API 函数都与DC有关。
二、 在Windows Application程序中画线
1. 定义两个全局变量用于记录鼠标按下的(x,y)坐标。
int nOrginX;
int nOrginY;
这两个变量如果定义为局部变量,放在Switch—Case语句中和回调函数中都将画不出线来。
2. 响应鼠标按下和鼠标抬起的消息:
在Swich中加入case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
3. 在鼠标按下时记录鼠标按下的(x,y)坐标,查MSDN得知WM_LBUTTONDOWN
lParam的低字存放x坐标,高字存放y坐标,将其取出存入nOrginX,nOrginY。
case WM_LBUTTONDOWN:
nOrginX=lParam & 0x0000ffff;
nOrginY=lParam >> 16 & 0x0000ffff;
break;
4. 在鼠标抬起时画线:
case WM_LBUTTONUP:
HDC hdc;
hdc=GetDC(hwnd);
PAINTSTRUCT ps;
::MoveToEx(hdc,nOrginX,nOrginY,NULL);
::LineTo(hdc,LOWORD(lParam),HIWORD(lParam) );
::ReleaseDC(hwnd,hdc);
三、在MFC程序中画线:
1. 在CxxxView(其中xxx是你的工程名字)中响应鼠标按下和鼠标抬起的消息(因为只有CxxxView中才能接收到鼠标消息):
使用ClassWizard加入WM_LBUTTONDOWN,WM_LBUTTONUP的消息响应函数OnLButtonDown, OnLButtonUp。
2. 在CxxxView中添加成员变量CPoint m_ptOrigin,用于记录鼠标按下的(x,y)坐标。
CPoint是一个用于描述点的简单的类,它有两个成员变量可以存放点的(x,y)坐标。
3. 在鼠标按下时记录该点的坐标:
m_ptOrigin =point;
其中point是调用OnLButtonDown传入的鼠标按下的点的坐标。
4. 在鼠标抬起时画线:
CClientDC dc(this);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
其中CClientDC 是一个CDC的子类,在它的构造函数中调用了GetDC,析构函数中调用了ReleaseDC,简化了用户的操作。
三、 实现橡皮筋功能:
1. 再定义一个成员变量,用于记录鼠标抬起的点,以便擦线。
CPoint m_ptEnd;
2. 在鼠标按下时记录该点的坐标:
m_ptOrigin=m_ptEnd=point;
3.使用ClassWizard加入WM_MOUSEMOVE的消息响应函数OnMouseMove。
在鼠标移动时判断鼠标左鍵是否按下,如果按下,就不断地擦去上一条线,画出鼠标按下点到鼠标移动的当前点之间的线。
if(MK_LBUTTON & nFlags)
{
CClientDC dc(this);
dc.SetROP2(R2_NOT);
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptEnd);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
m_ptEnd=point;
}
其中:
if (MK_LBUTTON & nFlags)
是判断鼠标左鍵是否按下。在调用OnMouseMove时,不仅为用户传来了坐标信息,还把鼠标左鍵是否按下,Shift鍵是否按下(详细信息可查MSDN)等信息放在UINT nFlags中传入OnMouseMove,用户可以检查相应位是否为1来判断相应键是否按下。
dc.SetROP2(R2_NOT);
该句设置逆转当前屏幕颜色的绘图模式。这种模式下,在屏幕上首次画出的线的是可见的,但在同一位置再画一遍时,线就不见了。这样可以方便的实现不断画线、擦线的效果。
四、 生成自定义的笔和刷子:
1. Windows的GDI对象:
A. CPen :笔是一促用来画线及绘制有形边框的工具,可以指定它的颜色及宽度,并且可以指定它的线型(实线、点线、虚线等)。
B.CBrush : 刷子定义了一种位图形式的象素,利用它可以对区域内部填充颜色。
C.CFont :字体是一种具有某种风格和尺寸的所有字符的完整集合。
D.CBitmap:位图是一种位矩阵,每一个显示象素都对应于其中的一个或多个位,可以利用位图来表示图象,也可以利用它来创建刷子。
E.CRgn :区域是由多边形、椭圆或二者组合形成的一种范围,可以利用它来进行填充、裁剪以及点中测试。
2. SelectObject函数:
当用户生成一个GDI对象时,它是不会生效的。必须用SelectObject将该GDI对象选入设备描述表,它才会在以后的绘制操作中生效。SelectObject函数会返回指向前一次被选对象的指针。
3. 自定义画笔:
CPen 类提供构造函数用于产生Cpen可以定义笔的线型、线宽和颜色。
CPen( int nPenStyle,int nWidth, COLORREFcrColor );
程序中,生成了一个实线,宽度为6,颜色为黑色的笔。要注意的是,有的线型只在线宽小于1时才有效。
CPen newpen(PS_SOLID ,6,RGB(0,0,0));
dc.SelectObject(&newpen);
4. 自定义刷子:
CBrush 提供用于产生刷子的构造函数:
CBrush( COLORREF crColor );
CBrush( int nIndex, COLORREF crColor );
CBrush(CBitmap* pBitmap );
I. CBrush( COLORREF crColor);
它可以产生某种颜色的实心刷子,下面的代码产生了一个红色的实心刷子。
CBrushbr(RGB(255,0,0));
dc.SelectObject(&br);
II. CBrush( int nIndex,COLORREF crColor );
它可以产生某种剖面线的刷子,下面的代码产生了一个红色的剖面线刷子。
CBrushbr(HS_FDIAGONAL,RGB(255,0,0));
dc.SelectObject(&br);
III. CBrush( CBitmap* pBitmap );
它可以产生位图刷子。
CBitmap bmp;
bmp.LoadBitmap(IDB_BITMAP1);
CBrush br(&bmp);
dc.SelectObject(&br);
这段代码首先装入了一幅位图(先在资源中添加一个位图资源,其ID指定为IDB_BITMAP1),再根据这幅位图产生了一个位图刷子。
IV. 产生空刷子
空刷子是一种特殊的刷子,有两种方法可以产生。
1) LOGBRUSH logbr;
logbr.lbStyle=BS_NULL;
br.CreateBrushIndirect(&logbr);
dc.SelectObject(&br);
2)HBRUSHhbr=(HBRUSH)::GetStockObject(NULL_BRUSH);
CBrush *pbr;
pbr=CBrush::FromHandle(hbr);
dc.SelectObject(pbr);
其中,CBrush::FromHandle是一个静态成员函数,它可以不生成类的实例而直接调用,但前面一定要加上类名,以表示它是哪个类的成员函数。
第五课 文本
一、与文本有关的知识点:
1. WM_CHAR消息:
用户按动一下某个键,系统将发出WM_KEYDOWN, WM_KEYUP,并且参数中提供该键的虚拟扫描码。但有时用户按下某个键, 只想得到一条表示用户输入了某个字符的消息,TranslateMessage将WM_KEYDOWN, WM_KEYUP消息对转换成一条WM_CHAR消息,并在消息补充参数中提供该字符的编码。
2. CString 类:
在MFC中将对字符串的操作都封装在Cstring类中,用它来操作字符串特别方便。这一课我们要用到的是:
Empty(): 将字符串的内容清空。
Left(): 取字符串的左面几个字符串。
Format(): 得到一个格式化字符串,它的功能象C语言中的sprintf();
如: CString str = "Some Data";
str.Format("%s%d",str, 123); //str
现在的值是”SomData123”
二、
文字处理:
1.
在Cview类中加入WM_CHAR消息的处理函数OnChar,在其中加入代码:
CClientDC dc(this);
CString str;
dc.TextOut(0,0,str);
这时,只能打出一个字符,因为str是局部变量,将它改为成员变量:
CString m_strInput;
2.
加入自定义字体:
CFontfn;
fn.CreatePointFont(200,"
楷体");
dc.SelectObject(&fn);
其中,CreatePointFont 是产生字体最简单的方法,它的第二个参数是字体的名字,具体你的系统中装了哪些字体,可以打开记事本,在其中的“格式”――“字体”中查到。
3.
设置文字颜色:
dc.SetTextColor(RGB(0,0,255));//
设置文字颜色为红色
4.
加入光标:
a.
在CView类中加入WM_CREATE消息的处理函数OnCreate,当窗口产生之后,会发出WM_CREATE消息,如果想让窗口一产生,就拥有光标,就应在OnCreate中加入代码。
CreateSolidCaret(100,100);//产生一个100象素*100象素大小的实心光标
ShowCaret();
//显示光标
*
注意: WM_CREATE消息只是代表窗口刚刚产生,此时窗口还不可见,因此在OnCreate中不能调用那些依赖于窗口完全激活状态的Windows函数。如:GetClientRect()
b.
调整光标的大小与字体相适应:
CClientDCdc(this);
CFont fn;
fn.CreatePointFont(200,"
楷体");
dc.SetTextColor(RGB(0,0,255));
dc.SelectObject(&fn);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm); //
得到DC中当前的字体信息
CreateSolidCaret(tm.tmAveCharWidth/8,tm.tmHeight);
//
根据当前的大小产生光标
ShowCaret(); //显示光标
c.
产生位图光标:
bmp.LoadBitmap(IDB_BITMAP1);
//bmp定义为成员变量,
//IDB_BITMAP1
为事先在资源面板中做好的位图的ID
CreateCaret(&bmp);
//产生位图光标
ShowCaret();
//显示光标
d.
调整光标位置,在OnChar 中加入:
CPointpt(tm.tmAveCharWidth*m_strInput.GetLength(),0);
//用平均字符宽度*字符个数来得到字符串长度
SetCaretPos(pt);
//设置光标位置
这种方法对于有些字体可以,有些字体不可以,会发生光标不在字符末尾的现象,因为在有些字体中,字符宽度并不一样,如W很宽,l很窄。
上段代码改为:
CSizesz=dc.GetTextExtent(m_strInput);
//得到字符串的大小
CPoint pt(sz.cx,0)
; //用得到的字符串的长度设置
SetCaretPos(pt);
//光标位置
5.
处理回车键:
a.
定义成员变量,用于记录字符串的输出位置。
CPoint m_ptOrigin; //
一定要在构造函数中赋初值
b.
在OnChar中加入代码:
if('\15'==nChar) //如果输入的是回车键
{
m_ptOrigin.x =0;
m_ptOrigin.y += tm.tmHeight;
//将输出字符串的位置加一行
m_strInput.Empty();
//清空字符串
}
else
m_strInput += nChar;
CSizesz=dc.GetTextExtent(m_strInput);
CPoint pt(sz.cx,m_ptOrigin.y);
SetCaretPos(pt);
dc.TextOut(m_ptOrigin.x ,m_ptOrigin.y,m_strInput);
6.
处理退格键:
if(8==nChar)
{
COLORREF oldclr=dc.SetTextColor(dc.GetBkColor());
//
将文字颜色设置为背景色
dc.TextOut(m_ptOrigin.x,m_ptOrigin.y,m_strInput);
//用背景色将字符串再输出一遍,相当于将字符串擦去
dc.SetTextColor(oldclr);
//
将文字颜色设置为正常颜色
m_strInput=m_strInput.Left(m_strInput.GetLength()-1);
//
将字符串最后的一个字符去掉
}
else if(
‘15‘==nChar)
………………
………………
三、
模拟卡拉OK
1.
OnDraw
函数:
OnDraw
是CView 类中的一个虚成员函数,当用户改变了窗口尺寸,或者当窗口恢复了先前被遮盖的部分,应用程序框架都会自动调用OnDraw函数。所以如果在OnDraw函数中加入绘制代码,得到的效果将是绘制的图形在窗口发生改变时还在。
for (inti=0;i<=300;i+=10)
{
pDC->MoveTo(0,i);
pDC->LineTo(300,i);
pDC->MoveTo(i,0);
pDC->LineTo(i,300);
}
将上面的代码加入OnDraw中,它绘制了由横、竖三十根线组成的网格,而且在窗口改变时不会消失。
2.
在绘制网格之前输出字符:
CString str1;
str1.LoadString(IDS_MYSTRING);
pDC->TextOut(0,50,str);
其中LoadString的作用是装入一个在资源面板中的String Table 中定义的字符串资源,IDS_MYSTRING是这个字符串资源的ID。使用字符串资源的好处是,可以不修改代码而改变字符的内容。
这时运行程序,会发现先输出的文字被后绘制的网格穿过。
3.
用剪切区保护文字不被网格穿过:
在DC中有一个剪切区的概念,在剪切区中的内容将被保护起来,不被后来的绘制操作破坏。产生剪切区有两种方法:
a.
由CRgn 来产生剪切区:
CSize sz=pDC->GetTextExtent(str); //
得到字符串的大小信息
CRgn rn;
rn.CreateRectRgn(0,50,sz.cx,sz.cy); //
产生一个区域覆盖输出的字符串
pDC->SelectClipRgn(&rn,RGN_DIFF); //
根据区域产生剪切区
b.
由Path来产生剪切区:
在DC中还有一个路径的概念,在BeginPath()函数和EndPath()函数之间DC上的笔所经过的路线定义了一条路径,可以通过路径来产生剪切区。需要特别强调的是:路径和区域不同,一个DC只对应一条路径。
pDC->BeginPath();
//开始记录一条路径
pDC->Rectangle(0,50,sz.cx,sz.cy); //DC
上笔所经过的路线写入路径
pDC->EndPath(); //
结束记录路径
pDC->SelectClipPath(RGN_DIFF); //
由路径产生剪切区
4.
模拟卡拉OK的文字输出效果:
a.
在OnCreate中,生成定时器。
SetTimer(1,100,NULL);
其中:
第一个参数是产生的定时器的标识号,一个窗口上可以安装多个定时器,我们可以用一个数字来标识产生的是几号定时器。
第二个参数是产生的定时器的时间间隔(毫秒),当规定的时间间隔到来时,系统会向消息队列里发送一个WM_TIMER消息。
第三个参数是一个指向定时器回调函数的指针。当应用程序取到WM_TIMER消息时就会执行这个函数。如果这个参数赋为NULL,取到WM_TIMER消息时就会定义在窗口中的函数。
b.
在CView 中加入成员变量 int m_nIndex,用于记录矩形的边界。
c.在CView 中加入WM_TIMER消息的处理函数OnTimer,在其中加入代码:
CString str;
str.LoadString(IDS_MYSTRING);
CClientDC dc(this);
dc.SetTextColor(RGB(0,0,255));
CRect rect;
rect.top=50;
rect.left=0;
CSize sz=dc.GetTextExtent(str);
rect.bottom=50+sz.cy;
rect.right=m_nIndex++;
dc.DrawText(str,&rect,DT_LEFT);
if(m_nIndex>sz.cx)
{
m_nIndex=0;
dc.SetTextColor(RGB(0,255,0));
dc.TextOut(0,90,str);
}
说明:
上面的代码利用DrawText函数来实现卡拉OK逐步变色的效果(用上面讲的剪切区也可以实现)。DrawText函数用于在一个矩形范围内输出文本,如果文本长度大于矩形宽度,超出的部分将被裁剪掉。
我们在OnDraw中输出了一次IDS_MYSTRING标识的字符串,在OnTimer中,将文字颜色设置为另一种颜色,然后用DrawText函数在原来的位置再输出一次字符串,开始在一个小矩形上输出,每调用一次OnTimer,矩形的宽度变大一点,所得到的效果就是文字在一点一点的变色,模拟了卡拉OK的逐渐变色的效果。
第六课:菜单(一)
一、有关菜单的一些基本知识:
1. 对于一个单文档的工程来说,菜单是在CxxxApp的Initinstance中产生的(Xxx为你的工程名字):
CSingleDocTemplate* pDocTemplate;
pDocTemplate= new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMenuDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CMenuView));
AddDocTemplate(pDocTemplate);
其中IDR_MAINFRAME是菜单的ID,我们在资源面板里可以看到,很多资源的ID都是IDR_MAINFRAME,包括菜单、工具栏、加速键、图标和字符串表,所以,一个ID可以标识多个资源。需要注意的是,工具栏是在CMainFrame的OnCreate函数中产生的:
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD| WS_VISIBLE | CBRS_TOP |CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failedto create toolbar\n");
return-1; // fail to create
}
2. 当我们点击菜单时,系统发出的都是WM_COMMAND消息,在这个消息的扩展参数wParam中,包含菜单的ID,用户可以通过ID来判断是哪个菜单被点击了。
3. 在单文档工程中,MFC AppWizard为我们生成的四个类里,都可以响应同一个菜单的WM_COMMAND消息。如果在这四个类里都加了同一个菜单的响应函数,四个响应函数将只有一个被执行,也就是说,一个菜单消息只能响应被一次。它们之间有一个优先顺序,依次是:ViewàDocumentàMainFrameàApp。具体响应函数应该放在哪个类里,就要看在这个响应函数里,完成的是什么功能,放在哪个类里方便。比如,如果点击一个菜单,想隐藏工具栏,它的响应函数就应放在CMainFrame里。
4. 菜单是一格一格弹出来的,每弹出一格发出一个ON_UPDATE_COMMAND_UI消息,我们可以响应这个消息,设置菜单弹出的状态,比如打勾、打点、变灰等。注意:ON_UPDATE_COMMAND_UI只能由拥有菜单的窗口发出,切记!切记!!切切记!!!
二、菜单的操作:
我们想实现的功能是,有四个菜单:点、直线、矩形和椭圆。用户点击相应的菜单,以后用鼠标就会画出相就的图形。
1. 建立菜单:在资源面板里先建立一个弹出菜单-“画图”,再在这个弹出菜单下建四个子菜单,“点”、“直线”、“矩形”和“椭圆”,然后修改它们的ID。当我们建立一个子菜单时,系统会分配给它一个类似ID_MENUITEM32780的ID。这个ID非常难记,最好把它改为一个容易记的ID,如IDM_DRAW_DOT,ClassWizard会根据这个ID来为菜单响应函数生成名字。另外,弹出菜单没有ID。
2. 在CView中加入成员变量:
CPoint m_ptOrigin; //用于记录鼠标按下的点
int m_nType; //用于记录当前所画的图形
在CView 的构造函数中对它们进行初始化:
m_nType=-1;
m_ptOrigin=0;
3. 在CView中加入菜单“点”、“直线”、“矩形”和“椭圆”的WM_COMMAND消息的响应函数。在其中设置m_nType为不同的值。
点的响应函数中: m_nType=0;
直线的响应函数中: m_nType=1;
矩形的响应函数中: m_nType=2;
椭圆的响应函数中: m_nType=3;
4. 在CView中加入WM_LBUTTONDONW和WM_LBUTTONUP的消息响应函数OnLButtonDown和OnLButtonUp 。
在OnLButtonDown中保存鼠标按下的点:m_ptOrigin=point;
在OnLButtonUp中根据m_nType的值画相应的图形:
CClientDC dc(this);
switch(m_nType)
{
case 0:
dc.SetPixel(point.x,point.y,RGB(255,0,0));
break;
case 1:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 2:
dc.Rectangle(m_ptOrigin.x,m_ptOrigin.y,point.x,point.y);
break;
case 3:
dc.Ellipse(m_ptOrigin.x,m_ptOrigin.y,point.x,point.y);
break;
default:
break;
}
5. 此时我们已经基本完成了要求的功能,但还无法判断当前所画的是什么图形,为了使界面更加友好,可以WM_UPDATE_COMMAND_UI消息,在当前所画的图形对应的菜单项上打勾。用ClassWizard为菜单“点”、“直线”、“矩形”和“椭圆”的ON_UPDATE_COMMAND_UI的响应函数,其形式如下:
voidCMenuView::OnUpdateDrawDot(CCmdUI* pCmdUI)
可以看到,在调用ON_UPDATE_COMMAND_UI的响应函数时,系统为用户传来了CCmdUI的指针。CCmdUI 是一个只被使用在ON_UPDATE_COMMAND_UI消息的响应函数中类,它可以控制正在被弹出的菜单项的状态。这个类中有一个成员变量m_nIndex表示菜单项在整个弹出菜单中的序号,另一个成员变量m_nID表示菜单的ID。我们来介绍一下它的成员函数:
Enable() : 设置菜单项是否有效;
SetCheck() :设置菜单项是否打勾;
SetRadio() :设置菜单项是否打点;
SetText() : 设置菜单项的文本。
我们就可以利用这些函数来控制菜单项的状态,比如设置当前所画的图形在对应的菜单项上打勾。
因为四个菜单的ON_UPDATE_COMMAND_UI响应函数的代码类似,我们只写出“点”的ON_UPDATE_COMMAND_UI的响应函数代码:
a. 直接判断:
if(m_nType==0)
pCmdUI->SetCheck(true);
else
pCmdUI->SetCheck(false);
b. 利用m_nIndex:
pCmdUI->SetCheck(m_nType==pCmdUI->m_nIndex);
c. 利用m_nID:
pCmdUI->SetCheck(m_nType+IDM_DRAW_DOT==pCmdUI->m_nID);
此种方法有一个条件,菜单“点”、“直线”、“矩形”和“椭圆”的ID必须是连续的。如果不连续,可以打开resource.h,将它们的ID修改为连续。
6. 快捷菜单:
在CView中加入WM_RBUTTONUP的响应函数OnRButtonUp,在其中加入代码:
ClientToScreen(&point);
GetParent()->GetMenu()->GetSubMenu(4)->TrackPopupMenu(
TPM_LEFTALIGN,point.x,point.y,GetParent());
说明:
a. OnRButtonUp中传入的坐标是相对于窗口的,而TrackPopupMenu函数需要的是相对屏幕的坐标,所以要用ClientToScreen转换一下。
b. 如果写成
GetMenu()->GetSubMenu(4)->TrackPopupMenu(
TPM_LEFTALIGN,point.x,point.y,GetParent());
运行时会报红框,因为这句代码会调用CView的成员函数GetMenu(),因为CView根本就没有菜单。
c. TrackPopupMenu函数的第四个参数指定拥有菜单的窗口,这个窗口将得到快捷菜单的所有WM_COMMAND消息。填成GetParent(),CMainFrame拥有快捷菜单,此时程序没有任何问题。如果填成this,快捷菜单将不能及时的打勾。因为ON_UPDATE_COMMAND_UI只能由拥有菜单的窗口发出。
d. 我们让CMainFrame拥有快捷菜单,它得到所有WM_COMMAND消息,在CView 中加入的WM_COMMAND消息的响应函数仍然会被运行。因为对于WM_COMMAND消息存在一个转发的过程。
e. 对于快捷菜单,有一个专门的消息以响应――WM_CONTEXTMENU。如果响应它来产生快捷菜单,就不用进行ClientToScreen的坐标转换了。
f. 产生快捷菜单,也可以点击ProjectàAdd ToProjectàComponents And ControlsàVisual C++ Components , 选择Pop-up Menu。
7.给菜单项增加图标:
a. 在CMainFrame中增加两个成员变量,用于装入两个位图,一个是选择菜单时的图标,一个是未被选择时的。
CBitmapbmp1;
CBitmapbmp2;
b. 在资源面板里增加两个位图,其ID为IDB_BITMAP1,IDB_BITMAP2。
c. 在CMainFrame的OnCreate函数中加入代码:
bmp1.LoadBitmap(IDB_BITMAP1);
bmp2.LoadBitmap(IDB_BITMAP2);
GetMenu()->GetSubMenu(4)->SetMenuItemBitmaps(
0,MF_BYPOSITION,&bmp1,&bmp2);
d. 此时位图可能在菜单中不能完全显示,用GetSystemMetrics()可以得到菜单图标的大小:
CString str;
str.Format("%d,%d",
GetSystemMetrics(SM_CXMENUCHECK),
GetSystemMetrics(SM_CYMENUCHECK));
MessageBox(str);
第七课:菜单(二)
一、系统菜单:
1. 系统菜单是有鼠标点击应用程序图标弹出的菜单。点击系统菜单时,发出WM_SYSCOMMAND消息,可添加此消息的响应函数来进行控制。
2. 操作系统菜单:
在CMainFrame的OnCreate函数中加入代码:
CMenu *pMenu=GetSystemMenu(false);
//得到系统菜单
pMenu->RemoveMenu(pMenu->GetMenuItemCount()-1,MF_BYPOSITION);
//移除系统菜单的最后一项
pMenu->EnableMenuItem(SC_CLOSE,MF_DISABLED| MF_BYCOMMAND|MF_GRAYED);
//将系统菜单的“关闭”菜单项设置为失效,变灰
pMenu->AppendMenu(MF_STRING,1111,"HELLO");
//在系统菜单中添加一个菜单
GetSystemMenu(true);
//重置系统菜单到默认状态
二、运行时产生菜单:
在CMainFrame的OnCreate函数中加入代码:
mnu.CreatePopupMenu();
//产生一个弹出菜单,注意:在WIN2000下mnu应定义为成员变量,否则会报红框,WINNT下可以定义为局部变量
mnu.AppendMenu(MF_STRING,1111,"HELOO");
//向刚产生的弹出菜单加入一个菜单项
GetMenu()->AppendMenu(MF_POPUP,(INT)mnu.m_hMenu,"heha");
//将弹出菜单添加到主窗口的菜单上
GetMenu()->GetSubMenu(4)->EnableMenuItem(32771,
MF_DISABLED| MF_BYCOMMAND|MF_GRAYED);:
//将新添加的菜单设置为失效,变灰
说明:运行程序时,会发现上面代码的最后一句没有生效。应在CMainFrame的构造函数将成员变量m_bAutoMenuEnable设为FALSE。当这个成员变量设置为TRUE时,如果菜单项没有ON_UPDATE_COMMAND_UI 或者ON_COMMAND的处理函数,用户点击弹出菜单时系统自动将菜单项设为失效。如果菜单项有ON_COMMAND的处理函数,菜单项自动被设置为有效。
三、动态增长菜单程序的编写
想实现的功能是,输入名字和电话,用空格格开,当用户按下回车键时把名字加入主菜单中,点击相应的菜单显示相应的名字和电话信息。
1. 在CView中定义成员变量
CString m_strLine; //用于记录用户的键盘输入
CMenum_mnuPhone; //用于产生弹出菜单
int m_nIndex; //用于为菜单产生可变的ID
CStringArraym_aInput; //用于保存用户所有的用户输入
2. 在ViewàResource Symbols 中New一个ID,用于为菜单的ID赋值,把它命名为IDM_PHONE。
3. 在CView中加入WM_CHAR消息的响应函数OnChar,加入以下代码:
if(0xd==nChar)
//如果输入的是回车键,就动态添加菜单
{
if(!m_mnuPhone.m_hMenu)
//如果弹出菜单没有产生
{
m_mnuPhone.CreatePopupMenu();
//产生弹出菜单
GetParent()->GetMenu()->AppendMenu(
MF_POPUP,
(UINT)m_mnuPhone.m_hMenu,
"Phone");
//将弹出菜单添加到主窗口的菜单上
GetParent()->DrawMenuBar();
//重绘菜单
}
m_mnuPhone.AppendMenu(MF_STRING,
IDM_PHONE+m_nIndex,
m_strLine.Left(m_strLine.Find(" ")));
//将用户输入的姓名(空格前面的字符)添加到弹出菜单上
m_aInput.Add(m_strLine);
//将用户输入的字符串保存到动态数组中
m_strLine.Empty();
//清空字符串,以便用户下次输入
m_nIndex++;//
Invalidate();
//使窗口重绘
}
else
{ //不是回车键
CClientDCdc(this); //生成DC
m_strLine+=nChar ; //将输入的字符保存
dc.TextOut(0,0,m_strLine); //输出到View上
}
说明:
a. 在窗口显示以后(如CView中的OnChar中)再添加菜单,需要调用DrawMenuBar()函数重绘一下菜单,否则新加入的菜单不会显示出来。而在在窗口显示之前(如CMainFrame的OnCreate中)则不用。
b. Invalidate()函数使窗口的全部客户区失效,它只是向窗口的消息队列中发送一个WM_PAINT消息,马上返回Invalidate()的下一句去执行,等Invalidate()所在的函数执行完毕之后,WM_PAINT消息才有机会被取到,去执行刷新操作。所以,如果在Invalidate()之后的代码中输出字符或图形,也将被稍后的刷新操作冲掉,不会看到输出效果。
4. 为动态产生的菜单添加响应函数
a. 手工添加:
i. 在CView中的消息映射表中添加消息映射:
ON_COMMAND(IDM_PHONE,OnPhone)
ii. 为CView添加成员函数OnPhone,
afx_msg void OnPhone1();
iii. 在OnPhone中添加代码:
void CMenu2View::OnPhone1()
{
MessageBox("phone1");
}
b. 利用ClassWizard添加:
i. 在资源面板里增加一个菜单,将其ID指定为IDM_PHONE
ii. 这时用ClassWizard可以为IDM_PHONE添加响应函数
iii. 从资源面板里删除刚刚增加的菜单项
c. 在OnCommand中添加控制代码:
所有的WM_COMMAND消息都会交给拥有菜单的窗口的成员函数OnCommand去处理,可以在其中处理所有的动态添加的菜单。
在CMainFrame中添加虚函数OnCommand,加入代码:
intnId=LOWORD(wParam); //取出菜单的ID
intnIndex=((CMenu2View*)GetActiveView())->m_nIndex;
//在CMainFrame中访问CView中的变量
if( nId>=IDM_PHONE && nId < IDM_PHONE+nIndex)
{ //判断如果是动态添加的菜单消息
CClientDCdc(GetActiveView());
//生成一个CView的DC
dc.TextOut(0,0,
((CMenu2View*)GetActiveView())->m_aInput.GetAt(nId-IDM_PHONE));
//在CMainFrame将保存在动态字符数组中的字符串打印到View上
returntrue;//返回,表示已经处理了WM_COMMAMD消息
}
else
returnCFrameWnd::OnCommand(wParam, lParam);
//其它的WM_COMMAMD消息交由父类去处理
第八课:对话框
一、概念
1.对话框可以在资源面板中的对话框编辑器中设计,添加各种控件,改变它们的外观,属性。对话框也用一个ID来标识,可以用ClassWizard生成类(从CDialog派生)来管理对话框。
1. 模态对话框:
模态对话框是主程序窗口打开的临时窗口,用于显示消息及取得用户数据,用户要关闭对话框才能恢复主窗口的工作。模态对话框可定义为局部变量
CMyDlg dlg; // CMyDlg是一个管理对话框的类,从CDialog派生
dlg.DoModal();//可以想象在此函数完成了对话框的显示,销毁操作。
注意,如果对话框中我们需要将某些变量取出,不能用
GetDlgItem(ID_..)->GetWindowText(),因为当DoMadle()返回时,对话框已经不存在了。
2.非模态对话框:
对话框不返回,可以切换到其他窗口,所以非模态对话框必须定义为全局变量或用 new产生,然后
dlg.Create(IDD_DIALOG1);
dlg.ShowWindow(SW_SHOW);
二、若你想在对话框上加几个控件,应在OnCreate()中加入代码,这时对话框刚刚产生,若想对控件操作,则应加在OnInitDialog()(若在OnCreate()会产生错误,因为子控件还不一定存在)。
注意:若你想改变主窗口的标题,应加在App的InitInstance()中,加在MainFrame和View中的PreCreateWindow()和OnCreate()中都不可以,因为当程序打开无标题文件时,主窗口标题被覆盖了。
三、对话框中的函数:
CWnd::GetDlgItem(nID); //得到指定控件临时对象的指针
CWnd::SetFocus(); //得到焦点
(Cedit*)GetDlgItem(IDC_EDIT1)->SetSel(0,3);//选择前4个字符
因为要用CEdit类中的SetSel()函数,所以要将得到指针进行类型转换。
使用前要得到焦点否则看不出显示效果。
CWnd::EnableWindow(false); //使得窗口失效
也可以采用发消息的方式实现:
GetDlgItem(IDC_EDIT1)->SendMessage(WM_SETTEXT,0,(LPARAM)“123456”);
<==>GetDlgItem(IDC_EDIT1)->SetWindowText(“123456”);
GetDlgItem(IDC_EDIT1)->SendMessage(WM_SETFOCUS);
GetDlgItem(IDC_EDIT1)->SendMessage(EM_SETSEL,0,-1);//选择全部文本
上句也可以SendDlgItemMessage(IDC_EDIT1,EM_SETSEL,0,-1);
但GetDlgItem(IDC_EDIT1)->SendMessage(WM_ENABLE,false,0);并不等效于EnableWindow(false);
注意:Windows消息可以分为两类,一种是发出消息指示去干什么。比如,WM_SETFOCUS,WM_SETTEXT,另一种是干了什么事之后发出消息,表明已干了什么,比如WM_ENABLE。
另外:从另一种角度来看,窗口类所共有的消息,叫WM_XXX,而一些类有特有的消息,比如EDIT类EM_SETSEL,同理Cbotton类特有的消息BM_XXX,ListBox特有的消息LB_XXX,etc.
四、数据交换:
为了和控件交换数据可以定义一个变量与某一个控件关联。
为了控制控件可以定义一个对象与一个控件相关联。
注意:一个控件只能和一个对象,一个变量相联,多了会产生错误。
UpdateData(true); //控件的值刷新至关联的变量
UpdateData(false); //变量的值刷新至控件
CDialog在OnInitDialog中,系统调用UpdataData(false);
CDialog::OnOk中系统调用UpdateData(true);
BTW:将某些类中常用的常量,定义为该类中的枚举。
如:CFile::中的enum OpenFlags
五、变量的存储:
1、堆:全局变量和用new产生的变量 手工释放
栈:局部变量{int x;…;}自动释放。
2、MFC中,关闭程序流程:
用户关闭主窗口->OnClose()->DestroyWindow()(销毁窗口,先销毁所有的子窗口,再销毁自己)->OnNcDestroy()->PostNcDestroy()->delete C++窗口对象
对于一个窗口,你一定要用DestroyWindow()不要用delete.
3、(1)MFC自动清除的类:(它们通常被分配在堆上)。
MainFrame窗口(直接或间接从CframeWnd派生)
View窗口(直接或间接从Cview派生)
(2)MFC不自动清除的类:(它们通常嵌入到别的C++对象,或者存放在栈上)
i. 标准Window控件(CStatic,CEdit,CListBox等)
ii. 从CWnd派生的子窗口。
iii.切分CSplitterWnd
iv. 缺省的控制条(从CControlBar派生)
v. 模态对话框,标准对话框(除了CFindReplateDialog),ClassWizard创建的默认对话框。
4、对于不自动清除的对象在调用DestroyWindow()后,C++对象依然存在,但m_hWnd=NULL;
对于自动清除的对象,C++对象已被清除(在PostNcDestroy()中被delete).
第九课:对话框(二)
建立一个基于对话框的应用程序,可以看到在CXxxApp的InitInstance()函数中:
CDlgaDlg dlg;
m_pMainWnd = &dlg;
应用程序启动时,必须对CXxxApp的成员变量m_pMainWnd进行赋值,否则无法运行。在单文档的工程中,我们看不到这种赋值操作,它是在
if(!ProcessShellCommand(cmdInfo))
returnFALSE;
在ProcessShellCommand函数中对m_pMainWnd赋了值。
一、returnfalse和return ture的区别:
1. 在CXxxApp的InitInstance()函数中:
return false:退出应用程序,不进入消息循环。
return ture:应用程序进入消息循环。
2. 在对话框类中的OnInitDialog()中:
return false:如果在OnInitDialog()函数中设置了某控件得到焦点,如:
GetDlgItem(IDC_EDIT1))->SetFocus();
应return false,否则上一句代码不会生效。
return ture:在OnInitDialog()函数中没有设置了某控件得到焦点,应return ture。
说明:要使对话框上的某个控件在一显示对话框,就具有焦点,还可以将该控件的
Table Order设为1。
二、在对话框中响应回车键:
在对话框中回车,会执行缺省按钮(Default Button)的函数,默认的缺省按钮是IDOK。如果没有缺省按钮,会执行对话框中的OnOK()函数。所以,在一个对话框中要控制回车键,可以采用下面的方法:
1. 在OnOK()函数中添加代码:
在对话框中添加四个文本框,我们想用户按回车键时,四个文本框依次循环得到焦点:
if(GetFocus()==GetDlgItem(IDC_EDIT4)) //如果第四个文本框得到焦点
GetDlgItem(IDC_EDIT1)->SetFocus(); //使第一个文本框得到焦点
else if (GetFocus()->GetDlgCtrlID()==IDOK) //如果“确定”按钮得到焦点
CDialog::OnOK(); //对话框返回
else
GetFocus()->GetNextWindow()->SetFocus();
//使当前具有焦点窗口的下一个(按照Table Order顺序)窗口得到焦点
2. 添加按钮,将其设置为缺省按钮,Visible属性设为false,为它添加响应函数,在其中编程(代码类似1)。
3. 更换回调函数:
我们更换第一个文本框的回调函数,让它不响应回车键。
i. 定义一个全局变量WNDPROC oldProc; 用于保存原来的回调函数的指针。
ii. 在对话框类中的OnInitDialog()中用SetWindowLong函数更换第一个文本框的回调函数为newProc
oldProc=(WNDPROC)SetWindowLong(GetDlgItem(IDC_EDIT1)->m_hWnd,GWL_WNDPROC,(long) newProc);
iii. 定义newProc函数:
LRESULT CALLBACK newProc (
HWND hwnd,UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if(uMsg==WM_CHAR) //如果是字符消息
{
if(0xd==wParam) //如果是回车键
return1; //不处理就返回,即不响应回车键
}
return oldProc (hwnd,uMsg,wParam,lParam);
//其它消息仍由原来的回调函数处理
}
说明:此种方法要文本设置文本框的MultiLine 和Want return属性设为有效。
三、制作一个可以响应WM_MOUSEMOVE消息的按钮:
1. 新建一个类CHide ,从CButton派生,增加成员对象CHide *m_btnFriend,在类中响应WM_MOUSEMOVE消息,在OnMouseMove中添加代码:
ShowWindow(SW_HIDE);
m_pmyFriend->ShowWindow(SW_NORMAL);
2. 在对话框的头文件中加入#include “hide.h”
3. 为对话框上的“确定”和“取消”按钮添加捆绑变量
i. 静态绑定:
a. 用ClassWizard为对话框上的“确定”和“取消”按钮添加捆绑变量,
CHide m_btnOK;
CHide m_btnCancel;
并将其中一个按钮的Visible属性设为false。
b. 此种方法会在对话框的DoDataExchange函数中添加
DDX_Control(pDX,IDCANCEL, m_btnCancel);
DDX_Control(pDX, IDOK,m_btnOK);
c. 在对话框类中的OnInitDialog()中添加代码:
m_btnOK.m_pmyFriend=&m_btnCancel;
m_btnCancel.m_pmyFriend=&m_btnOK;
ii. 动态绑定:
a. 定义两个成员变量:
CHide m_btnOK;
CHide m_btnCancel;
b. 在对话框类中的OnInitDialog()中添加代码:
m_btnOK.m_pmyFriend=&m_btnCancel;
m_btnCancel.m_pmyFriend=&m_btnOK;
m_btnOK.SubclassDlgItem(IDOK,this);
m_btnCancel.SubclassDlgItem(IDCANCEL,this);
四、用代码改变窗口的大小:
1. 在对话框类中定义两个成员变量:
CRect m_rectLarge; //用于保存大窗口的大小
CRect m_rectSmall; //用于保存小窗口的大小
2. 在对话框中添加一个Picture 控件,用于分隔大小窗口,将其IDIDC_LAND
3. 在OnInitDialog()中为其赋值:
GetWindowRect(&m_rectLarge); //得到大窗口的大小
m_rectSmall=m_rectLarge; //将小窗口先赋为大窗口
CRect rect;
GetDlgItem(IDC_LAND)->GetWindowRect(rect);// 得到分隔条的矩形,
m_rectSmall.bottom=rect.bottom; //为小窗口进行赋值
4. 添加成员函数EnableVisibleChildren(),将不属于当前窗口的控件设为失效。
CRect rectVisible,rectCtrl,rectTemp;
GetWindowRect(&rectVisible);
CWnd *pWnd=GetWindow(GW_CHILD);
while(pWnd)
{
pWnd->GetWindowRect(&rectCtrl);
if(rectTemp.IntersectRect(&rectVisible,&rectCtrl))
pWnd->EnableWindow(true);
else
pWnd->EnableWindow(false);
pWnd=pWnd->GetNextWindow();
}
5. 增加一个按钮,并为其添加响应函数,在其中改变窗口的大小:
CString str;
if(GetDlgItemText(IDC_BUTTON3,str),str=="<<collapse")
{
SetWindowPos(NULL,0,0,m_rectSmall.Width(),m_rectSmall.Height(),
SWP_NOMOVE|SWP_NOZORDER);
SetDlgItemText(IDC_BUTTON3,"expand>>");
}
else
{
SetWindowPos(NULL,0,0,m_rectLarge.Width(),m_rectLarge.Height(),
SWP_NOMOVE|SWP_NOZORDER);
SetDlgItemText(IDC_BUTTON3,"<<collapse");
}
EnableVisibleChildren();
五、隐藏对话框:
1. 可以在OnPaint()中
ShowWindow(SW_HIDE);
2. 可以用定时器:
在OnInitDialog()中
SetTimer(1,1000,NULL);
添加WM_TIMER的消息响应函数OnTimer,在其中添加:
ShowWindow(SW_HIDE);
3. 也可以在OnInitDialog()中发送消息
a. 在OnInitDialog()加入
PostMessage(WM_USER+1);
b. 在消息响应表中添加消息映射:
ON_MESSAGE(WM_USER+1,OnHideDlg)
c. 添加成员函数OnHideDlg
LRESULT CDlgaDlg::OnHideDlg(WPARAM wParam, LPARAM lParam)
{
ShowWindow(SW_HIDE);
return 1;
}
第十课:对话框之属性单
一、属性单
1. 属性单就是在属性对话框中常见的标签对话框,包括一个CpropertySheet和几个CpropertyPage。ClassWizard就是一个属性单,其中ClassWizard是一个CpropertySheet,而“Message Maps”,“Member Variables”,“ Automation”,“ ActiveX Event”和“Class Info”每一项是一个CpropertyPage。
2. 生成属性单:
a. 在资源面板里新建三个对话框,Caption属性改为容易区分的名字。每一个对话框对应于属性单中的一个CpropertyPage,对于CpropertyPage来说,它的样式有特殊的要求:Style是Child, Boder是Thin,确保TilteBar,Disable被选择。这些要求可以在MSDN中查“CPropertyPage,styles”得到。
b. 为每个对话框新建一个类Cpage1,Cpage2,Cpage3,基类选择CpropertyPage。
c. 将新建类的头文件加到 CxxxView的cpp中
#include "Page1.h"
#include "Page2.h"
#include "Page3.h"
d. 在资源面板里新建一个菜单,在CxxxView中添加它的处理函数,加入代码:
CPropertySheet ps;
Cpage1 pg1;
Cpage2 pg2;
Cpage3 pg3;
ps.AddPage(&pg1);
ps.AddPage(&pg2);
ps.AddPage(&pg3);
ps.SetWizardMode();
ps.DoModal();
3. 数据交换:
pg1.m_nList1=m_nList1;
if(IDOK==ps.DoModal())
{
m_nList1=pg1.m_nList1;
}
说明:以上代码是对话框和调用对话框的窗口交换数据的固定步骤。其中m_nList1是在对话框中定义的一个成员变量,想把它里面的数据保存出来,应该在调用对话框的类中也定义一个成员变量(最好名字相同,以便于记忆),在用户按OK键退出时,进行赋值。而在显示对话框之前,用保存的数据对对话框中的成员变量初始化。
4. 其它:
i. 单选按钮:
a. 单选按钮(Radio Button)复选按钮(Check Box)都是按钮,可以响应鼠标单击和双击的消息。
b. 对于单选按钮应设置组(Group),第一个单选按钮的Group设为有效,直到下一个设置Group的控件之前的单选按钮为一组,同一时间只能选一个。其中“下一个”是指TableOrder的顺序,而不是物理位置上的顺序。
c. 一组单选按钮只能捆绑一个int,表示该组中选中的单选按钮的序号。
ii.列表框:
a. 在列表框的属性页中不能添加数据,要想向列表框中添加数据,应该用CListBox的成员函数AddString或InsertString。
b. 在OnInitDialog()中为列表框添加数据“北京”,
((CListBox *)GetDlgItem(IDC_LIST1))->AddString("北京");
c. 为列表框捆绑一个变量
Cstring m_strList;
在属性单显示之前用保存的数据对m_nList1赋值,
pg1.m_nList1=m_nList1;// m_nList1的值为“北京”
此时“北京”应该被选中但没有选中,因为是CPropertyPage::OnInitDialog()调用的UpdateData(false), 而那时列表框中还没有“北京”这个数据,需要手工调用UpdateData(false)。
iii.组合框:
a. 组合框由一个文本框和一个列表框组成。当前选定的项将显示在组合框的文本框中。
b. 在组合框的属性页中有Data选项,可以直接加入数据。
c. 组合有三种样式,
DropDown:用户可在文本框中输入数据,点击下拉箭头时列表框部分才被显示。只能捆绑Cstring类型的变量。
DropList: 用户不能在文本框中输入数据,点击下拉箭头时列表框部分才被显示。相当于一个列表框,只能捆绑int 类型的变量。
Simple: 用户可在文本框中输入数据,列表框部分直接显示。只能捆绑Cstring类型的变量。
二、向导页
1. 在属性单DoModal()之前,
ps.SetWizardMode();
属性单即变为向导页。
2. 在显示每一个属性页时,向导页应该显示不同的按钮,如第一页应只有“下一步”,没有“上一步”。最后一页应该没有“下一步”,而有“完成”。可在属性页类中添加虚函数OnSetActive(),添加相应代码:
((CPropertySheet*)GetParent())->SetWizardButtons(PSWIZB_NEXT);
3. 在最后一页中设置“完成”按钮后,点击它时,它的处理函数并没有调用 UpdateData(),可加入虚函数OnWizardFinish(),在其中调用。
4. 在最后一页中设置“完成”按钮后,它的ID是ID_WIZFINISH(可在MSDN中查SetWizardMode()得到“上一步”,“下一步”,“完成”对应的ID),不能再用
if(ps.DoModal()==ID_OK )来判断,应改为:
if(ps.DoModal()==ID_WIZFINISH )
第十一课:Windows样式
一、首先回忆一下WinMain一课中的创建一个窗口的几个步骤。
二、首先回忆一下MFC一课中程序执行的顺序。
三、修改Windows样式:
1. 在CMainFrame::PreCreateWindow中修改:
在窗口产生之前,会调用它的成员函数PreCreateWindow,此时窗口还没有产生,相当于一栋大楼修建之前,在它的设计图纸上修改它的式样,然后按照修改后的图纸去修建大楼。
a. 直接修改CREATESTRUCT:(让最大化按钮不可用)
cs.style &= ~WS_MAXIMIZEBOX; //去掉窗口的最大化按钮
b. 重新注册WNDCLASS:(修改窗口的图标及背景)
对于窗口的图标、背景等,不能直接修改。需要重新注册WNDCLASS。我们以修改图标为例,在资源面板中添加一个图标,其ID为IDI_ICON1:
i. 用AfxRegisterWndClass函数:
cs.lpszClass=::AfxRegisterWndClass(NULL,NULL,NULL, AfxGetApp()->LoadIcon(IDI_ICON1));
ii. 重新填写WNDCLASS
WNDCLASS wndClass;
wndClass.style=CS_HREDRAW;
wndClass.lpfnWndProc=(WNDPROC)::DefWindowProc;
wndClass.cbClsExtra=0;
wndClass.cbWndExtra=0;
wndClass.hInstance=::AfxGetInstanceHandle();
wndClass.hIcon=LoadIcon(::AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON1));
wndClass.hCursor=LoadCursor(NULL,IDC_NO);
LOGBRUSH lgbr;
lgbr.lbStyle=BS_SOLID;
lgbr.lbColor=RGB(255,255,0);
lgbr.lbHatch=HS_CROSS;
wndClass.hbrBackground=CreateBrushIndirect(&lgbr);
wndClass.lpszMenuName=NULL;
wndClass.lpszClassName="It315";
RegisterClass(&wndClass);
cs.lpszClass="It315";
此时会发现窗口的图标改变了,但背景和光标没有变。应将此段代码拷入CXxxView::PreCreateWindow中。
iii. 利用GetClassInfo函数:
WNDCLASS wndclass;
::GetClassInfo(AfxGetInstanceHandle(),cs.lpszClass,&wndclass);
wndclass.hIcon=::LoadIcon(::AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON1));
wndclass.lpszClassName="It315";
::RegisterClass(&wndclass);
cs.lpszClass="It315";
2. 在窗口产生以后修改:
在CMainFrame::OnCreate中
a. 去掉最大化按钮:
SetWindowLong(m_hWnd,GWL_STYLE,
::GetWindowLong(m_hWnd,GWL_STYLE) &~WS_MAXIMIZEBOX);
b. 修改图标:
SetClassLong(m_hWnd,GCL_HICON,(long)AfxGetApp()->LoadIcon(IDI_ICON1))
3. 制作动画图标:
a. 在资源面板中添加三个图标,其ID为IDI_ICON1,IDI_ICON2,IDI_ICON3。
b. 定义一个成员变量 ,用于装入三个图标的句柄:
HICON m_hIcon[3];
c. CMainFrame的构造函数中装入三个图标的句柄:
m_hIcon[0]=AfxGetApp()->LoadIcon(IDI_ICON1);
m_hIcon[1]=AfxGetApp()->LoadIcon(IDI_ICON2);
m_hIcon[2]=AfxGetApp()->LoadIcon(IDI_ICON3);
d. 在CMainFrame::OnCreate中设置定时器:
SetTimer(1,200,NULL);
e. 加入WM_TIMER的消息响应函数OnTimer,在其中加入代码:
static i=0;
SetClassLong(m_hWnd,GCL_HICON,(LONG)m_hIcon[i]);
i=++i%3;
四、贴图:
在资源面板中装入一幅位图,其ID为IDB_BITMAP1,在CXxxView::OnDraw中贴图。
贴图必须以如下步骤进行:
1. 产生一个兼容DC:
CDC dccompatible;
dccompatible.CreateCompatibleDC(pDC);
2. 装入位图:
CBitmap bmp;
bmp.LoadBitmap(IDB_BITMAP1);
3. 将位图选入兼容DC:
dccompatible.SelectObject(&bmp);
4. 将兼容DC上的内容拷入当前DC上:
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&dccompatible,0,0,SRCCOPY);
说明:
1. BitBlt会按1:1的比例将位图拷入,如果想在CxxxView上显示整个位图,可以用StretchBlt函数:
BITMAP bitmap;
bmp.GetBitmap(&bitmap);
pDC->StretchBlt(0,0,rect.Width(),rect.Height(),
&dccompatible,0,0,
bitmap.bmWidth,bitmap.bmHeight,SRCCOPY);
2. 系统在发出WM_PAINT消息之前,会发出WM_EARSEBKGND消息,去执行真正的刷新背景的操作,可将以上代码剪贴到WM_EARSEBKGND消息的响应函数OnEraseBkgnd中去。然后将原来的return CView::OnEraseBkgnd(pDC);改为returntrue;
五、操作状态栏:
1. 在状态栏显示鼠标坐标:
在 CxxxView中加入WM_MOUSEMOVE的响应函数OnMouseMove,加入代码:
CString str;
str.Format("X=%d,Y=%d",point.x,point.y);
((CMainFrame*)GetParent())->m_wndStatusBar.SetWindowText(str);
此种方法要将CMainFrame中的m_wndStatusBar改为public类型,再在CxxxView的执行文件中加入#include "MainFrm.h"。它破坏了CMainFrame的封装性,不建议使用。改用下面的方法:
((CFrameWnd*)GetParent())->SetMessageText(str);
或者
(CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetMessageBar()->SetWindowText(str);
2. 加入自定义状态栏,显示系统时间:
a. 在资源面板中加入一个字符串资源,其ID为IDS_TIME。
b. 将IDS_TIME加入CMainFrame中控制状态栏的数组static UINT indicators[]中,IDS_TIME在数组中的序号决定自定义状态栏的位置。
c. 在CMainFrame::OnCreate中设置定时器和自定义状态栏的大小:
SetTimer(2,1000,NULL);
CTimetm=CTime::GetCurrentTime();
CString str=tm.Format("%H:%M:%S");
CClientDCdc(this);
CSizesz=dc.GetTextExtent(str);
m_wndStatusBar.SetPaneInfo(2,111,SBPS_POPOUT,sz.cx);
m_wndStatusBar.SetPaneText(2,str);
d. 在OnTimer中加入代码:
if(2==nIDEvent)
{
CTimetm=CTime::GetCurrentTime();
CStringstr=tm.Format("%H:%M:%S");
m_wndStatusBar.SetPaneText(2,str);
}
else
…………
3. 在状态栏中加入进度条:
a. CmainFrame中定义成员变量,用于产生进度条:
CProgressCtrl m_ctrlProg;
b. 在资源面板中加入一个字符串资源,其ID为IDS_PROGRESS。
c. 将IDS_ PROGRESS加入CMainFrame中控制状态栏的数组static UINT indicators[]。
d. 在CMainFrame中加入WM_PAINT的响应函数CMainFrame::OnPaint()
CRect rect;
m_wndStatusBar.GetItemRect(2,&rect);
if(m_ctrlProg.m_hWnd)
{
m_ctrlProg.SetWindowPos(NULL,rect.left,rect.top,
rect.Width(),rect.Height(),SWP_NOZORDER);
}
else
m_ctrlProg.Create(WS_CHILD|WS_VISIBLE,rect,&m_wndStatusBar,111);
e. 在OnTimer中加入触发进度条的代码:
m_ctrlProg.StepIt();
第十二课: 对话框综合应用
在第六课的基础上,我们添加功能。增加一个用于设置笔的样式和宽度的对话框,和用于设置颜色的对话框。
一、设置笔的样式和宽度的对话框
1. 在资源面板中添加一个对话框,在上面添加一个文本框用于改变笔的宽度,三个单选框用于选择笔的样式,一个组框用于画一条演示的线。完成后的对话框如下图:
2. 为对话框生成一个从Cdialog派生的类CsetDlg,将三个单选框设置为一组,分别为文本框和这组单选框设置一个捆绑变量。
UINT m_nWidth;
int m_nStyle;
在对话框的构造函数中为它们赋初值:
m_nWidth = 0;
m_nStyle = 0;
3. 为文本框添加EN_CHANGE消息的响应函数OnChangeEdit1(),这个函数将在文本框的文字发生改变时被调用。在其中加入下列代码:
UpdateData();
CPennewpen(m_nStyle,m_nWidth,RGB(0,0,255));
CclinetDC dc(this);
dc.SelectObject(&newpen);
CRect rect;
GetDlgItem(IDC_EXAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);
dc.MoveTo(rect.left,rect.top+rect.Height()/2);
dc.LineTo(rect.right,rect.top+rect.Height()/2);
这段文字生成一个用户定制的笔,然后在示例的组框上画一条演示的线。其中IDC_EXAMPLE是组框的ID。
注意:
a. 在生成新笔之前,一定要UpdateData()将控件的值刷新到它们的捆绑变量中去,否则产生的笔将不正确。
b. GetWindowRect函数得到的是相对于屏幕的坐标,而DC中的坐标总是相对于窗口的。因此要用ScreenToClient转换一下。否则画出的线的位置将不正确,而且有时可能看不到。
4. 此时运行程序,在用户输入一个比较大的宽度(比如5),再输入一个较小的宽度(比如1)时,将看不到细线,因为细线在粗线的上面画出,根本看不到效果。应将以上代码剪切至OnPaint()中(把其中的CclinetDC dc(this);语句注释,然后在OnChangeEdit1()调用Invalidate()函数。
5. 分别为三个单选按钮添加响应函数,OnRadio1(),OnRadio2(),OnRadi,3(),调用Invalidate()函数。
6. 在View中添加一个菜单响应函数OnDrawSetmode(),加入代码:
CSetDlg dlg;
dlg.m_nStyle=m_nStyle;
dlg.m_nWidth=m_nWidth;
dlg.m_clrDraw=m_clrDraw;
if(IDOK==dlg.DoModal())
{
m_nStyle=dlg.m_nStyle;
m_nWidth=dlg.m_nWidth;
}
7. 在OnLButtonUp中加入:
CClientDC dc(this);
CPen newpen(m_nStyle,m_nWidth,RGB(0,0,255));
CPen*oldpen=dc.SelectObject(&newpen);
switch(m_nDrawMode)
{
case 0:
dc.SetPixel(point.x,point.y, RGB(0,0,255));
break;
case 1:
dc.SetROP2(R2_MERGEPENNOT);
dc.MoveTo(m_ptOrigin);
dc.LineTo(m_ptEnd);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
m_ptEnd=point;
break;
case 2:
dc.SetROP2(R2_MERGEPENNOT);
dc.Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptEnd.x,m_ptEnd.y);
dc.Rectangle(m_ptOrigin.x,m_ptOrigin.y,point.x,point.y);
m_ptEnd=point;
break;
case 3:
dc.SetROP2(R2_MERGEPENNOT);
dc.Ellipse(m_ptOrigin.x,m_ptOrigin.y,m_ptEnd.x,m_ptEnd.y);
dc.Ellipse(m_ptOrigin.x,m_ptOrigin.y,point.x,point.y);
m_ptEnd=point;
break;
default:
break;
}
8. 在View的cpp文件中加入#include "setdlg.h"。
二、设置笔的颜色的对话框
MFC为我们提供了标准的颜色对话框CcolorDialog,可以用它来生成设置颜色对话框。
1. 在View中加入一个用于记录颜色的成员变量:
COLORREF m_clrDraw;
在View的构造函数中将其初始化:
m_clrDraw=RGB(0,0,255);
2. 在View中添加一个菜单响应函数OnDrawSetcolor(),加入代码:
CColorDialog dlg;
dlg.m_cc.Flags|= CC_FULLOPEN | CC_RGBINIT;
dlg.m_cc.rgbResult =m_clrDraw;
if(IDOK==dlg.DoModal())
{
m_clrDraw=dlg.GetColor();
}
3. 在OnLButtonUp中将生成笔的代码改为:
CPennewpen(m_nStyle,m_nWidth,m_clrDraw);
4. 在CsetDlg中加入一个用于记录颜色的成员变量,在OnDrawSetmode() 中对话框显示之前为其赋值,
dlg.m_clrDraw=m_clrDraw;
5. 将CsetDlg的OnPaint()中的生成笔的代码改为:
CPennewpen(m_nStyle,m_nWidth,m_clrDraw);
三、设置对话框中的控件的字体,颜色等
1. 在CsetDlg中加入成员变量fn,br,在构造函数中对它们进行初始化
br.CreateSolidBrush(RGB(0,0,255));
fn.CreatePointFont(300,"宋体");
2. 在CsetDlg中WM_CTLCOLOR消息的响应函数OnCtlColor,加入下列代码:
if(pWnd->GetDlgCtrlID()==IDC_SAMPLE)
pDC->SetTextColor(RGB(0,0,255));
elseif(pWnd->GetDlgCtrlID()==IDC_EDIT1)
{
pDC->SetBkMode(TRANSPARENT);
return (HBRUSH)br.m_hObject;
}
elseif(pWnd->GetDlgCtrlID()==IDC_TEST)
{
pDC->SelectObject(&fn);
}
说明:
a. 这段代码将示例组框(IDC_SAMPLE)的文本颜色设置为蓝色,将文本框(IDC_EDIT1)的背景设置为蓝色,将标签按钮设置为宋体300。如下图:
b. 判断当前正在画哪个控件应该用
if(pWnd->GetDlgCtrlID()==IDC_SAMPLE)
若改为
if(pWnd==GetDlgItem(IDC_SAMPLE))
将不起作用,因为OnCtlColor传入的参数pWnd和GetDlgItem返回的都是临时的窗口指针。
c. 修改窗口的背景应在OnCtlColor中返回一个刷子的句柄,
return(HBRUSH)br.m_hObject;
而不应该用
pDC->SelectObject(&br);
四、使用CbuttonST
1. 将CbuttonST类的两个文件BtnST.cpp和BtnST.h复制到当前工程的目录下,并通过ProjectàAdd ToProjectàFiles菜单加入到工程中。
2. 为“确定”按钮捆绑一个控制变量m_btnOk,其类型定义为CbuttonST。
3. 在OnInitDialog()中加入代码,调用CbuttonST的函数:
m_btnOk.SetActiveBgColor(RGB(0,0,255));
m_btnOk.SetActiveFgColor(RGB(255,0,0));
第十三课:重绘
一、基本知识:
1. OnPaint和OnDraw函数
View的父类的OnPaint函数调用了OnDraw函数,若在子类为WM_PAINT消息添加响应函数OnPaint,OnDraw函数将不会被调用。
2. CpaintDC和
CpaintDC的构造函数中调用了BeginPaint(),析构函数中调用了EndPaint();
CclietnDC的构造函数中调用了GetDC(),析构函数中调用了ReleaseDC()。
而BeginPaint(),EndPaint()只能用于响应WM-PAINT消息,否则将会出错。
二、利用动态数组:
1. 定义结构体LINE,用于保存线的数据。
struct LINE
{
CPoint m_pt1;
CPoint m_pt2;
};
2. 在View中定义一个动态数组,保存每一根线的指针。
CPtrArraym_ptrLines;
定义两个Cpoint的成员变量,保存线的起点和终点:
CPoint m_ptOld;
CPoint m_ptNew;
3. 在View中加入WM_LBUTTONDOWN,WM_LBUTTONUP的响应函数,在OnLButtonDown中为m_ptNew赋值,
m_ptOld=point;
4. 在OnLButtonUp中加入代码:
m_ptNew=point;
CClientDC dc(this);
dc.MoveTo(m_ptOld);
dc.LineTo(point);
LINE*pLn=new LINE;
pLn->m_pt1=m_ptOld;
pLn->m_pt2=m_ptNew;
m_ptrLines.Add(pLn);
5. 在OnDraw()中加入:
int sum=m_ptrLines.GetSize();
for(inti=0;i<sum;i++)
{
pDC->MoveTo(((Line*)m_ptrLines.GetAt(i))->m_pt1);
pDC->LineTo(((Line*)m_ptrLines.GetAt(i))->m_pt2);
}
6. 加入滚动条:将View的cpp文件和h文件中的CView全部替换成CScrollView。
7. 在view中加入虚函数OnInitialUpdate(),这个函数在View第一次刷新前被调用,在其中加入代码:
SetScrollSizes(MM_TEXT,CSize(1024,768));
这个函数也可在View的构造函数中调用。
8. 在OnLButtonUp中生成DC后加入
OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOld);
dc.DPtoLP(&m_ptNew);
三、利用CmetaFileDC重绘
1. 在View中定义成员变量:
CMetaFileDC m_dcMetaFile;
2. 在View的OnCreate中加入代码:
m_dcMetaFile.Create();
3. 在View的OnLButtonUp中,注释有关数组的代码,加入:
m_dcMetaFile.MoveTo(m_ptOld);
m_dcMetaFile.LineTo(m_ptNew);
4. 在OnDraw()中
HMETAFILE hmetafile;
hmetafile=m_dcMetaFile.Close();
pDC->PlayMetaFile(hmetafile);
m_dcMetaFile.Create();
m_dcMetaFile.PlayMetaFile(hmetafile);
::DeleteMetaFile(hmetafile);
5. 保存文件:
加入菜单响应函数,OnFileSave,加入代码:
HMETAFILE hmetafile;
hmetafile=m_dcMetaFile.Close();
::CopyMetaFile(hmetafile,"c:\\2.ddd");
m_dcMetaFile.Create();
m_dcMetaFile.PlayMetaFile(hmetafile);
::DeleteMetaFile(hmetafile);
6. 读出文件:
加入菜单响应函数,OnFileLoad,加入代码:
HMETAFILEhmetafile;
hmetafile=::GetMetaFile("c:\\2.ddd");
m_dcMetaFile.PlayMetaFile(hmetafile);
::DeleteMetaFile(hmetafile);
Invalidate();
四、利用兼容DC重绘:
1. 在View中定义成员变量:
CDC m_dcCompa;
2. 在OnLButtonDown中加入代码:
CClientDCdc(this);
if(!m_dcCompa.m_hDC)
{
m_dcCompa.CreateCompatibleDC(&dc);
CBitmap bmp;
CRect rect;
GetClientRect(&rect);
bmp.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());
m_dcCompa.SelectObject(&bmp);
m_dcCompa.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
}
m_dcCompa.MoveTo(m_ptOld);
m_dcCompa.LineTo(m_ptNew);
3. 在OnDraw中加入:
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompa,0,0,SRCCOPY);
第十四课:文件和注册表读写
一、文件操作:
1. C的方式:
FILE *p;
p=fopen("c:\\1.txt","w");
fwrite("abc",1,4,p);
fclose(p);
2. C++的方式:
ofstream f("c:\\1.txt");
f.write("hello",5);
3. MFC的方式:
I. 写文件:
CFilef("c:\\1.txt",CFile::modeWrite|CFile::modeCreate);
f.Write("hello",5);
a.几个标志的作用:
CFile::modeCreate:没有指定的文件就产生一个新文件,有就打开该文件,并将它裁剪到0;
CFile::modeNoTruncate :打开文件时不裁剪到0;
b.写数据到文件末尾:
CFile f("c:\\1.txt",CFile::modeWrite|CFile::modeCreate|
CFile::modeNoTruncate);
f.SeekToEnd();
f.Write("hello",5);
II. 读文件:
CFile f("c:\\1.txt",CFile::modeRead);
char buf[10];
memset(buf,0,10);
f.read(buf,5);
MessageBox(buf);
III. 文件对话框:
CFileDialog dlg(false); //生成保存对话框
dlg.m_ofn.lpstrFilter="abc文件(*.abc)\0*.abc\0文本\0*.txt\0all
file\0*.*\0\0";
//设置对话框的过滤器
dlg.m_ofn.lpstrTitle="保存"; //修改对话框的标题
dlg.m_ofn.lpstrDefExt="txt"; //设置对话框的默认扩展名
if(IDOK==dlg.DoModal())
{
ofstreamf(dlg.GetPathName());
//得到用户选择的文件的全路径名
f.write(aaa",3);
}
4. 文本文件和二进制文件的区别:
文件文件是一种特殊的二进制文件,当它遇到回车键10时,写入文件时会自动地在它的前面加一个13,而读出文件时遇到13 10 的组合时,又把它还原到10。而二进制文件就是把数据原封不动的写入文件,原封不动的再读取出来,没有文本文件的这种转换操作。
下面的代码演示了之间的这种区别:
写入文件时:
ofstream f("c:\\1.txt");
char buf[3];
buf[0]='a';
buf[1]='\n';
buf[2]='b';
f.write(buf,3);
读出文件时:
ifstreamf("c:\\1.txt");
f.setmode(filebuf::binary);
charbuf[5];
memset(buf,0,5);
f.read(buf,5);
CStringstr;
str.Format("%d,%d,%d,%d",buf[0],buf[1],buf[2],buf[3]);
MessageBox(str);
在写入文件时不指定格式,文件将按文本格式存储,此时读出文件时指定二进制格式,读出的数据如下图:
如果注释f.setmode(filebuf::binary);语句,文件将按文本文件读出,如下图:
二、注册表的操作
1. 读写win.ini文件:
a.写:
WriteProfileString("haha","test","123");
b.读:
char buf[5];
GetProfileString("haha","test","000",buf,5);
C.实现一个简单的计数器:
charbuf[5];
intx=GetProfileInt("haha","test",0);
sprintf(buf,"%d",x+1);
WriteProfileString("haha","test",buf);
2. 读写注册表:
a.使用旧函数:
在CwinApp中旧函数数据将写到注册表中,在InitInstance()中加入代码:
SetRegistryKey(_T("http://www.it315.org"));
int x =GetProfileInt("It315","count",0);
if(x>5)
return false;
WriteProfileInt("It315","count",x+1);
数据将写到注册表的
HKEY_CURRENT_USER\Software\http://www.it315.org\FileOp\It315键下的count中。
b. 使用新函数在注册表任意位置读写:
写:
RegCreateKey(HKEY_LOCAL_MACHINE,"software\\http://www.it315.org",&hKeyIt315);
读:
char*buf;
longlen;
RegQueryValue(HKEY_LOCAL_MACHINE,
"software\\http://www.it315.org\\It315\\abc",NULL,&len);
buf=newchar[len];
RegQueryValue(HKEY_LOCAL_MACHINE,
"software\\http://www.it315.org\\It315\\abc",buf,&len);
MessageBox(buf);
第十五课:文档系列化
一、在View中存取文件:
1. 用Carchive写文件:
CFileDialog dlg(false);
if(IDOK==dlg.DoModal())
{
CFilef(dlg.GetPathName(),CFile::modeCreate|CFile::modeWrite );
CArchivear(&f,CArchive::store);
CStringstr("abc");
ar<<3<<'c'<<6.6f<<str;
}
2. 用Carchive读文件:
CFileDialogdlg(true);
if(IDOK==dlg.DoModal())
{
CFilef(dlg.GetPathName(),CFile::modeRead);
CArchivear(&f,CArchive::load);
int x;
char c;
float d;
CString st;
ar>>x>>c>>d>>st;
CStringstr;
str.Format("%d,%c,%f,%s",x,c,d,st);
MessageBox(str);
}
说明:
a. 在代码中一个小数被默认为double类型;
b. Carchive 是Cfile的包装类,简化了存取文件的操作,但它只能存取有限种数据类型。
二、在Docment类中存取文件:
在Docment类的Serialize函数中加入:
if(ar.IsStoring())
{
CString str("abc");
ar<<3<<'c'<<6.6f<<str;
}
else
{
int x;
char c;
float d;
CString st;
ar>>x>>c>>d>>st;
CStringstr;
str.Format("%d,%c,%f,%s",x,c,d,st);
MessageBox(NULL,str,0,0);
}
说明:
1. 这段代码等效于“一”中的存取文件的代码。
2. Serialize函数会在用户点击MFC提供的有关文件的菜单时自动被调用,在调用之前,MFC会首先生成一个标准文件对话框,再根据用户的选择生成一个Cfile,再生成一个Carchive,把它传入Serialize。这大大简化了程序员的工作。
三、画线并将数据保存成文件:
1、 定义一个可序列化的类Cline:
a. 新建一个类,从Cobject派生,(在Class type中要选Generic Class,否则基类选不了Cobject。在类名中输入Cline;
b. 在Cline中覆盖Serialize成员函数。
c. 在类的声明中使用DECLARE_SERIAL宏:
DECLARE_SERIAL( Cline)
d. 定义一个一不带参数的构造函函数。
e. 在类的实现文件中使用IMPLEMENT_SERIAL宏:
IMPLEMENT_SERIAL( CMygraph, CObject, 1 )
f.加入两个成员变量,记录一条线的起点和终点:
CPoint m_ptOrigin;
CPoint m_ptEnd;
2、 在Document类中定义一个成员变量,用于保存每一根线的数据:
CObArray m_lnArray;
3、 在View类中加入WM_LBUTTONDOWN,WM_LBUTTONUP的响应函数,并加入一个成员变量,用于记录一条线的起点:
CPointm_ptOrigin;
4、 在OnLButtonDown中:
m_ptOrigin=point;
5、 在OnLButtonUp中:
Cline *pLine=newCline();
pline->m_ptOrigin=m_ptOrigin;
pline->m_ptEnd=point;
GetDocument()->m_lnArray.Add(pLine);
Invalidate();
6、 在View类的OnDraw函数中加入:
intx=pDoc->m_graphArray.GetSize();
for(int i=0;i<x;i++)
{ pDC->MoveTo(((Cline*)pDoc->m_lnArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CLine*)pDoc->m_lnArray.GetAt(i))->m_ptEnd);
}
7、 在Document类中的Serialize函数中:
m_lnArray.Serialize(ar);
四、DeleteContent:
在document类中加入DeleteContent虚函数,它会在用户点击“打开”和“新建”菜单时自动调用,这是删除文档数据的最好时机。删除时有两种常见的错误:
1.错误方法一:
for (inti=0;i<m_lnArray.GetSize();i++)
delete (Cline*)m_lnArray.GetAt(i);
m_lnArray.RemoveAll();
原因:每循环一次,m_lnArray.GetSize()返回的值都会减小,造成数据的漏删。
2.错误方法二:
int index=lnArray.GetSize();
for(int i=0;i<index;i++)
{
delete(Cline*)m_graphArray.GetAt(i);
m_lnArray.RemoveAt(i);
}
原因:每删除一个数组元素,数组都会重新排序,它的下标会变。
3.正确方法:
intindex=m_lnArray.GetSize();
while(index--)
delete(Cline*)m_lnArray.GetAt(index);
m_lnArray.RemoveAll();
第十六课:网络编程
一、UDP发送端:
建一个windows控制台工程,在main函数中加入:
1. 初始化DLL:
WSADATAwsaData;
WSAStartup(MAKEWORD(1,1),&wsaData);
Windows socket编程用到了微软提供的DLL,在使用前要对它进行初始化,Socket编程用到的DLL也分为不同的几个版本,WSAStartup函数的第一个参数就是用户所请求的版本号。
2. 生成socket:
SOCKET s= socket(AF_INET,SOCK_DGRAM,0);
3. Bind:
SOCKADDR_IN sockSrc;
sockSrc.sin_family =AF_INET;
sockSrc.sin_port =htons(3000);
sockSrc.sin_addr .S_un .S_addr=htonl(INADDR_ANY);
bind (s,(SOCKADDR*)&sockSrc,sizeof(SOCKADDR));
说明:
a.bind函数要指定IP地址和端口号:
IP地址:必须是执行这个程序所在计算机的IP地址,将其设定为INADDR_ANY,系统会自动将计算机的正确IP地址填入。
端口号:由于一台计算机可以启动多个网络程序,而IP地址只能保证网络数据到达指定的计算机,所以要指定端口号以区别数据是发给哪个网络程序。端口号是一个两个字节的整数,应把它设在1024到5000之间的值。若设为0,系统会将其设定为一个适当的数值。
B.由于各种计算机的数值读取方式不同(比如PC与UNIX系统就不相同),所以 在指定端口号和IP地址时,要把它们从主机次序转换到网络次序。Htons, htonl函数即实现的这种功能。
4. 发送数据:
SOCKADDR_INsockDest;
sockDest.sin_family=AF_INET;
sockDest.sin_port=htons(3001);
sockDest.sin_addr.S_un.S_addr =inet_addr("192.168.8.36");
char buf[1024];
strcpy(buf,"helloIt315!");
sendto(s,buf,strlen(buf)+1,0,(SOCKADDR*)&sockDest,sizeof(SOCKADDR));
其中:inet_addr用于将固定格式的字符串(形如"192.168.8.36")转换为一个整数。对应的一个函数inet_ntoa用于将整数转换为"192.168.8.36"形式的字符串。
5. 关闭socket:
closesocket(s);
6. 调用
WSACleanup();
7. 包含头文件:
#include "winsock2.h"
8. 加入LIB:
点击菜单ProjectàSettingàLink 在Object/librarymodules:中加入Ws2_32.lib
二、UDP接收端:
接收端和发送端基本相同,只有第4点不同,将其改为接收数据:
SOCKADDR_IN sockFrom;
char buf[1024];
memset (buf,0,1024);
int len=sizeof(SOCKADDR);
intx=recvfrom(s,buf,1024,0,(SOCKADDR*)&sockFrom,&len);
printf("%s\n",buf);
在main函数前加入#include"stdio.h"。
注意:一定要将接收端bind的端口号设为发送端发送数据的端口号,本例中设为3001。
现在可以测试程序,应该先启动接收端,再启动发送端,因为UDP是一个不可靠的协议,并不保证发送的数据一定到达接收端。
三、TCP服务器端:
1. 初始化DLL:
WSADATAwsaData;
WSAStartup(MAKEWORD(1,1),&wsaData);
2. 生成socket:
SOCKET s=socket(AF_INET, SOCK_STREAM,0);
3. Bind:
SOCKADDR_IN sockSrc;
sockSrc.sin_family =AF_INET;
sockSrc.sin_port =htons(3000);
sockSrc.sin_addr .S_un .S_addr=htonl(INADDR_ANY);
bind (s,(SOCKADDR *)&sockSrc,sizeof(SOCKADDR));
4. 设定socket为监听状态:
listen(s,5);
其中,第二个参数是可以等待连接的最大数目,但它不是一次在给定端口可以建立的连接的最大数目,而是可以放在队列中等待应用程序来接受它们的连接或部分连接的最大数目。
5. 接受连接:
int len=sizeof(SOCKADDR_IN);
SOCKADDR_IN addrClient;
SOCKETsockClient=accept(s,(SOCKADDR*)&addrClient,&len);
Accept函数会产生一个新的socket,在这个新产生的socket上和客房端进行数据交流。
6. 发送和接收数据:
char buf[1024];
strcpy(buf, "hello the world");
send(sockClient,buf,strlen(buf),0);
memset(buf ,0,1024);
recv(sockClient,buf,1024,0);
printf(buf);
7. 关闭socket:
closesocket(sockClient);
closesocket(s);
8. 调用
WSACleanup();
9. 包含头文件:
#include "winsock2.h"
#include "stdio.h"
10. 加入LIB:
点击菜单ProjectàSettingàLink 在Object/librarymodules:中加入Ws2_32.lib
四、TCP客房端:
前两点与TCP服务器端相同。
3.发出连接请求:
SOCKADDR_IN addrServer;
addrServer.sin_family =AF_INET;
addrServer.sin_port=htons(3000);
addrServer.sin_addr .S_un .S_addr=inet_addr("192.168.8.36");
connect(s,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));
4.发送和接收数据:
charbuf[1024];
memset(buf,0,1024);
recv(s,buf,1024,0);
printf(buf);
scanf("%s",buf);
send(s,buf,strlen(buf),0);
下同TCP服务器端的7,8,9,10。
测试程序时,应该先启动服务器端,再启动客房端。
第十七课:多线程
一、概念:
进程:一个进程就是一个运行的程序,它有独立的内存、文件句柄和其他系统资源。当启动一个进程时,操作系统会为此进程建立一个4GB的地址空间,进程是操作系统分配内存地址空间的单位。
线程:是操作系统分配处理器时间的最基本单元。所以一个进程必须包含一个线程,我们称之为主线程。如果需要,进程可以产生更多的线程,让CPU在同一时间执行 不同段落的代码。
二、举例:
1. 在单线程中执行多个死循环:
建立一个控制台工程,加入一个cpp文件,编写如下代码:
#include "stdio.h"
void fun()
{
while(1)
{
printf("thread2 is rurnning!\n");
}
}
void main()
{
fun();
while(1)
{
printf("thread1 is running!\n");
}
}
此时只能打出thread 2 is rurnning!,因为CPU总是在执行fun函数的代码 。没有机会向下执行其余代码。
2. 在单线程中执行多个死循环:
#include "stdio.h"
#include "windows.h"
DWORD WINAPI fun(LPVOID lpParameter)
{
while(1)
{
printf("thread2 is rurnning!\n");
}
}
void main()
{
CreateThread(NULL,0,fun,0,0,0);
while(1)
{
printf("thread1 is running!\n");
}
}
此时“thread 1 is rurnning!”,“thread 2 isrurnning!”都能打出来,两个死循环在同时运行。
三、编写一个聊天程序:
1. 建立一个基于对话框的MFC工程,删除提示用的标签控件。加入一个用于显示聊天对方发来的信息的列表框,一个用于输入输入发送给别人的信息的文本框,一个用于输入IP地址的IP Address控件。
2. 定义一个成员变量
SOCKET m_sockChat;
3. 在对话框中加入一个成员函数InitSocket(),对m_sockChat进行初始化:
m_sockChat=socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_INaddrChat;
addrChat.sin_family=AF_INET;
addrChat.sin_port=htons(3000);
addrChat.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
bind(m_sockChat,(SOCKADDR*)&addrChat,sizeof(SOCKADDR));
4. 加入OnOK函数,注释其中的CDialog::OnOK();语句,然后加入代码:
CString str;
GetDlgItemText(IDC_EDIT1,str);
//得到用户在文本框中输入的字符串
SOCKADDR_INaddrDest;
addrDest.sin_family=AF_INET;
addrDest.sin_port=htons(3000);
DWORD dwIP;
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
//得到用户在IP地址控件中输入的地址
addrDest.sin_addr.S_un.S_addr=htonl(dwIP); sendto(m_sockChat,str,str.GetLength(),
0,(SOCKADDR*)&addrDest,sizeof(SOCKADDR));
//发送数据到达指定的计算机
SetDlgItemText(IDC_EDIT1,"");
//将文本框清空
5. 定义一个结构体,用于向线程函数传递参数:
struct RECVPARAM
{
SOCKET sock;
HWND hwnd;
};
6. 在对话框的OnInitDialog()函数中加入:
InitSocket(); //初始化socket
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->SetAddress(192,168,8,255);
//设置IP地址控件的初始地址
RECVPARAM*precvparam=new RECVPARAM();
precvparam->sock=m_sockChat;
precvparam->hwnd=this->m_hWnd;
::CreateThread(NULL,0,recvproc,(LPVOID)precvparam,0,0);
//启动一个线程,在线程函数中接收数据
7. 定义线程函数:
DWORD WINAPI recvproc(LPVOID lpParameter)
{
SOCKETsockChat=((RECVPARAM*)lpParameter)->sock;
HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;
//取出传递进来的参数
detele (RECVPARAM*)lpParameter;
char buf[1024]; //定义用于接收数据的缓冲区
SOCKADDR_INaddrSrc;
int len,x;
char *str;
while (1)
{
memset(buf,0,1024); //清空缓冲区
len=sizeof(SOCKADDR);
recvfrom(sockChat,buf,1024,0,(SOCKADDR*)&addrSrc,&len);
//接收数据
sprintf(buf,"%s:%s:%d",buf,
inet_ntoa(addrSrc.sin_addr),ntohs(addrSrc.sin_port));
//得到对方的IP地址和端口号
x=strlen(buf);
str=newchar[x+1];
strcpy(str,buf); //将得到的数据转存
::PostMessage(hwnd,WM_USER+1,0,(LPARAM)str);
//向指定的窗口发送消息
}
}
8. 在对话框的消息映射表中添加:
ON_MESSAGE(WM_USER+1, OnRecvData)
9. 在对话框中添加成员函数OnRecvData:
LRESULT CChatDlg::OnRecvData(WPARAM wParam, LPARAMlParam)
{
((CListBox*)GetDlgItem(IDC_LIST1))->InsertString(0,(char*)lParam);
//将接收到的信息加入列表框中
delete(char*)lParam; //释放在线程中用new分配的空间
return 1;
}
第十八课:动态链接库
一、概念:
1、 静态链接库:
应用程序从函数库中得到所的函数的执行代码,然后把招生代码自身的执行文件中,应用程序在运行时,不再需要函数库的支持。
2、 动态链接库:
应用程序的中不包含函数库中的函数的执行代码,编译和连接时只是包含包含它们的参考,运行时再将它们的执行代码加入内存,所以在程序运行时需要函数的支持。
二、编写DLL:
建立一个Windows Dynamic-Link Library,选择建一个An Empty Dll Project,加入一个CPP文件,
int add(intx,int y)
{
return x+y;
}
extern"C" _declspec (dllexport) int add3(int x,int y,int z)
{
return add(x,y)+z;
}
说明:
1. Add是一个供DLL中其它函数调用的函数, Add3是DLL提供给其它应用程序调用的函数。
2. 当DLL中含有输出函数时,编译DLL时会生成DLL文件和LIB文件。LIB文件称为DLL的导入库,它是一个特殊的库文件。它不包含执行代码,只是用来提供给链接器关于DLL函数在DLL中的入口信息,从而使得可执行程序 中也不会包含所调用DLL函数 的代码,只保留对DLL函数的动态链接参考。
3. 导出函数的两种方法:
a. 使用微软专用的_declspec (dllexport):(如上面的例子)
cpp文件在编译为OBJ文件时要对函数进行重新命名,C语言会把函数name重新命名为_name,而C++会重新命名为_name@@decoration,
extern"C"表示用C语言的格式将函数重命名。
b. 使用模块定义文件:
模块定义文件即DEF文件,是包含一个或多个模块定义语句的文本文件,用来描述动态链接库的各种属性。
一个最小的·DEF文件包括以下模块定义语句:
l 第一条语句必须是LIBRARY语句,用来说明动态链接库的名字。
l 有EXPORTS语句之后列出动态链接库要输出的函数。用户可以在每个要输出的函数指定一个输出序列号,这只要在对应的函数名之后加上@符号心腹一个数字即可。注意:这个数字应该是从1到输出函数总数之间的没有重复的数字。指定函数的序列号后,动态加载该函数时,就会通过序列号而不是函数名来检索该函数,因此处理速度将会加快,战胜的内存会减少,从而增加动态链接时的效率。
将上面的例子改为用DEF文件输出:
int add(int x,int y)
{
return x+y;
}
extern "C" intadd3(int x,int y,int z)
{
return add(x,y)+z;
}
在工程中加入一文本文件,其名字为dll.def,加入以下语句:
LIBRARY dll
EXPORTS
add3 @ 1
三、访问动态链接库:
新建一个基于对话框的MFC工程,
1. 静态调用:
通过编译器提供给应用程序关于DLL的名称,以及DLL函数的链接参考。这种方式不需要在程序中用代码将DLL加载到内存。
a. 将DLL和LIB文件拷贝到工程目录下,最好在工程的DEBUG目录下也拷贝一份。
b. 在ProjectàSettingsàLink中的Object/LibraryModules:填入LIB文件的名字。本例中是dll.lib。
c. 在对话框中加入一个按钮控件,添加它的响应函数OnButton1(),在其中加入调用代码:
CString str;
str.Format("%d",add3(3,4,5));
MessageBox(str);
d. 加入函数声明:
l 用_declspec (dllexport)导出函数的DLL
extern "C"_declspec(dllimport) int add3(int x,int y,int z);
l 用DEF文件导出函数的DLL
int add3(intx,int y,int z);
2. 动态调用:
在程序中用语句显式地加载DLL,编译器不需要知道任何关于DLL的信息。
a. 将DLL文件拷贝到工程目录下,最好在工程的DEBUG目录下也拷贝一份。
b. 在对话框中加入一个按钮控件,添加它的响应函数OnButton1(),在其中加入调用代码:
CString str;
typedef int (*PADD3)(int x,int y,int z);
//定义一种新的数据类型—指向函数的指针
PADD3 add3;
HINSTANCE hDll=LoadLibrary("dll");
//将动态链接库加载到内存
add3=(PADD3)GetProcAddress(hDll,"add3");
//得到DLL中指定函数的指针
str.Format("%d",add3(3,4,5));
MessageBox(str);
FreeLibrary(hDll);//释放应用程序对DLL的控制权
说明:如果DLL用DEF文件导出函数时为其指定了序列号,比如1,则
add3=(PADD3)GetProcAddress(hDll,"add3");
可以改为:
add3=(PADD3)GetProcAddress(hDll,MAKEINTRESOURCE(1));
四、其它:
1. Windows如何定位DLL
不论是静态调用还是动态调用DLL,Windows会按以下顺序寻找DLL:
a. 当前路径
b. Windows的system路径。用API函数GetSystemDirectory可以得到;
c. Windows路径。用API函数GetWindowsDirectory可以得到;
d. 环境变量PATH所指定 的路径。
2. DLL也可以有一个入口函数DllMain(),当然也可以没有。如果有,DllMain会在进程加载、进程缷载、线程加载和线程缷载DLL时自动调用。
3. 为了用户使用方便,应该将导出函数的声明放在一个头文件中。可以使用预处理命令简化更换__declspec(dllexport)和__declspec(dllimport)的操作。这样,DLL和应用程序 可以使用相同的头文件:
#ifdefDLL_EXPORTS
#define DLL_API__declspec(dllexport)
#else
#define DLL_API__declspec(dllimport)
#endif
第十九课:ActiveX控件
本讲主要内容:
1、 首先认识什么叫ActiveX控件(VB演示MonthView控件与DatePicker控件)
演示一下在VB下的如何得到DTPicker下的日期内容及星期的内容,进而讲到属性,除了属性外还有方法和事件、属性页依次演示。
2、 VB中是很方便的,要是在VC中也能用就好了,开始在VC中演示如何使用ActiveX控件。同上一例子。(更进一步讲到属性、方法、事件的概念及容器与控件之间的关系等概念)演示如何得到日期的各个部分的内容。在此要讲到变体类型的变量(VARIANT结构及CComVariant类型及_variant_t类型的区别与联系)
3、 ActiveX控件中使用的技术分析,为了保证在各种开发工具上都能使用,对于在其内容实现时会有一些具体的要求。(字符串要使用(BSTR),颜色OLE_CORLOR,变体类型)
4、 注册与反注册(Regsvr32.exe工具的使用)
5、 创建一个ActiveX控件的方法,及常用工具的比较
6、 自动动手写一个时钟控件(工具介绍及常用选项说明及向导生成代码分析)
7、 修改图标
8、 在OnDraw中增加控件外观代码如下:(注意颜色转换的问题)
CBrush br;
br.CreateSolidBrush(TranslateColor(GetBackColor()));
pdc->FillRect(rcBounds,&br);
CTime tm=CTime::GetCurrentTime();
CString str=tm.Format("%H:%M:%S");
pdc->SetBkMode(TRANSPARENT);
pdc->SetTextColor(TranslateColor(GetForeColor()));
pdc->TextOut(0,0,str);
9、 解决不能自动更新的问题
a) 为控件响应WM_CREATE和WM_TIMER事件
b) 在OnCreate()中增加
SetTimer(1,1000,NULL);
c) 在响应onTimer中增加
InvalidateControl();
10、 增加一个新的属性,时间间隔(short UpdateInterval)
a) 在OnUpdateIntervalChanged 函数中修改(注:此时应该修改定时器的时间间隔)
SetTimer(1,m_updateInterval,NULL);
b) 进一步分析问题,解决输入时间不正确时的对策,对OnUpdateIntervalChanged函数中的代码做如下修改
if(m_updateInterval<1000||m_updateInterval>5000)//解决输入值不合要求的问题
m_updateInterval=1000;
m_updateInterval=m_updateInterval/1000*1000;//解决输入值不是整数的问题
SetTimer(1,m_updateInterval,NULL);
SetModifiedFlag();
11、 增加对字体的支持OnDraw()
CFont* pOldFont=SelectStockFont(pdc);
pdc->TextOut(0,0,tm.Format("%H:%M:%S"));
pdc->SelectObject(pOldFont);
12、 增加一个方法Beep()
MessageBox(“Thisis in Beep Method!”);
13、 增加自定义属性页
a) 修改属性页资源
b) 在DoPropExchange函数中增加一个相应的属性子项
PX_Short(pPX,"UpdateInterval",m_updateInterval,1000);
c) 增加一个成员变量并将其与属性相关的部分补全
14、 增加系统属性页
修改属性页映射宏
BEGIN_PROPPAGEIDS(CMyActiveXCtrl, 3)
PROPPAGEID(CMyActiveXPropPage::guid)
PROPPAGEID(CLSID_CColorPropPage)
PROPPAGEID(CLSID_CFontPropPage)
END_PROPPAGEIDS(CMyActiveXCtrl)
15、 增加控件事件
a) 系统内部事件OnClick
b) 自定义事件 NewMinute
在OnDraw函数中触发,增加如下代码:
if(tm.GetSecond()==0)
FireNewMinute();
16、 编写相应的客户端(VB与VC下)
(本部分略)
第二十课:勾子及数据库访问
本讲主要内容:
第一部分:勾子
1、 回忆第一讲内容:(操作系统与应用程序的关系)由如果想从中间控制应用程序的消息,应该从哪入手?(如果把以高速公路上的汽车比作消息,以检查流串犯为例引出需要在检查站处设岗)从而引入勾子的概念,从而讲到勾子链(后挂的排在勾子链上的前面)
2、 函数的讲解(SetWindowsHookEx()与UnHookWindowHookEx())设一个勾子也只是调用一个API函数SetWindowsHookEx()函数,要想取消勾子的作用就要卸掉勾子,可以使用如下API函数UnhookWindowsHookEx()函数。(例子:在一个exe文件中实现一个简单的勾子,以处理回车键为例(VK_RETURN))
2、在一个DLL文件中实现同上的一个简单的勾子(void setHook()、void UnHook()、setWindowsHookEx(WH_KEYBOARD,::GetModuleHandle(“HookDll”),GetCurrentThreadID()))
3、在一个DLL文件中实现一个系统级别的勾子(将线程号改为0)(此时可以用窃取别人密码为例子来讲)
3、 在一个EXE文件中实现一个自动启动全屏的应用程序并带一个DLL勾子
a、新建一个基于对话框的工程,去掉对话框类及相应的文件
b、从CWnd继承一个新类,实现其WM_PAINT消息响应函数
c、在InitInstance()中加入如下代码创建主窗口
CStringClassName=::AfxRegisterWndClass(0,0,(HBRUSH)::GetStockObject(
WHITE_BRUSH),0);
m_FullWnd.Create(ClassName,"",WS_CHILD,CRect(0,0,0,0),CWnd::GetDesktopWindow(),1);
m_FullWnd.SetWindowPos(&CWnd::wndTopMost,0,0,::GetSystemMetrics(SM_CXSCREEN),::GetSystemMetrics(SM_CYSCREEN),SWP_SHOWWINDOW);
m_FullWnd.ShowWindow(SW_SHOWNORMAL);
m_pMainWnd=&m_FullWnd;
setHook(m_FullWnd.GetSafeHwnd());
return true;
d、在HookDLL中增加如下说明语句:
LIBRARY HOOKDLL
SEGMENTS .It315READ WRITE SHARED
EXPORTS
setHook
unhook
.CPP文件中实现如下:
#pragma data_seg(".It315")
HWND hWnd=NULL;
#pragma data_seg()
消息处理代码的编写
LRESULT CALLBACK xx(int code, WPARAM wParam,LPARAM lParam)
{
if(wParam==VK_NUMPAD6)
{
::PostMessage(hWnd,WM_CLOSE,0,0);
unHook();
return1;
}
returnCallNextHookEx(hHook,code,wParam,lParam);
}
第二部分:ADO数据库编程
1、介绍ADO中的三大对象及其关系
2、给出一个实例
4、 各种不同的实现办法举例
在stdafx.h文件中加入如下代码,导入ADO中的对象及其相应的常量
#import “c:\program files\commonfiles\system\ado\msado15.dll” no_namespace rename(“EOF”,”adoEOF”)
voidCAdoTestDlg::OnButton1()
{
USES_CONVERSION;
::CoInitialize(NULL);
_ConnectionPtr con(__uuidof(Connection));
_RecordsetPtr rst(__uuidof(Recordset));
_CommandPtr cmd(__uuidof(Command));
con->Open(_bstr_t("Provider=SQLOLEDB;server=yly;database=Northwind;"),_bstr_t("sa"),_bstr_t(""),-1);
cmd->put_ActiveConnection(_variant_t((IDispatch*)con));
cmd->put_CommandText(_bstr_t("select* from Customers"));
cmd->put_CommandType(adCmdText);
rst=cmd->Execute(NULL,NULL,-1);
//rst->Open(_variant_t("select *from Customers"),_variant_t((IDispatch*) con),adOpenDynamic,adLockOptimistic,-1);
//rst=con->Execute(_bstr_t("select *from Customers"),NULL,-1);
while(!rst->adoEOF)
{
m_List.AddString(W2A(rst->GetCollect(_variant_t("city")).bstrVal));
rst->MoveNext();
}
}
注意:由于在代码中采用了USES_CONVERSION、W2A宏,还需要在相应的.CPP文件中包括如下的头文件
#include”atlbase.h”
附录:进程间通讯
一、说明进程间通讯的必要性及困难性
二、Socket的方法,对于不同机器上且数据量很的情况会有很大的帮助,但对于同一台机器之间的不同进程之间的通讯就不方便了 (代码量太多)
三、进程间通讯的剪切板方法
a、对于发送端:
CString str;
GetDlgItemText(IDC_EDIT1,str);
HANDLE hGlobal;
if(this->OpenClipboard())//获取剪切板的资源所有权
{
EmptyClipboard();//将剪切板的内容清空
hGlobal=GlobalAlloc(GMEM_MOVEABLE,str.GetLength()+1);//在堆上分配一块用于存放数据的空间,程序返回一个内存句柄
char*pBuf=(char*)GlobalLock(hGlobal);//将内存块句柄转化成一个指针,并将相应的引用计数器加一
strcpy(pBuf,str.GetBuffer(str.GetLength()));//将字符串拷入指定的内存块中
GlobalUnlock(hGlobal);//将引用计数器数字减一
::SetClipboardData(CF_TEXT,hGlobal);//将存放有数据的内存块放入剪切板的资源管理中
::CloseClipboard();//释放剪切板的资源占用权
}
b、对于客户端
if(this->OpenClipboard())//获取剪切板的资源所有权
{
HANDLE hGlobal=::GetClipboardData(CF_TEXT);从剪切板中取出一个内存的句柄
char*pBuf=(char*)GlobalLock(hGlobal);//将内存句柄值转化为一个指针,并将内存块的引用计数器加一
SetDlgItemText(IDC_EDIT2,pBuf);
GlobalUnlock(hGlobal);//将内存块的引用计数器减一
CloseClipboard();//释放剪切板资源的占用权
}
四、内存映射文件方法
1、 服务器端代码:
HANDLE hMapFile;
hMapFile=CreateFileMapping(NULL,NULL,PAGE_READWRITE,0,10,"YuanMap");
if (hMapFile == NULL)
{
AfxMessageBox("CreateFileMapping出错!");
return;
}
LPVOID pFile;
pFile=MapViewOfFile(hMapFile,FILE_MAP_WRITE|FILE_MAP_READ,0,0,0);
if (pFile == NULL)
{
AfxMessageBox("MapViewOfFile出错!");
return;
}
CString str;
GetDlgItemText(IDC_EDIT1,str);
strcpy((char*)pFile,str.GetBuffer(str.GetLength()));
//CloseHandle(hMapFile);//不能加,否则客户端收不到,所以一般会将这个句柄作为一个全局变量
2、 客户机端代码:
HANDLE hMap;
hMap= OpenFileMapping(FILE_MAP_READ|FILE_MAP_WRITE,
TRUE,
"YuanMap");
LPVOID pVoid;
pVoid=::MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);
CStringstr=(char*)pVoid;
SetDlgItemText(IDC_EDIT1,str);
UnmapViewOfFile(pVoid);
CloseHandle(hMap);
五、进程间通讯的邮槽方法
1、 邮槽采用的是一种广播机制。
2、 邮槽采用的是一种直接基于文件系统开发而成,所以它不依赖于某种具体的网络协议。
3、 邮槽每次传送的消息长度不能长于422字节。
4、 发送端代码如下:(客户端)
HANDLE hslot;
hslot=CreateFile("\\\\.\\mailslot\\myslot",GENERIC_WRITE,
FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,
NULL);
if(!hslot)
{
MessageBox("打开邮槽失败!");
return;
}
char *pBuf="专业的编程语言培训";
DWORD dwWrite;
WriteFile(hslot,pBuf,strlen(pBuf)+1,&dwWrite,NULL);
CloseHandle(hslot);
5、 接收端代码如下:(服务器端)
HANDLE hMail;
hMail=CreateMailslot("\\\\.\\mailslot\\myslot",0,
MAILSLOT_WAIT_FOREVER,NULL);
if(INVALID_HANDLE_VALUE==hMail)
{
MessageBox("创建邮槽失败!");
return;
}
HANDLEhEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
OVERLAPPED ovlap;
ZeroMemory(&ovlap,sizeof(ovlap));
ovlap.hEvent=hEvent;
char buf[200];
DWORD dwRead;
if(FALSE==ReadFile(hMail,buf,200,&dwRead,&ovlap))
{
if(ERROR_IO_PENDING!=GetLastError())
{
MessageBox("读取操作失败!");
CloseHandle(hMail);
return;
}
}
WaitForSingleObject(hEvent,INFINITE);
MessageBox(buf);
ResetEvent(hEvent);
CloseHandle(hMail);
六、进程间通讯的命令管道方法
A、对于发送端代码如下:
HANDLE handle;
handle=CreateNamedPipe("\\\\.\\pipe\\MyPipe",
PIPE_ACCESS_DUPLEX,PIPE_TYPE_BYTE| PIPE_READMODE_BYTE,
1,0,0,1000,NULL);//创建一个命名管道连结
ConnectNamedPipe(handle,NULL);//在命名管道实例上监听客户机连结请求
char buf[200]="http://www.it315.org";
DWORD dwWrite;
WriteFile(handle,buf,strlen(buf)+1,&dwWrite,NULL);//往管道里写数据
CloseHandle(handle);//关闭管道
B、对于接收端代码如下:
HANDLE hNamedPipe;
WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER);//等侯一个命名管道实例可供自己使用
hNamedPipe=CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ,FILE_SHARE_READ,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);//建立与命名管道的连结
char buf[200];
DWORD dwRead;
ReadFile(hNamedPipe,buf,200,&dwRead,NULL);//从命名管道中读取数据
MessageBox(buf);
CloseHandle(hNamedPipe);//关闭与命名管道服务器的连结
七、进程间通讯的匿名管道方法
父进程:
A、对于父进程中创建一个管道代码如下:
SECURITY_ATTRIBUTES sa;
sa.nLength=sizeof(sa);
sa.bInheritHandle=TRUE;
sa.lpSecurityDescriptor=NULL;
if(FALSE==CreatePipe(&hRead,&hWrite,&sa,0))//创建一个匿名的管道,得到一个用于从管道读取的句柄,一个用于向管道写数据用的句柄
{
MessageBox("Createpipe failed!");
return;
}
STARTUPINFO sui;
ZeroMemory(&sui,sizeof(sui));
sui.cb=sizeof(sui);
sui.dwFlags=STARTF_USESTDHANDLES;
sui.hStdInput=hRead;
sui.hStdOutput=hWrite;
sui.hStdError=GetStdHandle(STD_ERROR_HANDLE);
PROCESS_INFORMATION pi;
CreateProcess("..\\PipeCli\\Debug\\PipeCli.exe",NULL,
NULL,NULL,TRUE,CREATE_DEFAULT_ERROR_MODE,/*0*/
NULL,NULL,&sui,&pi);//创建一个新的子进程,并将准备好的句柄信息传给子进程
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
B、父进程中从管道读取代码如下:
char buf[200];
DWORD dwRead;
ReadFile(hRead,buf,200,&dwRead,NULL);
MessageBox(buf);
C、父进程中往管道写入代码如下:
char buf[200]="专业的编程语言培训";
DWORD dwWrite;
WriteFile(hWrite,buf,strlen(buf)+1,&dwWrite,NULL);
子进程:
首先得到用于管道读取与写入用的句柄值(最好是放在视图的初始化更新函数里)
hRead=GetStdHandle(STD_INPUT_HANDLE);
hWrite=GetStdHandle(STD_OUTPUT_HANDLE);
读取部分代码:
char buf[200];
DWORD dwRead;
ReadFile(hRead,buf,200,&dwRead,NULL);
MessageBox(buf);
写入部分代码:
char buf[200]="http://www.it315.org";
DWORD dwWrite;
WriteFile(hWrite,buf,strlen(buf)+1,&dwWrite,NULL);