MFC教程 -- Windows界面开发
Windows消息机制 初步认识MFC
要想熟练掌握 Windows 应用程序的开发, 首先需要理解 Windows 平台下程序运行的内部机制。如果想要更好的学习掌握 MFC,必须要先了解Windows 程序的内部运行机制,为我们扫清学习路途中的第一个障碍,为进一步学习 MFC 程序打下基础。
1.1 基本概念解释
我们在编写标准C程序的时候,经常会调用各种库函数来辅助完成某些功能:初学者使用得最多的C库函数就是printf了,这些库函数是由你所使用的编译器厂商提供的。在Windows平台下,也有类似的函数可供调用:不同的是,这些函数是由Windows操作系统本身提供的。
1.1.1 SDK和API
SDK: 软件开发工具包(Software Development Kit),一般都是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。
API函数: Windows操作系统提供给应用程序编程的接口(Application Programming Interface)
Windows应用程序API函数是通过C语言实现的,所有主要的 Windows 函数都在 Windows.h 头文件中进行了声明。Windows 操作系统提供了 1000 多种 API 函数。
1.1.2 窗口和句柄
窗口是 Windows 应用程序中一个非常重要的元素,一个 Windows 应用程序至少要有一个窗口,称为主窗口。窗口是屏幕上的一块矩形区域,是 Windows 应用程序与用户进行交互的接口。利用窗口可以接收用户的输入、以及显示输出。一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小化框、最大化框、 可调边框,有的还有滚动条。如下图:
窗口可以分为客户区和非客户区, 如上图。 客户区是窗口的一部分, 应用程序通常在客户区中显示文字或者绘制图形。 标题栏、 菜单栏、 系统菜单、 最小化框和最大化框、 可调边框统称为窗口的非客户区, 它们由 Windows 系统来管理, 而应用程序则主要管理客户区的外观及操作。
窗口可以有一个父窗口, 有父窗口的窗口称为子窗口。除了上图所示类型的窗口外, 对话框和消息框也是一种窗口。 在对话框上通常还包含许多子窗口, 这些子窗口的形式有按钮、 单选按钮、 复选框、 组框、 文本编辑框等。
在 Windows 应用程序中, 窗口是通过窗口句柄( HWND) 来标识的。 我们要对某个窗口进行操作, 首先就要得到这个窗口的句柄。 句柄( HANDLE) 是 Windows 程序中一个重要的概念, 使用也非常频繁。 在 Windows 程序中, 有各种各样的资源( 窗口、 图标、光标,画刷等), 系统在创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 即句柄。 在后面的内容中我们还会看到图标句柄( HICON)、 光标句柄( HCURSOR) 和画刷句柄( HBRUSH)。
1.1.3 消息与消息队列
Windows 程序设计是一种完全不同于传统的 DOS 方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这一事件,于是将这个事件包装成一个消息,投递到应用程序的消息队列中,然后应用程序从消息队列中取出消息并进行响应。在这个处理过程中,操作系统也会给应用程序“ 发送消息”。所谓“ 发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程。
消息
在 Windows 程序中,消息是由 MSG 结构体来表示的。MSG 结构体的定义如下:
该结构体中各成员变量的含义如下:
第一个成员变量 hwnd 表示消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。在 Windows 程序中,用 HWND类型的变量来标识窗口。
第二个成员变量 message 指定了消息的标识符。 在 Windows 中, 消息是由一个数值来表示的, 不同的消息对应不同的数值。 但是由于数值不便于记忆, 所以 Windows 将消息对应的数值定义为 WM_XXX 宏( WM 是 Window Message 的缩写) 的形式, XXX 对应某种消息的英文拼写的大写形式。 例如, 鼠标左键按下消息是 WM_LBUTTONDOWN, 键盘按下消息是 WM_KEYDOWN, 字符消息是 WM_CHAR , 等等。 在程序中我们通常都是以WM_XXX 宏的形式来使用消息的。
第三、 第四个成员变量 wParam 和 lParam,用于指定消息的附加信息。 例如, 当我们收到一个字符消息的时候,message 成员变量的值就是 WM_CHAR, 但用户到底输入的是什么字符,那么就由 wParam 和 lParam 来说明。wParam、lParam 表示的信息随消息的不同而不同。如果想知道这两个成员变量具体表示的信息,可以在 MSDN 中关于某个具体消息的说明文档查看到。WPARAM 和 LPARAM 这两种类型的定义,实际上就是 unsigned int和 long。
第五、第六个变量分别表示消息投递到消息队列中的时间和鼠标的当前位置。
消息队列
每一个 Windows 应用程序开始执行后, 系统都会为该程序创建一个消息队列, 这个消息队列用来存放该程序创建的窗口的消息。 例如, 当我们按下鼠标左键的时候, 将会产生WM_LBUTTONDOWN 消息, 系统会将这个消息放到窗口所属的应用程序的消息队列中,等待应用程序的处理。 Windows 将产生的消息依次放到消息队列中, 而应用程序则通过一个消息循环不断地从消息队列中取出消息, 并进行响应。 这种消息机制, 就是 Windows程序运行的机制。 关于消息队列和消息响应, 在后面我们还会详细讲述。
WinMain函数
当 Windows 操作系统启动一个程序时,它调用的就是该程序的 WinMain 函数( 实际是由插入到可执行文件中的启动代码调用的)。 WinMain 是 Windows程序的入口点函数,与 DOS 程序的入口点函数 main 的作用相同,当 WinMain 函数结束或返回时,Windows 应用程序结束。
1.2 Windows 编程模型
一个完整的Win32程序,该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序的实现步骤为:
-
- WinMain函数的定义
- 创建一个窗口
- 进行消息循环
- 编写窗口过程函数
1.2.1 WinMain函数的定义
WinMain函数的原型声明如下:
知识点补充:
在我们以后的学习中会经常遇到以下宏定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
__stdcall和__cdecl是两种函数名字修饰(注意是两个下划线),规定了函数参数的入栈方式。
相同点:
- __stdcall还是__cdecl函数参数都是从右向左入栈的
- 并且由调用者完成入栈操作
不同点:
- __stdcall方式在函数返回前自动清空堆栈
- __cdecl则由调用者维护内存堆栈
- 由__cdecl约定的函数只能被C/C++调用。
Windows上不管是C还是C++,默认使用的都是__stdcall方式。
__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
WinMain 函数接收 4 个参数,这些参数都是在系统调用 WinMain 函数时,传递给应用程序的。
第一个参数 hInstance 表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows 下运行时,它唯一标识运行中的实例( 注意,只有运行中的程序实例, 才有实例句柄)。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过 hInstance 参数传递给 WinMain 函数。
第二个参数 hPrevInstance 表示当前实例的前一个实例的句柄。通过查看 MSDN 我们可以知道,在 Win32 环境下,这个参数总是 NULL,即在 Win32 环境下,这个参数不再起作用。
第三个参数 lpCmdLine 是一个以空终止的字符串, 指定传递给应用程序的命令行参数。
第四个参数 nCmdShow 指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。
1.2.2 创建一个窗口
创建一个完整的窗口,需要经过下面几个步骤:
- 设计一个窗口类
- 注册窗口类
- 创建窗口
- 显示及更新窗口
下面详细对创建窗口的过程进行介绍:
据说微软开发MFC的时候为了和其它类库有所区别就在所有MFC的类库前加了一个C
结果后来被泛化了。在MFC中被封装的类中,各种类前面都加了一个C,谁能说说这个C具体代表了什么含义
如:CWinApp,CDocument
Class,类的意思。
表示它是一个类,而不是别的变量……
设计一个窗口类
一个完整的窗口具有许多特征, 包括光标( 鼠标进入该窗口时的形状)、 图标、 背景色等。窗口的创建过程类似于汽车的制造过程。我们在生产一个型号的汽车之前, 首先要对该型号的汽车进行设计, 在图纸上画出汽车的结构图, 设计各个零部件, 同时还要给该型号的汽车取一个响亮的名字, 例如“ 奥迪 A6”。
类似地, 在创建一个窗口前, 也必须对该类型的窗口进行设计, 指定窗口的特征。 当然, 在我们设计一个窗口时, 不像汽车的设计这么复杂, 因为 Windows 已经为我们定义好了一个窗口所应具有的基本属性, 我们只需要像考试时做填空题一样, 将需要我们填充的部分填写完整, 一种窗口就设计好了。 在 Windows 中, 要达到作填空题的效果, 只能通过结构体来完成, 窗口的特征就是由 WNDCLASS 结构体来定义的。 WNDCLASS 结构体的定义如下:
第一个成员变量 style 指定这一类型窗口的样式,常用的样式如下:
-
-
- CS_HREDRAW
-
当窗口水平方向上的宽度发生变化时, 将重新绘制整个窗口。 当窗口发生重绘时, 窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。
-
-
- CS_VREDRAW
-
当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。
-
-
- CS_NOCLOSE
-
禁用系统菜单的 Close 命令,这将导致窗口没有关闭按钮。
-
-
- CS_DBLCLKS
-
当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
第二个成员变量 lpfnWndProc 是一个函数指针,指向窗口过程函数,窗口过程函数是一个回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。
针对 Windows 的消息处理机制, 窗口过程函数被调用的过程如下(了解即可):
- 在设计窗口类的时候,将窗口过程函数的地址赋值给 lpfnWndProc 成员变量。
- 调用 RegsiterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。
- 当应用程序接收到某一窗口的消息时,调用 DispatchMessage(&msg)将消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。
一个 Windows 程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联( 通过 WNDCLASS 结构体中的 lpfnWndProc 成员变量指定), 基于该窗口类创建的窗口使用同一个窗口过程。
lpfnWndProc 成员变量的类型是 WNDPROC,WNDPROC 的定义如下:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
在这里又出现了两个新的数据类型 LRESULT 和 CALLBACK,它们实际上是 long 和__stdcall。从 WNDPROC 的定义可以知道, WNDPROC 实际上是函数指针类型。
第三个成员变量 cbClsExtra: 用于存储类的附加信息,一般我们将这个参数设置为 0。
第四个成员变量 cbWndExtra:窗口附加内存,一般我们将这个参数设置为 0。
第五个成员变量 hInstance 指定包含窗口过程的程序的实例句柄。
第六个成员变量 hIcon 指定窗口类的图标句柄。这个成员变量必须是一个图标资源的句柄,如果这个成员为 NULL,那么系统将提供一个默认的图标。在为 hIcon 变量赋值时,可以调用 LoadIcon 函数来加载一个图标资源,返回系统分配给该图标的句柄。 该函数的原型声明如下所示:
HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName)
第七个成员变量 hCursor 指定窗口类的光标句柄。 这个成员变量必须是一个光标资源的句柄, 如果这个成员为 NULL, 那么无论何时鼠标进入到应用程序窗口中, 应用程序都必须明确地设置光标的形状。在为 hCursor 变量赋值时,可以调用 LoadCursor 函数来加载一个光标资源, 返回系统分配给该光标的句柄。该函数的原型声明如下所示:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
第八个成员变量 hbrBackground 指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。我们可以调用 GetStockObject 函数来得到系统的标准画刷。 GetStockObject 函数的原型声明如下所示:
HGDIOBJ GetStockObject( int fnObject);
GetStockObject 函数不仅可以用于获取画刷的句柄, 还可以用于获取画笔、字体和调色板的句柄。由于 GetStockObject 函数可以返回多种资源对象的句柄,在实际调用该函数前无法确定它返回哪一种资源对象的句柄,因此它的返回值的类型定义为 HGDIOBJ, 在实际使用时,需要进行类型转换。
第九个成员变量 lpszMenuName 是一个以空终止的字符串, 指定菜单资源的名字。如果将lpszMenuName 成员设置为 NULL,那么基于这个窗口类创建的窗口将没有默认的菜单。要注意,菜单并不是一个窗口,很多初学者都误以为菜单是一个窗口。
第十个成员变量 lpszClassName是一个以空终止的字符串,指定窗口类的名字。
核心代码如下:
注册窗口类
设计完窗口类( WNDCLASS) 后, 需要调用 RegisterClass 函数对其进行注册,注册成功后, 才可以创建该类型的窗口。 注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数, 即上一步骤中所设计的窗口类对象的指针。
核心代码:
RegisterClass(&wc);
创建窗口
设计好窗口类并且将其成功注册之后, 就可以用 CreateWindow 函数产生这种类型的窗口了。 CreateWindow 函数的原型声明如下:
参数 lpClassName 指定窗口类的名称,即我们在步骤 1 设计一个窗口类中为 WNDCLASS的 lpszClassName 成员指定的名称。
参数 lpWindowName 指定窗口的名字。 如果窗口样式指定了标题栏, 那么这里指定的窗口名字将显示在标题栏上。
参数 dwStyle 指定创建的窗口的样式。要注意区分 WNDCLASS 中的 style 成员与 CreateWindow 函数的 dwStyle 参数, 前者是指定窗口类的样式, 基于该窗口类创建的窗口都具有这些样式, 后者是指定某个具体的窗口的样式。我们可以给创建的窗口指定WS_OVERLAPPEDWINDOW类型,这是一种多种窗口类型的组合类型。
参数 x,y,nWidth,nHeight 分别指定窗口左上角的 x,y 坐标,窗口的宽度,高度。如果参数 x 被设为 CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略 y 参数。如果参数 nWidth 被设为 CW_USEDEFAULT, 那么系统为窗口选择默认的宽度和高度, 参数 nHeight 被忽略。
参数 hWndParent 指定被创建窗口的父窗口句柄。
参数 hMenu 指定窗口菜单的句柄。
参数 hInstance 指定窗口所属的应用程序实例的句柄。
参数 lpParam:作为 WM_CREATE 消息的附加参数 lParam 传入的数据指针。 在创建多文档界面的客户窗口时, lpParam 必须指向 CLIENTCREATESTRUCT 结构体。多数窗口将这个参数设置为 NULL。
如果窗口创建成功,CreateWindow 函数将返回系统为该窗口分配的句柄,否则,返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。
核心代码:
显示及更新窗口
- 显示窗口
窗口创建之后,我们要让它显示出来,这就跟汽车生产出来后要推向市场一样。调用函数 ShowWindow 来显示窗口,该函数的原型声明如下所示:
BOOL ShowWindow( HWND hWnd, int nCmdShow );
ShowWindow 函数有两个参数, 第一个参数 hWnd 就是在上一步骤中成功创建窗口后返回的那个窗口句柄;第二个参数 nCmdShow 指定了窗口显示的状态,常用的有以下几种。
- SW_HIDE: 隐藏窗口并激活其他窗口。
- SW_SHOW: 在窗口原来的位置以原来的尺寸激活和显示窗口。
- SW_SHOWMAXIMIZED: 激活窗口并将其最大化显示。
- SW_SHOWMINIMIZED: 激活窗口并将其最小化显示。
- SW_SHOWNORMAL: 激活并显示窗口。如果窗口是最小化或最大化的状态,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。
- 更新窗口
在调用 ShowWindow 函数之后, 我们紧接着调用 UpdateWindow 来刷新窗口,就好像我们买了新房子,需要装修一下。UpdateWindow 函数的原型声明如下:
BOOL UpdateWindow( HWND hWnd );
其参数 hWnd 指的是创建成功后的窗口的句柄。 UpdateWindow 函数通过发送一个WM_PAINT 消息来刷新窗口, UpdateWindow 将 WM_PAINT 消息直接发送给了窗口过程函数进行处理, 而没有放到我们前面所说的消息队列里, 请读者注意这一点。 关于WM_PAINT 消息的作用和窗口过程函数, 后面我们将会详细讲解。
到此,一个窗口就算创建完成了。
1.2.3 消息循环
在创建窗口、显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用 GetMessage()函数,该函数的原型声明如下:
参数 lpMsg 指向一个消息( MSG) 结构体,GetMessage 从线程的消息队列中取出的消息信息将保存在该结构体对象中。
参数 hWnd 指定接收属于哪一个窗口的消息。通常我们将其设置为 NULL,用于接收属于调用线程的所有窗口的窗口消息。
参数 wMsgFilterMin 指定要获取的消息的最小值,通常设置为 0。
参数 wMsgFilterMax 指定要获取的消息的最大值。如果 wMsgFilterMin 和 wMsgFilter Max 都设置为 0, 则接收所有消息。
GetMessage 函数接收到除 WM_QUIT 外的消息均返回非零值。对于 WM_QUIT 消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数 hWnd 是无效的窗口句柄或 lpMsg 是无效的指针时。
通常我们编写的消息循环代码如下:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
前面已经介绍了,GetMessage 函数只有在接收到 WM_QUIT 消息时,才返回 0。此时while 语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到 WM_QUIT消息时,Windows 应用程序就通过这个 while 循环来保证程序始终处于运行状态。
TranslateMessage 函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用 GetMessage 函数时被取出。当我们敲击键盘上的某个字符键时, 系统将产生 WM_KEYDOWN 和 WM_KEYUP 消息。 这两个消息的附加参数( wParam 和 lParam) 包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的 ASCII 码,TranslateMessage 这个函数就可以将 WM_KEYDOWN 和 WM_KEYUP 消息的组合转换为一条 WM_CHAR 消息( 该消息的 wParam 附加参数包含了字符的 ASCII 码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。
DispatchMessage 函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage 实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理( 响应)。
Windows 应用程序的消息处理机制如下图所示:
-
- 操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。
- 应用程序在消息循环中调用 GetMessage 函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用 TranslateMessage 产生新的消息。
- 应用程序调用 DispatchMessage,将消息回传给操作系统。消息是由 MSG 结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此, DispatchMessage 函数总能进行正确的传递。
- 系统利用 WNDCLASS 结构体的 lpfnWndProc 成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理( 即“ 系统给应用程序发送了消息”)。
知识点补充:
进队列消息和不进队列消息
Windows 程序中的消息可以分为“ 进队消息” 和“ 不进队消息”。
- 不进队消息是指由Windows直接调用消息处理函数,把消息直接交给其处理。
- 进队消息是指Windows将消息放入到程序中的消息队列中,并通过程序中的消息循环,循环把消息取出,经过一定处理(如例子中经过translate),然后由函数DispathMessage函数将消息分发给消息处理函数处理。
进队消息基本上是用户的输入比如:
- 击键的消息(WM_KEYDOWN、WM_KEYUP)
- 键盘输入产生字符(WM_CHAR)
- 鼠标移动(WM_MOUSEMOVE)
- 鼠标左键(WM_LBUTTONDOWN)
- 计时消息(WM_TIMER)
- 刷新消息(WM_PAINT)
- 退出消息(WM_QUIT)
一般情况下,不进队消息的产生是由于调用了其他Windows函数。如,当调用CreateWindow时,Windows将创建WM_CREATE消息、当调用ShowWindow时,将产生WM_SIZE和WM_SHOWWINDOW消息、当调用UpdateWindow时创建的WM_PAINT消息(注意,并不是某个类型是进队消息就永远是进队消息,如WM_PAINT有进队的,也有不进队的)、还有其他进队消息也有可能在不进队消息中出现,整个处理过程是复杂的,但由于Windows已经解决大部分的问题,因此我们可以认为我们获得的消息是有序的、同步的。
发送消息:SendMessage 和 PostMessage
- SendMessage发送“不进队消息”,直接把消息发送给窗口,并调用该窗口的窗口过程函数进行处理。在窗口过程对消息处理完毕后,返回处理结果。
- PostMessage发送“进队消息”。将消息放入与创建窗口的线程相关联的消息队列后立即返回。
1.2.4 窗口过程函数
在完成上述步骤后,剩下的工作就是编写一个窗口过程函数, 用于处理发送给窗口的消息。 一个 Windows 应用程序的主要代码部分就集中在窗口过程函数中。窗口过程函数的声明形式,如下所示:
窗口过程函数的名字可以随便取, 如 WinSunProc, 但函数定义的形式必须和上述声明的形式相同。系统通过窗口过程函数的地址( 指针) 来调用窗口过程函数, 而不是名字。
WindowProc 函数的 4 个参数分别对应消息的窗口句柄、消息代码、消息代码的两个附加参数。一个程序可以有多个窗口,窗口过程函数的第 1 个参数 hwnd 就标识了接收消息的特定窗口。在窗口过程函数内部使用 switch/case 语句来确定窗口过程接收的是什么消息,以及如何对这个消息进行处理。
switch(uMsg)
{
case WM_LBUTTONDOWN:
break;
case WM_DESTROY:
PostQuiteMessage(0);
break;
case WM_CLOSE:
DestroyWindow(hWnd);
break;
……
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
- DefWindowProc函数
DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理。对于大多数的消息,应用程序都可以直接调用 DefWindowProc函数进行处理。在编写窗口过程时,应该将 DefWindowProc 函数的调用放到 default 语句
中,并将该函数的返回值作为窗口过程函数的返回值。
- WM_CLOSE 消息
对 WM_CLOSE 消息的响应并不是必须的,如果应用程序没有对该消息进行响应, 系统将把这条消息传给 DefWindowProc 函数而 DefWindowProc 函数则调用 DestroyWindow 函数来响应这条 WM_CLOSE 消息。
- WM_DESTROY消息
DestroyWindow 函数在销毁窗口后,会给窗口过程发送 WM_DESTROY消息,我们在该消息的响应代码中调用 PostQuitMessage 函数。PostQuitMessage函数向应用程序的消息队列中投递一条 WM_QUIT 消息并返回。我们在前边介绍过,GetMessage 函数只有在收到 WM_QUIT 消息时才返回 0,此时消息循环才结束,程序退出。要想让程序正常退出, 我们必须响应 WM_DESTROY 消息,并在消息响应代码中调用PostQuitMessage,向应用程序的消息队列中投递 WM_QUIT 消息。传递给 PostQuitMessage函数的参数值将作为 WM_QUIT 消息的 wParam 参数,这个值通常用做 WinMain 函数的返回值。
1.2.5 Windows编程模型
Windows程序使用的事件驱动的编程模型,应用程序通过处理操作系统发送来的消息来响应事件。事件可能是一次击键,鼠标单击或是要求窗口更新的命令以及其他事情。Windows程序的进入点函数WinMain,但是大多数操作是在称为窗口过程的函数中进行的。窗口过程函数处理发送给窗口的消息。WinMain函数创建该窗口并进入消息循环,即获取消息或将其调度给窗口过程。消息被检索之前处于消息队列中等待。一个典型的应用程序的绝大部分操作是在响应它收到的消息,除了等待下一个消息到达以外,它几乎什么也不做。
窗口过程一般要调用其他函数来帮助处理接收到的消息。它可以调用应用程序自己的函数,也可以调用Windows程序提供的API函数。应用程序不能处理的消息被传递给了名为DefWindowProc的API函数,该函数对未被处理的消息提供默认响应。
- Windows风格程序 – Hello MFC
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
-
- WM_PAINT 消息:
当窗口客户区的一部分或者全部变为“ 无效” 时, 系统会发送 WM_PAINT 消息,通知应用程序重新绘制窗口。当窗口刚创建的时候, 整个客户区都是无效的。因为这个时候程序还没有在窗口上绘制任何东西,当调用 UpdateWindow 函数时,会发送 WM_PAINT 消息给窗口过程,对窗口进行刷新。当窗口从无到有、改变尺寸、最小化后再恢复、被其他窗口遮盖后再显示时, 窗口的客户区都将变为无效, 时系统会给应用程序发送 WM_PAINT 消息,通知应用程序重新绘制。
-
- BeginPaint、EndPaint 函数
BeginPaint 函数的第 1 个参数是窗口的句柄,第二个参数是 PAINTSTRUCT 结构体的指针,该结构体对象用于接收绘制的信息。在 调 用 BeginPaint 时,如果客户区的背景还没有被擦除, 那么 BeginPaint 会 发 送WM_ERASEBKGND 消息给窗口, 系统就会使用 WNDCLASS 结构体的 hbrBackground 成员指定的画刷来擦除背景。
在响应 WM_PAINT 消息的代码中, 要得到窗口的 DC, 必须调用 BeginPaint 函数。BeginPaint 函数也只能在 WM_PAINT 消息的响应代码中使用, 在其他地方, 只能使用GetDC 来得到 DC 的句柄。 另外, BeginPaint 函数得到的 DC, 必须用 EndPaint 函数去释放。
-
- TextOut函数
调用 TextOut 函数在(300, 300) 的位置输出一个字符串“Hello,MFC!”。当发生重绘时,窗口中的文字和图形都会被擦除。在擦除背景后,TextOut 函数又一次执行,在窗口中再次绘制出 “Hello,MFC!”。 这个过程对用户来说是透明的,用户并不知道程序执行的过程,给用户的感觉就是你在响应 WM_PAINT 消息的代码中输出的文字或图形始终保持在窗口中。换句话说,如果我们想要让某个图形始终在窗口中显示, 就应该将图形的绘制操作放到响应 WM_PAINT 消息的代码中。那么系统为什么不直接保存窗口中的图形数据, 而要由应用程序不断地进行重绘呢?
这主要是因为在图形环境中涉及的数据量太大,为了节省内存的使用,提高效率,而采用了重绘的方式。
程序运行结果:
正在上传…重新上传取消
1.3 初步认识MFC
1.3.1 MFC是什么?
微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是一个微软公司提供的类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。
MFC把Windows SDK API函数包装成了几百个类,MFC给Windows操作系统提供了面向对象的接口,支持可重用性、自包含性以及其他OPP原则。MFC通过编写类来封装窗口、对话框以及其他对象,引入某些关键的虚函数(覆盖这些虚函数可以改变派生类的功能)来完成,并且MFC设计者使类库带来的总开销降到了最低。
1.3.2 编写第一个MFC应用程序
两个重要的MFC类:
-
- CWinApp 应用程序类
- CFrameWnd 窗口框架类
- CFrameWnd 框架窗口类
MFC的CWnd类以及其派生类为 窗口 或 应用程序创建的窗口 提供了面向对象的接口。CFrameWnd就是从CWnd派生出来的。CFrameWnd模仿框架窗口行为,我们可以把框架窗口作为顶层窗口看待,它是应用程序与外部世界的主要接口。
如果想要创建一个窗口,可以在此类中调用Create或CreateEX函数,并且在CWinApp::InitInstance中创建一个框架窗口对象,并通过ShowWindow函数将其显示出来。
CFrameWnd::Create原型如下:
正在上传…重新上传取消
Create接收的8个参数6个有默认值定义。我们只需要为函数的前两个参数指定值,剩下六个参数接收默认值。第一个参数lpszClassName指定了窗口基于WNDCLASS类的名称,为此将其设定为NULL将创建一个基于已注册的WNDCLASS类的默认框架窗口。lpszWindowName参数指定将在窗口的标题栏出现的文字。
- CWinApp应用程序类
MFC应用程序的核心就是基于CWinApp类的应用程序对象。CWinApp提供了消息循环来检索消息并将消息调度给应用程序窗口。它还包括可被覆盖的、用来自定义应用程序行为的主要虚函数。一旦包含Afxwin.h,就可以将CWinApp以及其他MFC类引入应用程序中。一个MFC程序可以有且仅有一个应用程序对象,此对象必须声明为在全局范围内有效,以便它在程序开始时即在内存中被实例化。
-
- InitInstance函数
CWinApp::InitInstance函数是一个虚函数,其默认操作仅有一条语句:
return TRUE;
InitInstance的目的是给应用程序提供一个自身初始化的机会,其返回值决定了框架接下来要执行的内容,如果返回FALSE将关闭应用程序,如果初始化正常返回TRUE以便允许程序继续进行。此函数是MFC应用程序的入口。
-
- m_pMainWnd 成员变量
在 CWinApp 中有一个名为 m_pMainWnd 的成员变量。 该变量是一个 CWnd 类型的指针,它保存了应用程序框架窗口对象的指针。也就是说,是指向 CFramWnd 对象(框架窗口类对象)的指针。
- 程序代码实现
正在上传…重新上传取消
正在上传…重新上传取消
1.4 消息映射
消息映射是一个将消息和成员函数相互关联的表。比如,框架窗口接收到一个窗口绘制消息,MFC将搜索该窗口的消息映射,如果存在一个处理WM_PAINT消息的处理程序,然后就调用OnPaint。MFC为执行消息映射在内部所做的工作隐藏在某些十分复杂的宏当中,但使用消息映射是相当简单的。下面是是将消息映射添加到一个类中所做的全部工作:
- 通过将DECLARE_MESSAGE_MAP添加到类声明中,声明消息映射。
正在上传…重新上传取消
- 通过放置标识消息的宏来执行消息映射,相应的类将在对BEGIN_MESSAGE_MAP和END_MESSAGE_MAP的调用之间处理消息。
正在上传…重新上传取消
- 添加成员函数来处理消息
正在上传…重新上传取消
1.5 窗口绘制
我们的程序如果想要随心所欲的在屏幕上进行绘制,必须响应来自windows的WM_PAINT消息,此消息通知它该更新窗口了。
WM_PAINT消息的发生可能有多重原因:由于移动了窗口;由于窗口原先被覆盖的部分显露了出来;或者由于窗口大小改变了等等。不论诱因是什么,都需要由应用程序来负责通过响应WM_PAINT消息绘制其窗口的客户区。由Windows来绘制非客户区,这样所有的应用程序将具有一致的外观。如果应用程序不为客户区执行其自身的绘制例程,窗口的内部将是一片空白。
在我们的上面的示例程序中,WM_PAINT消息是由CMainWindow::OnPaint来处理的,当一个WM_PAINT消息抵达时都将调用它。OnPaint的绘制是通过构造一个名为dc的CPaintDC对象开始的:
CPaintDC dc(this);
MFC的CPaintDC类是从CDC类派生出来的。CDC类封装了Windows设备环境,以及包含了绘制屏幕、打印机和其他设备的几十个成员函数。CPaintDC只在WM_PAINT消息处理程序中使用,它是CDC的一个特殊的例子。如果想在屏幕上进行绘制,就必须在OnPaint程序内使用CPaintDC对象。
1.6 文档/视图结构体系
MFC应用程序框架结构的基石是文档/视图体系结构,它定义了一种程序结构,这种结构依靠文档对象保存应用程序的数据,并依靠视图对象控制视图中显示的数据,把数据本身与它的显示分离开。数据的存储和加载由文档类来完成,数据的显示和修改则由视类来完成。 MFC在类CDocument和CView中为稳定视图提供了基础结构。CWinApp、CFrameWnd和其他类与CDocument和CView合作,把所有的片段连在了一起。
当然我们可以不使用文档/视图来编写MFC程序,但是想要从框架结构中获得最大的好处并利用MFC的某些高级特性就必须使用文档/视图体系结构。此处的文档是对程序数据的抽象表示,不能理解为其只对文字或电子表格程序有用。
CView类也派生与CWnd类,框架窗口是视图窗口的一个父窗口。主框架窗口(CFrameWnd)是整个应用程序外框所包括的部分,即图中粗框以内的内容,而视类窗口只是主框架中空白的地方。
正在上传…重新上传取消
我们后序会对文档/视图做详细介绍。
1.7 Widnows字符集和TEXT(_T)宏
- 8位的ANSI字符集 - 多字节字符集
在Windows98以及以前的版本使用8位ANSI字符集,它类似于我们程序员熟悉的ASCII字符集。
字符串表示方法:
char sz[] = “ABCDEFG”;
char *psz = “ABCDEFG”;
- 16位的Unicode字符集 – 宽字符集
在WindowsNT和Windows2000后开始使用16位的Unicode字符集,它是ANSI字符集的一个超集。Unicode适用于国际市场销售的应用程序,因为它包含各种各样来自非U.S.字母表的字符,比如中文,日文,韩文,西欧语言等。
字符串表示方法:
wchar_t wsz[] = L”ABCDEFG”;
wchar_t *pwsz = L”ABCDEFG”;
在字符串前加字母L表示将ANSI字符集转换成Unicode字符集。
- MFC中的TEXT(_T)宏
MFC中的TEXT宏可以自动适应字符类型,如果定义了预处理器程序符号_UNICODE,那么编译器将使用Unicode字符,如果没用定义该预处理器程序符号,那么编译器将使用ANSI字符。
使用TEXT宏修饰字符串常量就足以使一个应用程序完全不关心其字符集吗?
回答是并不一定,我们还需要做以下操作:
-
- 将字符声明为TCHAR类型,而不是char或wchar_t类型。如果定义了_UNICODE符号TCHAR将变为wchar_t类型。如果没用定义_UNICODE符号,TCHAR将变为普通古老的char类型。
- 不要使用char* 或者wchar_t* 来声明TCHAR字符串的指针,而应当使用TCHAR*,或者使用更佳的LPTSTR(指向TCHAR字符串的指针)和LPCTSTR(指向const TCHAR字符串的指针)数据类型。
- 不要认为一个字符只有8位宽,可以借助sizeof(TCHAR)来划分缓冲区长度。
1.8 用向导生成一个MFC应用程序
在VS中选择 文件 -- 新建 -- 项目…
正在上传…重新上传取消
选择 MFC – MFC应用程序,接下来我们创建一个单文档MFC标准类型应用程序
正在上传…重新上传取消
一路按默认值next,到最后一个页面
正在上传…重新上传取消
MFC自动为我们生成了四个类,它们的继承关系如下:
正在上传…重新上传取消
接下来就可以开启我们的MFC之旅了。。。
1.9 MFC框架中一些重要的函数
InitInstance函数
应用程序类的一个虚函数,MFC应用程序的入口,该知识点前边有讲到。
OnCreate函数
OnCreate是一个消息响应函数,是响应WM_CREATE消息的一个函数,而WM_CREATE消息是由Create函数调用的。一个窗口创建(Create)之后,会向操作系统发送WM_CREATE消息,OnCreate()函数主要是用来响应此消息的。
在MFC里面用一种消息映射的机制来响应消息,也就是可以用函数来响应相应的消息。就拿CMainFrame类来说,当窗口创建后会产生WM_CREATE消息,我们可以在OnCreate函数里实现我们要在窗口里面增加的东西,例如按扭,状态栏,工具栏等。这些子窗口一般是定义成类中的一个成员变量,因为要保证生命周期。一般以m_开头来表示成员(member)。
- OnCreate与Create的区别
- Create()负责注册并产生窗口,像动态创建控件中的Create()一样,窗口创建之后会向操作系统发送WM_CREATE消息。
- OnCreate()不产生窗口,只是在窗口显示前设置窗口的属性如风格、位置等。
- OnCreate()是消息WM_CREATE的消息响应函数。
PreCreateWindow函数
PreCreateWindow 允许应用程序访问通常由CDocTemplate 类内部管理的创建进程。框架在即将创建窗口前调用 PreCreateWindow,通过修改传递给 PreCreateWindow 的结构体类型参数CREATESTRUCT,应用程序可以更改用于创建窗口的属性。在产生窗口之前让程序员有机会修改窗口的外观。
当框架调用CreateEx函数创建窗口时,会首先调用PreCreateWindow函数,然后在调用CreateWindowEx函数完成窗口的创建。PreCreateWindow的参数CREATESTRUCT结构体中的字段与CreateWindowEx函数参数是一致的,只是顺序相反而已。
- CREATESTRUCT结构体成员与CreateWindowEx参数对比
正在上传…重新上传取消
而且PreCreateWindow函数的参数是引用类型,只要修改了结构体的值,在调用CreateWindowEx时,其参数也会发生相应改变,从而创建出一个符合我们要求的窗口。
正在上传…重新上传取消
正在上传…重新上传取消
正在上传…重新上传取消
备注:以上代码位于wincore.cpp文件中。
OnDraw和OnPaint
OnPaint是WM_PAINT消息的消息处理函数,在OnPaint中调用OnDraw,一般来说,用户自己的绘图代码应放在OnDraw中。
- OnPaint()是CWnd的类成员,负责响应WM_PAINT消息。
- OnDraw()是CView的成员函数,没有响应消息的功能。
当视图变得无效时(包括大小的改变,移动,被遮盖等等),Windows发送WM_PAINT消息。该视图的OnPaint 处理函数通过创建CPaintDC类的DC对象来响应该消息并调用视图的OnDraw成员函数。OnPaint最后也要调用OnDraw,因此一般在OnDraw函数中进行绘制。 通常我们不必编写重写的 OnPaint 处理成员函数。
当在View类里添加了消息处理OnPaint()时,OnPaint()就会覆盖掉OnDraw()。
知识点
extension
美 [ɪkˈstenʃ(ə)n]
英 [ɪk'stenʃ(ə)n]
- n.延伸;延长;延期;扩大
- 网络扩展;分机;伸展
- MFC 中后缀名为 Ex 的函数都是扩展函数。
- Afx 前缀的函数代表应用程序框架( Application Framework) 函数。 应用程序框架实际上是一套辅助我们生成应用程序的框架模型。该模型把多个类进行了一个有机的集成,可以根据该模型提供的方案来设计我们自己的应用程序。在 MFC 中, 以Afx 为前缀的函数都是全局函数,可以在程序的任何地方调用它们。
CDC 图形设备环境(DC :Device Context)
一、基本概念
SDK: 软件开发工具包
JDK:java开发工具包
IDE: 集成开发环境,VS qt
API:应用程序接口
WINAPI: windows平台下的系统调用, windows.h, 调用系统提供的特殊接口,得到系统提供的资源
窗口:父窗口和子窗口, 客户区和非客户区
句柄:结构体变量, 控件的标志,窗口句柄HWND, 图标句柄:HICO
消息队列
消息
窗口过程函数
main()
WinMain() //WINAPI 入口地址
二、winAPI窗口程序(#include <windows.h>)
1、定义入口函数WinMain()
2、创建一个窗口
a)设计窗口类WNDCLASS(给成员变量赋值)
b)注册窗口类
c)创建窗口
c)显示和更新窗口
3、消息循环
4、窗口过程函数
MSDN: winAPI, MFC
MFC参考文档(中文)
三、第一个MFC程序(纯代码)(#include <afxwin.h>)
1)应用程序类 CWinApp
2)框架类 CFrameWnd
class MyApp: public CWinApp
{
public:
//MFC程序的入口地址
virtual BOOL InitInstance();
};
class MyFrame: public CFrameWnd
{
public:
MyFrame();
private:
};
1)有且只有一个全局的应用程序类对象
2)在程序入口函数实现功能 InitInstance()
A) 给框架类MyFrame对象动态分配空间(自动调用它的构造函数)
a) 框架类MyFrame对象构造函数函数里创建窗口 CWnd::Create
B) 框架类对象显示窗口 CWnd::ShowWindow
C) 框架类对象更新窗口 CWnd::UpdateWindow
D) 保存框架类对象指针 CWinThread::m_pMainWnd
事件处理:
消息映射
1、所操作类中,声明消息映射宏
DECLARE_MESSAGE_MAP()
2、对应的.cpp 定义宏
BEGIN_MESSAGE_MAP(MyFrame, CFrameWnd) //派生类名, 基类名
ON_WM_LBUTTONDOWN() //消息映射入口标志
END_MESSAGE_MAP()
3、对应类中,消息处理函数的声明
4、对应.cpp 消息处理函数的定义
四、根据向导创建工程
1、文档视图结构
文档:它是一个类,这个类专门来存储加载(读写)数据
视图:它是一个类,这个类专门来显示和修改数据
框架类:一个容器,这个容器装了视图
2、几个比较重要的函数
a) 应用程序类CWinApp:InitInstance(), 程序的入口地址
b) 框架类CFrameWnd:
PreCreateWindow() 创建窗口之前调用
OnCreate() 创建窗口后,触发WM_CREATE, 它是WM_CREATE消息的处理函数
c)视图类CView:
OnDraw(): 绘图
WM_PAINT消息处理函数OnPaint() 内部调用 OnDraw()
OnPaint()和 OnDraw()同时存在,只有OnPaint()有效
3、事件的添加和删除
a) 框架和视图的区别
添加事件步骤:选择所需类 -> 右击 -> 属性 -> 消息 -> WM_LBUTTONDOW -> ADD
框架就相当于容器, 容器放视图
视图相当于壁纸,我们点击窗口,只有视图响应,框架被视图挡住,无法响应
四、字符集
ANSI 多字节(单字节)
char p[] = "abcdet"; //一个字符一个字节
unicode 宽字节, 一个字符2个字节
TCHAR *p = L"abc"; //一个字符2个字节
wcslen(p);
MFC:
TCHAR: 自动适应字节(条件编译), 相当于char
TEXT()
_T()
五、拓展
afx_xxxx: 全局函数,不属于某个类特有的
xxxEx: xxxW, 拓展
MFC命名规范:
类名和函数名字:单词首字母大小
class MyClass
{
};
void SetName()
{
}
函数形参:第二单词开始,首字母大写
isFlag
isPressTest
成员变量:
m_xxxx
m_hWnd
2. 简单绘图和文本编程
2.1 简单绘图
2.1.1 关于鼠标事件
根据我们前面学过是知识,用MFC向导生成一个单文档视图程序。在我们的主框架类中添加WM_LBUTTONDOWN消息的响应函数,具体操作如下:
正在上传…重新上传取消
从类视图中找到CMainFrame(继承自CFrameWnd),选择此类然后从属性面板中找到消息按钮
正在上传…重新上传取消 ,在消息列表中找到WM_LBUTTONDOWN消息,添加,然后看我们的工程文件中都多了些什么???
- 第一处:在框架类头文件中添加了鼠标左键消息函数的函数声明
正在上传…重新上传取消
- 第二处:在框架类cpp文件中添加了消息映射宏
正在上传…重新上传取消
- 第三处:在框架列cpp文件中添加了处理鼠标左键消息的函数定义:
正在上传…重新上传取消
根据前边我们学过的知识要完成MFC中的消息映射需要我们手动将这三处依次做一个添加,但是使用MFC的类向导我们很容易就能完成一个消息的添加,之后再对应生成的消息函数中做相应的处理即可。
我们再此OnLButtonDown函数中添加一个MessageBox消息,鼠标左键按下弹出一个提示框,然后执行程序。我们会惊奇的发现程序并未如我们所愿弹出消息框,why?我的世界凌乱了。。。
我们先搁置疑问,按照上述操作在视类中捕获鼠标左键消息并处理,看结果会如何?
正在上传…重新上传取消
此时弹出了对话框,说明视类捕获道理鼠标左键消息。但是为什么框架类捕获不到呢?我们前边在将文档/视图结构的时候说过,框架窗口是视窗口的父窗口,那么视类窗口就应该始终覆盖在框架类窗口之上。就好比框架窗口是一面墙,视类窗口就是墙纸,它始终挡在这面墙前边。也就是说,所有操作,包括鼠标单击、鼠标移动等操作都只能有视类窗口捕获,这也是框架类窗口为什么收不到鼠标左键消息的原因。
2.1.2 绘制线条
我们可以利用MFC提供的CClientDC类来实现这一功能,这个类跟CPaintDC一样,也派生于CDC类。CClientDC是窗口客户区的设备描述环境,它应用在WM_PAINT消息之外的消息处理函数中。我们只需要定义一个CClientDC对象,然后就可以利用该对象提供的函数进行绘图操作了。CClientDC对象构造时接收一个参数,即:要进行图像绘制的窗口指针对象。
CClientDC提供的划线函数:
- 绘制线条的起始点:MoveTo(CPoint p) p – 起始点坐标
- 绘制一条到指定点的线:LineTo(CPoint p) p – 终点坐标
在程序的视图窗口用鼠标画一条线:
在视类中定义一CPoint类型成员变量m_oldPoint,添加鼠标左键按下和弹起的消息处理函数,记录鼠标左键按下时的坐标,在鼠标弹起时,通过两个坐标点将线画到视类窗口屏幕区域。
正在上传…重新上传取消
2.1.3 绘制彩色线条
我们实现的画线功能,绘制的都是黑色的线条。这是因为设备描述表中有一个默认的黑色画笔。如果想要绘制其他颜色的线条,首先需要创建一个特定颜色的画笔,然后将此画笔选入设备描述表中,接下俩绘制的线条的颜色就由这个新画笔决定了。
我们可以通过MFC提供的类CPen来创建画笔对象。
CPen(int nPenStyle,int nWidth,COLORREF crColor);
第一个参数:指定笔的线型(实线、点线、虚线等)
第二个参数:线的宽度
第三个参数:颜色,是一个RGB宏
COLORREF RGB(BYTE bRed,BYTE bGreen,BYTE bBlue)
当构建一个GDI(图形设备接口)对象后,该对象并不会马上生效,必须选入设备描述表,它才会在以后的操作中生效。可以使用SelectObject函数把GDI对象选入设备描述表中,并且该函数会返回指向先前备选对象的指针,这主要是为了完成当前绘制之后再次利用SelectObject把先前的GDI对象选入设备描述表,以便使其恢复到先前状态。
正在上传…重新上传取消
2.1.4 绘制连续线条
如果我们想让程序实现Windows画图板那样的画线功能,应如何实现?
为了绘制连续的线条,首先需要得到线条的起点,前面我们已经实现了。然后需要捕获鼠标移动过程中的每个点,这可以通过捕获鼠标移动消息(WM_MOUSEMOVE)来实现。在此消息相应函数中,在依次捕获的各个点之间绘制一条非常短的线段,从而就可以绘制出一条连续的线条。
思路:
- 需要一变量来标识鼠标左键是否按下这一状态
- 在鼠标移动消息响应函数中,如果鼠标是按下状态就开始绘图,弹起之后停止绘图。
在视类头文件中添加私有成员变量:
BOOL m_bDraw;
在视类构造函数中将此变量初始化为FALSE
m_bDraw = FALSE;
当鼠标左键按下去时,在视类的OnLButtonDown函数中将此变量初始化为真
m_bDraw = TRUE;
当鼠标左键弹起,在视类的OnLButtonUp函数中将此变量初始化为假
m_bDrae = FALSE;
在视类的OnMouseMove函数中完成线段的绘制
正在上传…重新上传取消
CDC类中封装的绘图函数还有很多,直接通过CDC类对象调用直接使用即可,可以通过CPen来调节样式,图形函数的参数可参考具体的API使用文档。
正在上传…重新上传取消
2.1.5 使用画刷绘图
在默认情况下由Rectangle、Ellipse以及其它CDC输出函数画出的封闭图形填充着白色像素点。通过创建GDI画刷并在画图之前将它选入设备描述表可以改变图形的填充颜色。MFC的CBrush类封装了GDI画刷。
画刷有三种基本类型:单色,带阴影线,带图案。单色画刷填充的是单色,阴影线画刷采用预先定义好的交叉线图案填充图形,这中图案有六种,图案画刷用位图来填充图形。
CBrush类为每种画刷提供了一个构造函数。
- 单色:CBrush( COLORREF crColor );
crColor 指定画刷的前景色(RGB方式)。
- 阴影:CBrush( int nIndex, COLORREF crColor );
nIndex 指定画刷阴影线采用的风格,取值如下:
-
- HS_BDIAGONAL 45度的向下影线(从左到右)
- HS_CROSS 水平和垂直方向以网格线作出阴影
- HS_DIAGCROSS 45度的网格线阴影
- HS_FDIAGONAL 45度的向上阴影线(从左到右)
- HS_HORIZONTAL 水平的阴影线
- HS_VERTICAL 垂直的阴影线
creColor指定的阴影线的颜色。
- 图案:CBrush( CBitmap* pBitmap );
pBitmap 指向CBitmap对象的指针,该对象指定了画刷要绘制的位图。
说明:
类CBrush一共有四个覆盖的构造函数。不带参数的那个构造函数构造一个未初始化的CBrush对象,在使用该对象之前需要另外初始化。
如果使用了不带参数的那个构造函数,则必须用CreateSolidBrush、CreateHatchBrush、CreateBrushIndirect、CreatePatternBrush或CreateDIBPatternBrush来初始化返回的CBrush对象。如果使用了带参数的构造函数,则不再需要初始化CBrush对象。带参数的构造函数在出错时会产生一个异常,而不带参数的构造函数总是成功返回。
只带有一个参数COLORREF的构造函数用指定的颜色构造一个实线型的画刷。颜色是一个RGB值,可以用WINDOWS.H中的宏RGB构造出来。
带两个参数的构造函数构造一个阴影线型的画刷,参数:nIndex指定了阴影线模式的指数(index)。参数:crColor指定了画刷的颜色。
带有一个CBitmap型参数的构造函数构造一个模式化的画刷。参数指定一个位图。该位图应该是已经用CBitmap::CreateBitmap、CBitmap::CreateBitmapIndirect、CBitmap::LoadBitmap或CBitmap::CreateCompatiableBitmap建立或加载的位图。填充模式下的位图的最小尺寸为8像素×8像素。
Windows使用与设备无关的图形设备环境(DC :Device Context) 进行显示 。MFC基础类库定义了设备环境对象类----CDC类。
2.2 文本编程
2.2.1 创建插入符
在我们使用文本编辑器的时候,在这些文本处理程序的编辑窗口中都有一条闪烁的竖线,将之称为插入符。插入符可以用于提示用户:你输入的文字信息将在这个插入符所在的位置显示出来。
在程序中想要创建插入符,可以利用CWnd类的CreateSolidCaret()函数,该函数原型:
void CreateSolidCaret(int nWidth,int nHeight);
函数的两个参数分别代表插入符的宽度和高度,如何让插入符适合于当前字体的大小呢?首先我们需要得到设备描述表中当前字体的信息,然后根据字体信息来调整插入符的大小。可以通过调用CDC类的GetTextMetrics成员函数得到设备描述表中当前字体的度量信息。函数原型如下:
BOOL GetTextMetrics(LPTEXTMETRIC lpMetrics)const
我们使用CreateSolidCaret函数创建插入符之后,该插入符初始状态是隐藏的,必须调用ShowCaret()函数来显示插入符。
在前边我们已经讲过,视类窗口始终位于框架窗口之上,对窗口客户区的鼠标和键盘操作实际上都是在视类窗口上进行的,因此应该在视类窗口上创建插入符。
插入符的创建应该在窗口创建之后进行,可以在WM_CREATE消息的响应函数OnCreate中添加创建插入符的代码。
正在上传…重新上传取消
根据上述代码有同学可能会有疑问,为什么创建插入符时,要将字体的平均宽度除以8,这是一个经验,此时可以达到最优。
2.2.2 字符输入
想要实现字符的输入功能,也就是当我们按下键盘上某个键之后,要把该字符输出到程序窗口上,这就需要捕获键盘按下(WM_CHAR)这一消息。我们需要利用TextOut函数在窗口输出字符串,TextOut函数原型:
BOOL TextOut(int x ,int y ,const CString& str);
参数:
-
- x 指定文本起点的X逻辑坐标。
- y 指定文本起点的Y逻辑坐标。
- str 包含字符的CString对象。
根据函数我们可以知道输出字符时我们需要提供字符的x,y坐标,但是这是有难度的,因为每个字符在屏幕所占的宽度都不一样,所以我们获取下一个输入点的坐标就不太容易实现。所以我们可以采用一种简单的方式,把每次输入的字符都保存到一个字符串中,当每次输入新的字符时,我们就在窗口当前插入符的位置重新把字符串输出一次。
需要注意的到的几个问题:
- 程序在当前插入的符的位置输出字符。也就是说程序运行时,如果鼠标左键单击窗口中的某个位置,那么插入符就移动到这个地方,随后输入的字符都应该在此位置往后输出。把插入符移动到鼠标左键的单击处,可以利用CWnd类的SetCaretPos函数来实现,函数声明如下:
static void PASCAL SetCaretPos( POINT point );
参数: point 指定了插字符的新的x和y坐标(客户坐标)。
- 用来存储输入字符的字符串取值变化问题。
当鼠标左键单击窗口中一个新的地方时,插入符就会移动到这个位置,那么以后输入的字符都应该从这个位置开始输出,以前输入的字符不应该再从此位置输出,依次需要把存储字符的字符串清空。
- 每次输入的字符串都应该在当前插入符的位置,也就是鼠标单击的位置开始显示。这样就需要把鼠标左键单击的坐标保存起来,以便在On_Char函数中使用。
- 在输出字符时,还需要考虑到回车字符的处理,按下回车后,插入符应切换到下一行,随后的输入也应该新的一行开始输出,前面介绍过GetTextMetrics函数,可以获得当前设备描述表中字体高度信息。
- 在输出字符时,还需要另外一个字符处理,退格键。按下退格键需要删除屏幕上位于插入符前面的那个字符。我们可以采取最简单的实现方式,先将文本颜色设置为背景色,在窗口中将字符串输出一次,然后将字符串中最后一个字符删掉,再把文本颜色设置为原来是颜色,将字符串再输出一次。屏幕上就看到了正确的删除效果。获取背景颜色可以使用CDC类的GetBKColor函数。而设置文本颜色我们可以使用CDC类提供的另一个成员函数SetTextColor函数,这个函数将会返回文本之前的颜色。如果想要实现从字符串中删除一个字符,可以使用CString类的Left函数。函数原型: CString Left(int nCount) const;返回一个CString对象,即返回指定字符串左边指定数目(nCount参数指定)的字符。
完成上述代码之后,执行程序,可以在窗口中插入字符了,但是插入符的位置没有改变,一般情况我们需要插入随着字符的输入而移动,我们知道可以利用SetCaretPos函数来设置插入符的位置,但是移动的位置如何确定呢?实际上对于同一行输入来说,插入符横向移动的距离就是输入字符的宽度,纵坐标是没有改变的。可以利用函数GetTextExtent得到字符串的宽度。
核心代码实现:
正在上传…重新上传取消
正在上传…重新上传取消
2.2.3 设置字体
MFC中提供了CFront类专门来设置字体。这个类派生于CGdiObject类,封装了一个Windows图形设备接口的字体。在编程时,在构造了一个CFont对象后,还必须利用该类提供的几个初始化函数之一对该对象进行初始化,然后才能使用这个对象。
CFont提供的几个初始化函数如下表:
CreateFontIndirect | 初始化一个由LOGFONT结构给出其特征的CFont对象 |
CreateFont | 初始化用指定特性定义的CFont对象 |
CreatePointFont | 用指定高度(用0.1点)和字体初始化一个CFont对象 |
CreatePointFontIndirect | 与CreateFontIndirect相似,但字体高度用0.1点定义而不用逻辑单位定义 |
这些初始化函数的主要作用就是讲CFront这个C++对象与字体资源关联起来。
在程序中与其他GDI对象一样,当创建了一个字体对象并初始化后,还必须将他选入设备描述表,之后这个新字体才能发挥作用。这个可以利用CDC类的SelectObject函数来实现,同样函数会返回先前的字体,我们可以保存这个字体,在使用完新字体之后,再把设备描述表中的字体恢复为先前的字体。
正在上传…重新上传取消
2.2.4 字体变色功能实现
我们平时在唱卡拉OK时,应该注意到歌曲字幕会随着曲调的播放有一个平滑的变色过程。如何在程序中实现这种变色效果呢?
如果我们先把字体输出到屏幕上,接着把文本的颜色设置为新的颜色,然后一个字符一个字符的输出显示该字符串,也可以达到一种变色效果,但不能达到平滑的变色效果。为了达到卡拉OK那种平滑的变色效果我们需要利用CDC类提供的另一个文字输出的函数DrawText来实现,该函数声明如下:
int DrawText(const CString& str, LPRECT lpRect, UNIT nFormat);
该函数实际上是把文字输出局限在一个矩形范围内。当初始输出文本是先把矩形的宽度设置为一个较小的值,然后不断加大矩形的宽度,这样就可以不断的增加显示文字的内容,从而实现文字平滑变色的效果。
文字变色是一个不断变化、自行进行的过程,这意味着我们需要不断的调用DrawText函数,同时增大包含文本的矩形宽度。要实现这个功能,我们需要用到定时器,通过定时器自动控制文字颜色的进程。
利用CWnd类的SetTimer成员函数可以设置定时器。函数原型如下:
UINT SetTimer(UINT nIDEvent, UINT nElapse,
void (CALLBACK EXPORT*lpfnTimer) (HWND, UINT, UINT, DWORD) );
返回值:
如果函数成功,则返回新定时器的标识符。应用程序可以将这个值传递给KillTimer成员函数以销毁定时器。如果成功,则返回非零值;否则返回0。
参数:
- nIDEvent 指定了不为零的定时器标识符。
- nElapse 指定了定时值;以毫秒为单位。
- lpfnTimer 指定了应用程序提供的TimerProc回调函数的地址,该函数被用于处理WM_TIMER消息。如果这个参数为NULL,则WM_TIMER消息被放入应用程序的消息队列并由CWnd对象来处理。
具体步骤如下:
- 在视类的OnCreate函数中设置定时器。
- 在视类中对定时器消息进行处理,因此需要给视类添加WM_TIMER消息的响应函数
- 需要使DrawText的函数的第二个参数,即显示文字的矩形范围不断增加,所以需要设置一个变量,让它旳值不断增加,实现矩形宽度的不断增加。
核心代码实现:
正在上传…重新上传取消
3 菜单
菜单栏、工具栏和状态栏是组成Windows程序图形界面的三个主要元素。大多数Windows程序都提供了菜单,作为用于与应用程序之间交互的一种途径。本节主要介绍与菜单相关的编程知识。
3.1 菜单命令响应函数
创建一个MFC单文档工程并运行该程序,对应这个新创建的程序来说,MFC已经帮我们创建了一个菜单,并完成了一些菜单功能。,例如 【文件】 菜单下的 【打开】菜单命令,即可弹出打开文件对话框。
在VS中打开资源视图选型卡,可以看到Menu下有一个IDR_MAINFRAME的菜单资源,它就是我们应用程序界面看到的菜单。
正在上传…重新上传取消
VS为我们提供了一个所见即所得的资源编辑器,如果我们要为应用程序添加自己的菜单项,可以直接在这个菜单中添加,程序运行之后就能看到自己添加的菜单项。
正在上传…重新上传取消
当选中菜单项相应的就会在属性面板中显示出该菜单项的所用属性,也可以在次面板中对菜单项的属性做相应的修改。
正在上传…重新上传取消
在MFC中,把设置为Popup类型的菜单称为弹出菜单,一般默认顶层菜单为弹出式菜单,这种菜单不能响应命令,它的ID也是无法被编辑的,如果把Popup设置为False该菜单就不是弹出菜单,而成为一个菜单项,它的ID也能够被编辑了。MFC中都是采用大写字母来标识资源ID号的。
在菜单编辑器中给应用程序添加新的菜单项,然后为其添加消息响应函数。在新添加的菜单项上点鼠标右键,弹出菜单中选择 添加事件处理程序(A)…
正在上传…重新上传取消
弹出如下对话框:
正在上传…重新上传取消
我们打算在框架类中响应这个菜单命令,选择CMainFrame类,消息类型选择COMMAND,添加编辑,在框架类中会自动添加该菜单命令的响应函数。
正在上传…重新上传取消
运行程序,单击新添加的MyTest菜单项,会弹出一个消息框,说明OnMyTest函数被调用了。
3.2菜单命令的路由
下面我们研究一下程序中的各个类对菜单命令的响应顺序,是不是菜单命令只有框架类才能捕获呢?
按照上面讲到的方法依次为CMainFrame之外的其他类,视图类、文档类和应用程序类中添加菜单命令响应函数。
正在上传…重新上传取消
因为CMenuApp和CMenuDoc类都不是从CWnd类派生出来的,所有他们没有MessageBox成员函数,我们可以使用全局的MessageBox函数,或者使用应用程序框架的函数AfxMessageBox,这里我们使用的后者。
添加完成之后,执行程序,单击新添加的菜单项MyTest,弹出一个对话框
正在上传…重新上传取消
这就是说视类最先响应了这个菜单命令。关闭这个提示框在没有其他信息显示,说明其他几个菜单命令响应函数没有起作用。
下面,我们将视图类的OnMyTest响应函数删除,再次运行程序,我们会发现是文档类做出了响应。后面依次删除、运行。 实验发现:响应【MyTest】菜单项命令的顺序是:视图类、文档类、框架类,最后才是应用程序类。
菜单命令消息路由的具体过程:当点击某个菜单项时,最先接收到这个菜单命令消息的是框架类。框架类将把接收到的这个消息交给它的子窗口,即视图类,由视类首先进行处理。视类首先根据命令消息映射机制查找自身是否对此消息进行了响应,如果响应了,就调用相应响应函数对这个消息进行处理,消息路由过程结束;如果视类没有对此命令消息做出响应,就交由文档类,文档类同样查找自身是否对这个菜单命令进行了响应,如果响应了,就由文档类的命令消息响应函数进行处理,路由过程结束。如果文档类也未做出响应,就把这个命令消息交还给视类,后者又把该消息交还给框架类。框架类查看自己是否对这个命令消息进行了响应,如果它也没有做出响应,就把这个菜单命令消息交给应用程序类,由后者来进行处理。
正在上传…重新上传取消
Windows消息的分类:
- 标准消息
除WM_COMMAND之外,所有WM_开头的消息都是标准消息。从CWnd类派生的类,都可以接受这类消息。
- 命令消息
来自菜单,加速键或工具栏按钮的消息。这类消息都以WM_COMMAND形式呈现。从CCmdTarget派生的类,都可以接收这类消息。
- 通告消息
由控件产生的消息,例如按钮的单击,列表框的选择都会产生这类消息,目的是为了向其父窗口通知事件的发生。这类消息也是以WM_COMMAND形式呈现的。从CCmdTarget派生的类,都可以接收这类消息。
CWnd类派生于CCmdTarget类。也就是说,凡是从CWnd派生的类,他们既可以接收标准消息,也可以接收命令消息和通告消息。而对于那些从CCmdTarget派生的类则只能接收命令消息和通告消息,不能接收标准消息。
3.3 菜单的基本操作
3.3.1 标记菜单
我们想实现这样的功能:在【文件】子菜单中的【新建】菜单项上添加一个标记(√
)。因为程序的主菜单属于框架窗口,所以需要在框架类窗口创建完成之后再去访问菜单对象。可以在框架类的OnCreate函数的最后(但一定要在return语句之前)添加实现这个功能的代码。
首先要获得程序的菜单栏,也就是要在框架窗口中获得指向菜单栏的指针,这可以通过CWnd的成员函数:GetMenu来实现,函数声明如下:
CMenu* GetMenu() const;
该函数返回一个指向CMenu类对象的指针。CMenu类是一个MFC类,提供了一些与菜单操作有关的成员函数,其中就有获取一个菜单的子菜单功能的成员函数GetSubMenu函数。函数原型如下:
CMenu* GetSubMenu(int nPos) const;
这个函数的参数nPos指定了子菜单的索引号。
为了设置一个标记菜单,需要使用CMenu类的CheckMenuItem这个函数,该函数的功能就是为菜单项添加一个标记,或者移除菜单项的标记。该函数声明如下:
UINT CheckMenuItem( UINT nIDCheckItem, UINT nCheck );
正在上传…重新上传取消
3.3.2 默认菜单项
有些应用程序的子菜单下有一个菜单项是以粗体形式显示的,以这种形式显示的就是该子菜单的默认菜单项。 要实现的功能:将【文件】子菜单下的【新建】菜单项设置为该子菜单的默认菜单项。为了实现这种菜单项,可以利用CMenu类的SetDefaultItem成员函数来完成。这个函数的声明形式如下所示:
BOOL SetDefaultItem(UINT uItem,BOOL fByPos = FALSE);
默认菜单项的代码实现有两种方式:
- 利用位置索引的方式来实现,这时,SetDefaultItem函数的第二个参数应该设置为TRUE;
- 利用菜单项标识的方式来实现,这时,SetDefaultItem函数的第二个参数应该设置为FALSE。
正在上传…重新上传取消
注意:一个子菜单只能有一个默认菜单项。
3.3.3 分隔栏
要实现的功能:在子菜单中添加分隔栏。在新添加的子菜单的菜单项的属性页中进行修改,具体如下
选择菜单项,在菜单项的属性窗口中【杂项】的【Separator】设置为True,该菜单项会自动变成一个分隔符。
正在上传…重新上传取消
注意:分隔栏在子菜单中是占据索引位置的。
3.3.4 禁用菜单项
实现功能:屏蔽某一子菜单下某个菜单项的功能。这里我们将要禁用【文件】子菜单下的【打开】菜单项的功能。 利用CMenu类的成员函数:EnableMenuItem来完成。该函数的作用是设置菜单项的状态:能够使用、禁用或变灰显示。其声明形式如下所示:
UINT EnableMenuItem( UINT nIDEnableItem, UINT nEnable );
返回值:
返回以前的状态(MF_DISABLED, MF_ENABLED, 或MF_GRAYED),如果无效,则返回-1。
参数:
- nIDEnableItem 指定由nEnable决定的将要有效的菜单项。该参数既可以指定弹出菜单项,也可以指定标准菜单项。
- nEnable 指定了将要进行的动作。它可以是MF_DISABLED, MF_ENABLED,或MF_GRAYED与 MF_BYCOMMAND或MF_BYPOSITION的组合。这些值通过位与操作进行组合。这些值有下列含义:
- MF_BYCOMMAND 指定参数给出已存在的菜单项的命令ID号。此为缺省值。
- MF_BYPOSITION 指定参数给出已存在菜单项的位置。第一项所在的位置是0。
- MF_DISABLED 使菜单项无效,以便它不能被选择,但不变灰。
- MF_ENABLED使菜单项有效,以便它能够被选择,并可从变灰的状态中恢复出来。
- MF_GRAYED 使菜单项无效,以便它不能被选择并同时变灰。
正在上传…重新上传取消
问题:运行后,选择【文件】子菜单下的【新建】菜单项,但是这时仍会出现“打开文件”对话框,这说明【新建】菜单项未被禁用。
原因是这样的:
默认情况下,所有菜单项的更新都是由MFC的命令更新机制完成的。如果我们想自己更改菜单项的状态,那就必须先把m_bAutoMenuEnable变量设置为FALSE,之后我们自己对菜单项的状态更新才能起作用。因此,我们就在程序的CMainFrame类构造函数中把m_bAutoMenuEnable这个变量初始化为FALSE,
正在上传…重新上传取消
3.3.5 移除和装载菜单
实现功能:移除一个菜单。我们利用Cwnd类提供的SetMenu成员函数来实现,该函数的声明形式如下所示:
BOOL SetMenu( CMenu* pMenu );
这个函数有一个CMenu类型指针的参数,它指向一个新菜单对象。如果这个参数值为NULL,则当前菜单就被移除了。
正在上传…重新上传取消
如果想要装载一个菜单资源并显示。我们可将上面程序中移除的菜单在这里再把它显示出来。我们利用CMenu类提供的LoadMenu成员函数来实现:
正在上传…重新上传取消
该程序先定义了一个菜单对象,Menu实例程序主菜单的资源标识是IDR_MAINFRAME,把这个资源加载到菜单对象中,最后调用SetMenu函数,把程序的菜单设置为刚刚加载的菜单对象,运行会发现程序的菜单又出现了。
问题:
这里定义的CMenu对象:menu是一个局部对象。虽然运行时未出现错误,但在窗口的操作过程中还是会出现一些错误,这都是因为CMenu对象menu是一个局部对象造成的。
解决问题:
- 将CMenu对象定义为CMainFrame类的一个成员变量;
- 仍把这个菜单对象定义为局部对象,但在调用SetMenu函数把此对象设置为窗口的菜单之后,立即调用CMenu类的另一个成员函数Detach,以便把菜单句柄与这个菜单对象分离。
正在上传…重新上传取消
3.3.6 MFC菜单命令更新机制
如果要在程序中设置某个菜单项的状态,可以在菜单编辑器中为这个菜单项添加UPDATE_COMMAND_UI消息响应函数,然后在这个函数中进行状态的设置即可。 当程序框架捕获到了CN_UPDATE_COMMAND_UI消息后,最终还是交由该消息的响应函数来处理,我们会发现在这个函数有一个CCmdUI指针类型的参数,利用这个CCmdUI类,可以决定一个菜单项是否可以使用(Enable函数)、是否有标记(SetCheck函数),还可以改变菜单项的文本(SetText函数)。
正在上传…重新上传取消
实现功能:改变【文件】子菜单下的【保存】菜单项的状态。
正在上传…重新上传取消
我们可从工具栏上的保存按钮观察代码修改后的结果。
注意:如果要把工具栏上的一个工具按钮与菜单栏中的某个菜单项相关联,只要将它们的ID设置为同一个标识就可以了。
3.3.7 快捷菜单
我们平时在使用程序时,经常会用到单击鼠标右键显示快捷菜单(也称上下文菜单,右键菜单)这一功能。如果想要自己实现这个功能,需要通过以下步骤来完成:
- 为程序添加一个新的菜单资源。
可在【资源视图】选项卡上的Menu分支上单击鼠标右键,从弹出的菜单中选择【添加资源】菜单命令,
正在上传…重新上传取消
这时,在Menu分支下就多了一个名为IDR_MENU1的菜单资源,并同时在VS开发界面窗口的右边窗口中打开了这个菜单资源。接着就要为这个菜单资源添加菜单项了。因为在显示快捷菜单时顶级菜单是不出现的,所以可以给它设置任意的文本。
- 给视图类添加WM_RBUTTONDOWN消息响应函数。
我们可以使用TrackPopupMenu函数来显示一个快捷菜单。该函数声明形式如下:
BOOL TrackPopupMenu( UINT nFlags, int x, int y, CWnd* pWnd,
LPCRECT lpRect = NULL );
参数:
-
- nFlags 指定屏幕位置标志或鼠标键标志。
- 屏幕位置标志可以为下列值之一:
- TPM_CENTERALIGN 使弹出菜单在水平方向相对于x指定的坐标居中。
- TPM_LEFTALIGN 放置弹出菜单,以便弹出菜单在由坐标值x指定的位置左对齐。
- TPM_RIGHTALIGN 放置弹出菜单,以便弹出菜单在由坐标值x指定的位置右对齐。
- 鼠标键标志可以为下列值之一:
- TPM_LEFTBUTTON 导致弹出菜单追踪鼠标左键。
- TPM_RIGHTBUTTON 导致弹出菜单追踪鼠标右键。
- 屏幕位置标志可以为下列值之一:
- x 指定弹出菜单屏幕坐标的水平位置。根据参数nFlags的值,该菜单可以左对齐、右对齐或在该位置上居中。
- y 指定弹出菜单屏幕坐标的垂直位置。
- pWnd 标识拥有弹出菜单的窗口。该窗口接收来自菜单的所有WM_COMMAND消息。在Windows 3.1或以后版本中,窗口直到TrackPopupMenu返回才接收WM_COMMAND消息。在Windows 3.0中,窗口在TrackPopupMenu返回之前接收WM_COMMAND消息。
- lpRect 指向RECT结构或包含矩形的屏幕坐标CRect对象,用户在该矩形内部单击鼠标也不会消除弹出菜单。若该参数为NULL ,那么用户在该矩形外部单击鼠标,弹出菜单将消失。对于Windows 3.0,该值必须为NULL。
- nFlags 指定屏幕位置标志或鼠标键标志。
正在上传…重新上传取消
问题:
运行后发现这个快捷菜单显示的位置好像不太对,并不是在鼠标右键单击点处显示的。这是因为TrackPopupMenu函数中的x和y参数都是屏幕坐标,而鼠标单击点处的坐标是窗口客户区坐标,即以程序窗口左上角为坐标原点。
解决:
需要把客户区坐标转换为屏幕坐标。我们须在调用TrackPopupMenu函数之前调用ClientToScreen函数。函数原型如下:
void ClientToScreen( LPPOINT lpPoint ) const;
void ClientToScreen( LPRECT lpRect ) const;
参数:
lpPoint 指向一个POINT结构或CPoint对象,其中包含了要转换的客户区坐标。
lpRect 指向一个RECT结构或CRect对象,其中包含了要转换的客户区坐标。
正在上传…重新上传取消
- 为程序添加快捷菜单上各菜单项命令的响应函数。
在菜单资源编辑器中,分别为CMainFrame类和CXXView类添加一个响应快捷菜单项的函数,运行后你会发现是CXXView类中添加的响应在处理这个消息。我们将CXXView类对快捷菜单项命令消息的响应函数删除,再次运行,可是发现CMainFrame类对快捷菜单命令消息的响应函数还是没有反应。这主要是因为在创建快捷菜单时,即调用TrackPopupMenu函数时,对这个快捷菜单的拥有者参数传递的是this值,也就是视类窗口拥有这个快捷菜单。因此,只有视类才能对快捷菜单项命令做出响应。如果想让CMainFrame类能对这个快捷菜单项进行响应的话,就应该在调用TrackPopupMenu函数时把快捷菜单的拥有者指定为CMainFrame类窗口,为此我们可以修改CXXView类OnRButtonDown函数中队TrackPopupMenu函数的调用,如下:
正在上传…重新上传取消
注意:
即便如此,框架类窗口能有机会获得对该快捷菜单中的菜单项的命令响应,但最先响应还应是CXXView类窗口,除非CXXView类窗口没有设置快捷菜单的函数,才能由框架类窗口做出响应。这可由菜单命令消息路由的过程来解释。
3.3.8 菜单项动态操作
添加菜单项
通过代码来动态地在已有菜单项目后面添加新的菜单项。
我们可以利用CMenu类提供的一个成员函数:AppendMenu来完成。该函数的作用是把一个新菜单项目添加到一个指定菜单项目的末尾。该函数具体声明形式如下:
BOOL AppendMenu( UINT nFlags, UINT nIDNewItem = 0,
LPCTSTR lpszNewItem = NULL );
参数:
-
- nFlags
指定了增加到菜单中的新菜单项状态的有关信息。它包括说明中列出的一个或多个值。下面列出的是nFlags可以设置的值:
MF_CHECKED 该值的行为如同使用MF_UNCHECKED来作为一个标记,用于替换项前的检测标记。若应用支持检测标记位图(请参阅SetMenuItemBitmaps成员函数),那么将显示“检测标记打开”位图。
-
-
- MF_UNCHECKED 该值的行为如同使用MF_CHECKED来作为一个标记,用于删除项前的检测标记。若应用支持检测标记位图(请参阅SetMenuItemBitmaps成员函数),那么将显示“检测标记关闭”位图。
- MF_DISABLED 使菜单项无效以便它不能被选择,但菜单项不变灰。
- MF_ENABLED 使菜单项有效以便它能够被选择,并从灰色状态中恢复原样。
- MF_GRAYED 使菜单项无效以便它不能被选择,同时使菜单项变灰。
- MF_MENUBARBREAK 在静态菜单里的新行中或弹出菜单的新列中放置菜单项。新的弹出菜单列与老的菜单列将由垂直分割线分开。
- MF_MENUBREAK 在静态菜单里的新行中或弹出菜单的新列中放置菜单项。列与列之间没有分割线。
- MF_OWNERDRAW 指定菜单项为一个拥有者描绘的项。当菜单首次显示时,拥有该菜单的窗口将接收WM_MEASUREITEM消息,以获取菜单项的高度与宽度。WM_DRAWITEM消息将使属主窗口必须更新菜单项的可视界面。该选择项对于顶层菜单项无效。
- MF_POPUP 指定菜单项有与之相关联的弹出菜单。参数ID指定了与项相关联的弹出菜单的句柄。它用于增加顶层弹出菜单项或用于增加弹出菜单项的下一级弹出菜单。
- MF_SEPARATOR 绘制一条水平的分割线。它仅仅能用于弹出菜单项。该线不能变灰、无效或高亮度显示。其它的参数将被忽略。
- MF_STRING 指定菜单项为一个字符串。
-
下面列出的各组标志互相排斥,不能一起使用:
-
-
- MF_DISABLED, MF_ENABLED,和 MF_GRAYED
- MF_STRING, MF_OWNERDRAW, MF_SEPARATOR和位图版本。
- MF_MENUBARBREAK和MF_MENUBREAK
- MF_CHECKED 和MF_UNCHECKED
-
- nIDNewItem
指定了新菜单项的命令ID号,或如果nFlags被设置为MF_POPUP,该参数指定弹出菜单的菜单句柄(HMENU),否则就是添加新菜单项的命令ID。如果nFlags被设置为MF_SEPARATOR,那么参数NewItem将被忽略。
- lpszNewItem
lpszNewItem指定了新菜单项的内容。参数nFlags以下列方式解释lpszNewItem:
nFlags | lpszNewItem的解释 |
MF_OWNERDRAW | 包含一个应用支持的32 位值,应用可以用于维护与菜单项关联的附加数据。当应用进行了WM_MEASUREITEM和WM_DRAWITEM,该32位值有效。这些信息存储在提供这些消息的结构的itemData成员中 |
MF_STRING | 包含一个指向以空字符终止的字符串的指针。这是它的缺省说明 |
MF_SEPARATOR | 参数lpszNewItem被忽略 |
插入菜单项
通过代码来动态地在已有菜单项目之间添加新的菜单项。
我们可以利用CMenu类的InsertMenu成员函数来实现,该函数的声明形式如下:
BOOL InsertMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem = 0,
LPCTSTR lpszNewItem = NULL );
参数:
- nPosition
指定将要插入的新菜单项前的菜单项。参数nFlags被用于以下列方式解释:
MF_BYCOMMAND | 指定参数给出已存在菜单项的命令ID号。如果没有设置MF_BYCOMMAND或MF_BYPOSITION,那么此为缺省值 |
MF_BYPOSITION | 指定参数给出已存在菜单项的位置。第一项位于位置0。如果nPosition为-1,那么新菜单项将添加到菜单尾 |
- nFlags
指定nPosition值如何被解释,并指定要增加到菜单中新菜单项的状态。对于能够被设置的标志,请参阅成员函数AppendMenu。如果需要指定多个值,需使用位与操作来组合MF_BYCOMMAND或MF_BYPOSITION标志。
- nIDNewItem
指定新菜单项的命令ID号,或者,若nFlags被设置为MF_POPUP,则指定为弹出菜单的菜单句柄(HMENU)。若nFlags被设置为MF_SEPARATOR,那么参数nIDNewItem将被忽略。
- lpszNewItem
指定新菜单项的文本。nFlags被用于以下列方式解释lpszNewItem:
MF_OWNERDRAW | 该值含有应用用于包含菜单项附加数据应用提供的32位值。该32位值在由WM_MEASUREITEM和WM_DRAWITEM消息提供的itemData结构成员里对于应用有效。这些消息的发送是在菜单项最初显示或更改时发生 |
MF_STRING | 包含指向以空格位终止符的字符串指针。它为缺省解释 |
MF_SEPARATOR | 参数lpszNewltem将被忽略 |
删除菜单项。
CMenu提供一个DeleteMenu成员函数,函数声明如下:
BOOL DeleteMenu( UINT nPosition, UINT nFlags );
参数:
- nPosition
指定由nFlags决定的将要删除的菜单项。
- nFlags
以下列方式解释nPosition:
MF_BYCOMMAND | 指定参数给出已存在菜单项的命令ID号。如果没有设置MF_BYCOMMAND或MF_BYPOSITION,那么此为缺省值 |
MF_BYPOSITION | 指定参数给出已存在菜单项的位置。第一项位于位置0 |
利用这个函数可以删除一个菜单项目,包括子菜单,以及子菜单下的菜单项,主要取决于调用这个函数的对象,如果该对象是程序的菜单栏对象,那么删除的就是指定的子菜单;如果该对象是一个子菜单对象,那么删除的就是该子菜单的下一个菜单项。
知识拓展
模拟动画图标
当我们的程序启动之后,让程序的图标不断的变化,给人一种动画效果。
- 加载图标资源
- 在程序的【资源视图】中导入图标资源:
正在上传…重新上传取消
- 在程序的CMainFrame类中,定义一个图标句柄数组成员变量,用来存放在资源视图中添加的图标的句柄:
private:
HICON m_hIcons[4];
- 在CMainFrame类的OnCreate函数中利用LoadIcon加载添加的图标资源:
使用LoadIcon函数如果加载的是系统图标第一个参数应该设置为NULL,如果需要使用自定义图标,该函数的第一个参数应该设置为改程序当前的实例句柄。
HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);
获取当前程序实例句柄的方式:
-
- 使用AfxGetInstanceHandle()函数
- 使用全局实例对象theApp访问其数据成员m_hInstance
注意:在使用之前必须声明这个变量是在外部定义的
extern CMyApp theApp
-
- 使用全局函数AfxGetApp()获取当前应用程序指针,通过该指针来访问应用程序的成员变量m_hInstance
m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON1));
m_hIcons[1] = LoadIcon(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON2));
m_hIcons[2] = LoadIcon(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON3));
m_hIcons[3] = LoadIcon(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDI_ICON4));
MAKEINTRESOURCE宏:
将资源ID转换为相应的资源标识符字符串
- 定时器处理
- 在CMainFrame类的OnCreate函数中设置定时器:
SetTimer(1, 1000, NULL);
- 为CMainFrame类添加WM_TIMER消息相应函数OnTimer
- 在OnTimer函数中使用SetClassLong函数为应用程序设置自定义图标
函数原型如下:
DWORD SetClassLong(HWND hWnd,int nlndex,LONG dwNewLong)
参数:
- hWnd:窗口句柄及间接给出的窗口所属的类。
- nIndex: 指定要设置的索引
- GCL_HBRBACKGROUND 设置新的背景画刷
- GCL_HCURSOR 设置新的光标
- GCL_HICON 设置新的图标
- GCL_STYLE 设置新的窗口样式
void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
static int nIndex = 0;
SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[nIndex]);
nIndex = ++nIndex % 4;
CFrameWnd::OnTimer(nIDEvent);
}
LPSZ的全称:Long Pointer to Zero Terminated String | 中文意思:───长以零结尾的字符串的指针。
一、简单绘图
a)画直线
b)画笔CPen的使用
c)画刷CBrush的使用
d)写字,CFont的使用
二、文本编辑器
1)创建插入符CWnd::CreateSolidCaret()
a)创建CWnd::CreateSolidCaret()
b)显示CWnd::ShowCaret()
c)插入符的高度是根据字体的高度来确定
获取字体信息CDC::GetTextMetrics()
d)设置插入符位置CWnd::SetCaretPos()
2)在字符消息处理函数中写字
a)写字CDC::TextOutW()
b)获取字符串的尺寸信息CDC::GetTextExtent()
c)截取字符串(CString)左边指定长度的字符 str = str.Left(str.GetLength() - 1);
三、字体渐变
1)定时器的使用
a)设置定时器:CWnd::SetTimer()
b)关闭定时器:CWnd::KillTimer()
c)定时器消息:WM_TIMER
2)视图类中的OnDraw()中,写字 CDC::TextOutW()
3)指定区域写字:CDC::DrawText()
4)让窗口失效,产生WM_PAINT,间接调用OnDraw()函数: CWnd::Invalidate
四、菜单的相关操作
1)菜单的命令响应函数
a)弹式菜单,ID不可编辑,按下去,弹出一个菜单项
b)非弹式菜单,ID可编辑
c)菜单响应命令消息的路由(顺序)
d)消息类型
非标准消息 WM_COMMAND,命令消息、通告消息,CCmdTarget、CWnd子类子类能接收到非标准消息
标准消息 WM_XXXX CWnd子类才能接收到标准消息
命令消息WM_COMMAND:菜单处理函数选中
标准消息: 属性 -> 消息
通告消息:点击按钮,处理函数
CWnd可以接受任何消息
CCmdTarget不能接受标准消息
2)菜单相关静态操作
在框架类中进行相应操作,在OnCreate()中实现
A)获取菜单栏: CWnd::GetMenu
B)获取菜单栏中的子菜单: CMenu::GetSubMenu
a)标志菜单: CMenu::CheckMenuItem
MF_BYPOSITION: 通过位置
MF_BYCOMMAND:通过ID
b)设置默认菜单: CMenu::SetDefaultItem
注意:一个菜单项只能设置一个默认菜单
c)禁用菜单: CMenu::EnableMenuItem
注意:需要把 CFrameWnd::m_bAutoMenuEnable 成员变量设置为 FALSE
d)分隔线
C)移除菜单 CWnd::SetMenu
SetMenu(NULL);
D)装载菜单
a)创建菜单 CMenu::LoadMenu
b)设置菜单 CWnd::SetMenu
d)从CMenu 对象中分离Windows菜单 CMenu::Detach
E)菜单命令更新机制
F)快捷菜单,弹出菜单(一定要新建菜单,不能使用框架类的主菜单)
由于鼠标点击的区域是在视图区域,所以需要在视图类中处理
a)处理鼠标右击事件WM_RBUTTONDOWN
b)获取所需的子菜单
c)弹出菜单项 CMenu::TrackPopupMenu
d)客户区坐标转屏幕坐标:CWnd::ClientToScreen
3)菜单相关动态操作
a)创建空菜单 CMenu::CreateMenu
b)追加弹式菜单 CMenu::AppendMenu(MF_POPUP, )
每个菜单都有一个菜单句柄:CMenu::m_hMenu
重画菜单条:CWnd::DrawMenuBar
c)追加普通菜单 CMenu::AppendMenu(MF_STRING, )
d)插入菜单 CMenu::InsertMenu
c)删除菜单 CMenu::DeleteMenu
五、动态图标
1)定时器:CWnd::SetTimer
a)定时器信号:WM_TIMER
b)设置定时器应该放在OnCreate()
2)加载自定义图标(WinAPI): LoadIcon()
a)将资源ID转换为字符串 MAKEINTRESOURCE()
b)获取应用程序实例:
AfxGetInstanceHandle()
AfxGetApp()->m_hInstance
3)设置图标(WinAPI)SetClassLong()
窗口句柄:CWnd::m_hWnd
4 基于对话框的MFC应用程序
4.1 MFC编程
MFC 是 Visual C++ 的核心。虽然在 Windows 应用程序中可以直接调用 API 函数,但是一般不经常直接调用,而是从 MFC 类创建对象并调用属于这些对象的成员函数。MFC 是 Microsoft 公司提供的用来编写 Windows 应用程序的 C++ 类库,MFC 大约有 200 多个类,可以分成两种:一是 Cobject 类的派生类,它们以层次结构的形式组织起来,几乎每个子层次结构都与一个具体的 Windows 实体对应;二是非 Cobject 派生类,这些都是独立的类,如表示点的 CPoint 类,表示矩形的 CRect 类。
在 Visual C++ 中,可以创建以下 3 类典型的 Windows 应用程序,它们都是通过 MFC AppWizard(exe) (以下简称 AppWizard )向导创建的:
- 基于对话框的应用程序:这类程序适合于文档较少而交互操作较多的应用场合,如 Windows 自带的计算器程序。
- 单文档界面( SDI )应用程序:这类程序一次只能打开一个文档,如 Windows 自带的 Notepad 程序。
- 多文档界面( MDI )应用程序:这类程序可以同时打开多个文档并进行处理,处理的过程中很容易地进行切换,如 Microsoft Word 。
4.2 基于对话框的MFC应用程序
对话框是一种特殊类型的窗口,绝大多数Windows程序都通过对话框与用户进行交互。在Visual C++中,对话框既可以单独组成一个简单的应用程序,又可以成为文档/视图结构程序的资源。
对话框的种类
有两种对话框: 模态对话框(Modal)和非模态对话框
- 模态对话框
当其显示时,程序会暂停执行,直到关闭这个模态对话框之后,才能执行程序中的其他任务。我们平时遇到的对话框都是模态对话框。
-
- 模态对话框的创建
实现模态对话框的创建需要调用CDialog类的成员函数 DoModel(),该函数的功能就是创建并显示一个对话框。
CTestDlg dlg;
dlg.DoModel(); //显示模态对话框
- 非模态对话框
当非模态对话框显示时,运行转而执行程序中 的其他任务,而不用关闭这个对话框。典型的就是Windows记事本中提供的查找对话框,打开该对话框后仍可以与其他用户界面对象进行交互,一遍查找,一遍修改文章,这样就大大方便了使用。
-
- 非模态对话框的创建
如果要创建非模态对话框在需要使用CDialog的Create成员函数。
BOOL Create(LPCSTR lpszTemplateName, CWnd* pParentWnd = NULL);
BOOL Create(UNIT nIDTemplate, CWnd* pParentWnd = NULL);
-
-
- 返回值:
-
如果对话框创建和初始化成功,则返回非零值,否则为0。
-
-
- 参数:
- lpszTemplateName 对话框模板的名称。
- nIDTemplate 对话框资源的ID。
- pParentWnd 指向含有对话框的父窗口对象的指针。如果为NULL,对话框对象的父窗口设置为应用的主窗口。
- 参数:
-
利用Create函数创建非模态对话框时,还需要调用ShowWindow函数将这个对话框显示出来。
CTestDlg dlg;
dlg.Create(IDD_DIALOG1, this);
dlg.ShowWindow(SW_SHOW);
执行程序发现仍然没有弹出测试对话框,问题就出在创建的非模态对话框是一个局部对象,函数结束后,对话框的生命周期也就结束了。在创建非模态对话框时不能将其定义为局部对象。有两种解决方法:
- 把对话框对象定义为类的成员变量
- 将对话框对象定义为指针,在堆上分配内存
如果选择在堆上分配内存,关闭对话框时就需要释放堆内存,释放的方式有两种:
- 将指针定义为对话框所属的类的成员变量(比如视类),在指针所属的类的析构函数中释放
- 在对话框类中重载PostNcDestroy虚函数,释放this指针指向的内存。
void MyDialog::PostNcDestroy()
{
// TODO: 在此添加专用代码和/或调用基类
delete this;
CDialogEx::PostNcDestroy();
}
4.2.1 创建基于对话框的 MFC 应用程序框架
程序的创建过程:
- 选择“文件 | 新建 | 项目”菜单;
- 在“新建项目”对话框中,选择“ MFC 应用程序 ”,输入工程名称,选择“确定”。
正在上传…重新上传取消
- 选择“ 基于对话框”,即创建基于对话框的应用程序,选择“完成”。
正在上传…重新上传取消
4.2.2 对话框应用程序框架
用 AppWizard 创建基于对话框的应用程序框架(假定工程名为 Dialog )后,项目工作区上增加了一个“ 资源视图 ”选项卡。
正在上传…重新上传取消
- 资源视图
在 MFC中,与用户进行交互的对话框界面被认为是一种资源。展开“ Dialog ”,可以看到有一个 ID 为 IDD_ DIALOG _DIALOG(中间部分(DIALOG)与项目名称相同) 的资源,对应中间的对话框设计界面。不管在何时,只要双击对话框资源的 ID ,对话框设计界面就会显示在中间。
在MFC中对资源的操作通常都是通过一个与资源相关的类来完成的。
- 类视图
在类视图中,可以看到生成了3 个类: CAboutDlg 、 CDialogApp 和 CDialogDlg 。
正在上传…重新上传取消
-
- CAboutDlg:对应生成的版本信息对话框。
- CDialogApp:对话框类,从 Cdialog 继承过来的,在程序运行时看到的对话框就是它的一个具体对象。
- CDialogDlg:应用程序类,从 CWinApp 继承过来,封装了初始化、运行、终止该程序的代码。
- DoDataExchange函数,该函数主要完成对话框数据的交换和校验。
- OnInitDialog函数:相当于对对话框进行初始化处理
调用这个成员函数是对WM_INITDIALOG消息作出的反应。这条消息是在对话框即将显示之前,在Create,CreateIndirect或DoModal调用期间发出的。
- 对话框设计界面
对话框设计界面是进行对话框设计的地方,可以将各种控件放置在这里。
正在上传…重新上传取消
- 控件工具栏
我们可以根据实际需求,在开发过程中选择不同的控件,实现不同的功能。
正在上传…重新上传取消
4.3 静态文本框、命令按钮和编辑框
静态文本框、命令按钮和编辑框是Windows应用程序中最基本的控件。静态文本框是CStatic类的对象,命令按钮是CButton类的对象,编辑框是CEdit类的对象。这三个类都是从CWnd类直接派生来的,具有CWnd类的全部功能。
4.3.1 静态文本框
静态文本框是最简单的控件。它主要用来显示文本信息,不能接受用户输入,一般不需要连接变量,也不需要处理消息。
静态文本框的重要属性有:
- ID :所有静态文本框的缺省 ID 都是 IDC_STATIC ,一般不需要重新设置。
- 标题:需要显示的文本信息是在这里设置的。
- 边框:可以对属性对话框中的Sunken,Static Edge属性进行设置。
4.3.2 命令按钮
命令按钮是最常见的、应用最广泛的一种控件。在程序执行期间,当单击某个命令按钮后就会执行相应的消息处理函数。
命令按钮的主要属性是标题属性,标题属性用来设置在命令按钮上显示的文本。
命令按钮一般不需要连接变量。
命令按钮处理的最多的消息是:BN_CLICKED 。
思考问题:如果从CButton类派生新类CMyButton,如何与按钮控件关联?
只需要给按钮控件添加一个CMyButton类型的变量,按钮控件就可以使用CMyButton类中实现的所有操作。
- 实现一个逃跑按钮功能,当鼠标移动到按钮上时,按钮自动移动到窗口其他位置上。
- 在对话框窗口上,放置一个按钮
- 创建一个CButton类的派生类,在该类中捕获鼠标移动事件。
void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
//获取按钮的大小
CRect btnRect;
this->GetWindowRect(&btnRect);
//按钮的宽度
int btnWidth = btnRect.Width();
int btnHeight = btnRect.Height();
//获取父窗口的大小
CRect rect;
GetParent()->GetWindowRect(&rect);
int width = rect.Width();
int height = rect.Height();
//计算新坐标
CPoint pt = point;
//如果计算出的左边跟鼠标当前位置左边相等重新计算
while (pt == point)
{
//产生随机X坐标
pt.x = rand() % (width - btnWidth);
//产生随机y坐标
pt.y = rand() % (height - btnHeight);
}
//将按钮移动到新的位置
MoveWindow(pt.x, pt.y, btnWidth, btnHeight);
CButton::OnMouseMove(nFlags, point);
}
- 位图按钮实现
在对话框的OnInitDialog函数最后添加如下代码:
// 获取位图资源句柄
HBITMAP hBitmap = LoadBitmap(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDB_BITMAP1));
//按钮添加位图资源
m_btn.SetBitmap(hBitmap);
//将按钮设置成与位图同样大小
m_btn.MoveWindow(10, 10, 66, 77);
另外还需注意,在按钮上显示位图,需要将按钮的Bitmap属性设置为TRUE
4.3.3 编辑框
- 属性设置
编辑框的属性主要在 Styles 选项卡中设置。重要属性有:
-
- Multiline :定义该编辑框为多行文本框。
- Number :限定输入的字符只能是数字字符。
- Border :为控件创建边界。
- Read-only :编辑框成为只读的,禁止用户编辑。
- 成员函数
编辑框常用的成员函数见表。
成员函数 | 功能 | 应用实例 |
SetSel(n, m) | 选定编辑框中从第n个字符到底m个字符的内容。SetSel(0, -1)的作用是选定所有的内容。 | m_e.SetSel(0, -1) |
Copy() | 将编辑框中当前选定的内容复制到剪贴板 | m_e.Copy() |
Cut() | 将编辑框中当前选定的内容剪切到剪贴板 | m_e.Cut() |
Clear() | 删除编辑框中当前选定的内容 | m_e.Clear() |
Paste() | 把剪贴板中的内容粘贴到编辑框中光标所在的位置 | m_e.Paste() |
GetLine(n, ch) | 将多行编辑框中的第n行的内容复制到ch中。ch为一般的字符数组。 | TCHAR ch[80]; m_e.GetLine(0, ch); |
ReplaceSel(ch) | 将ch中的内容替换编辑框中选定的内容 | TCHAR ch[80] = “abcd”; m_e.ReplaceSel(ch); |
Undo() | 撤销编辑框的最后一次操作 | m_e.Undo(); |
此外 编辑框还可以使用 CWnd 类的成员函数。 CWnd 类的重要成员函数有:
- 获取编辑中的内容
- void GetWindowTextW ( CString& ) const;
说明:将编辑框中的内容复制到 CString 类对象 rString 中。
示例:将编辑框 m_e 中的内容复制到 CString 类对象 ch 中。
CString ch;
m_e.GetWindowTextW (ch);
- int GetWindowTextW ( LPTSTR lpszStringBuf, int nMaxCount ) const;
说明:将编辑框中的内容复制到lpszStringBuf中,最多复制nMaxCount 个字符。lpszStringBuf 是字符数组或字符指针。
示例:将编辑框 m_e 中的内容复制到字符数组 ch 中。
TCHAR ch[80];
m_e.GetWindowTextW (ch, 80);
- 设置编辑框中的内容
形式: void SetWindowText( LPCTSTR lpszString );
说明: 将 lpszString 中的内容替换编辑框中原有内容,lpszString 是字符数组或字符指针 。
示例: 设置编辑框中的内容为“ abcdefg ”
TCHAR ch[20] = "abcdefg";
m_e.SetWindowText(ch);
- 连接变量
编辑框在连接变量时,除了要指定变量名之外,还要确定变量类别。变量类别有两个可选择:
- Control,意味着该变量作控件使用,对应的变量类型只能是CEdit,可以使用该控件类的成员函数;
- Value,意味着该变量当作C/C++中普通的变量使用,对应的变量类型有CString、int、double等,可以使用这些数据类型的函数,但是不能使用控件的成员函数。
若一个编辑框连接了一个Value类别的变量,则该变量就表示这个编辑框,编辑框中显示的内容就是变量的值。但是,改变了编辑框的内容并不会自动更新对应的变量的值,同样,改变了变量的值也不会自动刷新编辑框的内容。若要保持一致,需要使用UpdateData()函数更新,如图所示。
- 若编辑框的内容改变了,则应使用语句UpdateData(TRUE) 获取对话框数据
- 若变量的值改变了,则应使用语句UpdateData(FALSE) 初始化对话框控件。
正在上传…重新上传取消
【 例 1 】 编写一个如图所示的应用程序。若单击“复制”按钮,则把上面的编辑框中的内容复制到下面的编辑框中;若单击“结束”按钮,则退出程序的运行。
正在上传…重新上传取消
新建一个基于对话框的MFC应用程序
- 放置控件
- 删除原有的控件。
- 放置所需的控件:两个编辑框和两个命令按钮。
方法是:先单击控件工具栏上的控件图标选择所需的控件,然后在对话框设计界面上按住鼠标左键拖拉出所需要的大小后释放。
- 设置控件属性
选择编辑框控件,在属性面板中对该控件属性进行设置。上面编辑框的属性设置为:
-
- 【Multiline】设置为True,编辑框中可以输入多行文本。
- 【Vertical scroll】和【Auto Vscroll】 属性设置为True,编辑框将出现垂直滚动条。
- 【Want return】设置为True,控件接收回车键。
正在上传…重新上传取消
下面编辑框属性设置与上面编辑框基本一样,只是可以不设定【Want return】为True。从图中可以看到编辑框的 ID 为 IDC_EDIT1 ,这是该控件的标识。任何一个控件都有一个 ID ,某些函数需要通过 ID 对控件进行操作。
- 连接变量
为控件连接变量就是为控件起一个名称。每一个控件都是一个对象,调用 MFC 类库中的函数都是通过对象来实现的。为 IDC_EDIT1 连接变量 m_e1 的步骤为:
-
- 在 IDC_EDIT1 编辑框的右键菜单中选“添加变量”,弹出 “添加成员变量向导”对话框。
正在上传…重新上传取消
-
- 在“ 添加成员变量向导 ”中,给“ IDC_EDIT1 ”,添加成员变量。
正在上传…重新上传取消
用同样的方法再为 IDC_EDIT2 连接一个变量 m_e2 。
- 添加并且编写消息处理函数。
本例要求单击“复制”按钮后上面编辑框中的内容复制到下面的编辑框中。也就是说,在程序运行时,在“复制”按钮上发生单击事件后, Windows 向对话框发出了一个 BN_CLICKED 消息, CTESTDlg 类应有一个处理该消息的函数。添加和编写这个消息处理函数的过程是:
-
- 直接双击“复制”按钮,程序中会自动添加相对应的响应函数。或者在按钮上单击鼠标右键,在右键菜单中单击【类向导】
正在上传…重新上传取消
弹出类向导对话框
正在上传…重新上传取消
选择按钮对应的ID,要添加的消息类型,选择添加处理程序按钮,按钮对应的消息相应函数添加完毕。
复制按钮的消息处理函数如下:
正在上传…重新上传取消
用同样的方法为“结束”按钮添加 、编写如下的消息处理函数:
正在上传…重新上传取消
【例2】 输入一元二次方程 y=ax2 +bx+c 的系数 a 、 b 、 c ,计算并输出两个根 x1 、x2 ,如图所示。
正在上传…重新上传取消
对话框上有5个静态文本框、5个编辑框和2个命令按钮。编辑框按表连接变量,其余控件不需要连接变量。
编辑框链接的变量
控件 | ID | 变量 | 类型 | 数据类型 |
Edit Box | 缺省 | m_a | value | double |
缺省 | m_b | value | double | |
缺省 | m_c | value | double | |
缺省 | m_x1 | value | double | |
缺省 | m_x2 | value | double |
为了要计算 b2 -4ac 的平方根,需要使用 sqrt() 函数,因此在 TESTDlg.cpp 文件的开始添加了文件包含命令:
#include "math.h" // "计算"按钮的函数 这条要放在最后
…………………………………………………………
void CTESTDlg::OnCalc()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
double a=m_a;
double b=m_b;
double c=m_c;
m_x1=(-b+sqrt(b*b-4*a*c))/(2*a);
m_x2=(-b-sqrt(b*b-4*a*c))/(2*a);
UpdateData(FALSE);
}
// “结束”按钮的函数
…………………………………………………………
void CTESTDlg::OnOk()
{
// TODO: Add your control notification handler code here
CDialog::OnOK(); //OnOK();
}
…………………………………………………………
- 消息处理函数
在编辑框能处理的消息中是最重要的是 EN_CHANGE ,这是编辑框中的文本被修改后发出的消息。
【例】字符串转换程序
转换规则为: 大写字母转换成小写;
将小写字母转换成大写;
换行符和回车符不变;
其余字符转换为“*”
要求:每输入一个字符立即转换
正在上传…重新上传取消
- 第1个编辑框连接变量m_e1 (Control )
属性:
- Multiline 多行,设置为True
- Vertical scroll 垂直滚动条,设置为True
- Auto VScroll(在多行控件中,当用户在最后一行按下ENTER键时自动向上滚动文本),设置为True
- Want return(使多行编辑器接收回车键,表示换行。如果不指定,按回车键会选择缺省的命令按钮,这往往会导致对话框的关闭),设置为True。
- 第2个编辑框连接变量m_e2 (Control )
属性:
- Multiline 多行,设置为True
- Vertical scroll 垂直滚动条,设置为True
- Auto VScroll(在多行控件中,当用户在最后一行按下ENTER键时自动向上滚动文本),设置为True
- Read-only 设置为True
- 对第1个编辑框添加EN_CHANGE 消息处理函数,首先在控件上选右键菜单
正在上传…重新上传取消
弹出事件处理程序向导对话框
正在上传…重新上传取消
相应的函数处理如下:
void CTESTDlg::OnChangeEdit1()
{
// TODO: Add your control notification handler code here
TCHAR s[80];
m_e1.GetWindowTextW(s, 80);
for (int i = 0; s[i] != '\0'; i++)
{
if (s[i] >= 'A' && s[i] <= 'Z')
{
//大写转换成小写
s[i] = s[i] + 'a' - 'A';
}
else if (s[i] >= 'a' && s[i] <= 'z')
{
//小写转换成大写
s[i] = s[i] + 'A' - 'a';
}
else if (s[i] == '\n' || s[i] == '\r')
{
//回车符合换行符不变
s[i] = s[i];
}
else
{
//其余字符全部变成*号
s[i] = '*';
}
}
//将转换完成的字符串显示到m_e2中
m_e2.SetWindowTextW(s);
}
4.4 组框、单选按钮和复选框
组框、单选按钮和复选框都是对话框的常见控件。组框与静态文本框一样是CStatic类的对象,单选按钮和复选框与命令按钮一样都是CButton类的对象。
4.4.1 组框
当对话框上控件较多时,可以使用组框将一组相关的控件框起来,达到标识一组控件的作用。组框不需要连接变量,也不需要处理消息。组框的重要属性有:
- ID :所有组框的缺省 ID 都是 IDC_STATIC ,不需要重新设置。
- 标题:该属性决定组框上的标题文本。
4.4.2 单选按钮
- 注意事项
同一组中的按钮必须一个接一个地放进对话框中,中间不能插入其他控件,并且一个组的第一个按扭的“Group”属性需要设置为True,表示一组控件的开始。
- 重要属性
- ID :每个命令按钮都有一个缺省 ID ,如 IDC_RADIO1 ,可以重新设置。
- 标题:该属性值就是单选按钮右边显示的文本标题。
- Group :一组中第一个按钮选中该属性,表示它是一组的开始。
- 消息
单选按钮能处理的消息是BN_CLICKED。
- 重要函数
- 设定单选按钮选中状态
初始时,可使用 Windows API 函数 CheckRadioButton() 设定一组单选按钮中选中的按钮。 形式:
void CheckRadioButton(int nIDFirstButton,
int nIDLastButton, int nIDCheckButton);
说明:
-
- nIDFirstButton 是一组中第一个单选按钮的 ID ;
- nIDLastButton 是一组中最后一个单选按钮的 ID ;
- nIDCheckButton 为初始时设置为选中的那个单选按钮的 ID 。
- 判定单选按钮是否被选定
函数原型如下:
UINT IsDlgButtonChecked( int nIDButton ) const;
说明:如果 ID 为 nIDButton 的按钮被选定,则该函数的返回值为 true ,否则为 false 。
【例】设计一个如图所示的程序。当单击“ 确定 ”后,用 MessageBox 函数显示一个如图所示的消息框。
正在上传…重新上传取消
- 分组
例:将radio1、radio2分为1组,radio3、radio4、radio5分为另一组;
分组方法:
第一组:
-
- 设置 radio1 的属性:group设置为true
- 设置 radio2 的属性: group设为false
第二组:
-
-
-
- 设置 radio3 的属性:group设置为true
- 设置 radio4 的属性:group设为false
- 设置 radio5 的属性:group设为false
-
-
按CTRL+D,保证同一组内的radio的tab序号是连续的;
正在上传…重新上传取消
- 界面设计
首先创建一个对话框程序框架,然后放置各控件,进行属性设置,将编辑框连接到 CString 类型的 m_edit 变量,其余控件不需要连接变量。将“男”和“教授”对应的单选钮的Group属性勾上。
- 初始化
单选按钮的初始化工作在对话框的初始化函数实现。
BOOL CTESTDlg::OnInitDialog()
{
┆
CheckRadioButton(IDC_RADIO1, IDC_RADIO2, IDC_RADIO1); //
CheckRadioButton(IDC_RADIO3, IDC_RADIO5, IDC_RADIO4);
┆
};
- 消息处理函数
void CTESTDlg::OnButton1()
{
// TODO: Add your control notification handler code here
//将控件中的数据更新到变量中
UpdateData(TRUE);
CString s;
s += m_edit;
if (IsDlgButtonChecked(IDC_RADIO1))
{
s += " 男 ";
}
else if (IsDlgButtonChecked(IDC_RADIO2))
{
s += " 女 ";
}
if (IsDlgButtonChecked(IDC_RADIO3))
{
s += " 教授 ";
}
else if (IsDlgButtonChecked(IDC_RADIO4))
{
s += " 副教授 ";
}
else
{
s += " 讲师 ";
}
MessageBox(s);
}
-
-
- 复选框
-
- 重要属性
- ID :每个复选框都有一个缺省 ID ,如 IDC_CHECK1 ,可以重新设置。
- 标题:该属性值决定了复选框右边显示的文本标题。
- 连接变量
复选框通常被连接到Value类别BOOL类型的变量。
- 消息
复选框能处理 BN_CLICKED 消息。
【例】设计一个如图所示的程序。当单击“确定”后,在右边的编辑框中显示有关信息。
正在上传…重新上传取消
- 界面设计
首先创建一个对话框程序框架,然后放置各控件,进行属性设计,各编辑框和复选框按表连接变量,其余控件不需要连接变量。
控件连接的变量
控件 | 标题 | 变量名 | 变量类别 | 变量类型 |
左编辑框 | 无 | m_e1 | Value | CString |
右编辑框 | 无 | m_e2 | Value | CString |
复选框 | 旅游 | m_c1 | Value | BOOL |
运动 | m_c2 | Value | BOOL | |
音乐 | m_c3 | Value | BOOL |
- 消息处理函数
void CTESTDlg::OnButton1()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
CString s;
s = m_e1;
s += " 爱好 :\r\n";
if (m_c1) s += " 旅游\r\n";
if (m_c2) s += " 运动\r\n";
if (m_c3) s += " 音乐\r\n";
m_e2 = s;
UpdateData(FALSE);
}
对话框是 Windows 应用程序中最常用的一种与用户交互的方式。用户通过对话框输入数据,程序通过对话框显示执行的情况。
【例】 设计如图所示的对话框。若选定了“日期”或“时间”复选框,则在对应的只读编辑框中显示系统当前日期或时间。
正在上传…重新上传取消
分析: CTime 是 MFC 中的一个类,封装了日期和时间。它有一个静态成员 GetCurrentTime() ,返回系统当前的日期和时间。
实现:
- 界面设计
首先创建一个对话框程序框架,然后放置各控件,按表进行属性设置和连接变量。
控件链接的变量
控件 | 属性设置 | 变量名 | 变量类别 | 变量类型 |
复选框 | 标题为“时间“ | m_k1 | Value | BOOL |
复选框 | 标题为“时间“ | m_k2 | Value | BOOL |
编辑框 | 设置“只读” | m_e1 | Value | CString |
编辑框 | 设置“只读” | m_e2 | Value | CString |
- 消息处理函数
…………………………………………………………
void CTESTDlg::OnCheck1()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
if(m_k1==TRUE)
{
CTime time=Ctime::GetCurrentTime();
// 构造“ YYYY.MM.DD ”形式的日期字符串
m_e1=time.Format("%Y.%m.%d");
}
else
m_e1="";
UpdateData(FALSE);
}
…………………………………………………………
void CTESTDlg::OnCheck2()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
if(m_k2==TRUE)
{
CTime time=Ctime::GetCurrentTime();
// 构造“ HH:MM:SS ”形式的时间字符串
m_e2=time.Format("%H:%M:%S");
}
else
m_e2="";
UpdateData(FALSE);
}
…………………………………………………………
4.5 列表框和组合框
列表框是CListBox类的对象,组合框是CComboBox的对象。
4.5.1 列表框
- 属性设置
列表框的重要属性有:
-
- Selection
该属性决定用户的选择方式,缺省值为 Single 。属性值有:
-
-
- Single ── 单项选择
- Multiple ── 多项选择,但是忽略 Ctrl 和 Alt 键。
- Extended ── 允许使用 Ctrl 和 Alt 进行多项选择
- None ── 禁止选择
-
正在上传…重新上传取消
-
- Sort
当该属性被设置为True后,列表框中的选项按字母顺序排列。
正在上传…重新上传取消
- 常用成员函数
- 添加项目
格式: int AddString( LPCTSTR lpszItem );
例如:语句 m_l. AddString(" 陈蓉 ") ; 把“陈蓉”添加到了列表框 m_l 中。
-
- 删除项目
格式: int DeleteString( UINT nIndex );
说明: nIndex 表示被删除项目在列表框中的位置,对于第一个选项nIndex应为 0 。
例如:语句 m_l. DeleteString (4) ;删除列表框中的第 5 个项目。
-
- 获取当前被选定的项目的序号
格式: int GetCurSel( ) const;
例如:语句int I=m_l.GetCurSel(); 将使I 获得 m_l 列表框中当前选定的项目的序号。
-
- 获取列表框中指定的项目
格式 1 : int GetText( int nIndex, LPTSTR lpszBuffer ) const;
格式 2 : void GetText( int nIndex, CString& rString ) const;
说明 :将列表框中第 nIndex 个选项的文本送到 lpszBuffer 或 rString 中。
例如:假定有说明语句 char s1[20]; CString s2;则语句 m_l.GetText(4, s1); 和 m_l. GetText(4, s2); 把第 5 项内容分别送到 s1 和 s2 中。
【例】 编写一个能对列表框进行项目添加、修改和删除操作的应用程序,如图所示。“添加”按钮的功能是将文本框中的内容添加到列表框,“删除”按钮的功能是删除列表框中选定的选项。如果要修改列表框,则首先选定选项,然后单击“修改”按钮,所选的选项显示在文本框中,当在文本框中修改完之后再单击“修改确定”按钮更新列表框。
正在上传…重新上传取消
列表框应用示例
- 界面设计
首先创建一个对话框应用程序框架,然后放置按钮。列表框和命令按钮按表连接变量,其余控件不需要连接变量。
控件连接的变量
控件 | 变量名 | 变量类别 | 变量类型 |
列表框 | m_l | Control | CListBox |
编辑框 | m_e | Value | CString |
- 初始化
列表框的选项在对话框的初始化函数中用 AddString 函数添加。
BOOL CTESTDlg::OnInitDialog()
{
┆
// TODO: Add extra initialization here
m_l.AddString(" 大学计算机基础 ");
m_l.AddString("C/C++ 程序设计 ");
m_l.AddString("VB 程序设计 ");
m_l.AddString(" 软件技术基础 ");
┆
}
- 消息处理函数
…………………………………………………………
void CTESTDlg::OnButton1() // 选择“添加”后执行的函数
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
m_l.AddString(m_e);
m_e="";
UpdateData(FALSE);
}
…………………………………………………………
void CTESTDlg::OnButton2() // 选择“删除”后执行的函数
{
// TODO: Add your control notification handler code here
int n=m_l.GetCurSel();
m_l.DeleteString(n);
}
…………………………………………………………
void CTESTDlg::OnButton3() // 选择“修改”后执行的函数
{
// 将选定的项目送编辑框供修改
// TODO: Add your control notification handler code here
CString s;
int n=m_l.GetCurSel();
m_l.GetText(n,s);
m_e=s;
UpdateData(FALSE);
}
…………………………………………………………
void CTESTDlg::OnButton4() // 选择“确定修改”后执行的函数
{
// 将修改后的项目送回列表框中,替换原项目,实现修改。
// TODO: Add your control notification handler code here
UpdateData(TRUE);
int n=m_l.GetCurSel();
m_l.DeleteString(n);
m_l.InsertString(n,m_e);
m_e="";
UpdateData(FALSE);
}
…………………………………………………………
4.5.2 组合框
- 属性设置
- 种类( Type )
该属性用于指定组合框的类型,共有三个选择: Simple 、 Dropdown 和 Droplist ,默认值为 Dropdown 。
正在上传…重新上传取消
-
- 键入列表框项( Enter listbox items )
在列表框的属性窗口有一个“ Data ”属性,这是在设计阶段输入选项的地方,如图所示。输入每一项都需要用分号分隔
正在上传…重新上传取消
- 常用成员函数
编辑框和列表框的成员函数几乎都可以用于组合框,但是列表框的成员函数 GetText() 在组合框中的是 GetLBText() 。
-
- int GetLBText( int nIndex, LPTSTR lpszText ) const;
- void GetLBText( int nIndex, CString& rString ) const;
说明 :使用 GetLBText 函数可以将组合框中的第 nIndex 个项目的文本送入 lpszText 或 rString 中。
- 组合框发出的消息
- CBN_SELECTCHANGE :组合框的列表框中的选项改变时发送。
- CBN_EDITCHANGE :组合框的编辑框中文本改变时发出。
【例】设计一个如图所示的对话框。如果单击“确定”按钮,则用 MessageBox 显示如图所示的信息框。
正在上传…重新上传取消
- 首先创建一个对话框程序框架,然后放置各控件,进行属性设置,组合框的选项在设计阶段直接输入,按表连接变量,其余控件不需要连接变量。
控件连接的变量
控件 | 变量名 | 变量类别 | 变量类型 |
组合框 | m_c | Value | CString |
编辑框 | m_e | Value | CString |
- 消息处理函数
void CTESTDlg::OnButton1()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
CString s;
s=" 品牌 :";
s=s+m_c;
s=s+"\n";
s=s+" 数量 :";
s=s+m_e;
MessageBox(s);
}
4.6 滚动条
滚动条是 CScrollBar 类的对象,是 Windows 应用程序中一个很重要的控件,通常附在对话框上用来协助观察数据或确定位置,也可以用来作为数据输入的工具。滚动条有水平和垂直两种。
正在上传…重新上传取消
【例】 建立一个水平滚动条,最小值为 0 ,最大值为 100 ,单击滚动条两端箭头时滑块移动的增量值为 2 ,单击滚动条中的空白处(滑块与两端箭头之间的区域)时滑块移动的增量值为 10 。另有一个只读的编辑框,显示了滑块当前位置所代表的值。
假定工程名为 TEST 。
- 界面设计
首先创建一个对话框应用程序框架,然后放置水平滚动条、编辑框,按表连接变量,编辑框设置为只读。
控件连接的变量
控件 | 变量名 | 变量类别 | 变量类型 |
滚动条 | m_s | Control | CScrollBar |
编辑框 | m_e | Value | int |
- 初始化
滚动条的初始化就是设置最小值,最大值,以及初始时滑块所代表的值。滚动条初始化在对话框的初始化函数中完成,涉及两个函数:
-
- 设置最大值和最小值
函数原型:
void SetScrollRange(int nMinPos , int nMaxPos , BOOL bRedraw = TRUE);
参数说明:
-
-
- nMinPos 表示最小值
- nMinPos 表示最大值 。
- 当 bRedraw 为 TRUE 时重画滚动条。
- 设置滑块的位置
-
格式: int SetScrollPos( int nPos , BOOL bRedraw = TRUE );
参数说明 :
- nPos 表示滑块的位置。
- 当 bRedraw 为 TRUE 时重画滚动条。
滚动条初始化代码如下:
BOOL CTESTDlg::OnInitDialog()
{
┆
// TODO: Add extra initialization here
m_s.SetScrollRange(0,100);
m_s.SetScrollPos(50);
m_e=50; // 初始时,编辑框显示 50 。
UpdateData(FALSE); // 更新编辑框显示的内容。
┆
};
- 编写消息处理函数
当用户在滚动条上进行操作时,滚动条接收不到任何消息。但是对话框却能接收到 WM_HSCROLL (水平滚动条)或 WM_VSCROLL (垂直滚动条)消息,也就是说,程序对滚动条的操作只能在 OnHScroll() 或 OnVScroll() 中编写代码。
滚动条处理 WM_Hscroll 消息的函数为:
void CTESTDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
// TODO: Add your message handler code here and/or call default
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
在对话框的属性面板中查找WM_HSCROLL命令,并添加响应函数
正在上传…重新上传取消
做如图选择,点击“<Add>”就把OnHScroll函数添加进去。
响应函数参数说明 :
① pScrollBar 指向用户正在进行操作的滚动条。
② nPos 表示滑块当前的位置。
③ nSBCode 指示用户正在进行的操作,其取值及其意义见表。
滚动条的通知消息
消息 | 用户操作 |
SB_THUMBTRACK | 拖动滑块 |
SB_LINELEFT / SB_LINEUP | 单击向左(上)的箭头 |
SB_LINERIGHT / SB_LINEDOWN | 单击向右(下)的箭头 |
SB_PAGELEFT / SB_PAGEUP | 单击向左(上)的箭头与滑块之间的区域 |
SB_PAGERIGHT / SB_PAGEDOWN | 单击向右(下)的箭头与滑块之间的区域 |
程序代码:
…………………………………………………………
void CTestDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{ // TODO: Add your message handler code here and/or call default
if (pScrollBar==&m_s)
{
int iNowPos;
switch(nSBCode)
{
case SB_THUMBTRACK: // 拖动滚动滑块时
m_s.SetScrollPos(nPos);
m_e=nPos;
break;
case SB_LINELEFT : // 单击滚动条向左的箭头
iNowPos=m_s.GetScrollPos(); // 获取滑块当前位置所代表的值
iNowPos=iNowPos-2;
if(iNowPos<0)
iNowPos=0;
m_s.SetScrollPos(iNowPos);
m_e=iNowPos;
break;
case SB_LINERIGHT : // 单击滚动条向右的箭头
iNowPos=m_s.GetScrollPos(); // 获取滑块当前位置所代表的值
iNowPos=iNowPos+2;
if(iNowPos>100)
iNowPos=100;
m_s.SetScrollPos(iNowPos);
m_e=iNowPos;
break;
case SB_PAGELEFT : // 单击滚动条左边的箭头与滑块之间的区域
iNowPos=m_s.GetScrollPos(); // 获取滑块当前位置所代表的值
iNowPos=iNowPos-10;
if(iNowPos<0)
iNowPos=0;
m_s.SetScrollPos(iNowPos);
m_e=iNowPos;
break;
case SB_PAGERIGHT : // 单击滚动条右边的箭头与滑块之间的区域
iNowPos=m_s.GetScrollPos(); // 获取滑块当前位置所代表的值
iNowPos=iNowPos+10;
if(iNowPos>100)
iNowPos=100;
m_s.SetScrollPos(iNowPos);
m_e=iNowPos;
break;
}
}
UpdateData(FALSE);
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
…………………………………………………………
说明:
- 由于对话框上可能有多个滚动条,因此函数首先需要根据指针 pScrollBar 确定事件发生在哪一个滚动条上,方法是使用表达式 pScrollBar==&m_s。因为, pScrollBar 指向发生事件的滚动条,所以这个表达式为真时,意味着用户对滚动条 m_s 进行了操作。
- 用户对滚动条的操作有 5 种,因此函数中必须以这 5 种情况分别进行处理。
4.7 程序举例
【例】 设计如图所示的对话框。单击“确定”按钮后,在列表框中显示选择的信息。
正在上传…重新上传取消
- 界面设计
首先创建一个对话框程序框架,然后放置各控件,按图所示设置属性和连接变量。其中, m_e2 编辑框设置为只读,组合框和列表框取消排序属性。
- 初始化
组合框的选项在设计阶段输入,其余控件的初始化在对话框的初始化函数中完成。
BOOL CTESTDlg::OnInitDialog()
{
┆
// TODO: Add extra initialization here
m_scroll.SetScrollRange(0, 110);
m_scroll.SetScrollPos(50);
CheckRadioButton(IDC_RADIO1, IDC_RADIO2, IDC_RADIO1);
m_check1 = TRUE;
m_workAge = 50;
UpdateData(FALSE);
m_combo.SetCurSel(0);
┆
}
- 滚动条消息处理函数
请参照滚动条部分自己完成。为了不影响其它控件,应开始处添加命令:
UpdateData(TRUE);
- 命令按钮的消息处理函数
…………………………………………………………
void CTESTDlg::OnButton1()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE); // 用各控件中的值更新对应的连接变量
m_list.ResetContent(); // 删除列表框中所有的内容
m_list.AddString(_T("姓名: ") + m_name);
if (IsDlgButtonChecked(IDC_RADIO1))
{
m_list.AddString(_T("性别: 男 "));
}
else
{
m_list.AddString(_T("性别: 女 "));
}
m_list.AddString(_T("爱好: "));
if (m_check1) m_list.AddString(_T(" 旅游 "));
if (m_check2) m_list.AddString(_T(" 运动 "));
if (m_check3) m_list.AddString(_T(" 音乐 "));
CString s;
// m_e2 是 int 类型,将它转换成字符串
s.Format(_T("%s%d"), _T("工龄: "), m_workAge);
m_list.AddString(s);
int nIndex = m_combo.GetCurSel();
CString comboText;
m_combo.GetLBText(nIndex, comboText);
m_list.AddString(_T("职称: ") + comboText);
}
…………………………………………………………
知识点补充:
屏蔽或接收键盘事件
在MFC对话框应用程序中,启动应用程序,默认状态下当我们按下键盘的回车键之后,程序就自动关闭了,如果不想让程序关闭,那么应该怎么办?如果按下ESC键之后想让程序关闭,那么又该怎么办?
处理方式:
屏蔽默认回车消息
当在对话框中按下回车键时,会选择对话框中默认的按钮消息响应函数来处理这一事件,而基类(CDialogEx)的IDOK按钮的默认响应函数(OnOk)的功能就是关闭对话框。因此,我们一但在对话框测试程序中按下回车键,这个对话框就关闭了。
为了屏蔽掉默认的回车键关闭对话框这一功能,应该在对话框子类中重写OK按钮的消息响应函数OnOk。
void CMy04_DlgEditDlg::OnOK()
{
//去掉此行, 子类中不能调用基类的OnOK函数
//CDialogEx::OnOK();
}
彻底屏蔽回车键
1. 在类视图中选中对话框类
正在上传…重新上传取消
2. 重写该类的虚函数PreTranslateMessage
正在上传…重新上传取消
3. 在该函数中截获键盘消息的return按钮事件,屏蔽掉即可
BOOL CMy04_DlgEditDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
if (pMsg->message == WM_KEYDOWN)
{
if (pMsg->wParam == VK_RETURN)
{
return TRUE;
}
}
return CDialogEx::PreTranslateMessage(pMsg);
}
4. 要处理ESC键消息亦是如此,只需要捕捉键盘Esc消息:
BOOL CMy04_DlgEditDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
if (pMsg->message == WM_KEYDOWN)
{
if (pMsg->wParam == VK_ESCAPE)
{
PostQuitMessage(0);
return TRUE;
}
}
return CDialogEx::PreTranslateMessage(pMsg);
}
在前几天学过的知识中讲过,程序在关闭时会调用PostQuitMessage(0) 函数,该函数执行完毕后会发送WM_QUIT消息,系统在接收到该消息之后会退出消息循环,应用程序随之退出。
一、对话框
1)模态对话框
a) 资源视图 -> Dialog -> 右击 -> 插入 Dialog
b) 创建对话框对象 CDialog
c) 以模态方式运行 CDialog::DoModal
2)非模态对话框
a)资源视图 -> Dialog -> 右击 -> 插入 Dialog
b)创建对话框对象,需要在.h的地方声明为成员变量 CDialog
c)创建对话框(在构造函数或OnCreate(),目的只创建一次) CDialog::Create
d)显示窗口 CWnd::ShowWindow
3)自定义对话框类(重要)
a)资源视图 -> Dialog -> 右击 -> 插入 Dialog
b)点击对话框模板 -> 右击 -> 添加类
c)多出来一个自定义的类, .h 类中有个枚举和对话框关联 enum { IDD = IDD_DIALOG2 };
二、基于对话框(控件)编程
1)基于对话框应用程序框架
a)应用程序类:继承于 CWinApp
InitInstance() : 程序的入口地址
b)对话框类:继承于 CDialogEx
OnInitDialog() :对话框的初始化工作
DoDataExchange() : 控件和变量的关联和交换
三、常用控件
1)静态控件CStatic(Qt中的标签QLabel)
显示一些文字信息
a) Caption: 修改显示的内容
b) ID: XXX_STATIC,静态ID,不响应任何消息(事件)
2)按钮 CButton
a) Caption: 修改显示的内容
b) 处理消息 BN_CLICKED,用户点击按钮自动触发:
1)属性 -> 控制事件 -> 选择所需事件
2)双击按钮,自动生成消息处理函数
3)逃跑按钮(类似于Qt提升)
a)自定义按钮类,继承于 CButton
选择类视图最开始的文件夹 -> 右击 -> 添加类 -> MFC -> MFC类
1)处理鼠标移动消息 WM_MOUSEMOVE
2)获取父窗口指针 CWnd::GetParent
3)获取父窗口客户区域的范围 CWnd::GetClientRect
4)获取按钮的范围 CWnd::GetWindowRect
5)产生随机坐标 rand()%w
6)移动按钮的位置 CWnd::MoveWindow
b)变量关联
选中按钮 -> 右击 -> 添加变量 -> 变量类型: MyButton -> 变量:button
最终,button和我们所选中的按钮关联成功,操作button,相当于操作ui上的按钮 (Qt的提升)
c)为按钮设置位图
1)按钮属性:Bitmap -> True
2)在对话框类中 OnInitDialog() 做如下处理
a)创建位图模板
b)创建位图对象 CBitmap
c)加载位图资源 CBitmap::LoadBitmap
b)按钮设置位图 CButton::SetBitmap
c)获取位图大小 CBitmap::GetBitmap
c)重新设置按钮大小(图片和按钮大小一致) CWnd::MoveWindow
4)编辑框CEdit
a)关联类别:Value, Control
1)Value:标准普通数据类型 CString str;
关联变量和控件数据的交互更新
a)把编辑区的内容更新到str中 UpdateData(TRUE);
b)把str的内容更新到编辑区中 UpdateData(FALSE);
2)Control:控件类型
控件类型的对象即为ui上的控件
b)常用属性设置
1)Number -> True 只能输入数字
2)Password -> True 密码模式
3)Want return -> True 接收回车键,自动换行,只有在多行模式下,才能换行
4)Multiline -> True 多行模式
5)Auto VScroll -> True
Vertical Scroll -> True 当垂直方式字符太多,自动出现滚动条
6)Read Only -> True 只读
c)复制小案例
关联 Control:控件类型,只能关联一次
1)获取编辑区内容 CWnd::GetWindowText
2)设置编辑区内容 CWnd::SetWindowText
3)关闭对话框窗口
CDialog::OnOK();
CDialog::OnCancel();
5)单选框、复选框 (特殊的CButton, 没有单选框, 复选框类型 )
a) 单选框
1) 属性设置:顺序排放 Ctrl+D 查看
2) 同组第一个按钮 Group 设置为 TRUE
3) 初始化单选框 CWnd::CheckRadioButton
4) 按钮是否按下 CWnd::IsDlgButtonChecked
b) 复选框
1) 常关联变量 BOOL UpdateData(TRUE), UpdateData(FALSE);
2) 设置按钮选择状态 CButton::SetCheck
3) 获取按钮选择状态 CButton::GetCheck
6)列表框CListBox
a) 给列表框添加一个字符串 CListBox::AddString
b) 选中列表列表框某一项,自动触发事件:LBN_SELCHANGE
1)获取当前选中项 CListBox::GetCurSel
2)获取指定位置的内容 CListBox::GetText
c) 删除指定位置的字符串 CListBox::DeleteString
d) 在指定位置插入字符串 CListBox::InsertString
7)组合框(下拉框)CComboBox
a) 获取内容:CComboBox::GetLBText
其它接口和 CListBox 的用法几乎一样
b) 属性设置
1) data: 设置内容,不同内容间同英文的分号“;”分隔
2) type
8)滚动条 CScrollBar
a) 设置给定滚动条的最小和最大位置:CScrollBar::SetScrollRange
b) 获取一个滚动框的当前位置: CScrollBar::GetScrollPos
c) 设置一个滚动框的当前位置:CScrollBar::SetScrollPos
d) 处理滚动条的事件,不是在滚动条控件本身处理,是在滚动条所属的父窗口处理(对话框类)
处理信号: WM_HSCROLL
e)滚动条位置关系
switch (nSBCode) //判断滚动条的哪一部分
{
case SB_THUMBPOSITION: //滑块位置
break;
case SB_LINELEFT: //向左的箭头
break;
case SB_LINERIGHT: //向左的箭头
break;
case SB_PAGELEFT: //箭头和滑块之间左边
break;
case SB_PAGERIGHT: //箭头和滑块之间右边
break;
default:
break;
}
9)微调(旋转)按钮 SpinControl 的使用
a)属性设置
Auto Buddy -> True
Set buddy integer -> True
b)微调(旋转)按钮的顺序比伙伴大1 (Ctrl + D 查看)
10)列表视图控件 CListCtrl
a) 属性设置 view -> Report (报表方式)
b) 常用接口
1)设置列表风格 CListCtrl::SetExtendedStyle
LVS_EX_FULLROWSELECT:选择整行
LVS_EX_GRIDLINES:网格方式
具体有哪些风格,可通过MSDN查看
2)获取列表风格 CListCtrl::SetExtendedStyle
具体有哪些风格,可通过MSDN查看
3)插入某列 CListCtrl::InsertColumn
4)字符串格式化
CString str;
str.Format(_T("张三_%d"), i);
5)插入新项后,才能设置子项内容
a)插入新项(确定第几行) CListCtrl::InsertItem
b)设置子项内容(设置第几列) CListCtrl::SetItemText
11)树视图控件 CTreeCtrl
a) 常用属性设置
has buttons -> true
has lines -> true
lines at root -> true
b) 写代码流程
1) 加载自定义图标
a) 获取应用程序对象指针 AfxGetApp()
b) 加载自定义图标 CWinApp::LoadIcon
2) 创建图像列表
a) .h 文件类中定义图形列表(CImageList)对象
b) 创建图像列表 CImageList::Create
c) 图像列表追加图标 CImageList::Add
4) 设置图形状态列表 CTreeCtrl::SetImageList
5) 插入节点 CTreeCtrl::InsertItem
6) 设置默认选中项 CTreeCtrl::SelectItem
12) 标签控件 CTabCtrl
1)在ui工具箱拖放 Tab Control
2)把 TabSheet.h和TabSheet.cpp 放在项目文件同级目录,并且添加到工程目录中
3)给ui上 Tab Control 关联Control类型(CTabSheet)
4)CTabSheet对象添加对话框
a) 资源视图 -> Dialog -> 右击 -> 插入 Dialog
b) 设置相应属性:
Style -> Child (子窗口)
Border -> None (无边框)
c) 自定义类:点击对话框模板 -> 右击 -> 添加类
d) 主对话框类中, 定义自定义类对象
e) 主对话框类中 OnInitDialog() 做初始化工作
f)CTabSheet添加对话框 CTabSheet::AddPage()
tmp.AddPage(_T("系统管理"), &t1, tab1);
tmp: 为CTabSheet对象
t1:需要添加对话框对象
tab1:对话框ID
g)显示窗口:CTabSheet::Show()
5 文档/视图
MFC应用程序模型历经多年以有了相当大的发展。有一个时期,它只是个使用应用程序对象和主窗口对象的简单模型。在这个模型中,应用程序的数据作为成员变量保持在框架窗口类中,在框架窗口的客户区中,该数据被提交显示器。随着MFC2.0的问世,一种应用程序结构的新方式----MFC文档/视结构出现了。在文档/视图结构中,CFrameWnd繁重的任务被委派给几个不同类,实现了数据存储和显示的分离。
5.1文档/视图应用程序组成
一般情况下,采用文档/视结构的应用程序至少应由以下对象组成:
- 应用程序是一个CWinApp派生对象,它充当全部应用程序的容器。应用程序沿消息映射网络分配消息给它的所有子程序。
- 框架窗口是一CFrmeWnd派生对象。
- 文档是一个CDocument派生对象,它存储应用程序的数据,并把这些信息提供给应用程序的其余部分。
- 视窗是CView派生对象,它与其父框架窗口用户区对齐。视窗接受用户对应用程序的输入并显示相关联的文档数据。
通常,应用程序数据存在于简单模型中的框架窗口中。在文档/视方式中,该数据移入称为document的独立数据对象。当然,文档不一定是文字,文档是可以表现应用程序使用的数据集的抽象术语。而用户输入处理及图形输出功能从框架窗口转向视图。单独的视窗完全遮蔽框架窗口的客户区,这意味着即使程序员直接绘画至框架窗口的客户区,视图仍遮蔽绘画,在屏幕上不出现任何信息。所以输出必须通过视图。框架窗口仅仅是个视图容器。
CDocument类对文档的建立及归档提供支持并提供应用程序用于控制其数据的接口。MDI应用程序可以处理多个类型的文档,每个类型的文档拥有一个相关联的文档模板对象。文档对象驻留在场景后面,提供由视图对象显示的信息。文档至少有一个相关联的视图。视图只能与一个文档相关联。
在文档/视方式中,对象的建立是由文档模板来管理的,它是CDocTemplate派生对象,建立并维护框架窗口,文档及视。
总之,在文档/视模式中,文档和视图是分离的,即:文档用于保存数据,而视图是用来显示这些数据。文档模板维护它们之间的关西。这种文档/视结构在开发大型软件项目时特别有用。
5.2. 文档、视图、框架之间的关联
MFC SDI/MDI 中的核心就在于文档、视图、框架之间的关联,形成了一个有机的可运作的整体。 MFC 提供了默认的关联关系,但是在实际的项目开发中很多时候需要动态进行他们的之间的关联。
5.2.1 与文档/视结构有关的各种类之间的关系
CWinApp对象拥有并控制文档模板,后者产生文档、框架窗口及视窗。这种相互关系如图所示:
正在上传…重新上传取消
从用户的角度来看,“视”实际上是一个普通的窗口。像其他基于Widnows应用的窗口一样,人们可以改变它的尺寸,对它进行移动,也可以随时关闭它。若从程序员的角度来看,视实际上是一个从MFC类库中的CView类所派生出的类的对象。文档对象是用来保存数据的,而视对象是用来显示数据的,并且允许对数据进行编辑。SDI或MDI的文档类是由CDocument类派生出来的,它可以有一个或多个视类,而这些视类最终都是由CView类派生出来的。视对象只有一个与之相联系的文档对象,它所包含的CView::GetDocument函数允许应用在视中得到与之相联系的文档,据此,应用程序可以对文档类成员函数及公共数据成员进行访问。如果视对象接受到了一条消息,表示用户在编辑控制中输入了新的数据,此时,视就必须通知文档对象对其内部数据进行相应的更新。
如果文档数据发生了变化,则所有的视都必须被通知到,以便它们能够对所显示的数据进行相应的更新。CDocument::UpdateAllViews函数即可完成此功能。当该函数被调用时,派生视类的CView::OnUpdate函数被触发。通常OnUpdate函数要对文档进行访问,读取文档数据,然后再对视的数据成员或控制进行更新,以便反映出文档的变化。另外,还可以利用OnUpdate函数使视的部分客户区无效,以便触发CView::OnDraw函数,利用文档数据来重新对窗口进行绘制。Invalidate()函数也可以使当前窗口无效,导致窗口重绘。
MDI中的指针列表
在MDI应用程序中,可以处理多个文档类型,即多个文档模板,每个模板又可以有多个文档,每个文档又可以多视显示。为管理方便,上一级往往保留了下一级的指针列表。如下图所示:
正在上传…重新上传取消
MDI中的存取关系
正在上传…重新上传取消
解释如下:
- 每个应用程序类(CWinApp的派生类)都保留并维护了一份所有文档模板的指针列表,这是一个链表结构。应用程序为所要支持的每个文档类型动态分配一个CMultiDocTemplate 对象,
CMultiDocTemplate(UINT nIDResource,
CRuntimeClass * pDocClass,
CRuntimeClass * pFrameClass,
CRuntimeClass * pViewClass );
并在应用程序类的CWinApp::InitInstance成员函数中将每个CMultiDocTemplate对象传递给CWinApp::AddDocTemplate。 该函数将一个文档模板加入到应用程序可用文档模板的列表中。函数原形为:
void AddDocTemplate(CdocTemplate * pTemplate);
应用程序可以用CWinApp::GetFirstDocTemplatePostion获得应用程序注册的第一个文档模板的位置,利用该值来调用CWinApp::GetNextDocTemplate函数,获得第一个CDocTemplate对象指针。函数原形如下:
POSITION GetFirstDocTemplate( ) const;
CDocTemplate *GetNextDocTemplate( POSITION & pos ) const;
第二个函数返回由pos 标识的文档模板。POSITION是MFC定义的一个用于迭代或对象指针检索的值。通过这两个函数,应用程序可以遍历整个文档模板列表。如果被检索的文档模板是模板列表中的最后一个,则pos参数被置为NULL。
使用 MFC 类向导生成 MFC SDI/MDI 程序,在 App 类的 InitInstance ()方法中有如下代码(假设 Project 名称均为 Test ):
SDI (单文档界面)中
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CMainFrame),// main SDI frame window
RUNTIME_CLASS(CTestView)
);
AddDocTemplate(pDocTemplate);
MDI(多文档界面) 中
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_TESTTYPE,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CTestView)
);
AddDocTemplate(pDocTemplate);
这里通过 CDocTemplate (无论是 SDI 中的 CSingleDocTemplate 还是 MDI 中的 CMultiDocTemplate )的构造函数,将文当、视图和框架( SDI 中与主框架, MDI 中与自框架)关联在一起了,形成了一个整体。
- 一个文档模板可以有多个文档,每个文档模板都保留并维护了一个所有对应文档的指针列表。应用程序可以用CDocTemplate::GetFirstDocPosition函数获得与文档模板相关的文档集合中第一个文档的位置,并用POSITION值作为CDocTemplate::GetNextDoc的参数来重复遍历与模板相关的文档列表。函数原形为:
viaual POSITION GetFirstDocPosition( ) const = 0;
isual CDocument *GetNextDoc(POSITION & rPos) const = 0;
如果列表为空,则rPos被置为NULL.
- 在文档中可以调用CDocument::GetDocTemplate获得指向该文档模板的指针。函数原形如下:
CDocTemplate * GetDocTemplate ( ) const;
如果该文档不属于文档模板管理,则返回值为NULL。
- 一个文档可以有多个视图。每一个文档都保留并维护一个所有相关视图的列表。CDocument::AddView将一个视图连接到文档上,将该视图加入到文档相联系的视图的列表中,并将视图的文档指针指向该文档。当有File/New、File/Open、Windows/New或Window/Split的命令而将一个新创建的视图的对象连接到文档上时, MFC会自动调用该函数,框架通过文档/视图的结构将文档和视图联系起来。当然,程序员也可以根据自己的需要调用该函数。
virtual POSITION GetFirstViewPosition( ) const;
virtual CViw * GetNextView( POSITION &rPosition) cosnt;
应用程序可以调用CDocument::GetFirstViewPosition返回与调用文档相联系的视图的列表中的第一个视图的位置,并调用CDocument::GetNextView返回指定位置的视图,并将rPositon的值置为列表中下一个视图的POSITION值。如果找到的视图为列表中的最后一个视图,则将rPosition置为NULL.
当在文档上新增一个视图或删除一个视图时,MFC会调用OnChangeViewList函数。如果被删除的视图是该文档的最后一个视图,则删除该文档。
- 一个视图只能有一个文档。在视图中,调用CView::GetDocument可以获得一个指向视图的文档的指针。函数原形如下:
CDocument *GetDocument ( ) const;
如果该视图没有与任何文档相连接,则返回NULL.
- MDI框架窗口通过调用CFrameWnd::GetActiveDocument 可以获得与当前活动的视图相连的CDocument 指针。函数原形如下:
virtual CDocument * GetActiveDocument( );
- 通过调用CFrameWnd::GetActiveView 可以获得指向与CFrameWnd框架窗口连接的活动视图的指针,如果是被CMDIFrameWnd框架窗口调用,则返回NULL。MDI框架窗口可以首先调用MDIGetActive找到活动的MDI子窗口,然后找到该子窗口的活动视。函数原形如下:
virtual Cdocument * GetActiveDocument( );
- MDI框架窗口通过调用CFrameWnd::GetActiveFrame, 可以获得一个指向MDI框架窗口的活动多文档界面子窗口的指针。
- CMDIChildWnd调用GetMDIFrame获得MDI框架窗口(CMDIFrameWnd)。
- CWinApp 调用AfxGetMainWnd得到指向应用程序的活动主窗口的指针。
遍历整个文档模板、文档和视图
下面一段代码,就是利用CDocTemplate、CDocument和CView之间的存取关系,遍历整个文档模板、文档以及视图。
//获取应用程序的对象指针
CMyApp * pMyApp = (CMyApp *)AfxGetApp();
POSITION p = pMyApp->GetFirstDocTemplatePosition();
while(p!= NULL)
{
CDocTemplate * pDocTemplate = pMyApp->GetNextDocTemplate(p);
POSITION p1 = pDocTemplate->GetFirstDocPosition();
while(p1 != NULL)
{
CDocument * pDocument = pDocTemplate->GetNextDoc(p1);
POSITION p2 = pDocument->GetFirstViewPosition();
while(p2 != NULL)
{
CView * pView = pDocument->GetNextView(p2);
}
}
}
5.2.2 MFC SDI/MDI 各个类之间的互访
在实际项目开发中用的最多就是各个类之间的互访问,这里将网络上和书籍中提到的做了一个总结,也是在实际开发中比较常用的。
访问对象 | 访问位置 | 访问实现 |
应用程序 App | 任何位置 | ① AfxGetApp(); ② 在要使用应用程序 App 的文件中加入: extern CApp theApp ,然后直接使用全局的 theApp 变量。 |
主框架窗口 | 任何位置 | ① AfxGetMainWnd(); ② AfxGetApp()->m_pMainWnd; |
视图 | 框架类中 | GetActiveView();// 当前的活动视图 |
文档类中 | GetFirstViewPosition (); // 可以获取全部视图 GetNextView (); | |
文档 | 视类中 | GetDocument() ; |
文当模版类中 | GetFirstDocPosition(); // 该文档模版对应全部文档 GetNextDoc(); | |
框架类中 | GetActiveDocument(); // 当前活动文当 | |
子框架类(MDI中) | 主框架类中 | ① MDIGetActive (); ② GetActiveFrame (); |
视图类中 | GetParentFrame(); | |
文档模版 | 文档类中 | GetDocTemplate(); |
应用程序 App 中 | GetFirstDocTemplatePosition(); GetNextDocTemplate(); |
5.3文档序列化
5.3.1 序列化定义
MFC文档-视图结构中,序列化机制可以实现内存中对象储存和加载。
序列化机制分为序列化和反序列化:
- 序列化是把内存中的对象以二进制文件的形式存储在磁盘中
- 反序列化是把序列化后生成的文件恢复到内存。
5.3.2 CArchive类
CArchive没有基类。CArchive允许以一个永久二进制(通常为磁盘存储)的形式保存一个对象的复杂网络,它可以在对象被删除时,还能永久保存。可以从永久存储中装载对象,在内存中重新构造它们。使得数据永久保留的过程就叫作 “序列化”。
可以把一个归档对象(CArchive)看作一种二进制流。像输入/输出流一样,归档与文件有关并允许写缓冲区以及从硬盘读出或读入数据。输入/输出流处理一系列ASCII字符,但是归档文件以一种有效率、精练的格式处理二进制对象。
5.3.3 CArchive对象处理基础类型数据
必须在创建一个CArchive对象之前,创建一个CFile对象。另外,必须确信归档文件的装入/存储与文件的打开模式是兼容的。每一个文件只限于一个活动归档文件。当构造一个CArchive对象时,要把它附加给表示一个打开文件的类CFile(或派生类)的对象上。还要指定归档文件将用于装载还是存储。
- 示例:
//将数据以二进制方式写入文件中
void CArchiveView::OnWritefile()
{
// TODO: 在此添加命令处理程序代码
//构造CFile文件对象
CFile file(_T("demo.txt"),
CFile::modeCreate | CFile::modeWrite);
//构造存档对象
CArchive ar(&file, CArchive::store);
int a = 12;
char ch = 'p';
double b = 12.345;
CString str = _T("大江东去浪淘尽, 千古风流人物");
//保存数据
ar << a << ch << b << str;
}
//从文件中读取数据
void CArchiveView::OnReadfile()
{
// TODO: 在此添加命令处理程序代码
//构造CFile文件对象
CFile file(_T("demo.txt"), CFile::modeRead);
//构造存档对象
CArchive ar(&file, CArchive::load);
//定义变量,存放加载出来的数据
int a;
char ch;
double b;
CString str;
//加载数据
ar >> a >> ch >>b >> str;
//格式化字符串
CString strResult;
strResult.Format(_T("%d\n%c\n%lf\n%s"), a, ch, b, str);
MessageBox(strResult);
}
5.3.4 CArchive对象处理自定义对象类型数据
CArchive对象不仅可以处理基础类型,而且还能处理为序列化而设计的特殊的类的对象。可以序列化的类有以下特点:
- 必须为CObject派生类。直接或者间接的继承CObject类。
- 必须重写CObject类的Serialize成员函数。
- 使用DECLARE_SERIAL和IMPLEMENT_SERIAL宏。
- 必须有一个默认构造函数
在Serialize成员函数中完成保存和加载的功能。
DECLARE_SERIAL( class_name ) 参数为当前类名。
IMPLEMENT_SERIAL( class_name, base_class_name, wSchema )
- 第一个参数为当前类名
- 第二个参数为父类类名
- 第三个参数为该类的特定整型标识,该标识将用来解序(重新实例化),最小为0。
示例:
//
DrawBase.h
//
class CDrawBase : public CObject //继承自CObject
{
public:
DECLARE_SERIAL(CDrawBase) //第一个宏的位置,参数为当前类名
CDrawBase(); //必须有一个默认构造函数
virtual void onDraw(CDC* pdc);
virtual ~CDrawBase();
public:
UINT m_PenStyle;
int m_PenWidth;
int m_BkMode;
int m_BrushStyle;
int m_issx;
int m_isyy;
COLORREF m_PenColor;
COLORREF m_BackgroundColor;
COLORREF m_BrushColor;
CPoint m_ptBegin;
CPoint m_ptEnd;
public:
void Serialize(CArchive& ar); //重写了Serialize成员函数
};
//
DrawBase.cpp
//
//第二个宏的位置,第一个参数为当前类名,第二个参数为父类类名,
//第三个参数为该类的特定整型标识,该标识将用来解序(重新实例化),
//最小为0
IMPLEMENT_SERIAL(CDrawBase, CObject, 1)
void CDrawBase::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if(ar.IsStoring()) //保存,加载为ar.IsLoading()
{
ar <<m_PenColor<<m_PenStyle<<m_PenWidth
<<m_BrushColor<<m_BrushStyle<<m_issx<<m_isyy;
}
else //加载
{
ar >>m_PenColor>>m_PenStyle>>m_PenWidth>>
m_BrushColor>>m_BrushStyle>>m_issx>>m_isyy;
}
}
5.3.5 总结
重载提取(>>)和插入(<<)是方便的归档编程接口。它支持主要类型和CObject派生类。
CArchive还支持使用MFC Windows套接字类CSocket和CSocketFile编程。IsBufferEmpty成员函数也支持这种使用。一些集合类也支持序列化,CObArray,Vector,CPtrArray。
在MFC文档-视图结构中,Doc类是被系统设定好支持序列化的类,在Doc类中重写Serialize成员函数。在函数中对你要保存的对象序列化。完成之后,点击菜单栏上的保存和打开就可以实现序列化了。
5.4. 文档/视图实例
【例5-4-1】文档视图结构应用程序例子(Ex_DocView)。
创建基于CFormView类的多文档应用程序
用MFC 应用程序向导创建一个默认的多文档应用程序Ex_DocView,但在向导的【生成的类】属性页将CEx_DocViewView的基类由默认的CView选择为CFormView类,如图1所示。
正在上传…重新上传取消
图1 设置视图的基类为CFormView类
添加应用程序所需的数据
- 第一步 在VS中,给新创建的工程添加一个新的MFC类,弹出新建类对话框。
正在上传…重新上传取消
正在上传…重新上传取消
图2 添加MFC类
在添加类向导对话框中,类名为CStudent,默认的类文件为Student.cpp,设置该类的父类为CObject,并单击【确定】按钮。如图3所示。
正在上传…重新上传取消
图3 添加CStudent类
- 第二步 打开项目工作区的文件视图,双击打开Student.h文件,如图4所示,编辑CStudent类定义的代码如下:
class CStudent : public CObject
{
public:
CStudent();
CStudent(long code, CString name, long age, double score);
virtual ~CStudent();
long m_nCode;
CString m_sName;
long m_lAge;
double m_lScore;
};
正在上传…重新上传取消
图4 打开Student.h文件
- 第三步 同上步,编辑Student.cpp文件,修改类CStudent的构造函数实现代码如下:
CStudent::CStudent(long code,CString name,long age,double score)
{
m_nCode=code;
m_sName=name;
m_lAge=age;
m_lScore=score;
}
- 第四步 在Ex_DocViewDoc.h文件中,为类CEx_DocViewDoc添加数据成员:
class CEx_DocViewDoc : public CDocument
{
……
public:
POSITION curPos;
CTypedPtrList <CObList, CStudent*> m_dataList;
……
}
说明:m_dataList定义为CStudent类指针的表CtypedPtrList变量,curPos定义为POSITION变量,用来指示当前指针。
- 第五步 在文件stdafx..h中加入添加如下代码:
#include<afxtempl.h>
因为在程序中使用了模板类CTypedPtrList<CObList, CStudent*>。
- 第六步 在Ex_DocViewDoc.h文件的头部添加如下代码:
#include "Student.h"
- 第七步 在文档关闭时,需要清除m_dataList占用的内存,利用属性窗口为CEx_DocViewDoc类加入虚函数DeleteContens(),并添加代码:
void CEx_DocViewDoc::DeleteContents()
{
while(!m_dataList.IsEmpty())
{
delete m_dataList.RemoveHead();
}
CDocument::DeleteContents();
}
修改IDD_EX_DOCVIEW_FORM对话框资源,添加应用程序所需控件:
- 第一步 打开项目工作区的资源视图,双击打开IDD_EX_DOCVIEW_FORM对话框资源如图5所示,编辑IDD_EX_DOCVIEW_FORM对话框资源,如图6所示。
正在上传…重新上传取消
图5 打开对话框资源
正在上传…重新上传取消
图6 编辑后的对话框控件
- 第二步 设置图6各控件的属性如表1所示。
表1 添加的控件
控件 | ID | 标题 | 其他属性 |
编辑框(学号) | IDC_CODE | 默认 | |
编辑框(姓名) | IDC_NAME | 默认 | |
编辑框(年龄) | IDC_AGE | 默认 | |
编辑框(成绩) | IDC_SCORE | 默认 | |
按钮(添加) | IDC_ADD | 添加 | 默认 |
按钮(下一个) | IDC_NEXT | 下一个 | 默认 |
- 第三步 打开MFC类向导的成员变量页面,确定类名是CEx_DocViewView,如图7所示。选中所需的控件ID号,双击鼠标。依次为下列控件添加成员变量,如下表2所示。
正在上传…重新上传取消
正在上传…重新上传取消
图7 为CEx_DocViewView添加成员变量
表2 控件变量
ID | 成员变量名 | 属性类型 | 变量类型 |
IDC_CODE | m_nCode | Value | UINT |
IDC_NAME | m_sName | Value | CString |
IDC_AGE | m_lAge | Value | int |
IDC_SCORE | m_lScore | Value | double |
为按钮编写消息响应函数
- 第一步 用MFC ClassWizard为按钮IDC_ADD添加BN_CLICKED的消息映射,如图8所示,单击【添加函数】按钮为CEx_DocViewView添加OnAdd( )成员函数,再单击【编辑代码】按钮为该含函数加入下列代码:
void CEx_DocViewView:: OnBnClickedAdd ()
{
UpdateData();
CEx_DocViewDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
CStudent *pStudent;
pStudent=new CStudent(m_nCode,m_sName,m_lAge,m_lScore);
pDoc->m_dataList.AddTail(pStudent);
pDoc->curPos=pDoc->m_dataList.GetHeadPosition();
}
正在上传…重新上传取消
图8 为按钮IDC_ADD编写消息响应函数
说明:
-
- UpdateData()函数迫使对话框编辑控件和相应变量之间传送数据,该函数原型为:
BOOL UpdateData(BOOL bSaveAndValidate::TRUE);
-
- 其中bSaveAndValidate为true时表示数据已经更新。
在CEx_DocViewView::OnAdd()函数中,取得指向文档的指针,操作文档对象的成员变量m_dataList,首先使用用户输入的变量值构造一个新CStudent对象,然后将其加入m_dataList表尾,最后将curPos指向表头。通过上面的操作,一个新CStudent对象就加到m_dataList表中。
- 第二步 同上步,用MFC ClassWizard为按钮IDC_NEXT添加BN_CLICKED的消息映射,并增加下列代码:
void CEx_DocViewView:: OnBnClickedNext ()
{
CEx_DocViewDoc *pDoc =GetDocument();
ASSERT_VALID(pDoc);
if(pDoc->curPos!=NULL)
{
CStudent *pStudent=(CStudent *)
pDoc->m_dataList.GetAt(pDoc->curPos);
m_nCode=pStudent->m_nCode;
m_sName=pStudent->m_sName;
m_lAge=pStudent->m_lAge;
m_lScore=pStudent->m_lScore;
UpdateData(false);
pDoc->m_dataList.GetNext(pDoc->curPos);
if(pDoc->curPos==NULL)
pDoc->curPos=pDoc->m_dataList.GetHeadPosition();
}
else
MessageBox("当前列表中没有数据!");
}
说明:CEx_DocViewView::OnNext函数用来循环遍历m_dataList表,首先得到文档指针,然后判断,如果curPos为空,就说明没有数据,因为在OnAdd函数中将其设在表头。如果表不空,就进行循环遍历。
【例5-4-2】文档视图结构应用程序例子(Editor)。
创建单文档应用程序Editor
- 第一步 用MFC AppWizard(exe)创建一个默认的单文档应用程序Editor,但在向导的【文档模板属性】页,设置文档视图结构的一些属性,如图9所示。
正在上传…重新上传取消
图9 Advanced Option对话框
该对话框中包含以下几项:
- 文件扩展名:指定应用程序创建的文档所用的文件名后缀。输入后缀名.txt,表明Editor使用文本文件的后缀名.TXT;
- 文件类型ID:用于在Windows的注册数据库中标识应用程序的文档类型;
- 主框架标题:主框架窗口标题,默认情况下与项目名相一致;
- 文档类型名称:指定与一个从CDocument派生的文档类相关的文档类型名;
- 筛选器名称:用作“打开文件”、“保存文件”对话框中的过滤器。Visual Studio会自动根据输入的后缀名生成一个过滤器:Editor Files(*.txt)。这样,当在Open File对话框中选择Editor Files(*.txt)时,只有以.txt为后缀名的文件名显示在文件名列表中;
- 文件的新短名称:用于指定在new对话框中使用的文档名。当应用程序支持多种文档类型时,选择【Fi1e】à【New】命令会弹出一个对话框,列出应用程序所支持的所有文档类型,供用户选择。选择一种文档类型后,自动创建相应类型的文档。
- 文件类型长名称:用于指定当应用程序作为OLE Automation服务器时使用的文档类型名,使用默认值。
添加应用程序所需的数据
- 第一步 打开项目工作区的文件视图,双击打开EditorDoc.h文件,如图10所示,并在该文件中,定义文档的数据成员,加入以下代码:
class CEditorDoc : public CDocument
{
……
public:
CStringList lines; // 链表CStringList来保存文本编辑器的数据
int nLineNum; // 用于指示当前编辑行行号
……
}
正在上传…重新上传取消
图10 打开EditorDoc.h文件
- 第二步 同上步,打开在EditorDoc.cpp文件,在CEditorDoc::OnNewDocument()成员函数加入初始化数据成员的代码:
BOOL EditorDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument()) return FALSE;
nLineNum=0;
POSITION pos; // pos指向链表当前元素。
pos=lines.GetHeadPosition(); // 返回链表头指针
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
}
lines.RemoveAll(); // 清除链表中的所有元素
return TRUE;
}
说明:语句((CString)lines.GetNext(pos)).Empty()的作用是:以当前pos为参数,返回下一个元素指针,同时修改pos,使它指向下一个元素。使用强制类型转换将GetNext()函数返回的元素指针转化为CString类型,然后调用Cstring::Empty()方法清除该行中的所有字符。通过一个while循环,清除所有文本行的数据。
一般地,类的数据成员的初始化都是在构造函数中完成的,但由于文档对象创建后,需要反复刷新而不是反复创建,因此文档类的数据成员初始化工作放在OnNewDocument成员函数中完成而不在构造函数中做这件事情。
- 第三步 用MFC ClassWizard为CEditorDoc类添加虚函数DeleteContents(),如图11所示。同时,增加下述代码:
void CEditorDoc::DeleteContents()
{
nLineNum=0;
POSITION pos;
pos=lines.GetHeadPosition();
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
}
lines.RemoveAll();
CDocument::DeleteContents();
}
正在上传…重新上传取消
图11 为CEditorDoc类添加虚函数
说明:在使用【File】à【Open】命令打开一个文档或关闭应用程序时,都需要清理文档对象中的数据。文档的清理是在文档的CDocument::DeleteContents()虚函数中完成的。DeleteContents()成员函数需要反复调用,它的功能是删除文档的数据,并确信一个文档在使用前为空。有读者可能想到在析构函数中清理文档数据,但析构函数只在文档对象结束时调用,用于清除那些在对象生存期都将存在的数据项,显然析构函数不能满足重复调用的要求。
处理键盘输入
- 第一步 在文档类的头文件EditorDoc.h中,定义视图类的数据成员,加入以下代码:
class CEditorView : public CView
{
……
int lHeight;
int cWidth;
……
}
- 第二步 用MFC ClassWizard为CEditorView类添加虚函数OnInitialUpdate(),并增加下列代码:
void CEditorView::OnInitialUpdate()
{
CDC *pDC=GetDC(); // 取得当前窗口的设备场境指针并存放在pDC中
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
CView::OnInitialUpdate();
}
说明:视图类一般在CView::OnInitialUpdate()成员函数来初始化视图类的数据成员。因为这时,视图窗口已经创建,马上开始更新,那么可能影响视图显示的数据一定要在这时初始化。
在以下情况下,应用程序将自动执行视图类的OnInitialUpdate()来初始化视图类数据成员:
-
- 调用CDocument::OnNewDocument时;
- 调用CDocument::OnOpenDocument时需要清除视图原有的显示内容。
TEXTMETRIC是一个数据结构,它包含字体的宽度、高度、字的前后空白等字段。调用CDC::GetTextMetrics()获取字体的TEXTMETRIC,从而取得字体的宽度和高度等信息。
- 第三步 用MFC ClassWizard为类CEditorView添加WM_CHAR的消息处理函数OnChar(),如图12所示。打开CEditorView::OnChar()函数进行编辑。修改后的OnChar函数如下:
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
CString line(""); // 存放编辑器当前行字符串
POSITION pos=NULL; // 字符串链表位置指示
if(nChar=='\r') // 若是回车,则增加一行
{
pDoc->nLineNum++;
}
else
{
// 按行号返回字符串链表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
// 没有找到该行号对应的行,因此它是一个空行,
// 把它加到字符串链表中
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else
{
// 当前文本行还没有换行结束,因此将文本加入到行末
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0, (int)pDoc->nLineNum*tm.tmHeight,
line,line.GetLength());
}
CView::OnChar(nChar, nRepCnt, nFlags);
}
正在上传…重新上传取消
图12 为CEditorView类添加消息处理函数
说明:编辑器要不断接收用户的键盘输入,就必须处理键盘消息。每按下一个字符,窗口就会接收到一个消息WM_CHAR。WM_CHAR消息是在视图类中处理的,对该消息的处理过程大致包括:读取用户输入的字符,如果输入是一个回车,则将总行数nLineNum加1,否则将输入字符加到当前行行末。最后调用TextOut函数输出当前编辑中的文本行。
- 第四步 修改CEditorView类的OnDraw()函数,编辑后该函数的代码如下:
void CEditorView::OnDraw(CDC* pDC)
{
CEditorDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
int y=0;
POSITION pos;
CString line;
if(!(pos=pDoc->lines.GetHeadPosition()))
{
return;
}
while(pos!=NULL)
{
line=pDoc->lines.GetNext(pos);
pDC->TextOut(0, y, line, line.GetLength());
y+=lHeight;
}
}
在OnDraw()函数中,首先调用GetDocument()函数,取得指向当前视图所对应的文档的指针。通过这个指针,来访问文档中的数据。以后在视图中修改文档中的数据,也是通过GetDocument()来取得文档指针,再通过该文档指针修改文档中的数据。
- 第五步 编译运行,结果如图13所示。
正在上传…重新上传取消
图13 Editor的运行结果
【例6-4-3】一个简单的文档序列化示例(Ex_SDIArchive)。
- 第一步 用MFC应用程序向导创建一个默认的单文档应用程序Ex_SDIArchive。
- 第二步 打开StringTable资源,将文档模板字符串资源IDR_MAINFRAME,该字符串资源实际上是由 \n字符分隔的7个字串,前两个 \n之间没有任何内容,所以文档才没有标题,我们可以在它们之间添加一个标题。如图16所示。
正在上传…重新上传取消
图16 文档模板字符串的设置
- 第三步 为CEx_SDIArchiveDoc类添加下列成员变量:
class CEx_SDIArchiveDoc : public CDocument
{
……
public:
CPoint points[100];
int m_index; // 表示数组中点的数目;
……
}
- 第四步 在CExSDIArchiveDoc::OnNewDocument函数中添加下列代码:m_index=0;
BOOL CEx_SDIArchiveDoc::OnNewDocument()
{
……
m_index=0;
return TRUE;
}
- 第五步 用MFC ClassWizard为类CEx_SDIArchiveView添加WM_LBUTTONDOWN的消息处理函数OnLButtonDown(),如图17所示。然后打开该函数进行编辑。修改后的OnLButtonDown函数如下:
void CEx_SDIArchiveView::OnLButtonDown(UINT nFlags, CPoint point)
{
CEx_SDIArchiveDoc* pDoc=GetDocument();
if(pDoc->m_index==100) return;
// 接受鼠标输入,将其添加到文档类
pDoc->points[pDoc->m_index]=point;
pDoc->m_index++;
pDoc->SetModifiedFlag(); // 设置文档修改标志
Invalidate(); // 更新客户区域
CView::OnLButtonDown(nFlags, point);
}
正在上传…重新上传取消
图17 为视图类添加WM_LBUTTONDOWN的消息处理函数
- 第六步 修改CEx_SDIArchiveView CEditorView类的OnDraw()函数,编辑后该函数的代码如下:
void CEx_SDIArchiveView::OnDraw(CDC* pDC)
{
……
int index;
index=pDoc->m_index;
for (int i=1;i<=index;i++)
{
pDC->Ellipse(pDoc->points[i].x-2,pDoc->points[i].y-2,
pDoc->points[i].x+2,pDoc->points[i].y+2);
}
}
- 第七步 用MFC ClassWizard为类CEx_SDIArchiveDoc添加Serialize虚函数,实现文档类中成员添加下列代码:
void CEx_SDIArchiveDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar<<m_index;
for (int i=0;i<=m_index;i++)
{
ar<<points[i].x<<points[i].y;
}
}
else
{
ar>>m_index;
for (int i=0;i<=m_index;i++)
{
ar>>points[i].x>>points[i].y;
}
}
}
- 第八步 编译运行并测试。结果如图18所示。
正在上传…重新上传取消
程序运行后,选择【文件】à【另存为】命令,指定一个文档名ab,然后选择【文件】à【新建】命令,再打开文档,结果就会弹出对话框,显示该文档的内容。
正在上传…重新上传取消
图18 Ex_SDIArchive的运行结果
【例6-4-4】一个简单的CArchive类用法的示例(Ex_Archive)。
- 第一步 用MFC 应用程序向导创建一个默认的基于对话框的应用程序Ex_Archive。
- 第二步 为新创建的工程添加一个新的.h文件, Person.h,单击【添加】按钮,如图19所示。在Person.h文件中输入下面的代码:
class CPerson : public CObject
{
DECLARE_SERIAL(CPerson)
public:
virtual void Serialize(CArchive& ar);
CPerson();
virtual ~CPerson();
UINT Load(void);
CString m_szName ;
UINT m_nAge;
};
正在上传…重新上传取消
图19 新建头文件
- 第三步 同上步,在项目中添加源文件Person.cp,在Person.cpp文件中输入下面的代码:
#include "stdafx.h"
#include "Person.h"
IMPLEMENT_SERIAL(CPerson, CObject, 1)
CPerson::CPerson()
{ }
CPerson::~CPerson()
{ }
void CPerson::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar<<m_szName<<m_nAge;
}
else
{
ar>>m_szName>>m_nAge;
}
}
UINT CPerson::Load()
{
CFile f;
char buf[512];
if( !f.Open( "person.dat", CFile::shareDenyWrite|CFile::modeRead) ) return 0;
else
{
CArchive ar( &f, CArchive::load, 512, buf );
Serialize(ar);
return 1;
}
}
说明:通过2、3两步建立了CPerson类,并且改类为可序列化的类,CPerson类的成员函数Load( )首先生成指向"person.dat"文件的CFile对象,然后构造读文件的CArchive对象ar,并从文件中读数据完成CPerson对象的初始化。
- 第四步 编辑IDD_EX_ARCHIVE_DIALOG对话框资源,如图20所示。
正在上传…重新上传取消
图20 对话框资源
设置图20控件的属性如表3示:
表3 添加的控件
控件 | ID | 标题 | 其他属性 |
编辑框(Name) 编辑框(Age) 按钮(保存) | IDC_EDIT_NAME IDC_EDIT_AGE IDC_SAVE | 保存 | 默认 默认 默认 |
- 第五步 打开MFC 类向导的成员变量页面,确定类名是CEx_ArchiveDlg,选中所需的控件ID号,双击鼠标。依次为下列控件添加成员变量,如下表4所示。
表4 控件变量
ID | 成员变量名 | 属性类型 | 变量类型 |
IDC_EDIT_AGE | m_nAge | Value | UINT |
IDC_EDIT_NAME | m_szName | Value | CString |
- 第六步 为类CEx_ArchiveDlg添加一个CPerson类的对象m_person成员。并在Ex_ArchiveDlg.h文件的头部添加以下语句:
#include "person.h"
- 第七步 修改CEx_ArchiveDlg的OnInitDialog()函数在该函数中添加从文件读取数据并初始化m_person的代码:
BOOL CEx_ArchiveDlg::OnInitDialog()
{
……
if(m_person.Load())
{
this->m_szName=this->m_person.m_szName;
this->m_nAge=this->m_person.m_nAge;
this->UpdateData(FALSE);
}
else
{
AfxMessageBox("load person data fails");
}
return TRUE;
}
- 第八步 用MFC ClassWizard为按钮IDC_SAVE添加BN_CLICKED的消息映射,并增加下列代码:
void CEx_ArchiveDlg::OnSave()
{
CFile f;
char buf[512];
if( !f.Open( "person.dat",CFile::modeCreate | CFile::modeWrite) )
{
exit( 1 );
}
CArchive ar( &f, CArchive::store, 512, buf );
UpdateData();
m_person.m_nAge=this->m_nAge;
m_person.m_szName=this->m_szName;
m_person.Serialize(ar);
}
- 第九步 编译运行并测试。
6 数据库编程
6.1 MFC ODBC连接数据库
ODBC是微软公司支持开放数据库服务体系的重要组成部分,它定义了一组规范,提供了一组对数据库访问的标准API,这些API是建立在标准化版本SQL(Structed Query Language,结构化查询语言)基础上的。ODBC位于应用程序和具体的DBMS之间,目的是能够使应用程序端不依赖于任何DBMS,与不同数据库的操作由对应的DBMS的ODBC驱动程序完成。
6.1.1 ODBC的构成
ODBC的结构如图所示。
正在上传…重新上传取消
使用ODBC的层次图
ODBC层由三个部件构成:
- ODBC管理器
ODBC管理器的主要任务是管理安装ODBC驱动程序,管理数据源。应用程序要访问数据库,首先必须在ODBC管理器中创建一个数据源。ODBC管理器根据数据源提供的数据库存储位置,类型及ODBC驱动程序信息,建立起ODBC与一个特定数据库之间的联系,接下来,程序中只需提供数据源名,ODBC就能连接相关的数据库。ODBC管理器位于系统控件面板中。
- 驱动程序管理器
驱动器管理器位于ODBC32.DLL,是ODBC中最重要的部件,应用程序通过ODBC API执行数据库操作。其实ODBC API不能直接操作数据库,需要通过驱动管理器调用特定的数据库的驱动程序,驱动程序在执行完相应操作后,再将结果通过驱动程序管理器返回。驱动器管理器支持一个应用程序同时访问多个DBMS中的数据。
- ODBC驱动程序
ODBC驱动程序以DLL文件形式出现,提供ODBC与数据库之间的接口。
6.1.2 MFC ODBC类
进行ODBC编程,有三个非常重要的元素:环境(Enviroment),连接(Connection)和语句(Statement),它们都是通过句柄来访问的。在MFC的类库中,CDatabase类封装了ODBC编程的连接句柄,CRecordset类封装了对ODBC编程的语句句柄,而环境句柄被保存在一个全局变量中,可以调用一个全局函数AfxGetHENV来获得当前被MFC使用的环境句柄。
此外CRecordView类负责记录集的用户界面,CFieldExchange负责CRedordset类与数据源的数据交换。
使用AppWizard生成应用程序框架过程中,只要选择了相应的数据库支持选项,你就能够很方便地获得一个数据库应用程序的框架.
- CDatabase类
CDatabase类的主要功能是建立与ODBC数据源的连接,连接句柄放在其数据成员m_hdbc中,并提供一个成员函数GetConnect()用于获取连接字符串。要建立与数据源的连接,首先创建一个CDatabase对象,再调用CDatabase类的Open()函数创建连接。Open()函数的原型定义如下:
virtul BOOL Open(LPCTSTR lpszDSN,
BOOL bExclusive=FALSE,
BOOL bReadOnly=FALSE,
LPCTSTR lpszConnect=”ODBC;”,
BOOL bUseCursorLib=TRUE);
其中:
lpszDSN指定数据源名,若lpszDSN的值为NULL时,在程序执行时会弹出数据源对话框,供用户选择一个数据源。
lpszConnect指定一个连接字符串,连接字符串中通常包括数据源名、用户ID、口令等信息,与特定的DBMS相关。
例如:
CDatabase db;
db.Open(NULL,FALSE,FALSE,“ODBC;DSN=HotelInfo;UID=SYSTEM;PWD=123456”);
从断开与一个数据源的连接,可以调用CDatabase类的成员函数Close()。
- CRecordset类
CRecordset类对象表示从数据源中抽取出来的一组记录集。CRecordset类封装了大量操作数据库的函数,支持查询,存取,更新数据库操作。
记录集主要分为两种类型:
- 快照(Snapshot)记录集
快照记录集相当于数据库的一张静态视图,一旦从数据库抽取出来,当别的用户更新记录的操作时不会改变记录集,只有调用Requry()函数重新查询数据,才能反映数据的变化。自身用户的添加记录操作重要调用Requry()函数重新查询数据,但快照集能反应自身用户的删除和修改操作。
- 动态(Dynaset)记录集
动态(Dynaset)记录集与快照记录集恰恰相反,是数据库的动态视图。当别的用户更新记录时,动态记录集能即时反映所作的修改。在一些实时系统中必须采用动态记录集,如火车标联网购票系统。但别的用户添加记录,也需要调用Requry()函数重新查询数据后才能反映出来。
CRecordset有六个重要的数据成员如下表所示.。
CRecordset 类的数据成员
数据成员 | 类型 | 说明 |
m_strFilter | CString | 筛选条件字符串 |
m_strSort | CString | 排序关键字字符串 |
m_pDatabase | CDatabase类指针 | 指向CDatabasec对象的指针 |
m_hstmt | HSTMT | ODBC语句句柄 |
m_nField | UINT | 记录集中字段数据成员总数 |
m_nParams | UINT | 记录集中参数数据成员总数 |
CRecordset的主要成员函数如下表所示:
CRecordset类的成员函数
成员函数 | 类 型 |
Move | 当前记录指针移动若干个位置 |
MoveFirst | 当前记录指针移动到记录集第一条记录 |
MoveLast | 当前记录指针移动到记录集最后一条记录 |
MoveNext | 当前记录指针移动到记录集下一条记录 |
MovePrev | 当前记录指针移动到记录集前一条记录 |
SetAbsolutePosition | 当前记录指针移动到记录集特定一条记录 |
AddNew | 添加一条新记录 |
Delete | 删除一条记录 |
Edit | 编辑一条记录 |
Update | 更新记录 |
CancelUpdate | 取消一条记录的更新操作 |
Requry | 重新查询数据源 |
GetDefaultConnect | 获得默认连接字符串 |
GetDefaultSQL | 获得默认SQL语句 |
DoFieldExchange | 记录集中字段数据成员与数据源中交换数据 |
GetRecordCount | 获得记录集记录个数 |
IsEOF | 判断当前记录指针是否在最后一个记录之后 |
IsBOF | 判断当前记录指针是否在第一个记录之前 |
CanUpdate | 判断记录集是否允许更新 |
- CRecordView类
CRecordView类是CFormView的派生类,支持以控件视图来显示当前记录,并提供移动记录的默认菜单和工具栏,用户可以通过记录视图方便地浏览、修改、删除和添加记录。记录视图与对话框一样使用DDX数据交换机制在视图中的控件的记录集成员之间交换数据,只需使用ClassWizard将控件与记录集的字段数据成员一一绑定。
CRecordView的主要函数如下表所示:
CRecordView类的主要成员函数
成员函数 | 类型 |
OnGetRecordset | 获得指向记录集的指针 |
OnMove | 当前记录指针移动时,OnMove()函数更新对当前记录所作的修改,这是将更新记录保存的方式。 |
IsOnFirstRecord | 判断当前记录是否为记录集的第一条记录 |
IsOnLastRecord | 判断当前记录是否为记录集的最后一条记录 |
- CFieldExchange类
CFieldExchange类支持记录字段数据的自动交换,实现记录集中字段数据成员与相应的数据源中字段之间的数据交换,类似于对话框数据自动交换机制。
6.2数据库应用程序的实现
6.2.1 创建并注册数据源
在创建数据库应用程序之前,先要准备好数据源。下面我们假设数据库应用程序要连接的数据库hotel.accdb存放在C盘根目录下,该数据库下有一张TblCustomer的表,如下图所示:
正在上传…重新上传取消
数据表“tblCustomer”
在Windows操作系统的控制面板中,可以找到数据源ODBC管理器的图标,如下图所示为Windows7中的ODBC的图标,它的位置在控制面板中的管理工具文件夹。由于所要连接的数据库是由Microsoft ACCESS创建,要求ODBC管理器中安装有Microsoft ACCESS的ODBC驱动程序。一般,只需安装了Microsoft ACCESS软件,相应的ODBC驱动程序就已经默认安装了。
正在上传…重新上传取消
ODBC图标
鼠标双击ODBC图标,弹出“ODBC数据源管理器”对话框,如下图:
正在上传…重新上传取消
ODBC数据源管理器
在用户DSN、系统DSN、文件DSN标签页中都可以创建一个数据源,但所创建的数据源的应用范围是不同的:
- 用户DSN: 用户数据源只对当前用户可见,而且只能用于当前机器上。
- 系统DSN:系统数据源对当前机器上的所有用户可见。
- 文件DSN:文件数据源可以由安装了相同驱动程序的用户共享。
可以根据所创建的数据源的不同的应用场合选择在不同的标签页下创建数据源,在本例中选择文件DSN。在标签页中的列表中显示的是在本机已创建的系统数据源的列表。
单击“添加”按钮,新建一个数据源,弹出“创建新数据源”对话框。如图所示,在ODBC驱动程序列表中选择“Microsoft Access Driver(*.mdb, *.accdb)”。
正在上传…重新上传取消
选择ODBC驱动程序类别
单击“下一步”按钮,
正在上传…重新上传取消
给保存的文件数据源起一个名字,单击“下一步”按钮,
正在上传…重新上传取消
单击“完成”按钮,弹出“ODBC Microsoft Access 安装”对话框,如图所示。单击“选择”按钮,
正在上传…重新上传取消
设置Microsoft Access数据源
弹出“选择数据库”对话框,选择数据库文件C:\hotel.accdb,连续单击“确定”按钮回到前一对话框。
正在上传…重新上传取消
选择数据库
正在上传…重新上传取消
最后在文件DSN标签中可以看到创建的数据源hotel.dsn出现在数据源列表中,如图
正在上传…重新上传取消
创建好的系统数据源
6.2.2创建数据库应用框架
〖例2-1〗使用MFC类向导可以方便地得到一个数据库应用程序的框架,创建一个MFC单文档EXE应用程序Exam2_1,在向导的[数据库支持]选项也中,选择单选项“不提供文件支持的数据库视图”,客户端类型选择 ODBC 如下图所示。
正在上传…重新上传取消
图9-9 设置数据库支持
单击“数据源…”按钮,弹出“选择数据源”对话框,
正在上传…重新上传取消
单击“新建”按钮,弹出跟上一节介绍的使用ODBC数据源管理器创建文件DSN一样的对话框,按照上面介绍的步骤新创建一个文件数据源,
正在上传…重新上传取消
创建完毕后文件数据源列表中会显示出新添加的数据域 hotel.dsn,单击“确定”按钮,再次弹出“ODBC Microsoft Access 安装”对话框,单击“确定”按钮
正在上传…重新上传取消
弹出“选择数据源对象”对话框,选择相应的数据库表,单击“确定”按钮,弹出“选择数据对象”对话框,列表框中列出了数据库中所包含的表和查询,选择应用程序所操作的表TblCustomer
正在上传…重新上传取消
选择数据库表
单击“确定”按钮,结束数据源的设置工作。
单击MFC应用程序向导对话框中的“完成”按钮,完成数据库应用程序框架的创建,编译运行这个程序,运行结果如下图所示。应用程序包含了数据库记录基本操作菜单和工具按钮,视图是一个对话框,可以添加控件。
正在上传…重新上传取消
数据库应用程序框架运行效果
选择工作区的ClassView,展开类树,进一步观察AppWizard自动添加的与数据库支持有关的内容。
增加了一个CExam2_1Set类,该类代表从数据库中选择的一组记录集。程序可以选择一个表作为一个记录集,本例选择了表tblCustomer中的记录构建记录集,也可以选择一个查询的结果集作为一个记录集。
如程序清单2-1所示。CExam2_1Set构造函数用于创建一个记录集对象,并把一个CDatabase对象的指针作为参数传递给构造函数,以便获得已由CDatabase对象建立起来的与数据源的连接。
CExam2_1Set类的成员函数GetDefaultConnect()用于获得定义了数据源类型和数据源名的连接字符串。GetDefaulltSQL()函数中定义了定义SQL语句的字符串,本例的SQL语句定义了查询一张表的完整记录。
CExam2_1Set类中定义了与数据源表的字段相对应的数据成员,成员函数DoFieldExchange()完成记录集上的字段数据成员与数据源上当前记录对应列之间数据的自动交换。
程序清单2-1:CExam2_1Set类 |
//CExam2_1Set.h class CExam2_1Set : public CRecordset { public: CExam2_1Set(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CExam2_1Set) int m_CustomerID; CStringW m_LastName; CStringW m_FirstName; CStringW m_HomeCountry; CStringW m_HomeState; CStringW m_PhoneNumber; CStringW m_Comments; // Overrides public: virtual CString GetDefaultConnect();//Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX);// RFX support // Implementation #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; //CExam2_1Set.cpp CExam2_1Set::CExam2_1Set(CDatabase* pdb) : CRecordset(pdb) { m_CustomerID = 0; m_LastName = L""; m_FirstName = L""; m_HomeCountry = L""; m_HomeState = L""; m_PhoneNumber = L""; m_Comments = L""; m_nFields = 7; m_nDefaultType = dynaset; } CString CExam2_1Set::GetDefaultConnect() { return _T("DBQ=C:\\USERS\\KEVIN\\Documents\\hotel.mdb; DefaultDir=C:\\USERS\\KEVIN\\Documents; Driver={Driver do Microsoft Access (*.mdb)}; DriverId=25;FIL=MS Access; FILEDSN=C:\\Users\\Kevin\\Documents\\hotel.dsn; MaxBufferSize=2048; MaxScanRows=8;PageTimeout=5;SafeTransactions=0; Threads=3;UID=admin;UserCommitSync=Yes;"); } CString CExam2_1Set::GetDefaultSQL() { return _T("[tblCustomer]"); } void CExam2_1Set::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); // RFX_Text() 和 RFX_Int() 这类宏依赖的是 // 成员变量的类型,而不是数据库字段的类型。 // ODBC 尝试自动将列值转换为所请求的类型 RFX_Int(pFX, _T("[CustomerID]"), m_CustomerID); RFX_Text(pFX, _T("[LastName]"), m_LastName); RFX_Text(pFX, _T("[FirstName]"), m_FirstName); RFX_Text(pFX, _T("[HomeCountry]"), m_HomeCountry); RFX_Text(pFX, _T("[HomeState]"), m_HomeState); RFX_Text(pFX, _T("[PhoneNumber]"), m_PhoneNumber); RFX_Text(pFX, _T("[Comments]"), m_Comments); } |
视图类CExam2_1View 是CRecordView的派生类,CRecordView是记录视图,支持在控件中显示数据库记录。默认提供了移动记录功能的实现(第一个,上一个,下一个,最后一个)。定义了一个指向记录集的指针m_pSet。
CRecordView类是CFormView类的派生类,CFormView类的视图对应一个对话框资源,所以CRecordView类从基类中继承的成员函数中最重要的是DoDataExchange()函数和UpdateData()函数,DoDataExchange()函数实现记录集的字段与视图中控件之间的自动数据交换,UpdateData()函数实现记录集的字段与视图中控件之间实时交换。
6.2.3 设计记录操作界面
〖例2-2〗打开资源管理器的 Dialog文件夹,选择IDD_EXAM2_1_FORM,在对话框中按下图添加静态控件和编辑框控件,设置ID编辑框的属性为只读。
正在上传…重新上传取消
记录操作界面
并按下表所示,修改编辑框控件的ID属性。
记录操作界面控件属性
控件ID | 控件类型 | 标题 |
静态文本控件 | ID | |
IDC_CustomerID | 编辑框控件 | |
静态文本控件 | 名 | |
IDC_LastName | 编辑框控件 | |
静态文本控件 | 姓 | |
IDC_FirstName | 编辑框控件 | |
静态文本控件 | 国家 | |
IDC_HomeCountry | 编辑框控件 | |
静态文本控件 | 电话 | |
IDC_PhoneCall | 编辑框控件 | |
静态文本控件 | 备注 | |
IDC_Comments | 编辑框控件 |
接下来,要将编辑框控件与一个记录集字段数据成员绑定,打开CExam_1View.cpp文件在DoDataExchange函数中添加控件“连接”到数据库字段的代码:
void CExam2_1View::DoDataExchange(CDataExchange* pDX)
{
CRecordView::DoDataExchange(pDX);
// 可以在此处插入 DDX_Field* 函数以将控件“连接”到数据库字段,例如
// DDX_FieldText(pDX, IDC_MYEDITBOX, m_pSet->m_szColumn1, m_pSet);
// DDX_FieldCheck(pDX, IDC_MYCHECKBOX, m_pSet->m_bColumn2, m_pSet);
// 有关详细信息,请参阅 MSDN 和 ODBC 示例
DDX_FieldText(pDX, IDC_CUSTOMERID, m_pSet->m_CustomerID, m_pSet);
DDX_FieldText(pDX, IDC_FIRSTNAME, m_pSet->m_FirstName, m_pSet);
DDX_FieldText(pDX, IDC_LASTNAME, m_pSet->m_LastName, m_pSet);
DDX_FieldText(pDX, IDC_HOMECOUNTRY, m_pSet->m_HomeCountry, m_pSet);
DDX_FieldText(pDX, IDC_PHONECALL, m_pSet->m_PhoneNumber, m_pSet);
DDX_FieldText(pDX, IDC_COMMENTS, m_pSet->m_Comments, m_pSet);
}
重新运行程序,运行结果如图所示。使用移动记录的四个工具按钮,前后浏览每一条记录,当移动到第一条记录时,“第一条”和“上一条”按钮变灰,当移动到最后一条记录时,“最后一条”和“下一条”按钮变灰。
在浏览记录的过程中,你可以修改各个编辑框中的内容,紧接着作一次移动记录操作,所作的修改就能被保存到数据库中。
正在上传…重新上传取消
增加了浏览功能后的应用程序
6.2.4 更新记录
更新记录操作包括修改,添加和删除记录,CRecordSet类提供了AddNew()、Delete()、Edit()、Update()、CancelUpdate()、Requery()等成员函数用于更新记录。
AddNew()函数用于添加一个新的空记录,所有字段数据成员的值都为NULL。Delete()函数用于删除当前记录,Edit()函数用于修改当前记录各字段数据成员的值。Update()函数用于AddNew和Edit操作后的数据的最后保存,CancelUpdate()函数用于取消任何由AddNew和Edit操作产生的待处理的更新。Requery()函数用于重新执行对记录集的查询,当记录集类型是快照型时,快照不反映用户添加的记录,这时需要调用该函数重新查询更新后的记录集。
下面在Exam2_1中增加添加新记录和删除记录的功能。
〖例2-3〗在“record”记录下添加三个菜单项如下图图9-17所示。一个菜单项是分割线,另外两个菜单项分别是“增加记录”和“删除记录”。菜单ID设置为ID_RECORD_ADD和ID_RECORD_DELETE。
正在上传…重新上传取消
增加菜单项
在菜单项上使用右键菜单在视图类中为菜单项ID_RECORD_ADD和ID_RECORD_DELETE映射COMMAND消息处理函数, 得到成员函数OnRecordAdd()和OnRecordDelete()。
正在上传…重新上传取消
正在上传…重新上传取消
添加CExam2_1View的BOOL类数据成员m_addflg,用以记录是否进入添加模式,当m_addflg的值为true时,进入添加模式。在CExam2_1View的构造函数中初始化m_addflg的值为false。
为成员函数OnRecordAdd()添加代码,增加一条空记录,并清除ID编辑框的只读属性。实现代码如程序清单2-2所示。
程序清单2-2:Add Record 菜单消息处理函数 |
void CExam2_1View::OnRecordAdd() { // TODO: Add your command handler code here m_pSet->AddNew(); //进入添加模式 m_addflg=true; //设置添加模式标志 CEdit* m_pCtrl=(CEdit*)GetDlgItem(IDC_CustomerID); m_pCtrl->SetReadOnly(false); //清除ID编辑框的只读属性 UpdateData(false); //用新记录的字段数据成员值更新控件显示 } |
使用ClassWizard添加CExam2_1View类的虚函数OnMove()函数,并在OnMove()函数中添加代码,通过移动记录将添加的新记录保存到表中。实现代码如程序清单2-3所示
程序清单2-3:OnMove()函数 |
BOOL CExam2_1View::OnMove(UINT nIDMoveCommand) { // TODO: Add your specialized code here and/or call the base class //添加模式处理 if (m_addflag) { m_addflag = false; //使用控件值更新记录集字段数据成员 UpdateData(true); //将记录集更新保存到表中 if (m_pSet->CanUpdate()) { m_pSet->Update(); } //重新查询记录集 m_pSet->Requery(); //以更新后的记录集数据成员更新控件显示 UpdateData(false); CEdit* m_pCtrl = (CEdit*)GetDlgItem(IDC_CUSTOMERID); //设置ID编辑框为只读 m_pCtrl->SetReadOnly(true); return true; } else { return CRecordView::OnMove(nIDMoveCommand); } } |
为成员函数OnRecordDelete()添加代码,删除当前记录,实现代码如程序清单2-4所示。
程序清单2-4:Delete Record菜单处理函数 |
void CExam2_1View::OnRecordDelete() { // TODO: Add your command handler code here m_pSet->Delete();//删除当前记录 m_pSet->MoveNext();//移到下一记录 if (m_pSet->IsEOF())//删除记录为最后一条记录处理 { m_pSet->MoveLast(); } if (m_pSet->IsBOF())//删空记录集处理 { m_pSet->SetFieldNull(NULL); } UpdateData(false);//更新控件显示 } |
6.2.5 排序和筛选
CRecordView类有两个重要的数据成员m_strFilter和m_strSort,m_strFilter是用于表示筛选记录的条件字符串,m_strSort是用于表示排序的关键字的字符串。只要对这两个数据成员赋值,只能实现排序和筛选。
〖例2-3〗首先在应用程序Exam2_1中建立两类排序,每一类是按ID号排序,另一类是按HomeCountry排序。
在“查看”菜单下添加三个菜单项:一条分隔线、“按ID排序”和“按国家排序”,菜单项ID设置为ID_VIEW_SORT_ID和ID_VIEW_SORT_COUNTRY。使用ClassWizard为两个菜单项在视图类中映射COMMAND消息处理函数得到,添加代码如程序清单2-5所示,实现排序。
程序清单2-5:实现排序的函数 |
void CExam2_1View::OnViewSortId() { // TODO: Add your command handler code here m_pSet->m_strSort="CustomerID";// 定义排序关键字按ID排序 m_pSet->Requery() ;// 重新查询 UpdateData(false); //更新控件显示 } void CExam2_1View::OnViewSortCountry() { // TODO: Add your command handler code here m_pSet->m_strSort="HomeCountry"; m_pSet->Requery() ; UpdateData(false); } |
接着添加筛选条件,在一个对话框中输入一个国别,则只显示属于该国别的顾客记录。步骤如下:
- 创建一个对话框,并添加控件,如下图所示。设置编辑框的ID为IDC_FILTER.。为该对话对话框添加一个新的对话框类CFilterDlg并使用ClassWizard为编辑框IDC_FILTER在对话框类CFilterDlg中添加一个Ctring类型的成员变量m_Filter。
正在上传…重新上传取消
筛选对话框
- 在“查看”菜单下添加一个新的菜单项“筛选”,菜单项ID设为ID_VIEW_FLITER,使用ClassWizard在CExam2_1View类中为菜单项ID_VIEW_FILTER映射菜单处理函数,得到函数OnViewFilter()。
- 在CExam2_1View类的实现文件中使用include命令包含“FilterDlg.h”文件,并在函数OnViewFilter()中添加代码,调用筛选对话框,并按对话框返回的字符串设置数据成员m_strFilter的值,实现筛选。若对话框返回空串,显示整个记录集。
程序清单2-6:实现筛选的函数 |
void CExam2_1View::OnViewFilter() { // TODO: Add your command handler code here CFilterDlg dlg; CString str; if (dlg.DoModal() == IDOK)//调用筛选对话框,按OK按钮返回 { if (dlg.m_Filter.IsEmpty())//编辑框为空,显示整个记录集 { str = ""; } else { //定义筛选字符串 str = "HomeCountry='" + dlg.m_Filter + "'"; } m_pSet->m_strFilter = str; m_pSet->Requery(); //重新查询记录集 UpdateData(false); //更新控件显示 } } |
6.3 直接访问MySql API 连接 mysql
- 第一步: 安装MySql
- 第二步: 在工程文件中添加mysql 头文件
- #include mysql.h
- 第三步: 在VS属性中做如下配置:
- 附加包含头文件mysql.h的目录,即安装目录下的include目录。
例如:C:\Program Files\MySQL\MySQL Server 5.6\include
-
- 附加依赖项,名称为libmysql.lib
#pragma comment( lib, "libmysql.lib");
此句话和在附加依赖项中增加libmysql.lib 的功能一样
- 第四步: 将libmysql.lib 和 libmysql.dll拷贝到工程目录下
这两个文件可以从mysql安装目录下的lib目录中拷贝