admin管理员组文章数量:1647017
目录(?)[+]
- 情境一 面向对象基础应用
- 任务类的声明及成员的访问控制
- 任务构造函数和析构函数的应用
- 任务成员函数的应用
- 任务继承性的作用
- 任务函数的重载的应用
- 任务虚函数的使用
- 任务函数模板和模板函数的运用
- 任务类模板与模板类的运用
- 情境二 创建简单的MFC应用程序
- 任务创建一个MFC工程
- 任务模式对话框与非模式对话框的创建
- 任务创建非模式对话框
- 任务分析对话框的数据交换机制
- 任务创建模式对话框
- 任务分析模式对话框和非模式对话框的区别
- 情境三 制作基于ADO的人员管理系统
- 相关技术介绍
- 制作过程任务分解及相关技术详解
- 任务创建一个基于MFC对话框应用程序
- 任务控件的放置及相应消息函数成员变量添加
- 任务编写相关功能函数代码
- ADO技术介绍
- 任务引入ADO库文件
- 任务初始化ADO环境
- 接口简介
- 情境四 运用MFC实现简单绘图
- 任务1 绘制图形
- 任务2 绘制文本
- 任务3 从任意位置装入一个图标并绘制
- 任务4 从任意位置装入一个位图和绘制一个位图
- 情境五 制作基于MFC的简单网络聊天程序
- 相关技术介绍
- 制作过程任务分解及相关技术详解
- 任务创建基于MFC对话框应用程序
- 任务控件的放置及相应消息函数成员变量添加
- 任务实现服务器端数据接收
- 任务实现客户端数据发送
- 情境六 制作基于多线程端口扫描器
- 一效果图
- 二相关技术介绍
- 三制作步骤
- 情境七 VC调试
- 基本设置
- 任务1设置断点
- 任务2查看变量值Watch
- 任务3查看内存Memory
- 任务4使用其他调试手段
- 附录一Visual C界面常用控件介绍
- 附录二Microsoft 命名习惯
目 录
情境一 面向对象基础应用.... 4
任务:类的声明及成员的访问控制... 4
任务:构造函数和析构函数的应用... 2
任务:成员函数的应用... 4
任务:继承性的作用... 6
任务:函数的重载的应用... 6
任务:虚函数的使用... 8
任务:函数模板和模板函数的运用... 10
任务:类模板与模板类的运用... 11
情境二 创建简单的MFC应用程序.... 15
相关技术介绍... 15
任务:创建一个MFC工程... 15
任务:模式对话框与非模式对话框的创建... 20
任务:创建非模式对话框... 27
任务:分析对话框的数据交换机制... 28
任务:创建模式对话框... 30
任务:分析模式对话框和非模式对话框的区别... 31
情境三 制作基于ADO的人员管理系统.... 32
·相关技术介绍... 33
·制作过程任务分解及相关技术详解... 33
任务:创建一个基于MFC对话框应用程序... 33
任务:控件的放置及相应消息函数,成员变量添加... 33
任务:编写相关功能函数代码... 33
·ADO技术介绍... 42
·任务:引入ADO库文件... 42
·任务:初始化ADO环境... 42
接口简介... 42
情境四 运用MFC实现简单绘图.... 55
·任务1 绘制图形... 55
·任务2 绘制文本... 58
·任务3 从任意位置装入一个图标并绘制... 60
·任务4 从任意位置装入一个位图和绘制一个位图... 61
情境五 制作基于MFC的简单网络聊天程序.... 66
·相关技术介绍... 66
·制作过程任务分解及相关技术详解... 66
任务:创建基于MFC对话框应用程序... 69
任务:控件的放置及相应消息函数,成员变量添加... 70
任务:实现服务器端数据接收... 72
任务:实现客户端数据发送... 74
情境六 制作基于多线程端口扫描器.... 77
一、效果图:... 77
二、相关技术介绍... 77
三、制作步骤... 80
情境七 VC调试.... 83
基本设置... 83
任务1:设置断点... 85
任务2:查看变量值(Watch)... 85
任务3:查看内存(Memory)... 86
任务4:使用其他调试手段... 88
附录一:Visual C++界面常用控件介绍.... 90
附录二:Microsoft 命名习惯.... 100
情境一 面向对象基础应用
任务:类的声明及成员的访问控制
类是面向对象程序设计的基础和核心,也是实现数据抽象的工具。类实质上是用户自定义的一种特殊的数据类型,特殊之处就在于,和一般的数据类型相比,它不仅包含相关的数据,还包含能对这些数据进行处理的函数,同时,这些数据具有隐蔽性和封装性。类中包含的数据和函数统称为成员,数据称为数据成员,函数称为成员函数,它们都有自己的访问权限。
1.类声明的形式
类的声明即是类的定义,其语法与结构的声明类似,一般形式如下:
class 类名
{
private:
私有数据成员和成员函数
protected:
保护数据成员和成员函数
public:
公有数据成员和成员函数
};
其中,class是声明类的关键字,类名是给声明的类起的名字;花括号给出了类的声明范围;最后的分号说明类的声明到此结束。
2.类声明的内容及类成员的访问控制
⑴ 类声明的内容
类声明的内容包括数据和函数两部分,是对类的数据和函数以及它们的访问权限的说明。
① 数据 声明数据成员的数据类型,名字,以及访问权限。
② 函数 定义成员函数及对它们的访问权限。可以在类内定义成员函数,也可以在类外定义成员函数。在类外定义成员函数时先在类内说明该成员函数的原型,再是在类外进行定义,也就是说,类内声明,类外定义。成员函数的定义方法将在后面介绍。
⑵ 访问控制 类的成员的访问控制是通过类的访问权限来实现的。访问权限分为三种:
① private 声明该成员为私有成员。私有成员只能被本类的成员函数访问,类外的任何成员对它的访问都是不允许的。私有成员是类中被隐蔽的部分,通常是描述该类对象属性的数据成员,这些数据成员用户无法访问,只有通过成员函数或某些特殊说明的函数才可访问,它体现了对象的封装性。当声明中缺省private时,系统默认该成员为私有成员。
② protected 声明该成员为保护成员,一般情况下与私有成员的含义相同,它们的区别表现在类的继承中对新类的影响不同。保护成员的具体内容将在有关的章节中介绍。
③ public 声明该成员为公有成员。公有成员可以被程序中的任何函数访问,它提供了外部程序与类的接口功能。公有成员通常是成员函数。
任务:构造函数和析构函数的应用
对象(object)在生成过程中通常需要初始化变量或分配动态内存,以便我们能够操作,或防止在执行过程中返回意外结果。例如,在前面的例子中,如果我们在调用函数set_values( ) 之前就调用了函数area(),将会产生什么样的结果呢?可能会是一个不确定的值,因为成员x 和 y 还没有被赋于任何值。
为了避免这种情况发生,一个class 可以包含一个特殊的函数:构造函数 constructor, 它可以通过声明一个与class同名的函数来定义。当且仅当要生成一个class的新的实例 (instance)的时候,也就是当且仅当声明一个新的对象,或给该class的一个对象分配内存的时候,这个构造函数将自动被调用。下面,我们将实现包含一个构造函数的CRectangle 类。
实例1:实现含有构造函数的CRectangle 类
―――――――――――――――――――――――――――――――――――――――
#include <iostream.h>
class CRectangle
{
int width, height;
public:
CRectangle(int,int);
int area(void) {return (width*height);}
};
CRectangle::CRectangle(int a, int b)
{
width = a;
height = b;
}
void main ()
{
CRectangle rect(3,4);
CRectangle rectb(5,6);
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
}
―――――――――――――――――――――――――――――――――――――――
运行结果:
rect area: 12
rectb area: 30
正如你所看到的,这个例子的输出结果与前面一个没有区别。在这个例子中,我们只是把函数set_values换成了class的构造函数constructor。注意这里参数是如何在class实例 (instance)生成的时候传递给构造函数的:
CRectangle rect (3,4);
CRectangle rectb (5,6);
同时你可以看到,构造函数的原型和实现中都没有返回值(return value),也没有void 类型声明。构造函数必须这样写。一个构造函数永远没有返回值,也不用声明void,就像我们在前面的例子中看到的。
析构函数Destructor 完成相反的功能。它在objects被从内存中释放的时候被自动调用。释放可能是因为它存在的范围已经结束了(例如,如果object被定义为一个函数内的本地(local)对象变量,而该函数结束了);或者是因为它是一个动态分配的对象,而被使用操作符delete释放了。
析构函数必须与class同名,加水波号tilde (~) 前缀,必须无返回值。
析构函数特别适用于当一个对象被动态分别内存空间,而在对象被销毁的时我们希望释放它所占用的空间的时候。
实例2:析构函数
―――――――――――――――――――――――――――――――――――――――
#include <iostream.h>
class CRectangle
{
int *width, *height;
public:
CRectangle (int,int);
~CRectangle ();
int area (void) {return (*width * *height);}
};
CRectangle::CRectangle (int a, int b)
{
width = new int;
height = new int;
*width = a;
*height = b;
}
CRectangle::~CRectangle ()
{
delete width;
delete height;
}
void main()
{
CRectangle rect (3,4), rectb (5,6);
cout << "rect area: " << rect.area() << endl;
cout << "rectb area: " << rectb.area() << endl;
return;
}
―――――――――――――――――――――――――――――――――――――――
运行结果:
rect area: 12
rectb area: 30
任务:成员函数的应用
成员函数又称为方法,成员函数是C++中的术语,方法是面向对象方法中的术语,它们是同一个实体。
1.成员函数的定义
成员函数有两种定义方式:第一种方式是在类声明中只给出成员函数原型的说明,而成员函数的定义则在类的外部完成。其一般形式是:
返回类型 类名∷函数明(参数表)
{
//函数体
}
在类外定义成员函数。
class point
{
private:
int x,y;
public:
void setpoint(int xx, int yy); //声明成员函数setpoint()的函数原型
int getx(); //声明成员函数getx()的函数原型
int gety(); //声明成员函数gety的函数原型
};
void point:: setpoint(int xx, int yy) //定义成员函数setpoint()
{
x=xx;
y=yy;
}
int point :: getx() //定义成员函数getx()
{ return x; }
int point :: gety() //定义成员函数gety
{ return y; }
本例中,函数setpoint()、getx()和gety()都在类外定义,但它们都是属于类point的成员函数,都可以直接访问类point的数据成员x和y。
用这种方法定义成员函数应注意以下事项:
⑴ 在所定义的函数名前必须缀上类名,类名与函数名之间必须加上作用域运算符∷,如上面例子中的point ::。
⑵ 在类内声明成员函数的函数原型时,参数表中的参数可以只说明参数的数据类型而省略参数名。但在类外定义成员函数时,参数表中的参数不但要说明参数的数据类型,而且要指定参数名。
⑶ 定义成员函数时,其返回值类型必须要与函数原型说明中的返回类型一致。
定义成员函数的第二种方式是在类的内部定义成员函数,即将成员函数声明为内联函数。内联函数的声明有显示声明和隐式声明两种形式。
①隐式声明 直接将成员函数定义在类内部,例如:
将成员函数隐式声明为内联函数
class point
{
private:
int x,y;
public:
void setpoint(int xx, int yy)
{
x=xx;
y=yy;
}
int getx()
{ return x; }
int gety()
{ return y; }
};
②显示声明 将内联函数定义在类外,其声明的形式与在类外定义成员函数的形式类似,但为了使成员函数起到内联函数的作用,在函数定义前要加关键字inline,以显示地声明这是一个内联函数。例如:
将成员函数显示声明为内联函数。
class point
{
private:
int x,y;
public:
void setpoint(int xx, int yy);
int getx();
int gety();
};
inline void point::point(int xx, int yy) //声明成员函数setpoint()为内联函数
{
x=xx;
y=yy;
}
inline int point :: getx() //声明成员函数getx()为内联函数
{ return x; }
inline int point :: gety() //声明成员函数gety为内联函数
{ return y; }
这里要说明的是,将简单的成员函数声明为内联函数可以提高程序的运行效率,但当函数体较长时,将使程序的长度增加很多,因此,一般对简单的函数才声明为内联函数。
任务:继承性的作用
1.基本概念
⑴ 继承的概念
在面向对象程序设计中,继承表达的是对像类之间的关系,这种关系使得一类对象可以继承另一类对象的属性(数据)和行为(操作),从而,提供了通过现有的类创建新类的方法,也提高了软件复用的程度。
⑵ 继承关系的特征
类之间如果有了继承关系,它们之间就有如下特征:
① 类间具有共享特征,包括数据和代码的共享。
② 类间具有差别或新增部分,包括非共享的数据和代码。
③ 类间具有层次关系。
⑶ 基类、派生类和类的层次
设有两个类A和B,若类B继承类A,则类B是由类A派生出来的,类B的对象就具有了类A的一切特征,包括数据和操作,此时称类A为基类(也称父类或超类),称类B为派生类(也称子类)。
若类B由类A派生而来,而类C又由类B派生而来,则这三个类就形成了层次结构关系。此时,称类A是类B的直接基类,是类C的间接基类。类C不但继承了直接基类的所有特征,还继承了他的所有间接基类的特征。
任务:函数的重载的应用
函数的重载也称多态函数,是实现编译时的多态性的形式之一。它使程序能用同一个名字来访问一组相关的函数,提高了程序的灵活性。
函数重载时,函数名相同,但函数所带的参数个数或数据类型不同,编译系统会根据参数来决定调用哪个同名的函数。
面向对象程序设计中,函数的重载表现为两种情况:第一种是参数个数或类型有所差别的重载,第二中是函数的参数完全相同但属于不同的类。第一种情况的内容已在第一章作了介绍,这里主要介绍第二种情况的内容。
当函数的参数完全相同但属于不同的类时,为了让编译能正确区分调用哪个类的同名函数,采用两种方法:
⑴ 用对象名区别
在函数名前加上对象名来限制。如,对象名.函数名。
⑵ 用类名和作用域运算符∷加以区别
在函数名前加“类名∷”来限制。如,类名∷函数名。
实例3:函数重载
―――――――――――――――――――――――――――――――――――――――
#include<iostream.h>
class point
{
int x,y;
public:
point(int xx,int yy)
{ x=xx; y=yy; }
float area()
{
return 0.0;
}
};
class circle:public point
{
int r;
public:
circle(int xx,int yy,int rr):point(xx,yy)
{ r=rr; }
float area()
{ return (float)3.1416*r*r; }
};
void main()
{
point pob(15,15);
circle cob(20,20,10);
cout<<pob.area()<<endl;
cout<<cob.area()<<endl;
cout<<cob.point::area()<<endl;
}
―――――――――――――――――――――――――――――――――――――――
运行结果:
0
314.16
0
任务:虚函数的使用
虚函数是重载的另一种形式,实现的是动态的重载,即函数调用与函数体之间的联系是在运行时才建立,也就是动态联编。
1、引入派生类后的对象指针
一般对象的指针之间没有联系,彼此独立,不能混用。但派生类是由基类派生出来的,它们之间有继承关系,因此,指向基类和派生类的指针之间也有一定的联系,如果使用不当,将会出现一些问题。请看下面的例子:
实例4:没有使用虚函数
―――――――――――――――――――――――――――――――――――――――
#include<iostream.h>
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{ x=xx; y=yy; }
void disp()
{ cout<<"base: "<<x<<" "<<y<<endl; }
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{ z=zz; }
void disp()
{ cout<<"base1:"<<z<<endl; }
};
void main()
{
base obj(3,4),*objp;
base1 obj1(1,2,3);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
}
―――――――――――――――――――――――――――――――――――――――
运行结果如下:
base: 3 4
base1: 1, 2
显然,虽然执行语句objp=&obj1之后,指针已经指向了对象obj1,但它调用的函数仍然是基类对象的函数而不是派生类对象的函数,原本想通过使用对象指针来达到动态调用即当指针指向不同的对象时执行不同的操作的目的没有达到。出现这种现象的原因是调用的成员函数在编译时静态联编了。为解决这个问题,就要引入虚函数的概念。
在介绍虚函数之间,现说明派生类对象指针使用时应注意的问题:
⑴ 声明为指向基类对象的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象。
⑵ 允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象。
⑶ 声明为指向基类对象的指针,当其指向它的公有派生类的对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问其公有派生类中的成员,可将基类指针用显式类型转换方式转换为派生类指针。
2、虚函数的定义和使用
1.虚函数的定义
虚函数的定义是在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual。当基类中的某个成员函数被声明为虚函数后,它就可以在派生类中被重新定义。在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数和类型、参数的顺序都必须与基类中的原型完全一致。
虚函数定义的一般形式为:
virtual<函数类型><函数名>(参数表)
{ 函数体 }
2.虚函数与重载函数的关系
在派生类中被重新定义的基类中的虚函数,是函数重载的另一种形式,但它与函数重载又有区别:一般的函数重载,要求其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同,但重载一个虚函数时,要求函数名、返回类型、参数个数、参数的类型和参数的顺序必须与基类中的虚函数的原型完全相同。如果仅返回类型不同,其余相同,则系统会给出错误信息;如果函数名相同,而参数个数、参数的类型或参数的顺序不同,系统认为是普通的函数重载,虚函数的特性将丢失。
实例5:使用虚函数
―――――――――――――――――――――――――――――――――――――――
#include<iostream.h>
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{ x=xx; y=yy; }
virtual void disp()
{ cout<<"base: "<<x<<" "<<y<<endl; }
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{ z=zz; }
void disp()
{ cout<<"base1:"<<z<<endl; }
};
void main()
{
base obj(3,4),*objp;
base1 obj1(1,2,5);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
}
―――――――――――――――――――――――――――――――――――――――
运行结果如下:
base: 3 4
base1: 5
任务:函数模板和模板函数的运用
函数模板的声明和模板函数的生成
1.函数模板的声明
函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计。它的最大特点是把函数使用的数据类型作为参数。
函数模板的声明形式为:
template<typename 数据类型参数标识符>
<返回类型><函数名>(参数表)
{
函数体
}
其中,template是定义模板函数的关键字;template后面的尖括号不能省略;typename是声明数据类型参数标识符的关键字,用以说明它后面的标识符是数据类型标识符。这样,在以后定义的这个函数中,凡希望根据实参数据类型来确定数据类型的变量,都可以用数据类型参数标识符来说明,从而使这个变量可以适应不同的数据类型。例如:
template<typename T>
T fuc(T x, int y)
{
T x;
//……
}
如果主调函数中有以下语句:
double d;
int a;
fuc(d,a);
则系统将用实参d的数据类型double去代替函数模板中的T生成函数:
double fuc(double x,int y)
{
double x;
//……
}
函数模板只是声明了一个函数的描述即模板,不是一个可以直接执行的函数,只有根据实际情况用实参的数据类型代替类型参数标识符之后,才能产生真正的函数。
关键字typename也可以使用关键字class,这时数据类型参数标识符就可以使用所有的C++数据类型。
2.模板函数的生成
函数模板的数据类型参数标识符实际上是一个类型形参,在使用函数模板时,要将这个形参实例化为确定的数据类型。将类型形参实例化的参数称为模板实参,用模板实参实例化的函数称为模板函数。模板函数的生成就是将函数模板的类型形参实例化的过程。例如:
使用中应注意的几个问题:
⑴ 函数模板允许使用多个类型参数,但在template定义部分的每个形参前必须有关键字typename或class,即:
template<class 数据类型参数标识符1,…,class 数据类型参数标识符n>
<返回类型><函数名>(参数表)
{
函数体
}
⑵ 在template语句与函数模板定义语句<返回类型>之间不允许有别的语句。如下面的声明是错误的:
template<class T>
int I;
T min(T x,T y)
{
函数体
}
⑶ 模板函数类似于重载函数,但两者有很大区别:函数重载时,每个函数体内可以执行不同的动作,但同一个函数模板实例化后的模板函数都必须执行相同的动作。
任务:类模板与模板类的运用
1.类模板
类模板也称为类属类或类生成类,是为类定义的一种模式,它使类中的一些数据成员和成员函数的参数或返回值可以取任意的数据类型。类模颁布是一个具体的类,它代表着一族类,是这一族类的统一模式。使用类模板就是要将它实例化为具体的类。
定义类模板的一般形式为:
template<class 数据类型参数标识符>
class 类名
{
//……
}
其中,template是声明类模板的关键字;template后面的尖括号不能省略;数据类型参数标识符是类模板中参数化的类型名,当实例化类模板时,它将由一个具体的类型来代替。
定义类模板时,可以声明多个类型参数标识符,各标识符之间用逗号分开。
类定义中,凡要采用标准数据类型的数据成员、成员函数的参数或返回类型的前面都要加上类型标识符。
如果类中的成员函数要在类的声明之外定义,则它必须是模板函数。其定义形式为:
template<class 数据类型参数标识符>
数据类型参数标识符类名<数据类型参数标识符>∷函数名(数据类型参数标识符 形参1,……,数据类型参数标识符形参n)
{
函数体
}
2.模板类
将类模板的模板参数实例化后生成的具体的类,就是模板类。由类模板生成模板类的一般形式为:
类名<数据类型参数标识符>对象名1,对象名2,…,对象名n;
这里的数据类型参数标识符对应的是对象实际需要的数据类型。
模板应用举例
实例6: 函数模板的声明和模板函数的生成
―――――――――――――――――――――――――――――――――――――――
#include<iostream.h>
template<typename T> //声明模板函数,T为数据类型参数标识符
void swap(T &x, T &y) //定义模板函数
{
T z; //变量z可取任意数据类型及模板参数类型T
z=y;
y=x;
x=z;
}
void main()
{
int m=1,n=5;
double a=8.9,b=3.4;
cout<<"m="<<m<<" n="<<n<<endl;
cout<<"a="<<a<<" b="<<b<<endl;
swap(m,n); //实例化为整型模板函数
swap(a,b); //实例化为双精度型模板函数
cout<<"m与a,n与b交换以后:"<<endl;
cout<<"m="<<m<<" n="<<n<<endl;
cout<<"a="<<a<<" b="<<b<<endl;
}
―――――――――――――――――――――――――――――――――――――――
运行结果:
m=1 n=5
a=8.9 b=3.4
m与a,n与b交换以后:
m=5 n=1
a=3.4 b=8.9
实例7: 类模板的声明和模板类的生成
―――――――――――――――――――――――――――――――――――――――
#include<iostream.h>
const int size=10;
template<class T>
class stack
{
T stck[size];
int t;
public:
stack()
{ t=0; }
void push(T ch);
T pop();
};
template<class T>
void stack<T>::push(T ch)
{
if (t==size)
{
cout<<"stack is full!"<<endl;
return;
}
stck[t]=ch;
t++;
}
template<class T>
T stack<T>::pop()
{
if (t==0)
{
cout<<"stack is empty!"<<endl;
return 0;
}
t--;
return stck[t];
}
void main()
{
stack<char>cs1,cs2;
int i;
cs1.push('a');
cs2.push('x');
cs1.push('b');
cs2.push('y');
cs1.push('c');
cs2.push('z');
for(i=0;i<3;i++)
cout<<"pop cs1:"<<cs1.pop()<<endl;
for(i=0;i<3;i++)
cout<<"pop cs2:"<<cs2.pop()<<endl;
stack<int>is1,is2;
is1.push(1); is2.push(2);
is1.push(3); is2.push(4);
is1.push(5); is2.push(6);
for(i=0;i<3;i++)
cout<<"pop is1:"<<is1.pop()<<endl;
for(i=0;i<3;i++)
cout<<"pop is2:"<<is2.pop()<<endl;
return;
}
―――――――――――――――――――――――――――――――――――――――
运行结果:
pop cs1:c
pop cs1:b
pop cs1:a
pop cs2:z
pop cs2:y
pop cs2:x
pop is1:5
pop is1:3
pop is1:1
pop is2:6
pop is2:4
pop is2:2
情境二 创建简单的MFC应用程序
相关技术介绍
MFC是Microsoft Foundation Class的缩写,是Microsoft公司封装Windows SDK的类库。如果说SDK是WINDOWS所固有的接口的话,MFC便是Microsoft公司提供给C++程序员的API接口。
MFC类库――MFCL (Microsoft Foundation Class Library)中的各种类结合起来构成了一个应用程序框架,它的目的就是让程序员在此基础上来建立Windows下的应用程序,这是一种相对SDK来说更为简单的方法。与此同时MFC的规模也在不断拓展,目前已包括有200多个类,涵盖了通用Windows 类、文档/视框架、OLE、数据库、Internet以及分布式功能等多方面的基本内容。这样一个坚实的程序开发基础无疑从很大程度上方便了程序设计人员对Windows 程序的开发。
任务:创建一个MFC工程
步骤:
1、新建一个MFC应用程序
图2-1
2、输入工程名称后点击确定
图2-2
3、选择应用程序类型
多文档 单文档 对话框
图2-3
如果我们选择对话框后续步骤如下图:
图2-4
如果我们选择单文档的后续步骤如下图:
图2-5
多文档创建与单文档类似,这里不作详细讲述
4、最后点击完成
任务:模式对话框与非模式对话框的创建
下面我们将通过一个实例来讲解模式对话框和非模式对话框的创建。
前面我们讲过建立一个MFC单文档应用程序,为该应用程序新添加两个菜单项,这两个菜单项分别负责弹出模式对话框和非模式对话框,然而我们并不需要创建两个对话框,只要一个对话框就足够了。对于模式对话框和非模式对话框只是在创建和显示时不同,因此,我们只用一个对话框模板资源就够。
具体步骤如下:
l 步骤1:新建一个MFC单文档应用程序,工程名为EX02_1或用户自定义。
l 步骤2:在AppWizard生成的标准菜单后面添加一个弹出式菜单“对话框”,下面有两个菜单项,分别为“模式对话框”和“非模式对话框”,它们的ID分别为“IDM_MODEDLG”和“IDM_MODELESSDLG”,如图2-1所示。并在CMainFrame类中添加这两个菜单项的COMMAND消息处理函数,分别为OnModedlg和OnModelessdlg。
图2-1 添加菜单“对话框”
l 步骤3:插入一个新的对话框资源并用对话框编辑器设计对话框。
图2-2
设置新插入的对话框的ID为IDD_MYDLG,将对话框的标题改为“对话框实例(实现两数相加)”,对话框的属性如图2-2所示。
图2-3 新插入的对话框属性
分别在对话框上添加三个静态正文控件和三个编辑框控件。将三个静态正文控件属性对话框上Styles选项卡上的Align text属性选为Right,意思是让静态正文框中的文本内容从右对齐。然后,将三个静态正文控件的Caption属性分别设为“加数”、“被加数”和“两数之和”,不需要修改它们的ID。
将三个编辑框控件的ID分别改为IDC_EDIT_NUM1、IDC_EDIT_NUM2、IDC_EDIT_SUM。
将对话框上的Cancel按钮拖到下面,将它的Caption属性改为“相加”。
最后对话框上的样式如图2-3所示。
图2-4 新添加对话框资源模板
l 步骤4:创建新的对话框类,该类应从CDialog类派生
对话框模板资源准备好之后,就可以创建新的对话框类了。
利用ClassWizard,用户可以十分方便的创建MFC窗口类的派生类,对话框类也不例外。请大家按以下几步操作:
选中IDD_MYDLG对话框模板,然后按Ctrl+W进入ClassWizard。进入ClassWizard后,ClassWizard发现IDD_MYDLG是一个新的对话框模板,于是会弹出Adding a class对话框,或者也直接双击对话框模板IDD_MYDLG,也会弹出Adding a class对话框,如图2-4所示。它会询问用户是否要为IDD_MYDLG对话框创建一个新的对话框类。按OK键确认。
图2-5 Adding a class对话框
按OK按钮确认之后,会弹出如图2-5所示的New Class对话框,在 New Class对话框中,在Name栏中输入CMyDlg,在Base class栏中由用户选择你所要创建的类的基类,这里选择CDialog(该类也是默认的),在Dialog ID栏中选择IDD_MYDLG(一般情况下,ClassWizard自动使类CMyDlg与IDD_ MYDLG模板联系起来),其他选项按默认值,单击OK按钮后,对话框类CMyDlg就被创建完成。
图2-6创建新类New Class对话框
再次单击OK按钮关闭ClassWizard,回到工作台的ClassView页面,我们会发现在工程的类浏览器中多了一个新类CMyDlg,如图2-6,此时,这个新类只有两个函数:一个构造函数和一个析构函数。
图2-7 新添加的新类CMyDlg
其实,完整这步以后就可以将对话框显示出来了,但此时显示的对话框不能进行任何相加操作,因为还没有给这个对话框类添加与控件相关联的数据成员,还不能进行数据交换。
l 步骤5:使用ClassWizard为对话框类加入与控件相关联的成员变量。
对话框的主要功能是进行数据交换,本例对话框的任务就是输入两个数据,然后显示两个数据之和。为实现这些功能,对话框需要有一组成员变量来存储数据。在对话框中,控件用来表示或输入数据,因此,存储数据的成员变量应该与控件相对应。
与控件对应的成员变量即可以是一个数据,也可以是一个控件对象,这将由具体需要来确定。例如,可以为一个编辑框控件指定一个数据变量,这样就可以很方便地取得或设置编辑框控件所代表的数据,如果想对编辑框控件进行控制,则应该为编辑框指定一个CEdit对象,通过CEdit对象,用户可以控制控件的行为。需要指出的是,不同类的控件对应的数据变量的类型往往是不一样的,而且一个控件对应的数据变量的类型也可能有多种。下表说明了控件的数据变量的类型。
控件对应的数据变量的类型
控件 | 数据变量类型 |
编辑框 | CString, int, UINT, long, DWORD, float, double, short, BOOL, COleDateTime, COleCurrency |
普通检查框 | BOOL(真表示被选中,假表示未选中) |
三态检查框 | int(0表示未选中,1表示选中,2表示不确定状态) |
单选按钮(组中的第一个按钮) | int(0表示选择了组中第一个单选按钮,1表示选择了第二个...,-1表示没有一个被选中) |
不排序的列表框 | CString(为空则表示没有一个列表项被选中), int(0表示选择了第一项,1表示选了第二项,-1表示没有一项被选中) |
下拉式组合框 | CString, int(含义同上) |
其它列表框和组合框 | CString(含义同上) |
利用ClassWizard可以很方便地为对话框类加入成员变量。请大家按下列步骤操作。
1. 按Ctrl+W进入ClassWizard。
2. 选择ClassWizard对话框的Member Variables选项卡,然后在Class name栏中选择CMyDlg。这时,在下面的控件列表中会出现对话框控件的ID,如图2-7所示。
3. 选中IDC_EDIT_NUM1控件,然后单击对话框右边的Add Variable按钮或双击列表中的IDC_EDIT_NUM1,会弹出Add Member Variable对话框,如图2-8所示。在Member variable name栏中输入变量名m_nNum1,在Category栏中选择Value,在Variable type栏中选择int。按OK按钮后,数据变量m_nNum1就会被加入到变量列表中。
4. 此时,再看ClassWizard多了一个成员变量,我们给这个成员变量加一个限定值,将m_ nNum1的值限制在0到200之间。方法是先选择m_ nNum1,然后在ClassWizard对话框的左下角输入最大和最小值。如图2-9所示。有了这个限制以后,对话框会对输入的值进行有效性检查,若输入的值不在限制范围内,则对话框会提示用户输入有效的值。
5. 按照上面步骤,为剩下的ID号为IDC_EDIT_NUM2和IDC_EDIT_SUM的两个控件添加成员变量分别为m_nNum2和m_nSum,添加过程和m_nNum1变量一样。将变量m_nNum2同m_nNum1一样,设置限定值为0至200,而变量m_nSum不需要设置。三个成员添加完毕的结果如图2-10所示。
图2-7 ClassWizard的Member Variables选项卡
图2-8 Add Member Variable对话框
图2-9 设置限定值0-200
图2-10 三个变量添加之后的结果
当对话框类中与控件相关联的成员变量添加完成之后,就可以添加代码来进行数据交换了。如果我们对添加的成员变量不满意的话,可以在ClassWizard的Member Variables选项卡上选中不满意的变量,然后单击对话框右边的Delete Variable变量就可以删除了。然后还可以再通过单击Add Variable按钮再次添加正确的自己满意的变量。
l 步骤6: 在对话框类中添加需要的消息处理
因为程序最终的目的是实现两数相加,我们在对话框上修改的名为“相加”按钮就是负责实现两数相加,因此需要添加该按钮的消息处理函数。方法很简单,打开IDD_MYDLG对话框模板资源,双击上面的“相加”按钮,随后弹出Add Member Function对话框,如图2-11所示。在这个对话框上,给出了默认的函数名OnCancel,通常应用程序是根据控件的ID号生成相应的函数名,我们就用这个默认的函数名,最后单击OK按钮,消息处理函数就添加好了。至于具体函数内容将在下面分别讲创建模式对话框和创建非模式对话框时会提到。
图2-11 添加命令按钮消息处理函数
到这,创建对话框的大部分工作都已经做完了,就差最后一步,将对话框窗口创建并显示出来,然后再根据对话框种类的不同进行数据交换,为什么说要根据对话框种类的不同而分别进行数据交换呢?是因为模式对话框和非模式对话框的数据交换是不太一样。下面我们就分别介绍如何创建显示模式对话框和非模式对话框及两者之间的差别。
任务:创建非模式对话框
由于非模式对话框是不垄断用户输入的,即使对话框不返回,也可以切换到其他窗口,所以非模式对话框必须定义为全局变量或用new产生一个。因此要创建非模式对话框,一般需要三个步骤:
1、 构造一个对话框类的对象。
这里有两种方法可以构造一个对话框类的对象:一种是在框架窗口类的头文件中中声明一个对话框类的对象成员变量,如下:
class CMainFrame : public CFrameWnd
{……
public:
CMyDlg m_Mydlg;
……
};
由于,在这里声明的是CMyDlg类的对象,因此,还要在框架窗口类的头文件中加入语句#include "MyDlg.h",将对话框类的头文件包含进来,否则编译时会报错。
第二种方法是在框架窗口类中声明一个对话框类的指针变量,然后使用new操作符动态创建,如CMyDlg* pMyDlg=new CMyDlg;不过不要忘了,在最后要使用delete操作符将其释放。
这里,我们选用比较简单的第一种方法。
2、 调用CDialog::Create函数来创建对话框窗口。
构造完对话框类的对象之后,就可以使用该对象来创建对话框窗口了,还记得先前添加的菜单“对话框”吗,我们的初衷是想当用户单击“非模式对话框”菜单项时,弹出非模式特点的对话框,因此,我们在菜单项“非模式对话框”的COMMAND消息处理函数中创建非模式对话框,代码如下:
void CMainFrame::OnModelessdlg()
{
// TODO: Add your command handler code here
if (!m_Mydlg.m_hWnd)
{
m_Mydlg.Create(IDD_MYDLG);
}
m_Mydlg.ShowWindow(SW_SHOW);
}
3、 调用CWnd::ShowWindow函数来显示对话框窗口。
代码中的if语句是用来判断该对话框是否已经被创建,如果已经创建,就不应该再创建了,而只是在把它显示出来。
使对话框窗口显示的代码同样是放在菜单项“非模式对话框”的COMMAND消息处理函数中,见上一步。最后运行结果如图2-12所示。
图2-12 非模式对话框运行结果
由图2-12非模式对话框运行结果不难看出,即使该对话框不关闭,用户也可以再次去选择其他菜单项。但目前还不具备数据交换功能,在给它加上这一功能之前,我们有必要了解一下对话框的数据交换机制。
任务:分析对话框的数据交换机制
对话框的数据成员变量存储了与控件相对应的数据。数据变量需要和控件交换数据,以完成输入或输出功能。例如,一个编辑框即可以用来输入,也可以用来输出。当用作输入时,用户在其中输入了数值之后,对应的数据成员应该更新与编辑框中的数值相同;当用作输出时,应及时刷新编辑框的内容以反映相应数据成员的变化。那么,对话框就需要一种机制来实现这种数据交换功能,这对对话框来说是至关重要的。
MFC提供了类CDataExchange来实现对话框类与控件之间的数据交换(DDX),该类还提供了数据有效机制(DDV)。数据交换和数据有效机制适用于编辑框、检查框、单选按钮、列表框和组合框。
数据交换的工作由CDialog::DoDataExchange来完成。大家可以在工作台的ClassView类浏览器中找到对话框类CMyDlg类的DoDataExchange函数,函数如下:
void CMyDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMyDlg)
DDX_Text(pDX, IDC_EDIT_NUM1, m_nNum1);
DDV_MinMaxInt(pDX, m_nNum1, 0, 200);
DDX_Text(pDX, IDC_EDIT_NUM2, m_nNum2);
DDV_MinMaxInt(pDX, m_nNum2, 0, 200);
DDX_Text(pDX, IDC_EDIT_SUM, m_nSum);
//}}AFX_DATA_MAP
}
其中的斜体部分并不是真的在应用程序中用斜体字键入,而是为了告诉大家,这段代码在VC中的显示是浅灰色的,表示该函数中的代码是由ClassWizard自动加入的。
可以看出,DoDataExchange函数只有一个参数,即一个CDataExchange对象的指针pDX。在该函数中调用了DDX函数来完成数据交换,调用DDV函数来进行数据有效检查。
当程序需要交换数据时,并不需要用户直接调用DoDataExchange函数,而应该调用CWnd::UpdateData函数。因为在UpdataData函数内部调用了DoDataExchange。UpdataData函数原形如下:
BOOL UpdateData ( BOOL bSaveAndValidaet = TRUE);
该函数只有一个布尔型参数bSaveAndValidate,它决定了数据传送的方向。若参数值为TURE,即调用UpdateData(TRUE),表示将数据从对话框的控件中传送到对应的数据成员中;若参数值为FALSE,即调用UpdateData(FALSE),则表示将数据从数据成员中传送给对应的控件。
对话框在弹出前会调用其成员函数OnInitDialog,缺省的CDialog::OnInitDialog函数中调用了UpdateData(FALSE),这样,在对话框创建时,数据成员的初值就会反映到相应的控件上,因此,通常对话框上控件的初始化工作都放在对话框类的OnInitDialog中。若用户是按了OK(确定)按钮退出对话框,则对话框认为输入有效,就会调用UpdataData(TRUE)将控件中的数据传给数据成员。
经过上面的分析,那么在非模式对话框的“相加”按钮的处理函数中就应该键入如下代码来实现数据交换:
void CMyDlg::OnCancel()
{
// TODO: Add extra cleanup here
UpdateData(TRUE);
m_nSum=m_nNum1+m_nNum2;
UpdateData(FALSE);
// CDialog::OnCancel();
}
首先将UpdateData函数参数置为TRUE,将用户输入到控件的数据传给数据成员,然后将两个数据成员的值相加赋值给另一个数据成员m_nSum,最后,在调用UpdateData函数将参数置为FALSE,刷新屏幕上控件中显示的值。
在CMyDlg::OnCancel函数代码段下面,把CDialog::OnCancel();注释掉,是不让它调用父类的OnCancel函数,若调用了,对话框会关闭。
运行结果如图2-13所示。
图2-13
任务:创建模式对话框
模式对话框是主程序窗口打开的临时窗口,用于显示消息及取得用户数据,用户要关闭对话框才能恢复主窗口的工作。因此,模式对话框可定义为局部变量。
创建模式对话框比创建非模式对话框看起来要简单一些,我们应该在菜单项“模式对话框”的消息处理函数OnModedlg中,来创建和显示,代码如下:
void CMainFrame::OnModedlg()
{
// TODO: Add your command handler code here
CMyDlg m_Mydlg;
m_Mydlg.DoModal();
}
DoModal负责创建和撤销模式对话框。在创建对话框时,DoModal的任务包括载入对话框模板资源、调用OnInitDialog初始化对话框和将对话框显示在屏幕上。完成对话框的创建后,DoModal启动一个消息循环,以响应用户的输入。由于该消息循环截获了几乎所有的输入消息,使主消息循环收不到对对话框的输入,致使用户只能与模式对话框进行交互,而其它用户界面对象收不到输入信息。
若用户在对话框内单击了ID为IDOK的按钮(通常该按钮的标题是“确定”或“OK”),或按了回车键,则对话框类CMyDlg 的父类CDialog的OnOK函数将被调用。OnOK首先调用UpdateData(TRUE)将数据从控件传给对话框成员变量,然后调用CDialog::EndDialog关闭对话框。关闭对话框后,DoModal会返回值IDOK。
若用户单击了ID为IDCANCEL的按钮(通常其标题为“取消”或“Cancel”),或按了ESC键,则会导致CDialog::OnCancel的调用。该函数只调用CDialog::EndDialog关闭对话框。关闭对话框后,DoModal会返回值IDCANCEL(该实例中,将CDialog::OnCancel的调用给注释掉了)。
程序根据DoModal的返回值是IDOK还是IDCANCEL就可以判断出用户是确定还是取消了对对话框的操作。
运行结果是,当模式对话框不退出时,用户无法访问其他菜单项或窗口。下面我们再看看模式对话框的数据交换。模式对话框“相加”按钮的消息处理函数中的代码和非模式对话框里的代码采用相同的,也就是不改变先前已经添加好的OnCancel中的代码。
这时大家可能很不理解,我们说过模式对话框和非模式对话框的数据交换是不太一样,那么为什么在OnCancel中的代码是一样的呢?
以上我们讨论的数据交换都局限在控件和数据成员之间的交换,而真正应用程序中说到的数据交换包括对话框窗口、对话框类对象和视图类对象三者之间的数据交换,而不是仅仅的控件和数据成员之间的传递。在非模式对话框中,对话框窗口、对话框类对象和视图类对象三者之间的数据传递都是要手动添加的,而在模式对话框中,对话框窗口和对话框类对象之间的数据交换是自动完成的,而自动完成的过程就实现在DoModal函数中。
任务:分析模式对话框和非模式对话框的区别
通过以上所讲述的关于模式对话框和非模式对话框的创建,大家不难分析二者之间的区别,基本如下:
l 非模式对话框的模板必须具有Visible风格,否则对话框将不可见,更保险的办法是调用CWnd::ShowWindow(SW_SHOW)来显示对话框,而不管对话框是否具有Visible风格。而模式对话框则无需设置该项风格。
l 非模式对话框对象是用new操作符在堆中动态创建的或定义为全局变量形式,而不是以成员变量的形式嵌入到别的对象中或以局部变量的形式构建在堆栈上。通常应在对话框的拥有者窗口类内声明一个指向对话框类的指针成员变量,通过该指针可访问对话框对象。
l 通过调用CDialog::Create函数来启动对话框,而不是CDialog::DoModal,这是模式对话框的关键所在,也是二者最大的区别。由于Create函数不会启动新的消息循环,对话框与应用程序共用同一个消息循环,这样对话框就不会垄断用户的输入。Create在显示了对话框后就立即返回,而DoModal是在对话框被关闭后才返回的。众所周知,在MFC程序中,窗口对象的生存期应长于对应的窗口,也就是说,不能在未关闭屏幕上窗口的情况下先把对应的窗口对象删除掉。由于在Create返回后,不能确定对话框是否已关闭,这样也就无法确定对话框对象的生存期,因此只好在堆中构建对话框对象,而不能以局部变量的形式来构建之。
l 必须调用CWnd::DestroyWindow而不是CDialog::EndDialog来关闭非模式对话框。调用CWnd::DestroyWindow是直接删除窗口的一般方法。由于缺省的CDialog::OnOK和CDialog::OnCancel函数均调用EndDialog,故用户必须编写自己的OnOK和OnCancel函数并且在函数中调用DestroyWindow来关闭对话框。
l 如果是用new操作符构造非模式对话框对象,必须在对话框关闭后,用delete操作符删除对话框对象。在屏幕上一个窗口被删除后,框架会调用CWnd::PostNcDestroy,这是一个虚拟函数,程序可以在该函数中完成删除窗口对象的工作。
l 必须有一个标志表明非模式对话框是否已经打开。由于非模式对话框在不被关闭的情况下,用户很有可能再次打开这个模式对话框,那样肯定是要报错的。因此,应该定义一个标志,程序根据标志来决定是打开一个新的对话框,还是仅仅把原来打开的对话框激活。通常可以用拥有者窗口中的指向对话框对象的指针作为这种标志,当对话框关闭时,给该指针赋NULL值,以表明对话框对象已不存在了。也可以将所创建的类的对象的成员变量m_hWnd作为标志。本实例中就是用后者作为标志的。
情境三 制作基于ADO的人员管理系统
系统效果图:
工程类图:
·相关技术介绍
1、 文本框及按钮的使用
2、 列表控件的使用
3、 使用ADO对数据库的操作
·制作过程任务分解及相关技术详解
任务:创建一个基于MFC对话框应用程序
任务:控件的放置及相应消息函数,成员变量添加
任务:编写相关功能函数代码
系统常用控件介绍
由于所有的控件类都是由CWnd类派生来的,因此,控件实际上也是窗口。控件通常是作为对话框的子窗口而创建的,控件也可以出现在视图窗口、工具栏和状态栏中。控件(Control)是独立的小部件,在对话框与用户的交互过程中,担任着主要角色。控件的种类很多,图3-1显示了对话框中的一些基本的控件。
图3-1
MFC的控件类封装了控件的功能,下表列出了一些常用的控件及其对应的控件类。
常用控件
控件 | 功能 | 对应控件类 |
静态正文(Static Text) | 显示正文,一般不能接受输入信息 | CStatic |
图片(Picture) | 显式位图、图标、方框和图元文件,一般不能接受输入信息 | CStatic |
编辑框(Edit Box) | 输入并编辑正文,支持单行和多行编辑 | CEdit |
命令按钮(Pushbutton) | 响应用户的输入,触发相应的事件 | CButton |
复选框(Check Box) | 用作选择标记,可以有选中、不选中和不确定三种状态 | CButton |
单选按钮(Radio Button) | 用来从两个或多个选项中选中一项 | CButton |
组框(Group Box) | 显示正文和方框,主要用来将相关的一些控件聚成一组 | CButton |
列表框(List Box) | 显示一个列表,用户可以从该列表中选择一项或多项 | CListBox |
组合框(Combo Box) | 是一个编辑框和一个列表框的组合。分为简易式、下拉式和下拉列表式 | CComboBox |
滚动条(Scroll Bar) | 主要用来从一个预定义范围值中迅速而有效地选取一个整数值 | CScrollBar |
树形控件(TreeCtrl) | 树形控件可以用于树形的结构,其中有一个根接点(Root)然后下面有许多子结点,而每个子结点上有允许有一个或多个或没有子结点。 | CTreeCtrl |
列表控件(ListCtrl) | 列表控件可以看作是功能增强的ListBox | CListCtrl |
属性页控件(TabCtrl) | 属性页控件可以在一个窗口中添加不同的页面,然后在页选择发生改变时得到通知 | CTabCtrl |
本程序涉及到的控件:静态正文(Static Text)、编辑框(Edit Box)、命令按钮(Pushbutton)、列表控件(ListCtrl)。下面我们就来看看这些控件是怎么使用的。
(1)静态正文(Static Text)
静态正文控件有时也称静态文本框,可用于显示文本(Text)、矩形(Rectangle)、图标(Icon)、光标(Cursor)、位图(Bitmap)以及元文件(Metafile)等。静态正文控件是一种单向交互的控件,只能支持应用程序的输出,而不能接受用户的输入。
静态正文控件主要起说明和装饰作用。MFC的CStatic类封装了静态正文控件。
在使用时,只要从控件栏拖到对话框相应位置就可以了。然后设置静态文本的内容。
操作如下图:
(鼠标右键单击静态文本)
图3-2
(2) 编辑框(Edit Box)
编辑框控件可以接受用户的输入和编辑。在编辑框中,用户可以输入文字、数字,并能用剪切、粘贴、拷贝、删除等操作来编辑用户的输入,编辑框既可以是单行的,也可以是多行的。当该控件获得焦点时,在其编辑框的内部会出现一个闪烁的光标,类似于编辑WORD文档时出现的光标,但是它绝不能像WORD一样可以允许用户插入表格和图片等,编辑框控件只能接受纯文本形式的输入。
MFC的CEdit类封装了编辑框控件。
CEdit类常用的成员函数:
l GetSel:获得编辑框控件中当前选择的其始字符和终止字符的位置。
l SetSel:选择编辑框控件中的一个字符范围。
l GetLineCount:获得多行编辑框的行数。
l Undo/Clear/Copy/Cut/Paste:剪贴板操作,分别用于撤消上一次键入、清除编辑框中被选择的正文、把在编辑框中选择的正文拷贝到剪贴板中、清除编辑框中被选择的正文并把这些正文拷贝到剪贴板中、将剪贴板中的正文插入到编辑框的当前插入符处。
下图为Edit Box属性设置:
图3-3 (设置多行编辑框,支持回车换行)
注意要想在程序中使用编辑框,最好把编辑框作为对话框类的一个成员变量,那么如何是编辑框变为一个成员变量呢?我们来看下这个操作过程的步骤截图:
(一)鼠标右键点击要设置的编辑框,然后再点击建立类向导。
图3-4
(二)添加成员变量
图3-5
图3-6
(三)成员变量添加成功
图3-7
本程序内我们要设置的编辑框的控件ID有IDC_USERID、IDC_USERNAME、IDC_OLD。在MFC应用程序中,每一个控件都有其控件ID,要想对某个控件操作必须先找到其控件ID,这点要注意!
(3)命令按钮(Pushbutton)
在Windows系统中,命令按钮、复选框、单选按钮和组框都是由MFC类Cbutton来管理的,可统称为按钮控件。按钮是指可以响应鼠标点击的小矩形子窗口,它可以向父窗口发出两种控件通知消息:BN_CLICKED(在按钮上单击)和BN_DOUBLECLICKED(在按钮上双击)。
按钮控件包括命令按钮(Pushbutton)、检查框(Check Box)、单选按钮(Radio Button)、组框(Group Box)和自绘式按钮(Owner-draw Button)。命令按钮的作用是对用户的鼠标单击作出反应并触发相应的事件,在按钮中既可以显示正文,也可以显示位图。复选框控件可作为一种选择标记,可以有选中、不选中和不确定三种状态。单选按钮控件一般都是成组出现的,具有互斥的性质,即同组单选按钮中只能有一个是被选中的。组框用来将相关的一些控件聚成一组。自绘式按钮是指由程序而不是系统负责重绘的按钮。
本程序中的命令按钮就是平常的命令按钮,可能同学们关心的是为什么按钮点下去就会触发一段代码呢?那么我们就来实现一下怎么把一个按钮和一段代码绑定。请看下面步骤截图:
(一)右键点击“添加记录”按钮,然后左键点击“建立类向导”
图3-8
(二)添加BN_CLICKED消息函数
图3-9
图3-10
(三)在对话框源文件会生成一个函数如图:
图3-11
(四)把相关代码写在这个函数里如图:
图3-12
(4)列表控件(ListCtrl)
列表控件可以看作是功能增强的ListBox,它提供了四种风格,而且可以同时显示一列的多中属性值。MFC中使用CListCtrl类来封装列表控件的各种操作。
设置列表控件外观参数介绍:
● LVS_ICON、LVS_SMALLICON、LVS_LIST、 LVS_REPORT 这四种风格决定控件的外观,同时只可以选择其中一种,分别对应:大图标显示,小图标显示,列表显示,详细报表显示
● LVS_EDITLABELS 结点的显示字符可以被编辑,对于报表风格来讲可编辑的只为第一列。
● LVS_SHOWSELALWAYS 在失去焦点时也显示当前选中的结点
● LVS_SINGLESEL 同时只能选中列表中一项
在本程序中,在对话框初始化函数中
BOOL CADOTest1Dlg::OnInitDialog()
{ ……
::SendMessage(m_userlist.m_hWnd, LVM_SETEXTENDEDLISTVIEWSTYLE,LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
……
}
这句代码正是设置列表控件的外观风格.
之所以在对话框初始化函数中设置列表控件的外观风格,是为了让用户第一眼看到的列表控件有着美观的外观。当然任何与对话框相关的且需要初始设置的控件或者变量,都可以在对话框初始化函数进行初始设置。(注:OnInitDialog()对应消息为WM_INITDIALOG)
接着我们来介绍如何设置列表控件使之成为对话框类的成员变量,请看步骤截图:
(一)鼠标右键点击要设置的列表控件,然后再点击建立类向导。
(与编辑框添加成员变量设置一样)
(二)添加成员变量
图3-13
图3-14
大家会发现过程和设置编辑框成为成员变量的过程基本一样。这里强调一点控件成员变量的设置都是通过建立类向导然后添加成员变量得到的。请大家牢记这点!
(5)日期时间选择控件
要想方便的取得该控件的日期时间,那么就必须添加该控件的成员变量,如下图:
图3-15
图3-16
以上是对系统涉及的控件进行介绍,下面我们对ADO对数据库的操作进行详细的介绍。
·ADO技术介绍
ADO技术是基于OLE DB的访问接口,它继承了OLE DB技术的优点,并且,ADO对OLE DB的接口作了封装,定义了ADO对象,使程序开发得到简化,ADO技术属于数据库访问的高层接口。
ADO是ActiveX数据对象(ActiveX Data Object),这是Microsoft开发数据库应用程序的面向对象的新接口。ADO访问数据库是通过访问OLE DB数据提供程序来进行的,提供了一种对OLE DB数据提供程序的简单高层访问接口。
ADO技术简化了OLE DB的操作,OLE DB的程序中使用了大量的COM接口,而ADO封
装了这些接口。所以,ADO是一种高层的访问技术。
ADO是Microsoft为最新和最强大的数据访问范例 OLE DB 而设计的,是一个便于使用的应用程序层接口。ADO使你能够编写应用程序以通过 OLE DB 提供者访问和操作数据库服务器中的数据。ADO 最主要的优点是易于使用、速度快、内存支出少和磁盘遗迹小。ADO 在关键的应用方案中使用最少的网络流量,并且在前端和数据源之间使用最少的层数,所有这些都是为了提供轻量、高性能的接口。之所以称为 ADO,是用了一个比较熟悉的暗喻,OLE 自动化接口。
·任务:引入ADO库文件
使用ADO前必须在工程的stdafx.h文件里用直接引入符号#import引入ADO库文件,以使编译器能正确编译。代码如下所示:
#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF")
这行语句声明在工程中使用ADO,但不使用ADO的名字空间,并且为了避免冲突,将EOF改名为adoEOF。
在本程序中,引入ADO库文件语句我们写在StdAfx.h中,StdAfx.h文件是MFC工程创建后自动生成的一个头文件,大家可以把一些工程里公用的一下头文件或库文件的引入写在这个文件里。
·任务:初始化ADO环境
在使用ADO对象之前必须先初始化COM环境。MFC下初始化COM环境可以用以下代码完成:AfxOleInit()
在初始化COM环境后,就可以使用ADO对象了,如果在程序前面没有添加此代码,将会产生COM错误。
在本程序中,AfxOleInit()是MFC工程自动生成的,写在
BOOL CADOTest1App::InitInstance()函数中,该函数为程序初始化函数,可以用来初始化一些程序的全局变量。比如数据库的连接等。
接口简介
·ADO库包含三个基本接口
_ConnectionPtr接口、_CommandPtr接口、_RecordsetPtr接口
1)_ConnectionPtr接口
返回一个记录集或一个空指针。通常使用它来创建一个数据连接或执行一条不返回任何结果的SQL语句,如一个存储过程。用_ConnectionPtr接口返回一个记录集不是一个好的使用方法。使用它创建一个数据连接,然后使用其它对象执行数据输入输出操作。
2)_CommandPtr接口
返回一个记录集。它提供了一种简单的方法来执行返回记录集的存储过程和SQL语句。在使用_CommandPtr接口时,可以利用全局_ConnectionPtr接口,也可以在_CommandPtr接口里直接使用连接串。如果只执行一次或几次数据访问操作,后者是比较好的选择。但如果要频繁访问数据库,并要返回很多记录集,那么,应该使用全局_ConnectionPtr接口创建一个数据连接,然后使用_CommandPtr接口执行存储过程和SQL语句。
3)_RecordsetPtr接口
是一个记录集对象。与以上两种对象相比,它对记录集提供了更多的控制功能,如记录锁定,游标控制等。同_CommandPtr接口一样,它不一定要使用一个已经创建的数据连接,可以用一个连接串代替连接指针赋给_RecordsetPtr的connection成员变量,让它自己创建数据连接。如果要使用多个记录集,最好的方法是同Command对象一样使用已经创建了数据连接的全局—ConnectionPtr接口,然后使用_RecordsetPtr执行存储过程和SQL语句。
接下来让我们整合一下上面讲的相关技术,具体了解一下ADO数据库开发的基本流程:
(1)初始化COM库,引入ADO库定义文件
(2)用Connection对象连接数据库
(3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。
(4)使用完毕后关闭连接释放对象。
准备工作:
为了大家都能测试本章提供的例子,我们采用Access数据库,你也可以直接在我们提供的示例代码中找到这个test.mdb。
下面我们将详细介绍上述步骤并给出相关代码。
任务1:COM库的初始化
我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码:
BOOL CADOTest1App::InitInstance()
{
AfxOleInit();
......
任务2:用#import指令引入ADO类型库
我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到)
#import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF")
这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。
几点说明:
(1) 环境中msado15.dll不一定在这个目录下,请按实际情况修改
(2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。
msado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned
任务3:创建Connection对象并连接数据库
首先我们需要添加一个指向Connection对象的指针:
_ConnectionPtr m_pConnection;
任务:创建Connection对象实例及如何连接数据库并进行异常捕捉。
BOOL CADOTest1App::InitInstance()
{
AfxEnableControlContainer();
AfxOleInit();///初始化COM库
连接数据库//
HRESULT hr;
try
{
hr = m_pConnection.CreateInstance("ADODB.Connection");
///创建Connection对象
if(SUCCEEDED(hr))
{
hr = m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown);///连接数据库
///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为:Provider=Microsoft.Jet.OLEDB.3.51;
}
}
catch(_com_error e)///捕捉异常
{
CString errormessage;
errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage());
AfxMessageBox(errormessage);///显示错误信息
return FALSE;
}
……
}
在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型
HRESULT Connection15::Open ( _bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options )
ConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权,
Options可以是如下几个常量:
adModeUnknown:缺省。当前的许可权未设置
adModeRead:只读
adModeWrite:只写
adModeReadWrite:可以读写
adModeShareDenyRead:阻止其它Connection对象以读权限打开连接
adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接
adModeShareExclusive:阻止其它Connection对象打开连接
adModeShareDenyNone:允许其它程序或对象以任何权限建立连接
我们给出一些常用的连接方式供大家参考:
子任务:通过JET数据库引擎对ACCESS2000数据库的连接
m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown);
子任务:通过DSN数据源对任何支持ODBC的数据库进行连接:
m_pConnection->Open("DataSource=adotest;UID=sa;PWD=;","","",adModeUnknown);
子任务:不通过DSN对SQL SERVER数据库进行连接:
m_pConnection->Open("driver={SQL Server};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown);
其中Server是SQL服务器的名称,DATABASE是库的名称
Connection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State
ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如:
m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒
m_pConnection->Open("Data Source=adotest;","","",adModeUnknown);
State属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如:
if(m_pConnection->State)
m_pConnection->Close(); ///如果已经打开了连接则关闭它
任务4:执行SQL命令并取得结果记录集
为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;
并为其创建Recordset对象的实例:
m_pRecordset.CreateInstance("ADODB.Recordset");
SQL命令的执行可以采用多种形式,下面我们一进行阐述。
任务:利用Connection对象的Execute方法执行SQL命令
Execute方法的原型如下所示:
_RecordsetPtr Connection15::Execute(_bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一:
adCmdText:表明CommandText是文本命令
adCmdTable:表明CommandText是一个表名
adCmdProc:表明CommandText是一个存储过程
adCmdUnknown:未知
Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。
_variant_t RecordsAffected;
任务:执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday
实现:m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText);
子任务:往表格里面添加记录
实现:m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) VALUES (1,Washington,25,1970/1/1)",&RecordsAffected,adCmdText);
子任务:将所有记录old字段的值加一
实现:
m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText);
子任务:执行SQL统计命令得到包含记录条数的记录集
实现:
m_pRecordset=m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);
_variant_t vIndex = (long)0;
_variant_t vCount = m_pRecordset->GetCollect(vIndex);
子任务:取得第一个字段的值放入vCount变量
实现:m_pRecordset->Close();//关闭记录集
CString message;
message.Format("共有%d条记录",vCount.lVal);
AfxMessageBox(message);//显示当前记录条数
子任务:利用Command对象来执行SQL命令
实现: _CommandPtr m_pCommand;
m_pCommand.CreateInstance("ADODB.Command");
_variant_t vNULL;
vNULL.vt = VT_ERROR;
vNULL.scode = DISP_E_PARAMNOTFOUND;//定义为无参数
m_pCommand->ActiveConnection = m_pConnection;
//非常关键的一句,将建立的连接赋值给它
m_pCommand->CommandText = "SELECT * FROM users";//命令字串
m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);
//执行命令,取得记录集
在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。
子任务:直接用Recordset对象进行查询取得记录集
实现:
m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch *)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
Open方法的原型是这样的:
HRESULT Recordset15::Open ( const _variant_t & Source, const _variant_t & ActiveConnection, enum CursorTypeEnum CursorType, enum LockTypeEnum LockType, long Options )
其中:
①Source是数据查询字符串
②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象)
③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:
enum CursorTypeEnum
{
adOpenUnspecified = -1,
//不作特别指定
adOpenForwardOnly = 0,
//前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用
adOpenKeyset = 1,
//采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。
adOpenDynamic = 2,
//动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。
adOpenStatic = 3
//静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。
};
④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:
enum LockTypeEnum
{
adLockUnspecified = -1,///未指定
adLockReadOnly = 1,//只读记录集
adLockPessimistic = 2,
//数据在更新时锁定其它所有动作,这是最安全的锁定机制
adLockOptimistic = 3,
//只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、//删除等动作
adLockBatchOptimistic = 4,
//编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。
};
⑤Options请参考本章中对Connection对象的Execute方法的介绍
任务5:记录集的遍历、更新
根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday
子任务:
打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,更改其年龄,保存到数据库。
实现:
_variant_t vUsername,vBirthday,vID,vOld;
_RecordsetPtr m_pRecordset;
m_pRecordset.CreateInstance("ADODB.Recordset");
m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
while(!m_pRecordset->adoEOF)
//这里为什么是adoEOF而不是EOF呢?还记得rename("EOF","adoEOF")这一句吗?
{
vID = m_pRecordset->GetCollect(_variant_t((long)0));
//取得第1列的值,从0开始计数,你也可以直接给出列的名称,如下一行
vUsername = m_pRecordset->GetCollect("username");
//取得username字段的值
vOld = m_pRecordset->GetCollect("old");
vBirthday = m_pRecordset->GetCollect("birthday");
//在DEBUG方式下的OUTPUT窗口输出记录集中的记录
if(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)
TRACE("id:%d,姓名:%s,年龄:%d,生日:%s\r\n",vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday);
m_pRecordset->MoveNext();///移到下一条记录
}
m_pRecordset->MoveFirst();///移到首条记录
m_pRecordset->Delete(adAffectCurrent);///删除当前记录
///添加三条新记录并赋值
for(int i=0;i<3;i++)
{
m_pRecordset->AddNew();///添加新记录
m_pRecordset->PutCollect("ID",_variant_t((long)(i+10)));
m_pRecordset->PutCollect("username",_variant_t("张三"));
m_pRecordset->PutCollect("old",_variant_t((long)71));
m_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));
}
m_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));
//从第一条记录往下移动一条记录,即移动到第二条记录处
m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));
//修改其年龄
m_pRecordset->Update();///保存到库中
任务6:关闭记录集与连接
记录集或连接都可以用Close方法来关闭
m_pRecordset->Close();///关闭记录集
m_pConnection->Close();///关闭连接
至此,我想大家已经熟悉了ADO操作数据库的大致流程
·程序中相关功能函数及变量介绍
(1)全局变量设置
图3-17
在MFC工程中设置在应用程序类下的Public的成员变量,在该工程下其他的类可以通过(theApp.变量名)访问。这里把数据库连接对象作为工程的全局变量。
(2)主对话框成员变量设置
图3-18
注意:以上各变量一般只能在主对话框类CADOTest1Dlg中访问。
(3)相关功能函数介绍
Ø 主对话框初始化函数
功能:初始化列表控件和数据集对象,并把数据显示到列表控件上
BOOL CADOTest1Dlg::OnInitDialog()
{
CDialog::OnInitDialog();
m_cDelItem.EnableWindow(FALSE);
::SendMessage(m_userlist.m_hWnd, LVM_SETEXTENDEDLISTVIEWSTYLE,LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
//为列表控件添加列//
m_userlist.InsertColumn(0,"用户ID",LVCFMT_LEFT,60);
m_userlist.InsertColumn(1,"用户名",LVCFMT_LEFT,100);
m_userlist.InsertColumn(2,"年龄",LVCFMT_LEFT,60);
m_userlist.InsertColumn(3,"生日",LVCFMT_LEFT,100);
//读取数据库中的信息添加到列表控件///
int nItem;
_variant_t vUsername,vBirthday,vID,vOld;
try
{
m_pRecordset.CreateInstance("ADODB.Recordset");
m_pRecordset->Open("SELECT * FROM users",_variant_t((IDispatch*)theApp.m_pConnection,true),adOpenStatic,adLockOptimistic,adCmdText);
m_bSuccess = TRUE;
while(!m_pRecordset->adoEOF)
{
vID = m_pRecordset->GetCollect("ID");
vUsername = m_pRecordset->GetCollect("username");
vOld = m_pRecordset->GetCollect("old");
vBirthday = m_pRecordset->GetCollect("birthday");
nItem=m_userlist.InsertItem(0xffff,(_bstr_t)vID);
m_userlist.SetItem(nItem,1,1,(_bstr_t)vUsername,NULL,0,0,0);
m_userlist.SetItem(nItem,2,1,(_bstr_t)vOld,NULL,0,0,0);
m_userlist.SetItem(nItem,3,1,(_bstr_t)vBirthday,NULL,0,0,0);
m_pRecordset->MoveNext();
}
}
catch(_com_error e)///捕捉异常
{
AfxMessageBox("读取数据库失败!");///显示错误信息
}
……
return TRUE; // return TRUE unless you set the focus to a control
}
Ø 保存数据函数
功能:将编辑框的数据保存到记录集与列表框
说明:该函数为自定义成员函数
void CADOTest1Dlg::SaveData()
{
if(!m_pRecordset->adoEOF && m_nCurrentSel >= 0 && m_bAutoSave)
{
vUserID = (long)m_nUserID;
vUsername = m_sUsername;
vOld = (long)m_nOld;
vBirthday = m_tBirthday;
m_pRecordset->PutCollect("ID",vUserID);
m_pRecordset->PutCollect("username",vUsername);
m_pRecordset->PutCollect("old",vOld);
m_pRecordset->PutCollect("birthday",vBirthday);
m_userlist.SetItem(m_nCurrentSel,0,LVIF_TEXT,(_bstr_t)vUserID,NULL,0,0,0);
m_userlist.SetItem(m_nCurrentSel,1,LVIF_TEXT,(_bstr_t)vUsername,NULL,0,0,0);
m_userlist.SetItem(m_nCurrentSel,2,LVIF_TEXT,(_bstr_t)vOld,NULL,0,0,0);
m_userlist.SetItem(m_nCurrentSel,3,LVIF_TEXT,(_bstr_t)vBirthday,NULL,0,0,0);
}
}
Ø 数据加载函数
功能:将记录集中的数据加载到编辑框
说明:该函数为自定义成员函数
void CADOTest1Dlg::LoadData()
{
m_pRecordset->Move(m_nCurrentSel,_variant_t((long)adBookmarkFirst));
vUserID = m_pRecordset->GetCollect("ID");
vUsername = m_pRecordset->GetCollect("username");
vOld = m_pRecordset->GetCollect("old");
vBirthday = m_pRecordset->GetCollect("birthday");
m_nUserID = vUserID.lVal;
m_sUsername = (LPCTSTR)(_bstr_t)vUsername;
m_nOld = vOld.lVal;
m_tBirthday = vBirthday;
UpdateData(FALSE);
}
Ø 添加数据函数
功能:取得编辑框数据并添加到列表控件上及数据库中。
说明:由“添加记录”按钮生成的BN_CLICKED消息映射函数
void CADOTest1Dlg::OnAdditem()
{
if(UpdateData())
if(m_sUsername.GetLength()>0)
{
m_pRecordset->AddNew();
m_nCurrentSel = m_userlist.InsertItem(0xffff,"");
SaveData();///保存数据 m_userlist.SetItemState(m_nCurrentSel,LVIS_SELECTED|LVIS_FOCUSED,LVIS_SELECTED|LVIS_FOCUSED);
m_userlist.SetHotItem(m_nCurrentSel);
m_userlist.SetFocus();
}
else
AfxMessageBox("请输入用户名");
}
Ø 删除数据函数
功能:删除列表控件上选中的数据行以及删除数据库中对应的数据行。
说明:由“删除记录”按钮生成的BN_CLICKED消息映射函数
void CADOTest1Dlg::OnDelitem()
{
m_bAutoSave = FALSE;
if(m_nCurrentSel >= 0)
{
m_userlist.DeleteItem(m_nCurrentSel);
int count = m_userlist.GetItemCount();
if(count <= m_nCurrentSel)
m_nCurrentSel = count-1;
m_pRecordset->Delete(adAffectCurrent);
m_pRecordset->MoveNext();
LoadData();
m_userlist.SetItemState(m_nCurrentSel,LVIS_SELECTED|LVIS_FOCUSED,LVIS_SELECTED|LVIS_FOCUSED);
m_userlist.SetFocus();
}
m_bAutoSave = TRUE;
}
Ø 列表控件记录选择函数
功能:在选择列表框的时候调用
说明:由列表控件生成的LVN_ITEMCHANGED消息映射函数
void CADOTest1Dlg::OnItemchangedUserlist(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
if(pNMListView->uNewState&LVIS_SELECTED)
{
UpdateData();
SaveData();//保存旧数据
m_nCurrentSel = pNMListView->iItem; //取得被选择记录的行号
LoadData();//加载新数据
m_cDelItem.EnableWindow();
}
*pResult = 0;
}
说明:
以上各函数代码,大家可以把它们各自拷贝到自己的工程中,运行!
情境四 运用MFC实现简单绘图
位图和图标允许给应用程序添加色彩和风格。因为所有的 Windows界面在本质上都是相 同的,实际上,商标和启动窗口是用来区别不同应用程序外观的唯一方法。显然,绘图对于 创建自己的控件,以及在CAD应用程序中显示图形也是重要的。可以用本章中的例子给我们的应用程序增添一些独特的风格。
任务1 绘制图形 学会一些 MFC绘图工具。
任务2 绘制文本 学会怎样绘制文本。
任务3 从任意位置装入一个图标并绘制 学会从磁盘装入一个图标的方法。
任务4 从任意位置装入一个位图和绘制一个位图 学会从磁盘装入一个位图的方法。
·任务1 绘制图形
目标
绘制一个如图 4-1中看到的图形。
图4-1 用MFC可以绘制这些图型
策略
应用MFC的CDC类的不同绘图工具。
步骤
在Windows应用程序中绘图用一个设备环境完成,该设备环境定义用户在哪里绘图、用什么工具绘图以及采用什么绘图模式;设备环境取消了重复的参数调用,因而有助于简化Windows绘图工具。
1. 创建一个设备环境
1) 如果处理一个WM_PAINT消息或其他类似的消息,则可以提供一个设备环境,如果没 有提供,则必须自己创建一个;如果要绘制一个屏幕,可以用下面的代码创建一个设备环境,这里的pWnd是CWnd类的实例(实际窗口类)的指针。该类的实例应该拥有需要绘制的窗口。
CDC *pDC = pWnd->GetDC();
2) 如果创建一个自己的设备环境,用完后必须销毁它;否则,会发生另一种内存泄漏,称为资源泄漏。销毁一个设备环境,用:
pWnd->ReleaseDC(pDC);
注意:调用这些函数的类应控制需要绘制的窗口;如果没有这样的窗口类实例存在,可以使用AfxGetMainWnd()->GetDC()或::GetDC(NULL),它将返回一个用桌面窗口特征 初始化的设备环境。后面的函数返回一个设备环境句柄,在此处使用该返回的设备环境句柄,必须把它封装到CDC类的实例中。 设备环境具有一些预定义的绘图特征,其中之一是绘制线条的宽度和颜色,这一特征实际上包含在该设备环境指向的对象中。该对象叫做画笔(Pen),它默认为绘制一个像素宽的黑色线条;如果需要别的特征,则需要创建自己的画笔对象。
2. 创建一个画笔
1) 根据画线所需的特征创建 CPen类的一个实例。
CPen pen(
PS_SOLID, // 画笔样式还有
// PS_DASH,PS_DOT, PS_DASHDOT,
// PS_DASHDOTDOT, PS_INSIDEFRAME
// PS_NULL
2 ,// 所占像素大小
RGB(128 , 128 , 128) ) ; // 画笔颜色
2) 让设备环境指向该新画笔对象,但还要保存一个旧画笔的指针,以便以后能恢复它。 CPen *pPen=pDC->SelectObject(&pen) ;//保存一个旧画笔
另一个在设备环境中预定义的特征是填充色(用来绘制封闭图形内部的颜色),它与一个绘制封闭图形的函数一起使用;默认的颜色是白色,但通过告知设备环境使用一个新的画刷对象,可以改变填充颜色。
3. 创建一个画刷
1) 用需要的颜色创建 CBrush类的一个实例。
CBrush(RGB(128 , 128 , 128);// 画刷颜色
2) 让设备环境指向该新画刷对象,但还要保存旧对象的指针,以便以后能恢复它。
CBrush *pBrush=pDC->SelectObject(&brush);//保存旧画刷
注意:上面两步代表了创建CPen和CBrush类实例的最基本的方法
4. 用CDC类成员函数绘制图形
1) 用该设备环境画一条直线,用:
pDC->MoveTo( 5 , 5 );
pDC->LineTo( 25 , 25 );
一条线的起始和结束坐标分成两个函数调用,以便绘制多条相邻的线条时,具有最小量的参数进出栈。这里的数字,以及本例中的其余数字,都用逻辑单位;当绘制屏幕时,逻辑单位等于屏幕像素。
注意:本例和本章中的其他例子使用实例调用参数;当然也可以使用自己的函数。
2) 绘制一个矩形,用:
pDC->Rectangle( CRect( 5 , 55 , 50 , 85 ) ) ;
3) 绘制弧,用:
pDC->Arc( CRect ( 5 , 115 , 50 , 145 ) ,// 弧所包围的区域的矩形坐标
CPoint(5 , 115), // 弧起点坐标
CPoint (50 , 115)); // 弧终点坐标
4) 绘制圆角矩形,用:
pDC-> RoundRect ( CRect( 5 , 185 , 50 , 215 ) , CPoint(15,15));
5) 绘制椭圆或圆,用:
pDC->Ellipse ( CRect( 250 , 5 , 305 , 25 ) );
6) 绘制饼图,用:
pDC->Pie ( CRect ( 250 , 55 , 305 , 85 ) , // 所在矩形坐标
CPoint ( 250 , 55 ) , // 起点坐标
CPoint ( 305 , 55 ) ) ; // 终点坐标
7) 绘制一个外观与控件、弹出式和重叠窗口等同的窗口框架,用:
pDC->DrawEdge( CRect( 250 , 115 , 305 , 145 ) ,
EDGE_BUMP, // 还可以是EDGE_ETCHED,EDGE_RAISED,EDGE_SUNKEN
BF_RECT); // 还可以是 BF_LEFT, BF_BOTTOM , BF_RIGHT, BF_TOP
8) 绘制一系列相临的线,用:
POINT pt[8];
pt[0].x = 495; pt[0].y = 5;
pt[1].x = 510; pt[1].y = 10;
pt[2].x = 515; pt[2].y = 12;
pt[3].x = 495; pt[3].y = 15;
pt[4].x = 550; pt[4].y = 25;
pDC->Polyline(pt, 5);
9) 绘制一个具有多条相临边的封闭图形,用:
pt[0].x=495; pt[0].y=55;
pt[1].x=550; pt[1].y=55;
pt[2].x=530; pt[2].y=65;
pt[3].x=550; pt[3].y=85;
pt[4].x=520; pt[4].y=70;
pt[5].x=495; pt[5].y=85;
pt[6].x=510; pt[6].y=65;
pt[7].x=495; pt[7].y=55;
pDC->Polygon(pt, 8);
说明
1)一个设备环境只是一段内存,初始化计划进行绘制的设备的所有特征。使用一个设备环境可以提高性能,因为不用传输10或15个参数到CDC:LineTo(),而只需传输两个。线条的起始位置、颜色、厚度、绘制的合法位置、绘制的设备类型已经在环境中定义。
2)有4种类型的设备环境,除了屏幕环境以外,还有一个内存设备环境,一个打印机设备环境和一个信息的设备环境。信息设备环境比其他三种类型更简洁,并只用来获取一个设备的信息而不是用来绘图。在本章的后面部分将使用一个内存设备环境,在内存中创建一个位图图像。一个打印机设备环境用来满足需要,虽然,它可能在创建的时候与你期望的需要不同。
3)绘制图形远不止本例所介绍的内容,参见MSDN MFC文档,以获取更详细的内容。
·任务2 绘制文本
目标
在视图中绘制文本 (见图2-2 )。
图4-2 绘制文本的两个例子
策略
用CDC类的CDC::TextOut()和CDC::DrawText()成员函数绘制文本
步骤:
如果不存在设备环境,则用前面例子中介绍的技术创建一个。一种常见的方法如下: CDC *pDC=pWnd->GetDC(); //这里pWnd是指向MFC窗口类指针
1. 使用TextOut()
1) 绘制一个文本串,用:
CString str("This is drawn text") ;
pDC->TextOut (
x,y, //文本的中心位置
str, //文本
str.getLength()); // 文本长度
x和y变量定义文本位置的左上角,如果要 x和y指示别的位置,如文本的中心位置,可以用CDC::SetTextAlign()改变x和y的含义。
2) 使x和y代表文本中央位置,在调用TextOut()之前调用下面的函数:
改变 x对齐方式:
pDC->SetTextAlign(TA_CENTER); //(还可以是 TA_RIGHT )
也可以用下面的函数,改变 y对齐方式:
pDC->SetTextAlign(TA_BOTTOM); //(还可以是TA_BASELINE)
3) 用不同的标准字形绘制文本,在绘制前使用下面的函数调用 (参见M F C文档有关其他库存字体):
pDC->SelectStockObject(ANSI_VAR_FONT);
4) 创建绘制文本的字体,可以用:
CFont font;
font.CreateFont(
-22, // 字体点数
0, 0, 0,
FW_NORMAL, //字体大小, 还可以是 FW_BOLD :加粗
0, // 如果为 1 那么为斜体
0, // 如果为 1 那么加下划线
0, // 如果为 1 那么字体加边框
0, 0, 0, 0, 0,
"Courier"); // 字体
CFont *pFont = (CFont *)pDC->SelectObject(&font);
或者,如果只想选取一种基于点数和字形的字体,用 CreatePointFont() 。
CreateFont() 不是真正地创建一种字体,相反它扫描系统当前安装的字体,寻找一种与用 户调用参数中指定的标准最匹配的字体,并使用这种字体。
5) 改变窗口的默认字体,可以用:
pwnd->SetFont(pFont);
默认字体被自动地选进从那个窗口创建的任一设备环境中。
6) 改变文本的颜色,用:
pDC->SetTextColor(RGB(100,100,100));
7) 改变文本的背景色,用:
pDC->SetBkColor(RGB(200,200,200));
除非背景模式设为不透明,否则背景色被忽略。不透明意味着在绘制文本前,先绘制背景矩形;透明模式意味着文本被绘制在当前背景之上。
8) 打开不透明背景模式,用:
pDC->SetBkMode(OPAQUE);
9) 打开透明背景模式,用:
pDC->SetBkMode(TRANSPARENT);
CDC::TextOut()是最简单的绘图函数,如果希望系统做其他工作,可以用CDC::DrawText()。DrawText()允许在一个指定的矩形区域内绘制多行文本,也允许在该矩形内指定文本对齐方式。
2.使用DrawText()
用DrawText()绘制文本,用:
pDC->DrawText(
str, //字符串
rect, //所在矩形区域
DT_CENTER); //字符串对齐方式:居中
说明
其他文本绘制函数包括:
1)ExtTextOut(),该函数裁剪给定矩形外的绘制文本。
2)TabbedTextOut(),使用用户提供给该函数的跳格键位置表,扩大插入文本中的跳格距离。
3)DrawState(),用来绘制无效文本,该文本看起来被蚀刻一样。
注:在执行该工程时,可以看到视图中有两行绘制文本。
·任务3 从任意位置装入一个图标并绘制
目标
从资源文件或直接从一个图标文件中装入一个图标,在应用程序中绘制。
策略
用三种不同的方法装入一个图标。第一种方法,使用一个称为 LoadIcon( )的应用程序类 的成员函数,它从应用程序的资源中装入一个图标;第二种方法,用 Window API 函数 LoadImage( )直接从一个磁盘文件中装入一个图标;第三种方法,用Windows API 函数 ExtractIcon( )从另一个应用程序的可执行文件中抽出一个图标。
步骤
1. 从应用程序的资源中装入一个图标装入一个在应用程序资源中定义的图标,用:
HICON hicon;
hicon = AfxGetApp()->LoadIcon(IDR_MAINFRAME) ;
2. 直接从一个 .ico磁盘文件中装入一个图标
从一个.ico文件装入一个图标,使用下面的方法。本例从test.ico装入一个图标。
hicon = (HICON)LoadImage(
NULL, // 应用程序句柄
"test.ico", // 图标文件名
IMAGE_ICON , // 图像的类型
// 可以是 IMAGE_CURSOR 或 IMAGE_ICON
0 , 0 , // 图像的高度和宽度,指定为0,指图像原始大小
LR_LOADFROMFILE); // 加载方式-从文件加载
3. 从一个DLL或.exe文件中装入一个图标
从另一个应用程序的可执行文件中抽取一个图标,可以用下面的方法。本例中抽取在
Wzd.exe中发现的第二个图标。
HINSTANCE hinst=AfxGetInstanceHandle();
hicon=ExtractIcon(hinst,"Debug\\test.exe",1);
要确定一个可执行文件或 DLL文件有多少个图标,用{序号 – 1}索引调用 ExtractIcon( ) ,图标数量返回到hIcon中。
4. 绘制一个图标
用下面的方法可以把一个图标绘制到任何窗口。这里的(0 , 0)是图标的左上角坐标。
pDC->DrawIcon(0,0,hicon);
5. 销毁一个图标
必须手工销毁任何一个装入的或者用 LoadImage( )装入或 ExtractIcon( ) 抽取的图标,以避免资源内存泄漏。
DestroyIcon(hicon);
说明
1)在应用程序终止前,用 LoadIcon( ) 装入的图标实际上一直保留着,以后装入同样的图标资源,只要返回当前驻留内存的图标对象的句柄。
·任务4 从任意位置装入一个位图和绘制一个位图
目标
从资源文件或任意位图文件中装入一个位图。
策略
首先,用 CBit map类装入一个在应用程序资源中定义的位图;然后,用Windows API函数LoadImage( )从一个.bmp文件中装入一个位图。
步骤
1. 添加位图到应用程序的资源中
有两种把位图装入到工程中的方法。
第一种,用 Developer Studio 的位图编辑器创建一个位图;操作步骤如下图:
1)
图4-3
2)
图4-4
3)
图4-5
4)
图4-6
5)
在VC Developer Studio右侧出现如下图:
图4-7
第二种,用 Developer Studio 的Insert/Insert Resource菜单命令下的 Import命令装入一个位图。注意分配给位图的ID。操作步骤如下图:
1)
图4-8
2)
图4-9
2. 从应用程序的资源中装入位图 要把位图装入应用程序中进行绘制,用下面的方法,可以用自己的位图ID替代IDB_TEST。
CBitmap bitmap;
bitmap.LoadBitmap(IDB_TEST) ;
3. 从一个.bm文件中装入位图 在运行时用 LoadImage( )装入一个位图。
CBitmap bitmap;
HBITMAP hbitmap = (HBITMAP)::LoadImage(
NULL , // 进程句柄
"test.bmp", // 图像文件名
IMAGE_BITMAP, // 图像类型可以是 IMAGE_CURSOR 或 IMAGE_ICON
0 , 0 , // 默认图片宽度、高度
LR_LOADFROMFILE) ; // 从文件加载
4. 绘制一个位图
绘制一个位图可以用下面的方法,注意用 BitBlt( )需要两个设备环境,而不是一个。
CDC dcComp;
dcComp.CreateCompatibleDC(pDC);
dcComp.SelectObject(&bitmap);
//取得图像的信息,如图像大小
bitmap.GetObject(sizeof(bmInfo),&bmInfo);
//使用BitBlt画出图像
pDC->BitBlt(0,0,bmInfo.bmWidth,bmInfo.bmHeight,&dcComp,0,0,SRCCOPY);
说明
1) 位图和图标间的主要的区别,在于可以用透明色创建一个图标。每次使用一个透明色的图标时,绘制图标处的背景将显示出来。可以用LoadImage( )以及LR_LOADFROMFILE标志和LR_LOADTRANSPARENT标志的“或”操作假造这一效果,LoadImage( )用应用程序背景色替换位图中的一种颜色 (如按钮色);LoadImage( ) 根据位图左上角像素颜色,选择要替换的颜色。
情境五 制作基于MFC的简单网络聊天程序
系统效果图:
图5-1
工程类图:
图5-2
·相关技术介绍
1、SOCKET套接字工作原理
2、基于MFC的CSocket类的运用
·制作过程任务分解及相关技术详解
任务:创建基于MFC对话框程序
任务:控件的放置及相应消息函数,成员变量添加
任务:编写服务器端相关功能函数代码
任务:编写客户端端相关功能函数代码
SOCKET套接字工作原理图:
图5-3 面向连接服务的socket调用模型
图5-3显示了程序使用面向连接协议(TCP)时,进行的典型socket调用过程。图的左边显示了服务器端的函数调用过程,右边显示了客户端的调用过程,服务器端和客户端之间的连线和箭头显示了两个程序之间的网络通信过程。服务器端使用socket函数建立一个socket,由系统为其返回一个socket句柄留待以后使用;接着,服务器调用bind函数将此socket和本地协议端口联系起来;然后调用listen函数将此socket置于监听状态,让这个socket对进来的连接进行监听并确认连接请求。一个正在监听的socket将给将给每个请求发送一个确认信息,告诉发送者主机已收到连接请求,但正在监听的socket实际上并不接收连接请求,为了真正接收和建立连接,必须调用accept函数。客户端也调用socket函数建立一个socket,但使用像TCP这样面向连接协议的客户程序不必关心协议使用什么样的本地地址,因此,客户端不需要调用bind函数。接着,面向连接的客户通过调用connect函数启动网络对话。在客户端和服务器端建立连接以后,双方就可以通过send和recv或其它面向连接的Socket API函数进行网络通信了。
图5-4显示了程序使用非连接协议(UDP)时,进行的典型socket调用过程。与面向连接的服务器相似,图5-4中的无连接服务器通过调用socket和bind函数建立socket及绑定本地协议端口,由于是面向非连接协议的服务,因此,不需要进行调用listen和connect进行监听和连接,只需要在客户端建立好socket之后直接使用recvfrom和sendto函数在两者之间进行网络对话就可以了。请务必记住,使用无连接协议时并不在两个端口之间建立点到点的连接,因此不必调用listen、connect函数。
图5-4 面向非连接服务的socket调用模型
客户端与服务端的通信简单来讲:服务端 socket 负责监听,应答,接收和发送消息,而客户端 socket 只是连接,应答,接收,发送消息。
在编程之前,还有必要讲述一下: 网络传输服务提供者, ws2_32.dll , socket 间的关系。
网络传输服务提供者(transport service provider)是以 DLL 的形式存在的,在 windows 操作系统启动时由服务进程 svchost.exe 加载。当 socket 被创建时,调用 API 函数Socket(在 ws2_32.dll 中),Socket 函数会传递三个参数 : 地址族,套接字类型 和协议,这三个参数决定了是由哪一个类型的网络传输服务提供者来启动网络传输服务功能。所有的网络通信正是由网络传输服务提供者完成 , 这里将网络传输服务提供者称为网络传输服务进程更有助于理解。
下图描述了网络应用程序、 CSocket ( WSock32.dll )、 Socket API(ws2_32.dll) 和 网络传输服务进程 之间的接口层次关系:
图5-5
任务:创建基于MFC对话框应用程序
步骤:
1)创建MFC应用程序
2)选择Windows Socket
3)选择使用MFC静态库
4)完成创建MFC对话框应用程序
任务:控件的放置及相应消息函数,成员变量添加
步骤:
1)控件放置
在放置控件时,要做到控件的对齐,我们可以利用VC提供给我们的界面排版功能。
操作步骤:按住ctrl,然后左键连续点击要对齐的控件(至少点击2个控件),然后用右键点击选中的控件,在弹出式菜单里选择靠左排列或靠上排列。一般靠左排列是为了是控件的左边坐标一致,而靠上排列是为了控件的顶部坐标一致。
程序中的编辑框控件的外观设置都在该控件的属性的样式里。如下图:
这里我们设置了编辑框的只读属性
2)添加控件成员变量(具体操作过程见情境三中的类似操作)
3)生成消息映射函数
任务:实现服务器端数据接收
子任务1:创建服务器端CSocket对象
实现:
CSocket serverSocket;
serverSocket.Create(port);
Create函数原形:
BOOL Create(UINT nSocketPort = 0, int nSocketType=SOCK_STREAM,
LPCTSTR lpszSocketAddress = NULL);
参数解释:
UINT nSocketPort = 0 | 端口号,默认为0,故要设置一个>0的值 |
int nSocketType=SOCK_STREAM | 套接字类型,默认为流式套接字 |
LPCTSTR lpszSocketAddress = NULL | 服务器地址,默认为空 |
子任务2:服务器端开启侦听
实现:
serverSocket.Listen(5);
Listen函数原形:
BOOL Listen(int nConnectionBacklog=5);
参数解释:
nConnectionBacklog=5 | 同时可以接受5个来自客户端的请求 |
子任务3:接受来自客户端的连接请求
实现:
CSocket serverSocket,acceptSocket;
serverSocket.Accept(acceptSocket);
Accept函数原形:
BOOL Accept(CAsyncSocket& rConnectedSocket,
SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL);
参数解释:
CAsyncSocket& rConnectedSocket | 接受来自客户端CSocket对象 |
SOCKADDR* lpSockAddr = NULL | 指向客户端socket信息结构体指针,默认为空 |
int* lpSockAddrLen = NULL | 指向客户端socket信息结构体大小的指针,默认为空 |
子任务4:接收来自客户端的数据
实现:
serverSocket.Accept(acceptSocket); //接受新的连接
char recvbuf[8096] = {0}; //开辟内存,并初始化内存块为空
acceptSocket.Receive(recvbuf,8096); //通过接受来的acceptSocket 来接收客户端的数据
函数原形:
int Receive(void* lpBuf, int nBufLen, int nFlags = 0);
参数解释:
void* lpBuf | 接收数据所开辟内存块指针 |
int nBufLen | 数据长度 |
int nFlags = 0 | 接收标志,一般为0 |
子任务5:关闭服务器端Socket对象
实现:
serverSocket.Close();
acceptSocket.Close();
函数原形:
void Close();
无参数。
以上5个子任务组成了本程序中服务器端接受数据代码如下:
void CChatServerDlg::OnRecv()
{
SetWindowText("现在开始侦听……,准备接收");
CSocket serverSocket,acceptSocket;
int rc = serverSocket.Create(port);
if (!rc)
{
AfxMessageBox("SOCKET_ERROR 创建失败");
return;
}
rc = serverSocket.Listen(5);
if (!rc)
{
AfxMessageBox("SOCKET_ERROR 创建失败");
return;
}
serverSocket.Accept(acceptSocket);
char recvbuf[8096] = {0};
acceptSocket.Receive(recvbuf,8096);
CString strTime;
CTime tTime = CTime::GetCurrentTime();
strTime = tTime.Format("%Y-%m-%d %H:%M:%S");
m_sRecvTxt += recvbuf;
m_sRecvTxt += "[" + strTime + "]";
m_sRecvTxt += "\r\n";
UpdateData(false);//窗体刷新,才能把信息显示到编辑框里
serverSocket.Close();
acceptSocket.Close();
SetWindowText("完成数据接收");
}
任务:实现客户端数据发送
子任务1:建立客户端CSocket对象
实现:
CSocket clientSocket;
int rc = clientSocket.Create();
子任务2:连接服务器端
实现:
clientSocket.Connect(m_sIP,port);
函数原形:
BOOL Connect(LPCTSTR lpszHostAddress, UINT nHostPort);
参数解释:
LPCTSTR lpszHostAddress | 服务器IP |
UINT nHostPort | 端口号 |
子任务3:发送数据到服务器端
实现:
clientSocket.Send(m_sSendTxt,m_sSendTxt.GetLength());
函数原形:
int Send(const void* lpBuf, int nBufLen, int nFlags = 0);
参数解释:
const void* lpBuf | 要发送的数据 |
int nBufLen | 数据长度 |
int nFlags = 0 | 发送标志,一般为0 |
子任务4:关闭客户端Socket对象
实现:
clientSocket.Close();
以上4个子任务组成了本程序中客户端发送数据代码如下:
void CChatServerDlg::OnSend()
{
SetWindowText("");
if (m_sIP == "")
{
AfxMessageBox("请填写对方IP");
return;
}
if (m_sSendTxt == "")
{
AfxMessageBox("请填写聊天内容");
return;
}
if (m_sSendTxt.GetLength() > 8096)
{
AfxMessageBox("聊天内容不能超过8096字节");
return;
}
CSocket clientSocket;
int rc = clientSocket.Create();
if (!rc)
{
AfxMessageBox("socket创建失败!");
return;
}
rc = clientSocket.Connect(m_sIP,port);
if (!rc)
{
AfxMessageBox("无法连接");
return;
}
rc = clientSocket.Send(m_sSendTxt,m_sSendTxt.GetLength());
if (!rc)
{
AfxMessageBox("发送失败");
return;
}
AfxMessageBox("发送成功!");
m_sSendTxt = "";
UpdateData(false);
clientSocket.Close();
}
说明
1)在本程序中所有的编辑框的值发生变化,都要调用UpdateData(false)该函数的意思是:窗体刷新,由于我们的编辑框都设置成了Value,因此只有这样才能把信息显示到编辑框里。
调用UpdateData(false)的原因如下图:
2)本程序中当接收到消息时,会在消息后加上时间,代码如下:
任务:取得系统当前时间
实现:
CString strTime;
CTime tTime = CTime::GetCurrentTime();
strTime = tTime.Format("%Y-%m-%d %H:%M:%S");
情境六 制作基于多线程端口扫描器
一、效果图:
图6-1 效果图
二、相关技术介绍
1. 线程
进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
图6-2 线程与进程间的关系
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。
线程有就绪、执行、阻塞、结束四种状态,它们之间的转换有派生、阻塞、激活、调度、结束五种操作,转换过程如下图:
图6-3 线程状态
多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,目前大多数的计算机都是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
图6-4 单线程与多线程
由于同一进程中的所有线程共享改进程的所有资源和地址空间,任何线程对资源的操作都会对其他相关进程带来影响。因此,必须为线程的执行提供同步控制机制,防止线程的执行而破坏其他的数据或者给其他线程带来不利的影响。如下图所示的就是多线程进行文件下载的情况,由于不允许多个线程同时写入文件,所以要提供缓冲区暂存数据,并设置一种同步机制,只让一个线程写文件,其他线程阻塞等待。
图 6-5 线程同步
Win32 SDK函数支持进行多线程的程序设计,并提供了操作系统原理中的各种同步、互斥和临界区等操作。Visual C++ 6.0中,使用MFC类库也实现了多线程的程序设计,使得多线程编程更加方便。
Win32 API中常用的线程函数:
CreateThread 创建一个新的线程
SuspendThread 挂起指定的线程
ResumeThread 结束线程的挂起状态,执行线程
ExitThread 线程终结自身的执行
TerminateThread 终止某个线程的执行
Sleep 线程休眠若干时间
MFC中常用的线程函数:
AfxBeginThread 创建一个新的线程
2. 端口扫描器
服务器上所开放的端口就是潜在的通信通道,也就是一个入侵通道。对目标计算机进行端口扫描,能得到许多有用的信息,进行端口扫描的方法很多,可以是手工进行扫描、也可以用端口扫描软件进行。
扫描器通过选用远程TCP/IP不同的端口的服务,并记录目标给予的回答,通过这种方法可以搜集到很多关于目标主机的各种有用的信息,例如远程系统是否支持匿名登陆、是否存在可写的FTP目录、是否开放TELNET服务和HTTP服务等。
TCP connect()扫描是最基本的TCP端口扫描方式,操作系统提供的connect()系统调用可以用来与每一个感兴趣的目标计算机的端口进行连接。如果端口处于侦听状态,那么connect()就能成功。否则,这个端口是不能用的,即没有提供服务。这个技术的一个最大的优点是,你不需要任何权限。系统中的任何用户都有权利使用这个调用。另一个好处就是速度,如果对每个目标端口以线性的方式,使用单独的connect()调用,那么将会花费相当长的时间,使用者可以通过同时打开多个套接字多线程来加速扫描。
3.WinSock API
Socket接口是网络编程(通常是TCP/IP协议,也可以是其他协议)的API。最早的Socket接口是Berkeley接口,在Unxi操作系统中实现。WinSock也是一个基于Socket模型的API,在Microsoft Windows操作系统类中使用,各种语言都可以调用这个用户接口进行网络通信。
CSocket实际上是MFC对WinSock API的封装,用起来更加便捷。
前一章所讲的是用CSocket进行网络通讯,但是CSocket是非线程安全的,如果在多线程中使用CSocket会出现很多指针异常。所以本实例采用WinScoket(API)进行端口扫描,原理还是一样的。
三、制作步骤
图6-6 端口扫描工作流程图
1.创建基于对话框的MFC应用程序,工程名为ThreadSoc
2.在StdAfx.h 中添加以下两句,用于引入winsock库
#include <winsock2.h>
#pragma comment(lib,"wsock32.lib")
3.简单设计窗体,添加一个列表框和一个按钮,并为列表框添加变量m_list,如图:
4.在ThreadSocDlg.cpp中写入以下代码
struct PortRange //创建一个结构体
{
int from; //开始端口
int to; //结束端口
CThreadSocDlg *pdlg; //对话框窗体指针
};
UINT threadfun(LPVOID lp) //全局函数,线程执行的内容
//lp是一个任意类型的指针,指向所要传递参数
{
PortRange *p = (PortRange*)lp; //将指针转换为PortRange类型指针
int from = p->from; //取出开始端口
int to = p->to; //取出结束端口
CThreadSocDlg *pdlg=p->pdlg; //取出窗体指针
SOCKET sock=socket(AF_INET,SOCK_STREAM,0); //创建一个流式套接字(即TCP)
struct sockaddr_in sin; //定义一个目标结构体,包括了IP和端口
memset(&sin,0,sizeof(sin)); //相应的空间清零
sin.sin_family=AF_INET; //设置协议为Internet协议
sin.sin_addr.s_addr=inet_addr("127.0.0.1"); //设置目标IP
//127.0.0.1是本机,也可以是其他机子IP
CString temp; //临时字符串变量,等下用于显示结果
for(int i = from; i <= to; i++) //执行一个循环,搜索从from到to的端口
{
sin.sin_port=htons((u_short)i); //设置目标端口,htons是将整数转换成网络字节顺序。
//进行连接,如果是0表示连接成功(即端口是开启的),如果是-1表示连接失败
int re=connect(sock,(struct sockaddr*)&sin,sizeof(sin));
if (re==0)
{
temp.Format("%d ok",i);
pdlg->m_list.AddString(temp); //输出结果到列表框中
}
}
return 0;
}
void CThreadSocDlg::OnButton1() //按下按钮1
{
WSADATA wsa; //WinSocket的初始化信息
WSAStartup(0x0202,&wsa); //进行WinSocket的初始化
for (int i = 0; i < 500; i++) //执行500次循环,每个循环里开一个线程
{
PortRange p; //创建一个结构体PortRange的实例
p.pdlg = this;
p.from = i*20+1; //设置开始端口,如i等2,则from等41
p.to = (i+2)*10; //设置结束端口,如i等2,则to等60
//即第3个线程扫描端口从41至60
//总共500个线程,扫描端口从1至10000
AfxBeginThread(threadfun,&p); //MFC的线程函数,比CreatThread来得安全
//创建并开启一个新线程,调用threadfun函数,传过去的参数为p指针
}
}
5.可以进行相应的扩展,让IP地址、线程数、端口范围可以从对话框中输入,以及在对话框中显示线程的运行状态。
情境七 VC调试
调试是一个程序员最基本的技能,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即使会一门语言,却不能编制出任何好的软件。
基本设置
为了调试一个程序,首先必须使程序中包含调试信息。一般情况下,一个从AppWizard创建的工程中包含的Debug Configuration自动包含调试信息,但是是不是Debug版本并不是程序包含调试信息的决定因素,程序设计者可以在任意的Configuration中增加调试信息,包括Release版本。
为了增加调试信息,可以按照下述步骤进行:
打开Project settings对话框(可以通过快捷键ALT+F7打开,也可以通过IDE菜单Project/Settings打开) 如图:
命令行 | Project settings | 说明 |
无 | None | 没有调试信息 |
/Zd | Line Numbers Only | 目标文件或者可执行文件中只包含全局和导出符号以及代码行信息,不包含符号调试信息 |
/Z7 | C 7.0- Compatible | 目标文件或者可执行文件中包含行号和所有符号调试信息,包括变量名及类型,函数及原型等 |
/Zi | Program Database | 创建一个程序库(PDB),包括类型信息和符号调试信息。 |
/ZI | Program Database for Edit and Continue | 除了前面/Zi的功能外,这个选项允许对代码进行调试过程中的修改和继续执行。这个选项同时使#pragma设置的优化功能无效 |
选择C/C++页,分类中选择“常规” ,则出现一个“调试信息”下拉列表框,
可供选择的调试信息方式包括:
选择“连接”页,选中复选框"产生调试信息",这个选项将使连接器把调试信息写进可执行文件和DLL 如图:
如果C/C++页中设置了Program Database以上的选项,则Link incrementally可以选择。选中这个选项,将使程序可以在上一次编译的基础上被编译(即增量编译),而不必每次都从头开始编译。
任务1:设置断点
断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。
设置断点:可以通过下述方法设置一个断点。首先把光标移动到需要设置断点的代码行上,然后 按F9快捷键 弹出Breakpoints对话框,方法是按快捷键CTRL+B或ALT+F9,或者通过菜单Edit/Breakpoints打开。打开后点击Break at编辑框的右侧的箭头,选择 合适的位置信息。一般情况下,直接选择line xxx就足够了,如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。
去掉断点:把光标移动到给定断点所在的行,再次按F9就可以取消断点。同前面所述,打开Breakpoints对话框后,也可以按照界面提示去掉断点。
条件断点:可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就被中断。底下设置包括“观察数组或者结构的元素个数”,似乎可以设置一个指针所指向的内存区的大小,但是我设置一个比较的值但是改动 范围之外的内存区似乎也导致断点起效。最后一个设置可以让程序先执行多少次然后才到达断点。
数据断点:数据断点只能在Breakpoints对话框中设置。选择“Data”页,就显示了设置数据断点的对话框。在编辑框中输入一个表达式,当这个 表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成,例如:在编辑框中输入 g_bFlag这个全局变量的名字,那么当程序中有g_bFlag= !g_bFlag时,程序就将停在这个语句处。
消息断点:VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。
在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在上面那个对话框中写入消息处理函数的名字,那么 每次消息被这个函数处理,断点就到达(我觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在底下的下拉 列表框选择一个消息,则每次这种消息到达,程序就中断。
设置断点皆图:
任务2:查看变量值(Watch)
VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。
观看变量的值最简单,当断点到达时,把光标移动到这个变量上,停留一会就可以看到变量的值。
VC提供一种被成为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择Quick Watch, 就弹出一个对话框,显示这个变量的值。
单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4)如下图:
在该视图中输入变量或者表达式,就可以观察 变量或者表达式的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致软件的逻辑被破坏。
任务3:查看内存(Memory)
由于指针指向的数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用memory功能。在 Debug工具条上点memory按钮,就弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。如下图:
Varibles
Debug工具条上的Varibles按钮弹出一个框,显示所有当前执行上下文中可见的变量的值。特别是当前指令涉及的变量,以红色显示。如下图:
寄存器
Debug工具条上的Reigsters按钮弹出一个框,显示当前的所有寄存器的值。
如下图:
进程控制
VC允许被中断的程序继续运行、单步运行和运行到指定光标处,分别对应快捷键F5、F10/F11和CTRL+F10。各个快捷键功能如下:
快捷键 | 说明 |
F5 | 继续运行 |
F10 | 单步,如果涉及到子函数,不进入子函数内部 |
F11 | 单步,如果涉及到子函数,进入子函数内部 |
CTRL+F10 | 运行到当前光标处。 |
Call Stack
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。如下图:
任务4:使用其他调试手段
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。
系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下:
宏名/函数名 | 说明 |
TRACE | 使用方法和printf完全一致,他在output框中输出调试信息 |
ASSERT | 它接收一个表达式,如果这个表达式为TRUE,则无动作,否则中断当前程序执行。对于系统中出现这个宏 导致的中断,应该认为你的函数调用未能满足系统的调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。 |
VERIFY | 和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值。 |
说明
一个好的程序员不应该把所有的判断交给编译器和调试器,应该在程序中自己加以程序保护和错误定位,具体措施包括: 对于所有有返回值的函数,都应该检查返回值,除非你确信这个函数调用绝对不会出错,或者不关心它是否出错。
一些函数返回错误,需要用其他函数获得错误的具体信息。例如accept返回INVALID_SOCKET表示accept失败,为了查明 具体的失败原因,应该立刻用WSAGetLastError获得错误码,并针对性的解决问题。
有些函数通过异常机制抛出错误,应该用TRY-CATCH语句来检查错误。程序员对于能处理的错误,应该自己在底层处理,对于不能处理的,应该报告给用户让他们决定怎么处理。如果程序出了异常, 却不对返回值和其他机制返回的错误信息进行判断,只能是加大了找错误的难度。
另外:VC中要编制程序不应该一开始就写cpp/h文件,而应该首先创建一个合适的工程。因为只有这样,VC才能选择合适的编译、连接 选项。对于加入到工程中的cpp文件,应该检查是否在第一行显式的包含stdafx.h头文件,这是Microsoft Visual Studio为了加快编译 速度而设置的预编译头文件。在这个#include "stdafx.h"行前面的所有代码将被忽略,所以其他头文件应该在这一行后面被包含。
对于.c文件,由于不能包含stdafx.h,因此可以通过Project settings把它的预编译头设置为“不使用”,方法是:
1)弹出Project settings对话框
2)选择C/C++
3)Category选择Precompilation Header
4)选择不使用预编译头。
附录一:Visual C++界面常用控件介绍
一、列表CList控件
(一)列表控制的主要功能
列表控制和视(List Control&View)主要用来以各种方式显示一组数据记录供用户进行各种操作,Windows98/95中资源管理器中的“查看”标签下的“大图标|小图标|列表|详细资源”就是一个非常好的典型应用。列表中的记录可以包括多个数据项,也可以包括表示数据内容的大小图标,用来表示数据记录的各种属性。
列表控制提供了对Windows列表功能操作的基本方法,而使用列表视的视函数可以对列表视进行各种操作,通过调用视成员GetListCtrl获取嵌在列表视内列表控制的引用(GetListCtrl& ctrlList = GetListCtrl()),就可以和列表控制一样进行各种操作。操作一个列表控制和视的基本方法为:创建列表控制;创建列表控制所需要的图像列表;向列表控制添加表列和表项;对列表进行各种控制,主要包括查找、排序、删除、显示方式、排列方式以及各种消息处理功能等;最后撤消列表控制。
对于一个列表控制,其最典型最常用的显示控制方式为:大图标方式(LVS_ICON)、小图标方式(LVS_SMALLICON)、列表显示方式(LVS_LIST)和详细资料(即报告LVS_REPORT)显示方式。这可以通过设置其显示方式属性来实现。要控制列表所在窗口的风格,可通过功能函数GetWindowLong和SetWindowLong来实现,要控制列表图标的对齐方式,可通过设置列表窗口的风格LVS_ALIGNTOP或LVS_ALIGNLEFT来实现,
(二)列表控制的对象结构
1、列表控制的建立方法
CListCtrl&listCtrl 定义列表对象的结构
Create 建立列表控制并绑定对象
列表控制CListCtrl::Create的调用格式如下:
BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );
其中参数dwStyle用来确定列表控制的风格;rect用来确定列表控制的大小和位置;pParentWnd用来确定列表控制的父窗口,通常是一个对话框;nID用来确定列表控制的标识。其中列表控制的风格可以是下列值的组合:
LVS_ALIGNLEFT 用来确定表项的大小图标以左对齐方式显示;
LVS_ALIGNTOP 用来确定表项的大小图标以顶对齐方式显示;
LVS_AUTOARRANGE 用来确定表项的大小图标以自动排列方式显示;
LVS_EDITLABELS设置表项文本可以编辑,父窗口必须设有LVN_ENDLABELEDIT风格;
LVS_ICON 用来确定大图标的显示方式;
LVS_LIST 用来确定列表方式显示;
LVS_NOCOLUMNHEADER 用来确定在详细资料方式时不显示列表头;
LVS_NOLABELWRAP 用来确定以单行方式显示图标的文本项;
LVS_NOSCROLL 用来屏蔽滚动条;
LVS_NOSORTHEADER 用来确定列表头不能用作按钮功能;
LVS_OWNERDRAWFIXED 在详细列表方式时允许自绘窗口;
LVS_REPORT 用来确定以详细资料即报告方式显示;
LVS_SHAREIMAGELISTS用来确定共享图像列表方式;
LVS_SHOWSELALWAYS 用来确定一直显示被选中表项方式;
LVS_SINGLESEL 用来确定在某一时刻只能有一项被选中;
LVS_SMALLICON 用来确定小图标显示方式;
LVS_SORTASCENDING 用来确定表项排序时是基于表项文本的升序方式;
LVS_SORTDESCENDING 用来确定表项排序时是基于表项文本的降序方式;
2、列表控制的属性类
列表控制的属性类包括取得列表控制的背景色GetBkColor、设置列表控制的背景色SetBkColor、取得列表控制的图像列表GetImageList、设置列表控制的图像列表SetImageList、取得列表项数目GetItemCount、取得列表控制的属性GetItem、取得与表项相关的数据GetItemData、设置表项的属性SetItem、设置与表项相关的数值SetItemData、取得相关联的下一个表项GetNextItem、设置列表控制的文本颜色SetTextColor、取得列表控制的文本背景颜色GetTextBkColor、设置表项的最大数目SetItemCount和取得被选中表项的数目GetSelectedCount等。
3、列表控制的操作方法
列表控制的操作方法包括插入一个新的表项InsertItem、删除一个表项deleteItem、排序表项SortItems、测试列表的位置HitTest、重绘表项RedrawItems、插入一个表列InsertColumn、删除一个表列deleteColumn、编辑一个表项文本EditLabel和重绘一个表项DrawItem等。
二、状态条CStatusBar控件
(一)状态条控制的主要功能
状态条控制(Status Bar Control)比较容易理解,使用起来也比较简单。状态条是位于父窗口底部的一个水平子窗口,它可以被分成多个显示信息的小区域。其MFC中封装的CstatusBarCtrl控制类提供了应用的基本方法。
(二)状态条控制的对象结构
1、状态条控制的建立方法
CStatusBarCtrl &StatusBarCtrl 建立状态条控制对象结构
Create 建立状态条控制对象并绑定
状态条控制类CstatusBarCtrl::Create的调用格式如下:
BOOL Create( DWORD dwStyle,const RECT& rect,CWnd* pParentWnd,UINT nID);
其中参数dwStyle用来确定状态条的控制风格;参数rect用来确定状态条窗口的大小和位置;参数pParentWnd用来确定状态条父窗口的指针;nID用来确定状态条控制的标识符。
状态条控制风格可以是下列值的组合:CCS_BOTTOM、CCS_NODIVIDER、CCS_NOHILITE、CCS_NOMOVEY、CCS_NOPARENTALIGN、CCS_NORESIZE和CCS_TOP等,具体内容和含义请见工具条控制中的有关部分。
2、状态条控制的类属性
状态条控制类属性包括设置给定部分显示文本SetText、取得给定部分的文本GetText、设置状态条区域划分数及其每部分的右边坐标SetParts、取得状态条区域划分数量GetParts、取得状态条的水平和垂直宽度GetBorders和取得状态条矩形区域GetRect。
(三)状态条控制的应用技巧
状态条控制除可以显示一定的帮助和提示信息外,还可以实现响应鼠标输入等功能。这里以在状态条上显示鼠标移动坐标为例,具体说明其应用技巧。
利用应用程序向导生成的程序代码中,状态条作为主窗口的子窗口,其具有一个AFX_IDW_STATUS _BAR标识符,通过它调用函数GetDescendantWindow()和AfxGetMainWnd(),就可以取得状态条窗口的指针。由于基于文档的应用程序在建立时就具有状态条区域,所以只要利用类向导简单地加入鼠标移动消息处理功能函数和下述函数代码,就可以实现这一功能:
Void CTestView::OnMouseMove(UINT nFlags,Cpoint point)
{
CclientDC dc(this);//建立设备文本
OnPrepareDC(&dc,NULL);//设备映射处理
dc.DPtoLP(&point);//鼠标指针转换
char text[128];
CstatusBar *pStatus=(CstatusBar *)AfxGetApp()->m_pMainWnd->
GetDescendanWindow(AFX_IDW_STATUS_BAR);//取得窗口指针
If(pStatus) {//如果存在显示鼠标坐标
Sprintf(text,”X坐标=%4d,Y坐标=%4d”,point.x,point.y);
pStatus->SetPaneText(0,text);}
CscrollView::OnMouseMove(nFlags,point);
}
三、进度条CProgress控件
(一)进度条的主要功能
进度条控制(Progress Control)主要用来进行数据读写、文件拷贝和磁盘格式等操作时的工作进度提示情况,如安装程序等,伴随工作进度的进展,进度条的矩形区域从左到右利用当前活动窗口标题条的颜色来不断填充。
进度条控制在MFC类库中的封装类为CProgressCtrl,通常仅作为输出类控制,所以其操作主要是设置进度条的范围和当前位置,并不断地更新当前位置。进度条的范围用来表示整个操作过程的时间长度,当前位置表示完成情况的当前时刻。SetRange()函数用来设置范围,初始范围为0-100,SetPos()函数用来设置当前位置,初始值为0,SetStep()函数用来设置步长,初始步长为10,StepIt()函数用来按照当前步长更新位置,OffsetPos()函数用来直接将当前位置移动一段距离。如果范围或位置发生变化,那么进度条将自动重绘进度区域来及时反映当前工作的进展情况。
1、进度条的对象结构
进度条控制的建立方法
CProgressCtrl &ProgressCtrl 建立进度条控制对象结构
Create 建立进度条控制对象并绑定对象
进度条控制类CprogressCtrl::Create的调用格式如下:
BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );
其中参数dwStyle用来确定进度条控制的控制风格;参数rect用来确定进度条控制的大小和位置;参数pParentWnd用来确定进度条父窗口指针;参数nID用来确定进度条控制的控制符ID值。
2、进度条控制的类属性
进度条控制的类属性包括设置进度条最大最小控制范围SetRange、设置进度条当前位置 SetPos、设置进度条当前位置偏移值OffsetPos和设置进度条控制增量值SetStep。
3、进度条控制的操作方法
进度条控制的操作方法主要是使进度条控制并重绘进度条的StepIt函数。
进度条控制的应用技巧示例
1、利用应用程序向导AppWizard生成基于对象框的应用程序CProgDlg;
2、在对话框中设置进度条和静态文本控制,其ID分别为IDC_PROG和IDCPERCENT;
在对话框初始代码中增加控制的范围和位置:
在ProgDlg.h中设置两个数据成员,用来表示进度条的最大值和步长:
//ProgDlg.h
class CProgDlg:public Cdialog
{ ......//其它代码
public:
int m_nMax,m_nStep;
...... //其它代码
}
(2)在ProgDlg.cpp中设置初始状态
BOOL CProgDlg::OnInitDialog()
{ Cdialog::OnInitDialog();
......//其它代码
//TODO:Add extra initialization here
CProgressCtrl *pProgCtrl=(CProgressCtrl*)GetDlgItem(IDC_PROG);
pProgCtrl->SetRange(0,200);//设置进度条范围
......//其它代码
m_nMax=200;
m_nStep=10;
SetTimer(1,1000,NULL);//设置进度条更新时钟
return TRUE;
}
(3)完善WM_TIMER消息处理,使进度条按照当前步长进行更新,同时完成进度条的百分比显示:
void CProgDlg::OnTimer(UINT nIDEvent)
{
CProgressCtrl *pProgCtrl=(CProgressCtrl*)GetDlgItem(IDC_PROG);
int nPrePos=pProgCtrl->StepIt();//取得更新前位置
char test[10];
int nPercent=(int)(((nPrePos+m_nStep)/m_nMax*100+0.5);
wsprintf(test,?%d%%?,nPercent);
GetDlgItem(IDC_PERCENT)->SetWindowText(text);
Cdialog::OnTimer(nIDEvent);
}
四、树视图CTreeCtrl控件
树视图控件具有层次分明、结构化强、美观、灵活等特点,在各种操作系统中广为应用,是人们最熟悉、最常应用的控件。
从树视图控件出现到现在,它们一直被认为非常复杂并难于编程,与其它如编辑框、单选钮、复选框等控件进行比较,要使其正常运行,开发人员需要多做一些工作。然而,在使用复杂的同时,树视图控件又提供给开发人员更多的能力与空间。这里笔者就VC++中树视图控件的编程使用作一些介绍。
MFC提供的树视图控件CTreeCtrl类用于封装树视图控件的功能,同时它只是一个很“瘦”的包装器。它应用在对话框中或视图窗体中,同其他控件一样,可直接拖放到窗口中,改变其位置、大小和一些基本属性。
下面开始建立一个CTreeCtrl,步骤如下:
1.将CtreeCtrl拖到视图窗口中,调整位置、大小,并定义其对象标识为IDC_TREE。
2.改变其属性,选中Has buttons、Has lines复选框,这样用起树视图控件就同Windows中资源管理器中的一样了。
3.定义一个从CtreelCtrl继承的类CNewTree,在MFC ClassWizard中建立对新定义类的成员变量为m_MyTree,以后程序中对该控件的控制通过此成员变量来实现。这么做是为了以后方便对其添加其他用户自定义的功能。
做完以上几步,我们就可以开始编写代码了。首先,初始化树视图控件,为其关联一个图像列表;然后,用InsertItem函数增加节点。在视图窗口CMyFormView中的OnInitialupdate()事件中加入下面代码:
同CtreeView相比,CtreeCtrl是CtreeView的一个“轻巧”版本,编程也相对简单。
void CMyFormView::OnInitialupdate()
{
HICON hIcon[7];
CImageList m_imagelist;
m_imagelist.Create(16,16,0,7,7);
//建立一个图像列表
m_imagelist.SetBkColor(RGB(255,255,255));
hIcon[0]=AfxGetApp()->LoadIcon (IDI_ BMP0);
hIcon[1]=AfxGetApp()->LoadIcon (IDI_ BMP1);
……
hIcon[6]=AfxGetApp()->LoadIcon (IDI_ BMP6);
for(int i=0;i<=6;i++)
{
m_imagelist.Add (hIcon[i]);
}
m_MyTree.SetImageList (&m_imagelist,TVSIL_NORMAL)
//为m_MyTree设置一个图像列表,使CtreeCtrl的不同节点显示不同的图标
HTREEITEM m_item
m_item=m_MyTree.InsertItem ("Root",0,0,0,0);
//根节点的图标为IDI_BMP0
if (m_item!=NULL)
//根节点建立成功
{
m_MyTree.InsertItem("SubItem1",1,1,m_item)
//在根节点下建立一个子节点名为SubItem1,所显示的图标为IDI_BMP1。同理,可建//立其它节点,同一层次的节点显示相同的图标
}
……
}
CtreeCtrl类没有提供节点查找的函数,所以要求程序员自己编写特定条件的查找函数。
通常点击不同节点所触发的事件是不同的,此时,要增加OnSelchangedTree事件。在ClassWiard窗口中,选择CmyFormView类,对象标识为IDC_TREE,消息为TVN_SELCHANGED,添加函数,然后编辑代码。
void CMyFormView::OnSelchangedTree(NMHDR pNMHDR, LRESULT pResult)
{
HTREEITEM SelItem;
MyStructure ItemData; //MyStructure为用户定义的结构类型
SelItem=m_MyTree.GetSelectedItem ();
ItemData=GetItemData(SelItem);
//获得该节点的数据指针
Switch (ItemData->value1)
{
case 0:{……}
//用户指定的操作
case 1:{……}
……
}
}
在实际编程中,可能不仅仅是为了显示,树视图控件上的每一个节点都对应特定的值,所以要将指向具体数据的指针赋给对应的节点。具体做法是在用户自定义类CNewTree中新增一过程SetValue(HTREEITEM)。具体代码如下:
void CNewTree::SetValue(HTREEITEM Item_parm,int Value1,int value2…..)
{
MyStructure ItemData
ItemData= new MyStructure;;
ItemData-〉value1=value1;
ItemData-〉value2=value2;
……
SetItemData(Item_parm ,(DWORD)ItemData);
}
调用时,传入对应的参数,即可对给定的节点赋值。当然这里用了动态分配地址new,因此,在程序结束前,一定不要忘记删除这些空间。
void CNewTree::deleteData(HTREEITEM Item)
{
MyStructure ItemData;
ItemData=GetItemData(Item);
//获得该节点的数据指针
if (ItemData!=NULL){ delete[] (char)ItemData;}
//删除所占用的空间
……
}
根据树视图的结构特点,我们采用递归遍历的方法来查找节点,当然你可根据条件缩小遍历的范围。这里笔者以节点值匹配为条件,编写自定义的函数FindNode(),返回第一个符合条件的节点的句柄,具体代码如下:
HTREEITEM CNewTree::FindNode(HTREEITEM NodeItem, int &&NodeValue)
{
MyStructure ItemData;
HTREEITEM NextItem;
if (NodeItem= =NULL)
return NULL; //递归出口
else
{
while(NodeItem!=NULL)
{
ItemData=GetItemData(NodeItem);
If (ItemData-〉value1= =NodeValue)
return NodeItem;
NodeItem=GetChildItem(NodeItem);
//得到当前节点的第一个子节点的句柄
If(FindNode(NodeItem, NodeValue)= =NULL);
//递归查找
NodeItem=GetNextSiblingItem(NodeItem);
//得到当前节点的兄弟节点的句柄
}
}
}
到此为止,介绍了一些树视图控件编程方法,包括树视图控件的建立、节点值的赋予和删除、查找。当然,它应用的方面很广,使用方法也很多。这里提供了构建树视图控件的基本框架,在此基础上,可进行扩展,从而完成更强大的功能,如同列表视图控件结合,为其加上弹出式选单等等。感兴趣的同学不妨自己扩展该控件试试。
五、组合框ComboBox控件
我们经常会使用到组合框,而组合框是是有2种功能的--下拉和列表。一般情况下,列表框的宽度和选择框是一样宽的,但是我们有些时候确实很需要把列表框的宽度变大,以便让我们能更好的看我们要选的东西。
为了能有这个功能,我写了下面的这个函数。首先得在你的对话框中添加一个的WM_CTLCOLOR的消息句柄,或者使用CComboBox的继承类,而且在其中添加下面的代码:
HBrush tvisualcombo::onctlcolor(CDC* pdc, CWND* pwnd, UINT nctlcolor)
{
HBrush hbr = CComboBox::onctlcolor(pdc, pwnd, nctlcolor);
switch (nctlcolor) {
case ctlcolor_edit:
break;
case ctlcolor_listbox:
if (listwidth > 0) {
// new width of a listbox is defined
CRect rect;
pwnd->GetWindowRect(&rect);
if (rect.Width() != listwidth) {
rect.right = rect.left + listwidth;
pwnd->MoveWindow(&rect);
}
}
break;
}
return hbr;
}
这样之后还没有完全好,你还得刷新一下列表框,那样才能随时根据列表框中的文本的长度,而改变列表框的宽度,要想这样的话,你还得这样,你必须扫描列表框中的条目,还得计算其中文本的长度(通过pdc),这样你如果再刷新列表框的话,才能一条目中比较长的来显示。
上面的方法是通过WM_CTLCOLOR消息来实现的,后来才知道在MFC的CComboBox类中有一个函数也可以实现同样的功能,就是:
CComboBox::SetdroppedWidth(int width);
通过这个函数,你可以把宽度设成你自己喜欢的值,而它的实际的宽度是下面2个值中的最大值:
1.你所设置的值(就是通过上面的函数所设置的值)
2. 列表框的值
附录二:Microsoft 命名习惯
M i c r o s o f t已为MFC C++编程时使用的程序变量设立了一种非正式的自愿的命名习惯。尽管这种习惯并不强求在创建一个 M F C应用程序时使用,也许并不总是实用的,但它确实有一些优点。除了能给一个变量常规的描述名以外,用这种习惯还可以分辨一个变量的类型以及是否一个类的成员变量。
以下面符号开始的变量含义:
m _ 一个类的成员变量
m _ b 布尔变量
m _ n 整型变量
m _ p 指针变量
m _ d w D W O R D变量
m _ c l a s s n a m e 在c l a s s n a m e中指定的类类型
批评这种习惯的人指出,这有悖于语言的灵活性,例如在命名变量 m _ b F l a g以后,现在要将它转变为整型,而不编辑所有出现的地方或不管它,是非常困难的,并有可能迷惑未来的软件开发者。然而,据笔者个人经验,很少需要改变变量类型,而且能够迅速区别一个变量做什么用,远远胜过它具有的不足。
版权声明:本文标题:MFC 利用小型数据库Access 少步惆 教你用VC开发 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1729477964a1202267.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论