VC精华讲义最新.docx
《VC精华讲义最新.docx》由会员分享,可在线阅读,更多相关《VC精华讲义最新.docx(95页珍藏版)》请在冰豆网上搜索。
![VC精华讲义最新.docx](https://file1.bdocx.com/fileroot1/2023-2/1/910b3031-4f9a-4086-bd07-ec5f2a238969/910b3031-4f9a-4086-bd07-ec5f2a2389691.gif)
VC精华讲义最新
在VC++中,对于自定义的菜单,图标,光标,对话框等都是以资源的形式进行管理的,它们的定义与描述存放在资源文件中(扩展名为.rc),资源文件是文本格式,读者可以用notepad.exe打开,阅读里面的信息。
在VC++中是以“所见即所得”的方式打开资源文件的,在编辑窗口中看到的和编辑完后的结果即程序运行时的效果。
对于每一个资源及资源中的子项都是用一个标识号来标识的,通常称为ID,同一个ID可以标识多个不同的资源。
注意区别资源的ID号与句柄的区别,ID号是应用程序指定的,可以在资源还没在内存中产生前指定,也可在设计阶段就指定,基本上是固定的。
而句柄则是资源在内存中产生时由操作系统临时安排的,每次产生的句柄可能都不一样,一个ID号标识的资源可在内存中产生多个实例句柄。
资源文件中的ID标识符必须在"resouce.h"头文件中用宏定义成一个整数,这样程序中用到的一个ID号标识符实际上就是那个整数。
LoadIcon的第二个参数是LPCTSTR类型,用gotodefinition功能发现它实际被定义成CONSTCHAR*,是字符串常量指针,而图标的ID是一个整数。
参看msdn中的提示,对于这样的情况我们需用MAKEINTRESOURCE这个宏把资源ID标识符转换为需要的LPCTSTR类型。
使用gotodefinition功能,或在MSDN中都可以看到MAKEINTRESOURCE的定义:
#defineMAKEINTRESOURCE(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(
LPCTSTRlpClassName,//pointertoregisteredclassname
LPCTSTRlpWindowName,//pointertowindowname
DWORDdwStyle,//windowstyle
intx,//horizontalpositionofwindow
inty,//verticalpositionofwindow
intnWidth,//windowwidth
intnHeight,//windowheight
HWNDhWndParent,//handletoparentorownerwindow
HMENUhMenu,//handletomenuorchild-windowidentifier
HANDLEhInstance,//handletoapplicationinstance
LPVOIDlpParam//pointertowindow-creationdata
);
参数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函数从应用程序消息队列中取走一条消息,该函数的原型如下:
BOOLGetMessage(
LPMSGlpMsg,//addressofstructurewithmessage
HWNDhWnd,//handleofwindow
UINTwMsgFilterMin,//firstmessage
UINTwMsgFilterMax//lastmessage
);
参数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,orWM_SYSKEYUP消息对转换成一条WM_SYSCHAR消息,并将转换后得到的新消息投递到程序的消息队列中。
转换过程不会影响原来的消息,只在消息队列中增加新消息。
DispatchMessage函数将取道的消息传递到窗口的回调函数中去处理。
可以理解成该函数通知操作系统,让操作系统去调用窗口的回调函数来处理收到的消息。
所有Windows程序的消息处理代码基本上都是相同的,如没特殊需要,可以照搬照抄上述代码。
顺便提示:
从队列中取消息还有PeekMessage函数,PeekMessage函数有两种取消息的方式,第一种与GetMessage一样,从队列中直接取走消息,第二种是只取消息的一个副本,并不将消息从队列中取走。
无论哪种方式,PeekMessage都不会因队列中没有满足条件的消息而阻塞,当取到满足条件的消息,该函数返回非零值反之,返回零。
向队列中发送消息有PostMessage和SendMessage,PostMessage函数发送消息后立即返回,而SendMessage需等到发送的消息被处理完后才能返回。
还有一个PostThreadMessage用于向线程发送消息,关于线程,请在以后的章节再学,但我们也因此想到消息不一定总是与窗口相关的,也就是说,对于某些消息,其对应的MSG结构中的hwnd可以为NULL。
7.回调函数
回调函数的原型为:
LRESULTCALLBACKWindowProc(
HWNDhwnd,//handletowindow
UINTuMsg,//messageidentifier
WPARAMwParam,//firstmessageparameter
LPARAMlParam//secondmessageparameter
);
我们可以将函数名WindowProc改为我们喜欢的名称,如DefWndProc,该函数的四个参数对应消息的窗口句柄,消息码,消息码的两个补充参数。
在该函数内部是一个庞大的switch语句,用于对各种感兴趣的消息进行处理。
我们分析程序中的代码。
LRESULTCALLBACKDefWndProc(HWNDhWnd,UINTMsg,WPARAMwParam,LPARAMlParam)
{
HDChDC;
switch(Msg)
{
caseWM_CHAR:
charstr[20];
sprintf(str,"thecharcodeis%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键,在这里将弹出"thecharcodeis8"消息。
当用户在窗口上按下鼠标左按钮时,程序弹出一条消息框对收到的WM_LBUTTONDOWN进行响应,以证明按下鼠标左按钮动作与WM_LBUTTONDOWN消息的这种对应关系,另外程序还将在窗口上鼠标所按下的位置写上一串文字。
当我们要在窗口上写字、绘图,并不直接在这些设备上操作,而是在一个称为设备描述表(DeviceContexts,简称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标签,然后在左侧选择WindowsApplication,在右侧的Projectname:
文本框中为新建的工程起一个名字,VC++会为新建的工程在硬盘上建一个与工程同名的文件夹,这个文件夹放在Location:
指定的路径下,你可以点击Location旁边的"..."按钮来改变路径。
接下来一定要选择Createnewworkspace单选按钮,并勾选Platforms:
中的Win32复选框。
完成后的界面如图1-1所示:
2)这一课对第一次接触Windows编程的读者来说,新的东西太多。
但只要你把这些知识基本理解和掌握,学好VC++的日子离你就不太遥远了。
惟有如此,你才可能精通VC++编程。
第二课:
C++经典知识回顾
一、类的定义与应用
在C语言中,我们学过结构体,用于将描述某一对象的若干变量包装成一个整体使用,但没有但没有将与该对象相关的函数包含进来。
C语言中的结构体只能描述一个对象的特征(属性),不能描述一个对象的动作(方法)。
在C++中,我们是通过类的定义来解决这个问题的,在类的定义中,不仅可以包含变量,还可以包含函数。
我们通过一段程序来讲解类的使用。
#include"iostream.h"
classCPoint
{
public:
intx1;
inty1;
voidOutput();
CPoint();
CPoint(intx2,inty2)
~CPoint();
private:
intx2;
inty2;
int*pCount;
};
//注意类和结构定义完后,一定要用";"号结尾,忘记";"是许多人常犯的错误。
//在c++中,//......用于注释一行,/*......*/可以注释多行。
voidCPoint:
:
Output()
{
if(pCount)
(*pCount)++;
else
{
pCount=newint;
*pCount=1;
}
cout<<"thefirstpointis("<cout<<"thesecondpointis("<}
CPoint:
:
CPoint()
{
pCount=0;
cout<<"thefirstconstructoriscalling"<}
CPoint:
:
CPoint(intx2,inty2)
{
this->x2=x2;
this->y2=y2;
pCount=0;
cout<<"thesecondconstructoriscalling"<}
CPoint:
:
~CPoint()
{
if(pCount)
{
cout<<"你调用了Output成员函数共"<<*pCount<<"次"<deletepCount;
}
else
cout<<"你还没有调用过Output成员函数"<cout<<"thedeconstructoriscalling"<}
voidOutput(CPointpt)
{
cout<<"thefirstpointis("<//cout<<"thesecondpointis("<//上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。
}
voidmain()
{
if(1==1)//限定pt变量的有效范围
{
CPiontpt;
cout<<"请输入两个整数";
cin>>pt.x1>>pt.y1;
//pt.x2=10;
//pt.y2=10;
//上面被注释的语句会造成编译错误,因为不能从类的外部访问类中的私有成员。
pt.Output();
pt.Output();
pt.Output();//故意演示Output被调用多次的情况。
Output(pt);
}
CPointpt(10,10);
pt.Output();
}
上面的代码定义了一个类CPoint,其中包含有变量,称之为成员变量,也包含有函数的声明,称之为成员函数。
在类定义之外,我们必须对成员函数进行实现,成员函数的实现格式为:
返回类型 类名:
:
函数名(参数列表)
{
函数体代码
}
上面的代码也编写了一个名为Output的全局函数,注意与类CPoint中的Output成员函数区别。
上面的代码还编写了