第一章 Windows程序内部运行机制

1.1 API和SDK

  • API:Windows操作系统提供给应用程序编程的接口。
  • SDK(软件开发包):用于开发的所有资源的集合。

1.2 窗口和句柄

  • 窗口
  • 句柄:系统在创建资源时会为他们分配内存,并返回这些资源的标识号,即句柄。(类似于指针)
    窗口句柄(HWND)、图标(HICON)、光标(HCURSOR)、画刷(HBRUSH)

1.3 消息和队列

消息(先进先出)

1
2
3
4
5
6
7
8
typedef struct tagMSG {
HWND hwnd; //HWND:窗口变量,hwnd:消息所属的窗口
UINT message; //message消息标识符,数值定义为WM_XXX (P4)
WPARAM wParam;
LPARAM lParam; //指定消息的附加信息,ASCII等
DWORD time; //消息投递到消息队列中的时间
POINT pt; //当前鼠标的位置
} MSG, *PMSG;

1.4 WinMain

  • Win32实现的步骤
    1. 定义WinMain函数;
    2. 创建窗口;
    3. 消息循环;
    4. 窗口过程函数。

1.4.1 WinMain函数的定义

winmain函数是程序入口函数,由系统调用。

1
2
3
4
5
6
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance,always NULL in Win32
LPSTR lpCmdLine, // 空终止的字符串,指定传递给应用程序的命令行参数
int nCmdShow // 指定窗口应该如何显示
);

1.4.2 窗口的创建

  • 步骤
    1. 设计窗口类;
    2. 注册窗口;
    3. 创建窗口;
    4. 显示及更新窗口;

1.4.2.1 设计窗口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _WNDCLASS                //窗口类
{
UINT style; /*(P7知识点方框)窗口样式;
CS_XXX(都只有一位是1,且1位各不相同:位标志);
多种特点组合用(|)号;
去掉style中的样式用(&~)*/
WNDPROC lpfnWndProc; //指向回调函数的指针;在特定的事件发生时,用于对该事件的响应
int cbClsExtra;
int cbWndExtra; //cbClsExtra和cbWndExtra两个附加内存,一般都为0
HINSTANCE hInstance; //实例句柄
HICON hIcon; //图标句柄;HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);
HCURSOR hCursor; //光标句柄;HCURSOR LoadCursor(HINSTANCE hInstance,LPCTSTR lpCursorName);
HBRUSH hbrBackground; //画刷句柄,背景颜色;wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
LPCTSTR lpszMenuName; //菜单名字,NULL
LPCTSTR lpszClassName; //类名,和创建窗口CreateWindow函数中的lpClassName一致
} WNDCLASS, *PWNDCLASS;

1.4.2.2 注册窗口

1
ATOM RegisterClass(  CONST WNDCLASS *lpWndClass ); // 注册窗口

1.4.2.3 创建窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HWND CreateWindow(  
LPCTSTR lpClassName, // registered class name,与窗口类WNDCLASS中的lpszClassName一致
LPCTSTR lpWindowName, // window name,
DWORD dwStyle, /*window style;和WNDCLASS中的style不同,style是指定具体窗口的样式,dwstyle是窗口都具有的样式
常用WS_OVERLAPPEDWINDOW*/
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height;
/*如果x设为CW_USEDEFAULT,系统为窗口选择左上角并忽略y;
nWidth设为CW_USEDEFAULT,系统为默认大小并忽略nHeight。
窗口之间的父子关系,子窗口必须有WS_CHILD*/
HWND hWndParent, // handle to parent or owner window(父窗口句柄);NULL
HMENU hMenu, // menu handle or child identifier;NULL
HINSTANCE hInstance, // handle to application instance;hInstance
LPVOID lpParam // window-creation data;NULL
);

注意:创建窗口成功,函数返回系统为该窗口分配的句柄,否则返回NULL。创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。

1.4.2.4 显示及更新窗口

显示窗口

1
2
3
4
BOOL ShowWindow(
HWND hWnd, // 哪一个窗口?
int nCmdShow // 如何显示?;常用SW_SHOWNORMAL
);

更新窗口

1
2
3
BOOL UpdateWindow(
HWND hWnd // handle to window
);

1.4.3 消息循环

1
2
3
4
5
6
BOOL GetMessage(
LPMSG lpMsg, // 指向一个消息结构体MSG,GetMessage取出的消息放入该结构体对象中
HWND hWnd, // handle to window,设为NULL接收所用窗口
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message;范围过滤,获得所有消息则设为0
);

注意:GetMessage函数除了接收到WM_QUIT(退出消息)外都返回非零值,出错返回-1。

通常编写的消息循环代码如下:

1
2
3
4
5
6
MSG msg;//声明消息结构体变量
while(GetMessage(&msg,NULL,0,0))//没接收到WM_QUIT时一致循环
{
TranslateMessage(&msg);//将WM_KEYDOWN和WM_KEYUP转化为WM_CHAR,不修改原消息
DispatchMessage(&msg);//将消息发送至操作系统,后者用窗口过程函数对消息响应; Dispatch:派遣
}

IMG_20210623_120844.jpg

1.4.2窗口创建和1.4.3消息循环都包含在WinMain函数体内

1.4.4 窗口过程函数

即回调函数

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // 窗口句柄
UINT uMsg, // 消息代码
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
  1. 过程函数名WindowProc可以换,但要和声明保持一致。
  2. 使用switch/case来对不同消息作出不同反应。

如下方例子中的过程函数所示。

课后程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#define _CRT_SECURE_NO_DEPRECATE
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
#include <stdexcept>

/*******************函数声明****************************/
LRESULT CALLBACK WinSunProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);

/*******************WinMain函数*************************/
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // command line
int nCmdShow // show state
)
{
/*设计一个窗口类*/
WNDCLASS wndcls;
wndcls.cbClsExtra = 0;
wndcls.cbWndExtra = 0;
wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndcls.hCursor = LoadCursor(NULL, IDC_CROSS);
wndcls.hIcon = LoadIcon(NULL, IDI_ERROR);
wndcls.hInstance = hInstance;
wndcls.lpfnWndProc = WinSunProc;
wndcls.lpszClassName="hezexian2021";
wndcls.lpszMenuName = NULL;
wndcls.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wndcls);

/*创建窗口*/
HWND hwnd;
hwnd = CreateWindow("hezexian2021", "He Zexian's home.", WS_OVERLAPPEDWINDOW, 0, 0, 600, 400, NULL, NULL, hInstance, NULL);

/*显示及刷新窗口*/
ShowWindow(hwnd, SW_SHOWNORMAL);
UpdateWindow(hwnd);

/*消息循环*/
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);//将WM_KEYDOWN和WM_KEYUP转化为WM_CHAR,不修改原消息
DispatchMessage(&msg);//将消息发送至操作系统,后者用窗口过程函数对消息响应
}
return msg.wParam;

}

/*******************窗口过程函数*************************/
LRESULT CALLBACK WinSunProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
switch (uMsg)
{
case WM_CHAR:
char szChar[20];
sprintf(szChar, "char code is %d", wParam);
MessageBox(hwnd, szChar, "char", 0);
break;

case WM_LBUTTONDOWN:
MessageBox(hwnd, "mouse clicked", "message", 0);
HDC hdc; //要在窗口中输出文字或显示图形,要用到设备描述表(DC)。DC是一个包含设备信息的结构体,所有的图形操作都是利用DC来完成。
//定义类型为HDC的变量hdc
hdc = GetDC(hwnd); //GetDC返回与特定窗口相关联的DC句柄
TextOut(hdc, 0, 50, "何泽贤之家",strlen("何泽贤之家"));
ReleaseDC(hwnd, hdc); //在使用完GetDC后一定要注意释放
break;

case WM_PAINT://窗口重绘
HDC hDC;
PAINTSTRUCT ps;
hDC = BeginPaint(hwnd, &ps);
TextOut(hDC, 0, 0, "http://www.hezexian.org", strlen("http://www.hezexian.org"));
EndPaint(hwnd, &ps);//WM_PAINT、BeginPaint、EndPaint一起用,而不能用GetDC
break;

case WM_CLOSE://关闭消息
if (IDYES == MessageBox(hwnd, "是否真的结束了?", "message", MB_YESNO))
{
DestroyWindow(hwnd);//销毁窗口
}
break;

//此时窗口销毁,但程序还在后台运行

case WM_DESTROY:
PostQuitMessage(0);//在响应消息后,投递一个退出的消息使用程序安全退出
break;

default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);//调用缺省的消息处理过程函数,对没有处理的其他消息以默认处理

}
return 0;
}

补充:
1、如遇到cannot convert from ‘const char [7]’ to ‘LPCWSTR’的错误,请对工程进行字符设置菜单栏-<项目-<xxx 属性-<配置属性-<高级-<字符集,改成“not set”或改成“Use Multi-Byte Character Set”。
2、vs2019—Windows桌面应用程序

参考文献

[1] VC深入详解 孙鑫 第一章 Windows程序内部运行机制 - focusahaha - 博客园 (cnblogs.com).https://www.cnblogs.com/focusahaha/p/12601081.html
[2]孙鑫.VC深度详解修订版[M]. 北京:电子工业出版社, 2012. 1-26.


第二章 掌握C++

2.1 从结构到类

2.1.1 结构体的定义

  1. C++相比于C的特性:封装性、继承性、多态性;
  2. 对象具有状态和行为,状态保存在成员变量中,行为通过函数实现;
  3. 标准输入输出流对象:cin(>>)默认键盘 和 cout(<<)、cerr(<<)默认显示器;自动根据数据类型调整输入输出格式;
  4. 结构体中的函数称为成员函数。

程序2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std; //error C1083: 无法打开包括文件:“iostream.h”: No such file or directory

struct point
{
int x, y; //状态
void output() //行为
{
cout << "x="<< x << endl << "y=" << y << endl;//endl是换行符
}
}pt;//注意分号

int main()
{
cout << "请输入x和y的值:" << endl;
cin >> pt.x;
cin >> pt.y;
pt.output(); //注意括号
return 0;
}

2.1.2 结构体与类

在C++中,结构体(struct)和类(class)可以通用。
区别在于访问控制的差异:

  1. struct默认访问控制标识符public;public:可以在类的外部进行访问
  2. class默认访问控制标识符private;private:只能在类的内部进行访问
  3. protective。

程序2.2:将程序2.1中的struct point修改为class point

1
2
3
4
5
6
7
8
9
class point   //类:抽象出一些事物共有的属性
{
public:
int x, y;
void output()
{
cout << "x="<< x << endl << "y=" << y << endl;
}
}pt; //实例化了一个对象==类的实例化:具有具体的属性值

2.2 C++的特性

2.2.1 类与对象

类:抽象出一些事物共有的属性;
对象:有具体的属性值。

2.2.2 构造函数

作用:在定义对象的同时,对成员变量进行初始化;
创建对象本身(分配内存)
规定:构造函数的名字和类名相同(唯一性);
没用返回值;
可以有参数。
注意:

  1. 如果一个类没有定义任何构造函数,那么C会提供一个默认的不带参构造函数。而只要类中定义了一个构造函数,C便不再提供任何其他构造函数;
  2. 每个类必须有一个构造函数,没有构造函数不能创建任何对象。构造函数如代码2.3所示。

2.2.3 析构函数

~类名(); //对象生命周期结束,释放其占用资源;是一个对象最后调用的成员函数
注意:析构函数不允许有返回值,更不允许有参数,并且一个类中只有一个构造函数。
代码2.3:构造函数和析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class point       //类名:point
{
public:
int x , y;
point() //构造函数 point()
{
x = 0; //在类中定义成员变量时,不能直接给其赋值(如int x=0;),而是在构造函数中赋值(P37提示)
y = 0;
}
~point() //析构函数 ~point()
{
}
};

2.2.4 函数的重载

条件:函数参数类型、个数不同才能构成重载。
注意:

  1. 只有函数的返回值类型不同,不能重载;
  2. 重载时,注意函数带有默认参数的情况。(P38)
  3. 对比覆盖:重载是发生在同一个类当中;覆盖是发生在父类和子类之间

代码2.4 函数的重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class point
{
public:
int x , y;
point() //函数1
{
x = 0;
y = 0;
}
point(int a, int b) //重载,函数2
{
x = a;
y = b;
}
};

void main()
{
point pt(5,5); //C++根据参数类型和个数,执行函数2
}

2.2.5 this指针

  1. this指针是一个隐含的指针,指向对象本身,代表对象的地址。
  2. 用法:当形参的变量名和成员变量的变量名冲突时,可用this->来区别对哪一个变量进行赋值。(P40)

this->x是对成员变量进行赋值;
x是对形参赋值。

2.2.6 类的继承

2.2.6.1 继承

例如:class fish : public animal{};

  1. animal是父类,fish是子类。子类fish以public(公有)的方式继承父类animal。
  2. 子类除了自己的成员变量和成员方法外,还可以继承父类的成员变量和成员方法。
  3. 构造函数和析构函数的调用次序:

image.png

2.2.6.2 在子类中调用父类带参数的构造函数

例如:父类构造函数: animal(int h , int w){}
则在构造子类时,应该显式地去调用父类的带参数构造函数: fish():animal(400,300){}

2.2.6.3 类的继承及类中成员的访问特性

3种访问权限修饰符:
public:定义的成员可以在任何地方被访问;
protected:定义的成员只能在该类及其子类中访问;
private:定义的成员只能在该类自身中访问。==>不能被子类继承

3种继承方式:

QQ图片20210627115117.jpg

2.2.6.4 多重继承

定义形式:class B: public C , public D
了解父类表顺序对调用构造函数和析构函数的影响。对于上面的例子,先构造C再构造D,先析构D再析构C。

2.2.7 虚函数与多态性、纯虚函数

2.2.7.1虚函数与多态性

程序 2.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(P49)...
class animal
{
public:
...
void breathe(){cout<<"animal breathe"<<endl;} /*注意此时不是虚函数*/
};

class fish : public animal
{
public:
...
void breathe(){cout<<"fish bubble"<<endl;}
}
void breathetest(animal* pan)
{
pan->breathe();
}

void main()
{
animal* pan; //指向animal类的指针pan
fish fi;
pan = &fi; //将fish类的对象fi的地址直接赋给指向animal类的指针变量pan
breathetest(pan);
}

/*输出animal breathe*/

1、fish对象也是一个animal类,C++自动进行类型转换;反之,不能把animal对象看成fish对象。

2、当我们将fish转化为animal时,该对象就会被认为是原对象内存模型的上半部分。

IMG_20210627_153537.jpg

3、将父类函数修改为虚函数,输出结果为fish bubble。

1
2
3
4
(P49)...
virtual void breathe() //虚函数
...
/*输出fish bubble*/

虚函数:用virtual关键字申明的函数—>多态性;
迟邦定:编译时并不确定具体调用的函数,而是在运行时依据对象的类型来确认调用的是哪一个函数。

概括(用法):在父类的函数前加上virtual关键字,在子类重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

2.2.7.2 纯虚函数

  1. 纯虚函数不被具体实现,是抽象类,不能实例化对象。
  2. 纯虚函数可以让类先具有一个操作名称,而没有操作内容,在子类继承时再去具体定义。

写法:

1
virtual void breathe() = 0 ;    //1、等于0;2、无函数体

注意:子类如果有对父类虚函数的覆盖定义,无论该覆盖定义是否有virtual,都是虚函数。

2.2.8函数的覆盖和隐藏

2.2.8.1 覆盖

1、覆盖(P52):发生在父类和子类之间 的 函数完全一样(函数名、参数列表),编译器根据实际类型确定要调用的函数。
2、对比重载:重载是发生在同一个类当中;覆盖是发生在父类和子类之间

程序2.6 函数的覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class animal
{
public:
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};

class fish : public animal
{
public:
void breathe() //覆盖
{
cout<<"fish bubble"<<endl;
}
};

void main()
{
fish fi;
fi.breathe();
}
/*输出fish bubble*/

2.2.8.2 隐藏

两种父类函数隐藏的情况:

  1. 子类和父类函数完全相同(函数名、参数列表),但父类函数没有virtual,则父类函数被隐藏。
  2. 子类和父类函数同名,但参数列表不同,则父类函数被隐藏。

区别隐藏与覆盖:覆盖是发生在父类与子类之间,两个函数必须完全相同且都是虚函数。否则就是隐藏。

2.2.9 引用

定义形式:

1
2
int a;
int &b = a; //引用必须在申明时就初始化,与a绑定;后面不会再与其他变量绑定

注意:

  1. 引用只是一个别名,不占用内存空间;此处要和指针区分开来。
  2. 引用多用于函数的形参定义。如程序2.4所示。

程序2.7 引用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std; //换成引用意思表达更清晰:

/*以下为用指针交换数据*/
void change(int* a,int* b); //void change(int& a,int& b);

int main()
{
int x = 5, y = 3;
cout << "x=" << x << endl;
cout << "y=" << y << endl;
change(&x,&y); //是对x和y的地址互换还是对x和y互换? change(x,y);
cout << "x=" << x << endl;
cout << "y=" << y << endl;
return 0;
}

void change(int* a, int* b) //void change(int a,int b)
{ //{
int c; // int c;
c = *a; // c = a;
*a = *b; // a = b;
*b = c; // b = c;
} //}

2.2.10 C++类设计习惯和头文件重复包含问题的解决

1、头文件存放类的定义及类成员函数的声明;
源文件存放类中成员函数的实现。

2、注意文件包含;
#include "animal.h" //"",从当前目录开始搜索,然后是系统目录和PATH环境变量所列出的目录
#include <iostream> //<>,从系统目录开始搜索,然后是PATH环境变量所列出的目录。``不搜索当前目录
注意子类的头文件中也要包含父类的头文件,如fish.h中的#include “animal.h”。

3、::是作用域表示符,表明函数或数据成员属于哪个类;
前面如果不跟类名,表示全局函数(即非成员函数)或全局数据。

4、在头文件中声明函数时用了virtual,在源文件中就不用写virtual。

5、要解决类重复定义的问题,要使用条件预处理指令(如课后程序animal.h和fish.h所示)

程序2.8 条件预处理指令解决类重复定义的问题

1
2
3
4
5
6
7
8
9
#ifndef ANIMAL_H_H     //如果没有定义ANIMAL_H_H,就定义ANIMAL_H_H,接着申明animal类
//如果定义过了ANIMAL_H_H,直接跳至endif
#define ANIMAL_H_H

class animal
{
};

#endif

2.2.11 VC++程序编译连接的原理与过程

1、编译:头文件不参与编译
源文件单独编译
XXX.obj目标文件
2、链接

编译链接过程如下。

IMG_20210627_171323.jpg

课后程序

animal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*animal.h*/

#ifndef ANIMAL_H_H //如果没有定义ANIMAL_H_H,就定义ANIMAL_H_H,接着申明animal类
//如果定义过了ANIMAL_H_H,直接跳至endif
#define ANIMAL_H_H

class animal
{
public:
animal();
~animal();
void eat();
void sleep();
virtual void breathe();
};

#endif

animal.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*animal.cpp*/

#include <iostream>
#include "animal.h"

using namespace std;

animal::animal()
{
}

animal::~animal()
{
}

void animal::eat()
{
}

void animal::sleep()
{
}

void animal::breathe() //在头文件中有virtual,则在源文件中不用加virtual
{
cout << "animal breathe" << endl;
}

fish.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*fish.h*/

#include "animal.h" //fish类从animal类继承

#ifndef FISH_H_H

#define FISH_H_H
class fish :public animal //结构体、类名后面没有括号()
{
public:
void breathe();
};

#endif

fish.cpp

1
2
3
4
5
6
7
8
9
10
11
/*fish.cpp*/

#include <iostream>
#include "fish.h"

using namespace std;

void fish::breathe()
{
cout << "fish bubble" << endl;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*main.cpp*/

#include "animal.h"
#include "fish.h"

void breathetest(animal* pan)
{
pan->breathe();
}

int main()
{
animal* pan;
fish fi;
animal an;
pan = &fi;
breathetest(pan);
pan = &an;
breathetest(pan);
return 0;
}

运行结果

image(1).png

问题及反思

vs2019编程出现的问题:
1、建立C++空项目后,要在右侧工程的源文件的文件夹下创建XXX.cpp文件;

否则编译时无法启动程序XXX.exe,系统找不到指定文件。

2、头文件#include 后添加using namespace std;
否则报错:error C1083: 无法打开包括文件:“iostream.h”: No such file or directory
3、定义结构体或类时,别忘记最后的分号;结构体名或类名后面没有小括号;
4、引用成员函数别忘记括号:pt.output();
5、.sln为项目文件

image(2).png

image(3).png

参考文献

[1]<https://blog.csdn.net/weixin_43751983/article/details/91147918 >this指针
[2]孙鑫.VC++深度详解修订版[M]. 北京:电子工业出版社, 2012. 27-62.


第三章 MFC框架程序剖析

3.1创建MFC AppWizard

如何利用vs2019创建MFC应用见参考文献[1]
需要注意的地方有
[1]创建MFC单文档应用程序

屏幕截图 2021-06-28 101949.jpg

[2]开启类视图窗口

image(4).png

3.2基于MFC的程序框架剖析

在MFC中,类的命名都以C开头;对于单文档应用程序,如图3.2所示,都有:

  • CAboutDlg帮助类,同于说明这个工程的开发信息;
  • CMainFrame主框架类;
  • C工程名App应用程序入口;
  • C工程名Doc文档类,用来管理、存放数据;
  • C工程名View用来将文档中的数据可视化。

CMainFrame类和CTestView类都有一个共同的基类:Cwnd类,其封装了与窗口相关的操作。

3.2.1 MFC中的WinMain函数

文件路径(在安装路径下直接搜索MFC,找到mfc):D:\Program Files (x86)\visualstudio\VC\Tools\MSVC\14.29.30037\atlmfc\src,打开appmodul.cpp 查找WinMain

1
2
3
4
5
6
7
8
extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, //右键_tWinMain,单击转到定义:#define _tWinMain WinMain
_In_ LPTSTR lpCmdLine, int nCmdShow)
#pragma warning(suppress: 4985)
{
// call shared/exported WinMain
return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

3.2.1.1 theApp全局对象

以简单的C++源程序为例,在入口函数main()加载之前,就已经为全局变量(对象)分配了内存空间,并为其赋了初值。对于一个全局对象来说,此时就会调用该对象的构造函数构造该对象并进行初始化操作,然后才是进入main()函数。(P72例3-4已标出先后顺序)

对于MFC来说,通过产生一个应用程序类的对象来唯一标识应用程序的实例。每个MFC程序有且仅有一个从应用程序类(CWinApp)派生的类;每个MFC程序实例有且仅有一个该派生类的实例化对象,即theApp全局对象,theApp表示了该应用程序本身。theApp的定义如程序3.1所示,在test.cpp中查看。

程序3.1 theApp全局对象

1
2
3
4
5
6
7
8
9
/*test.cpp*/
// 唯一的 CtestApp 对象
CtestApp theApp; //theApp是CtestApp的一个对象,注意其是一个全局对象

/*test.h*/
class CtestApp : public CWinApp //CtestApp继承于CWinApp,后者表示应用程序类
{
......
}

文件路径(在安装路径下直接搜索MFC,找到mfc):D:\Program Files (x86)\visualstudio\VC\Tools\MSVC\14.29.30037\atlmfc\src,打开appcore.cpp 查找CWinApp(184行)其中,
程序3.2

1
2
3
4
5
CWinApp::CWinApp(LPCTSTR lpszAppName)  //注意此处有参数
{ ...
pModuleState->m_pCurrentWinApp = this; //此处this代表子类CTestApp的对象,即theApp
...
}

程序3.3

1
2
3
4
5
class CWinApp : public CWinThread
{...
explicit CWinApp(LPCTSTR lpszAppName = NULL); // app name defaults to EXE name;构造函数形参有默认值默认值
...
};

补充:如果某个函数的参数有默认值,那么在调用该函数时可以传参,也可以不传参直接使用默认值。

由程序3.3可见,来CWinApp类的定义时,CWinApp的构造函数的形参有默认值NULL。因此,在调用CWinApp类的构造函数时,不用显式地传参。

3.2.1.2 AfxWinMain函数

WinMain函数实际上是通过调用AfxWinMain函数来完成它的功能的。
其中,Afx前缀的函数代表应用程序框架函数,辅助我们生成应用程序框架的应用模型。在MFC中,Afx为前缀的函数都是全局函数,可以在程序的任何位置调用。

在AfxWinMain函数的定义中,有
程序3.4

1
2
3
4
5
6
7
8
9
10
11
12
13
/*pThread和pApp这两个指针是一致的,这两个指针都指向CTestApp类的对象,即theApp全局对象*/  
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();

/*MFC内部管理所调用的函数*/
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;

/*调用的是子类InitInstance():因为在父类CWinApp中的InitInstance()是虚函数*/
if (!pThread->InitInstance()){}

/*消息循环*/
nReturnCode = pThread->Run();

3.2.1.3 InitInstance函数

见程序3.4的第九行。

3.2.2 MFC框架窗口

3.2.2.1 设计和注册窗口

窗口类的注册是由位于wincore.cpp的AfxEndDeferRegisterClass函数完成的。AfxEndDeferRegisterClass函数预定义了几种缺省的窗口类,首先判断窗口类的类型,再赋予其相应的类名。部分代码如书本p79所示。

接着调用AfxRegisterClass函数注册从窗口类,该函数首先获得窗口类信息,窗口已经注册,返回真。否则注册该窗口类。

注意:AfxRegisterClass实际上就是AfxEndDeferRegisterClass(宏定义);

3.2.2.2 创建窗口

窗口的创建是由CWnd类中的CreateEx函数完成的。定义:afxwin.h,实现:wincore.cpp。(以Ex结尾的函数表示扩展函数。)

CreateEx函数不是虚函数,CFrameWnd类的Create函数内调用的实际上就是CWnd类的CreatEx函数。CreateEx函数内部调用的PreCreateWindow函数是一个虚函数,在产生窗口之前有机会修改窗口外观。

3.2.2.3 显示和更新窗口

CTestApp中名为m_pMainWnd的成员变量保存了应用程序框架窗口(CMainFrame)对象的指针,在InitInstance函数(初始化工作:注册、显示、更新)内部:
程序3.5

1
2
3
// 唯一的一个窗口已初始化,因此显示它并对其进行更新
m_pMainWnd->ShowWindow(SW_SHOW);//显示
m_pMainWnd->UpdateWindow();//更新

3.2.3 消息循环

见程序3.4的第12行。在thrdcore.cpp中/*消息循环*/ nReturnCode = pThread->Run();

书本p85例3-16,该函数主要结构是一个for循环,在收到WM_QUIT时退出。在循环中的PumpMessage()与第一章的SDK编程的消息处理代码一致。

3.2.4 MFC运行过程梳理

IMG_20210628_164535.jpg

3.3 窗口类、窗口对象与窗口

3.3.1 三者之间的关系

:: 前面没有东西,表示所使用的函数是一个全局函数。如果当前定义的成员函数与内部调用的API函数重名,后者必须加 :: ,否则报错。

C窗口类对象与窗口并不是一回事,他们之间唯一的关系是C窗口类对象内部定义了一个窗口句柄变量,保存了与这个C窗口类对象相关的那个窗口的句柄。窗口销毁时,与之对应的C窗口类对象销毁与否要看其生命周期是否结束。但C++窗口类对象销毁时,与之相关的窗口也将销毁。

上段话用自己的话说:在窗口销毁后,CWnd的成员变量m_hWnd设为NULL,并没有被销毁(也有可能被销毁:对象生命周期结束(函数运行到右大括号“}”));而在C++窗口类对象析构时,窗口被销毁。

在系统文件afxwin.h中,CWnd已有一个用于保存句柄的成员变量m_hWnd,ShowWindow()和UpdateWindow()不需要再传递这个句柄,因为它已经是成员变量。
程序3.6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*afxwin.h*/
class CWnd : public CCmdTarget
{
DECLARE_DYNCREATE(CWnd)
protected:
static const MSG* PASCAL GetCurrentMessage();

// Attributes
public:
HWND m_hWnd; // must be first data member
......
};


/*lesson3:\\wainmain.cpp*/
int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // command line
int nCmdShow // show state
)
{
//设计窗口类
...
//注册窗口类
...

//创建窗口类
CWnd wnd;
wnd.CreateEx(...);
wnd.ShowWindow(SW_SHOWNORMAL);
wnd.UpdateWindow();

/*
对比第一章:
创建窗口
HWND hwnd;
hwnd = CreateWindowEx();

显示及刷新窗口
::ShowWindow(hwnd, SW_SHOWNORMAL);
::UpdateWindow(hwnd);

注意ShowWindow和UpdateWindow的参数,原因是:CWnd类定义过了一个HWND类型的成员变量m_hWnd用于保存这个窗口的句柄,
在调用CWnd类中的ShowWindow显示窗口时,就不在需要传递这个句柄了,因为它已经是成员变量了,该函数可以直接使用它。

在窗口销毁后,CWnd的成员变量m_hWnd设为NULL,并没有被销毁;而在C++窗口类对象析构时,窗口被销毁。
*/

//消息循环
...

return 0;
}

3.3.2 在窗口中显示按钮

CButton的Create函数声明:
BOOL Create( LPCTSTR lpszCaption, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );

  • lpszCaption:按钮文本;
  • dwStyle:按钮风格+窗口风格;
  • rect:定义一个矩形区域;
  • pParentWnd:指定父窗口。MFC不再通过窗口句柄,而是通过一个与窗口相关的C++窗口类对象指针来传递窗口对象。
  • nID:按钮控件标识。可取整数随机值。在框架窗口产生之后,再创建该标识,否则没地方放置。

窗口创建时都会产生WM_Create消息,OnCreate函数用于相应这条窗口创建消息。

如果将按钮创建在CMainFrame::OnCreate()函数内,按钮的父窗口是主框架窗口,此时按钮遮挡住了保存等按钮。

image(5).png

改为在CTestView.cpp中创建button,首先在testView.cpp中创建OnCreate函数,步骤如问题及反思[3]所示。运行结果如下。

屏幕截图 2021-06-29 103602.jpg

而将m_btn.Create()中的this改为GetParent(),运行结果变为

image(6).png

可见,按钮的位置与其父窗口有关,与创建它的代码所在的类无关。

将按钮窗口销毁,m_btn并没有销毁,因为m_btn是CTestView类的一个成员变量,其生命周期与CTestView对象一致。

课后程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*testview.h*/
class CtestView : public CView
{
...
private:
CButton m_btn; //在定义类的成员变量时都以"m_"为前缀,表明这个变量是类的成员变量
};

/*testview.cpp*/
int CtestView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;

/*CButton的Create函数声明:BOOL Create( LPCTSTR lpszCaption, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );*/

// m_btn.Create(_T("button"), WS_CHILD | BS_DEFPUSHBUTTON, CRect(0, 0, 100, 100), this, 123);
// m_btn.ShowWindow(SW_SHOWNORMAL);//窗口显示

m_btn.Create(_T("button"), WS_CHILD | BS_DEFPUSHBUTTON |WS_VISIBLE , CRect(0, 0, 100, 100), this, 123);
// m_btn.Create(_T("button"), WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, CRect(0, 0, 100, 100), GetParent(), 123);

/*
"按钮":名称; CRect(0,0,100,100):矩形区域; 123:ID号。
*
WS_CHILD(窗口风格):The window is a child window. A window with this style cannot have a menu bar.
BS_DEFPUSHBUTTON(按钮风格):下按按钮风格
WS_VISIBLE:The window is initially visible.
*
this指针(代表对象本身)
GetParent():Call this function to get a pointer to a child window's parent window
*/

/*
C2664 “BOOL CButton::Create(LPCTSTR,DWORD,const RECT &,CWnd *,UINT)”: 无法将参数 1 从“const char [7]”转换为“LPCTSTR” test E:\VCProject\Lesson3\test\test\MainFrm.cpp 68

解决方法:
方法1、"button"改为_T("button")[2]
方法2、调试>>XXX调试属性>>配置属性>>高级>>高级属性>>字符集,改为:使用多字节字符集[3]
*/
return 0;
}

运行结果:

屏幕截图 2021-06-29 103602(1).jpg

问题及反思

[1]如何利用vs2019创建MFC应用见参考文献[1]

[2]C2664 “BOOL CButton::Create(LPCTSTR,DWORD,const RECT &,CWnd *,UINT)”: 无法将参数 1 从“const char [7]”转换为“LPCTSTR”
解决方法:
方法1、”button”改为_T(“button”)
方法2、调试>>XXX调试属性>>配置属性>>高级>>高级属性>>字符集,改为:使用多字节字符集

[3]vs2019为一个类添加某函数的方法如下所示。第四步单击最右侧向下小三角,选择Add OnCreate。

屏幕截图 2021-06-29 103208.jpg

参考文献

[1] <https://blog.csdn.net/m0_37062716/article/details/113827243?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control >.安装MFC,创建MFC工程文件

[2][https://blog.csdn.net/huijie4728/article/details/50487315](https://blog.csdn.net/huijie4728/article/details/50487315) . 问题及反思[2]

[3][https://blog.csdn.net/feilong911hao/article/details/39231533?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control](https://blog.csdn.net/feilong911hao/article/details/39231533?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control) . 问题及反思[2]

[4]孙鑫.VC++深度详解修订版[M]. 北京:电子工业出版社, 2012. 63-99.


第四章

4.1 MFC消息映射机制

4.1.1 classwizard

所有操作,包括鼠标点击鼠标移动等操作都只能由视类窗口捕获。因为视类窗口在框架窗口之上,如果在框架窗口操作。就会被视类窗口覆盖而看不见了。

删除通过向导添加的消息响应函数时,应在向导中删除。

4.1.2 消息映射机制

一个MFC消息响应函数在程序中有3处相关信息:

1、消息响应函数原型

1
2
3
4
5
6
/*drawView.h*/
// 生成的消息映射函数
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

2、消息映射宏

消息映射宏ON_WM_LBUTTONDOWN()的作用:把消息(WM_LBUTTONDOWN)与消息响应函数(OnLButtonDown)关联起来。

1
2
3
4
5
6
7
BEGIN_MESSAGE_MAP(CdrawView, CView)
// 标准打印命令
ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CView::OnFilePrintPreview)
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()

3、消息响应函数的定义

1
2
3
4
5
6
7
/*drawView.cpp*/
void CdrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
MessageBox("click down");
CView::OnLButtonDown(nFlags, point);
}

MFC消息映射方式的具体实现:在每个能接受和处理消息的类中,定义一个消息和消息函数静态对照表,即消息映射表。在消息映射表中,消息和对应的消息处理函数指针成对出现。某个类能处理的所有信息及其对应的消息处理函数地址都列在这个类对应的静态表中。当有消息需要处理时,程序只能搜索该消息静态表,查看表中是否含有该消息,就知道该类能否处理此消息。如果能处理,则同样依照静态表能很容易找到并调用对应的消息处理函数。

4.2 绘制线条

在基类中添加成员变量的原因:可以一直存在;而在子类中,程序运行到右},变量的生命周期就结束了。

1
2
3
4
class CdrawView : public CView{...
private:
CPoint m_ptOrigin;
...};

在构造函数中将m_ptOrigin赋0以初始化后,在消息响应函数中将鼠标按下的信息保存到基类的成员变量m_ptOrigin中

1
2
3
4
5
6
7
8
void CdrawView::OnLButtonDown(UINT nFlags, CPoint point)
{//CPoint point接收按下鼠标左键的位置
m_ptOrigin = point;
/*保存该位置到基类的成员变量;
**当前的鼠标坐标也就是划线的起点坐标。
*/
CView::OnLButtonDown(nFlags, point);
}

最后添加WM_LButtonUp.

4.2.1 利用SDK全局函数实现划线功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//具体实现如下:
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// 获取设备描述表
HDC hdc;
hdc = ::GetDC(m_hWnd);

//移动到线条的起点
MoveToEx(hdc , m_ptOrigin.x , m_ptOrigin.y , NULL);

//划线
LineTo(hdc , point.x , point.y );

//释放设备描述表
::ReleaseDC(m_hWnd , hdc);

CView::OnLButtonUp(nFlags, point);

1、获取设备描述表(平台SDK的GetDC)

1
2
3
4
5
//字典
//平台SDK的GetDC
HDC GetDC(
HWND hWnd
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//本例
HDC hdc;//HDC:A handle to a device context (DC).
/*device context (DC):There are four types of DCs: display,
printer, memory (or compatible), and information.
Each type serves a specific purpose*/

hdc = ::GetDC(m_hWnd);
/*用自己的话说(暂时的理解是),HWND hWnd就是当前窗口,HDC hdc就是用来画画的,
GetDC()就是把当前窗口中用来画画的一部分(DC)取出来赋给hdc变量,
意思是接下来的画的东西都会保存在hdc上,也就是保存在了hWnd当前窗口的DC上
*/

//注意最后释放
::ReleaseDC(m_hWnd , hdc);//将m_hWnd窗口的hdc释放

2、鼠标移动到线条的起点

1
2
3
4
5
6
7
8
BOOL MoveToEx(
HDC hdc, //设备描述表句柄
int x,
int y, //起点坐标
LPPOINT lppt //指向point结构体的指针,保存移动操作前鼠标的位置坐标
);

MoveToEx(hdc , m_ptOrigin.x , m_ptOrigin.y , NULL);

3、划线

1
2
3
4
5
6
7
BOOL LineTo(
HDC hdc,
int x,
int y//终点坐标
);

LineTo(hdc , point.x , point.y );

4.2.2 利用MFC的CDC类实现画图功能

MFC为我们提供了一个设备描述表的封装类CDC,该类封装了所有绘图操作。该类中的数据成员m_hDC保存与CDC类相关的DC句柄,因此不需要DC句柄作为参数,其作用类似m_hWnd。而平台SDK提供的全局TextOut()则不然。

1
2
3
4
5
6
7
8
9
10
11
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CDC* pDC = GetDC(); //此处的GetDC是CWnd()的而不是平台SDK的。
pDC->MoveTo(m_ptOrigin);//从什么位置
/*pDC是指向CDC类的指针,MoveTo()是CDC类的函数,所以用指向符->*/
/*如果是CDC类的对象的话就用“.”号*/
pDC->LineTo(point);//画线到什么位置
ReleaseDC(pDC);

CView::OnLButtonUp(nFlags, point);
}

此处的GetDC是CWnd()的而不是平台SDK的。对比4.2.1的GetDC

1
2
3
//字典
CWnd::GetDC
CDC* GetDC( );

4.2.3 利用MFC的CClientDC类实现画图功能

CClientDC类派生于CDC类,并在构造函数时自动调用GetDC和ReleaseDC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CClientDC dc(this);
/*CClientDC dc(CWnd* pWnd);
**CWnd* pWnd是指向窗口的指针,此处需要指向视类窗口对象,即CdrawView。
**在CdrawView::OnLButtonUp(...)中使用this指向CdrawView自己。
*/

/*DetParent()返回父类窗口的指针*/

dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
/*这里CClientDC类型的变量dc是一个对象,因此使用点操作符来调用该对象的函数;
**4.2.2中,pDC是指向CDC类的指针,MoveTo()是CDC类的函数,所以用指向符->
*/

CView::OnLButtonUp(nFlags, point);
}

4.2.4 利用MFC的CWindowDC类实现画图功能

CWindowDC类派生于CDC类,并在构造函数时自动调用GetDC和ReleaseDC,调用方式与CClientDC一样。

不同于CClientDC的是,CWindowDC可以访问整个窗口区域,包括框架窗口的客户区(菜单栏以下)和非客户区(标题栏和菜单栏);而CClientDC最多只能到客户区。

1
2
3
4
5
6
7
8
9
10
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CWindowDC dc(GetDesktopWindow());//整个桌面窗口
//CWindowDC dc(GetParent());//框架窗口,包括客户区和非客户区
//CWindowDC dc(this);//视类窗口
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);

CView::OnLButtonUp(nFlags, point);
}

4.2.5 在桌面窗口划线

见4.2.4。

4.2.6 绘制线条色彩

1
2
3
4
5
6
7
8
9
10
11
12
13
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
CClientDC dc(this);
//只创建CPen不会生效,还要将其选入设备描述表
CPen* pOldPen = dc.SelectObject(&pen);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//还原设备描述表
dc.SelectObject(pOldPen);

CView::OnLButtonUp(nFlags, point);
}

图4.2 绘制彩色线条

1、Cpen类创建画笔对象,该类封装了画笔的相关操作

CPen (int nPenStyle , int Width , COLORREF crColor);

三个参数分别代表线形、线宽和颜色。

颜色用RGB(r,g,b)表示,每个参数都是0~255之间。黑色全0,白色全255.

2、在程序中构造GDI对象后,该对象不会立即生效,必须(作用1)选入设备描述表后,才会生效。SelectObject()函数除了实现以上操作,还会(作用2)返回指向先前被选中对象的指针,以在完成绘画后,还原设备描述表。

小结:需要使用SelectObject()选入设备描述表:CPen类、Rectangle函数

4.3 画刷

4.3.1 简单画刷

利用CBrush类创建画刷对象,以填充一块区域。

CBrush(COLORREF crColor )

1
2
3
4
5
6
7
8
9
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CBrush brush(RGB(255 , 0, 0));//创建红色画刷
CClientDC dc(this);//创建并获取设备描述表
//利用红色画刷填充鼠标拖拽过程中产生的矩形区域
dc.FillRect(CRect(m_ptOrigin, point), &brush);

CView::OnLButtonUp(nFlags, point);
}

图4.3 红色简单画刷

1、FillRect函数:利用画刷填充鼠标拖拽过程中产生的矩形区域,包括左边和上部边界,不填充右边和底部边界。

void FillRect( LPCRECT lpRect, CBrush* pBrush );

1、lpRect:指向一个RECT结构体或CRect对象指针。本例使用CRect()函数,即 CRect( POINT topLeft, POINT bottomRight );第一个参数是左上角点,第二个参数是右下角点,以确定一个矩形。

2、pBrush:指向用于填充矩形的画刷对象的指针。

4.3.2 位图画刷

CBrush类中有如下构造函数:CBrush (CBitmap* pBitMap);其中,CBitmap类是位图类。创建CBitmap对象时,仅调用其构造函数,并不能得到有用的位图对象,还需要调用初始化函数来初始化该位图对象。本例中用加载位图的初始化函数:LoadBitmap(UINT nIDResource);。其中,nIDResource是资源ID号,在资源视图中查看,如图4.6所示。

1
2
3
4
5
6
7
8
9
10
11
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CBitmap bitmap;//创建位图对象
bitmap.LoadBitmap(IDB_BITMAP1);//初始化位图对象
CBrush brush(&bitmap);//将位图对象装进画刷
CClientDC dc(this);//创建并获取设备描述表
//用位图填充矩形区域
dc.FillRect(CRect(m_ptOrigin , point) , &brush);

CView::OnLButtonUp(nFlags, point);
}

图4.3 位图画刷运行结果

1、位图的创建过程如下所示。

_4EcJoeM6pn.png>)

图4.5 位图的创建

2、资源ID在资源视图中查看

图4.6 查看资源ID

4.3.3 透明画刷

1
2
3
4
5
6
7
8
9
10
11
12
13
void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
CClientDC dc(this);
//创建空画刷
CBrush* pBrush = CBrush::FromHandle(
(HBRUSH) GetStockObject(NULL_BRUSH));
//将画刷选入设备描述表
CBrush* pOldBrush = dc.SelectObject(pBrush);
dc.Rectangle(CRect(m_ptOrigin, point));
dc.SelectObject(pBrush);

CView::OnLButtonUp(nFlags, point);
}

图4.7 透明画刷

1、GetStockObject()函数,获取一个黑点或白色的画刷句柄

GetStockObject(NULL_BRUSH);获取一个空画刷

1
2
3
HGDIOBJ GetStockObject(
int fnObject // stock object type
);

再利用强制类型转换,将HGDIOBJ类型转换为HBRUSH类型

1
(HBRUSH) GetStockObject(NULL_BRUSH);

2、FromHandle()函数将画刷句柄转化为画刷对象

1
2
3
4
5
6
7
8
9
static CBrush* PASCAL FromHandle( HBRUSH hBrush );
/*Parameters(参数)
hBrush
Specifies the HANDLE to a Windows CE GDI brush.
*/

/*Return Value
A pointer to a CBrush object if successful; otherwise, it is NULL
*/

static静态成员函数

P126

(1)静态成员函数和静态成员变量属于类本身,而不属于某一个变量,在类加载的时候,既为他们分配了空间,所以可以通过类名::函数名类名:变量名来访问。而非静态成员函数和非静态成员变量属于对象的方法和数据,即应先产生类的对象,再通过类的对象去引用。

(2)在静态成员函数中是不能调用非静态成员的,包括非静态成员函数和非静态成员变量;静态成员函数只能访问静态成员变量。而非静态成员函数中是可以调用静态成员函数的

(1)和(2)小结:也就是说,无论采用什么样的操作,代码都是在内存中运行的。程序只有在内存中占有一席之地,才能够访问它。

(3)对于静态成员变量,必须对其初始化,并且应在类的定义之外.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class Point
{
public:
static void init();//静态成员函数
{
cout<<"x = "<<x<<endl;
}
static int x;//静态成员变量
};

int Point::x = 0;//知识点(3):1初始化2在类外初始化

void main()
{
Point::init();//知识点(1):用类名::函数名来直接调用
}

/*结果输出x = 0*/

4.4 绘制连续线条

为捕获鼠标移动过程的每一个点,可以通过捕获鼠标移动消息(WM_MouseMove)来实现。只要鼠标在程序窗口中移动,都会进入这个消息响应函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*drawView.h*/
class CdrawView : public CView
{
...
private:
BOOL m_bdraw;//判断鼠标左键是否按下去,按下为真,否则为假
};

/*drawView.cpp*/
void CdrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
m_ptOrigin = point;
m_bdraw = 1;
CView::OnLButtonDown(nFlags, point);
}


void CdrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
m_bdraw = 0;
CView::OnLButtonUp(nFlags, point);
}

void CdrawView::OnMouseMove(UINT nFlags, CPoint point)
{
CClientDC dc(this);
CPen pen(PS_SOLID , 1 , RGB(255 , 0 , 0));
CPen* pOldPen = dc.SelectObject(&pen);
if (m_bdraw == 1)
{
dc.MoveTo(m_ptOrigin);//第一次左键按下
dc.LineTo(point);//将起点与终点连线
m_ptOrigin = point;
/*绘制完当前线段后,立即将上一段的终点位置赋给下一段的起点*/
dc.SelectObject(pOldPen);
}

CView::OnMouseMove(nFlags, point);
}

图4.8 连续绘图运行结果

4.5 绘制扇形效果线条

1
2
3
4
5
6
7
8
void CdrawView::OnMouseMove(UINT nFlags, CPoint point)
{
...
dc.LineTo(m_ptOld);//表示鼠标移动过程中的连线中,每个极小的线段
//m_ptOrigin = point;//修改起点信息
m_ptOld = point;//m_ptOld用于保存鼠标上一个移动点
...
}

图4.9 带边扇形

问题及反思

[1]位图创建过程见4.3.2

参考文献

[1]孙鑫.VC++深度详解修订版[M]. 北京:电子工业出版社, 2012. 100-134.


第五章

5.1 插入符

5.1.1 创建插入符

在窗口创建之后的WM_CREATE消息响应函数OnCreate中添加创建插入符代码:

void CreateSolidCaret(int nWidth , int nHeight);若参数为0,则使用系统定义。

接着,使用ShowCaret();显示插入符。

对于CreateSolidCaret()的两个参数,可以根据字体度量信息自动调整插入符:

BOOL GetTextMetrics( LPTEXTMATRIC lpMetrics ) const;其中,参数要求一个指向TEXTMETRIC结构体的指针,函数从DC中获取字体度量信息后填充进这个结构体。

经常用到的TEXTMETRIC结构体变量:

  • int tmHeight; //tmAscent+tmDescent
  • int tmAscent; //升序高度
  • int tmDescent; //降序高度
  • int tmAveCharWidth; //平均宽度
  • int tmMaxCharWidth; //最大字符宽度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int CtextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;

// TODO: 在此添加您专用的创建代码
CClientDC dc(this);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
CreateSolidCaret(tm.tmAveCharWidth / 8 , tm.tmHeight); //除以8是经验值
ShowCaret();

return 0;
}//创建插入符

5.1.2 创建图形插入符

void CreateCaret(CBitmap* pBitmap);创建图形插入符

1
2
3
4
5
6
7
8
9
10
11
12
13
int CtextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;

// TODO: 在此添加您专用的创建代码
//CBitmap对象在基类CView中创建
bitmap.LoadBitmap(IDB_BITMAP1);
CreateCaret(&bitmap);
ShowCaret();

return 0;
}//位图插入符

5.2 窗口重绘

5.2.1 OnDraw函数

窗口重绘时,已输入的文字或图形就会被擦除。如想要保留,则需要WM_PAINT的消息响应函数OnCreate将内容再次输出(重绘)。

字符串类CString,其对象可以当作普通变量赋值、相加。

CDC类封装的TetOut(x,y,str);在指定位置显示字符串。

5.2.2 添加字符串资源

BOOL LoadString(UINT nID);

其中,nID为字符串资源标识,如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CtextView::OnDraw(CDC* pDC)
{
CtextDoc* pDc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CString str;
str = "VC++深入编程";
pDC->TextOut(0,0,str);

str.LoadString(IDS_STRINGVC);
pDC->TextOut(0,50,str);
}

5.3 路径层

1、步骤:

  1. 打开路径层:BeginPath
  2. 利用GDI提供的绘图函数绘图
  3. 关闭路径层:EndPath

2、CSize GetTextExtent(str):获取某个特定字符串的宽度和高度,返回CSize对象。和GetTextMetrics(字体度量)区分。

3、裁剪区域:可以理解为一个绘图区域,大小可以由我们来控制,以后的绘图操作仅限于这个矩形区域内。

CDC类提供了BOOL SelectClipPath(int nMod);用于把当前设置的路径层和DC中已有的剪裁区域按照一定指定的模式进行一个互操作。

nMod是用于指定互操作的模式,如:

RGN_DIFF:新的剪裁区域包含当前剪裁区域,但排除当前路径层区域。

RGN_AND:剪裁区域和路径层的交集

小结:如果希望整幅图形中的某一部分与其他部分有所区别,就可以把这个部分的图形放置在一个路径层中,然后利用SelectClipPath函数设置一种模式,让路径层和剪裁区域进行互操作以达到一种特殊的效果。

5.4 字符输入

利用CString类对象可以直接字符输入。

输入字符,就会有WM_CHAR消息,在OnChar消息处理函数中写代码,说明要如何处理该消息。

设置插入符位置函数:CWnd类中的SetCaretPos(POINT point);函数。将插入符移动到鼠标左键单击处。

清除字符可以直接用CString类中的Empty() 来实现。

1、回车处理(ASCII为0x0d)

插入符横坐标不变,纵坐标发生变化:当前插入点的纵坐标加上字体高度

1
2
3
4
5
6
if(0x0d == nChar)
{
m_strLine.Empty();//清空当前字符串
m_ptOrigin.y += tm.tmHeight;//移动插入点
dc.TextOut(m_ptOrigin.x , m_ptOrigin.y , m_strLine);//在新的插入点输出
}

2、退格键(ASCII为0x08)

步骤:

  1. 设置文本颜色为背景色:virtual COLORREF SetTextColor( COLORREF crColor );返回原来的颜色。CDC::GetBkColor()获取背景色。
  2. 输出
  3. 在字符串变量中删除要删除的文字CString Left( int nCount ) const;其中,nCount指字符串左边指定数目的字符。int GetLength( ) const;获取长度。
  4. 还原文字颜色
  5. 输出
1
2
3
4
5
6
7
8
if(0x08 == nChar)
{
COLORREF clr = dc.SetTextColor(dc.GetBkColor());//设置文本颜色为背景色
dc.TextOut(m_ptOrigin.x , m_ptOrigin.y , str);//输出
m_strLine = m_strLine.Left(m_strLine.GetLength() - 1);//在字符串变量中删除要删除的文字
dc.SetTextColor(clr);//还原文字颜色
dc.TextOut(m_ptOrigin.x , m_ptOrigin.y , m_strLine);//输出
}

3、设置字体

1、创建字体对象;

2、CFont类设置字体

BOOL CreatePointFont( int nPointSize, LPCTSTR lpszFaceName, CDC* pDC = NULL );

(字体高度,字体名称,NULL)

3、将字体选入DC,SelectObject(CFont *font);

4、字幕变色功能

CDC::DrawText函数 在指定矩形范围内输出文字。

int DrawText(const CString& str, LPRECT lpRect, UINT nFormat );

(要输出的字符串,文字显示范围的矩形,文本的输出格式)

CWnd::SetTimer函数 定时器函数
UINT SetTimer( UINT nIDEvent, UINT nElapse, void (CALLBACK EXPORT* lpfnTimer)( HWND, UINT, UINT, DWORD) );

  • UINT nIDEvent:非零定时器标识,函数调用成功,则返回标识;
  • UINT nElapse:以毫秒为单位的时间间隔,即每隔多长时间发送一次定时器消息WM_TIMER。程序在OnTimer函数中编写。
  • CALLBACK EXPORT* lpfnTimer:如果设置了该回调函数,则系统调用该回调函数处理消息。如果NULL,则窗口对象CWnd来处理

参考文献:
[1]孙鑫.VC++深度详解修订版[M]. 北京:电子工业出版社, 2012. 135-161.

第六章 菜单

一、创建菜单项

资源管理器→menu项→双击IDR_MAINFRAME打开菜单编辑器

pop-up类型的菜单是弹出式菜单,不能进行命令响应。

ID号的命名规则:

IDM菜单资源

IDC光标资源

IDI图标资源

二、为菜单项添加响应函数

打开ClassWizard【类向导】,

  1. 区别Message中的COMMAND和UPDATE_COMMAND_UI
    COMMAND处理该菜单对应的功能
    UPDATE_COMMAND_UI处理菜单应对的用户界面
  2. 菜单项响应顺序:view→doc→mainframe→App
  3. Windows消息的分类
    1、标准消息:除 WMCOMMAND 之外,**所有以 WM开头的消息都是标准消息。
    CWnd类,都可以接收到这类消息。
    2、命令消息:来自菜单、加速键或工具栏按钮的消息。这类消息都
    以 WM_COMMAND 形式呈现
    在 MFC 中,通过菜单项的标识(ID)来区分不同的命令消息;在 SDK 中,通过消息的wParam 参数识别。从 CCmdTarget 派生的类,都可以接收到这类消息。
    3、通告消息:由控件产生的消息,例如按钮的单击、列表框的选择等。目的是为了向其父窗口(通常是对话框)通知事件的发生。这类消息也是
    以 WM_COMMAND 形式呈现。从CCmdTarget 派生的类,都可以接收到这类消息。
    实际上,CWnd 类派生于 CCmdlTarget类。也就是说
    凡是从 CWnd 派生的类,它们既可以接收标准消息,也可以接收命令消息和通告消息。而对于那些从 CCmdTarget 派生的类,则只能接收命令消息和通告消息,不能接收标准消息。**
  4. 菜单消息响应过程:mainframe类接收消息→view首先处理,若不行→doc→mainframe→App

三、基本菜单操作

1、了解菜单结构

程序的主菜单属于框架窗口,因此要在框架类的OnCreate中编程。

2、标记菜单

获取程序菜单栏:CWnd类 CMenu* GetMenu( ) const;

获取子菜单:CMenu类CMenu* GetSubMenu( int nPos ) const;参数是子菜单的索引号,注意分隔栏也占据索引号。

添加或移除标记:CMenu类UINT CheckMenuItem( UINT nIDCheckItem, UINT nCheck );

Parameters
nIDCheckItem:Specifies the menu item to be checked, as determined by nCheck.
nCheck_Specifies :how to check the menu item and how to determine the position of the item in the menu. The _nCheck parameter can be a combination of MF_CHECKED or MF_UNCHECKED with MF_BYPOSITION or MF_BYCOMMAND flags.

  • MF_BYCOMMAND 指定第一个参数通过ID
  • MF_BYPOSITION 指定第一个参数通过索引号
  • MF_CHECKED 设置复选标记
  • MF_UNCHECKED 移走复选标记
1
GetMenu()->GetSubMenu(0)->CheckMenuItem(0,MF_BYPOSITION | MF_CHECKED);

3、默认菜单项(粗体)

注意子菜单只能有一个默认菜单项。

CMenu类BOOL SetDefaultItem( UINT uItem, BOOL fByPos = FALSE );

1
GetMenu()->GetSubMenu(0)->SetDefaultItem(2,MF_BYPOSITION);

4、图形标记菜单

CMenu类:BOOL SetMenuItemBitmaps( UINT nPosition, UINT nFlags, const CBitmap* pBmpUnchecked, const CBitmap* pBmpChecked );

pBmpUnchecked是取消选中时的位图;pBmpchecked是选中状态时的位图。

获取系统的信息度量:int GetSystemMetrics( int nIndex);其中,参数用于指定希望获取哪部分的系统信息。SM_CXMENUCHECK和SM_CYMENUCHECK用于获取菜单项标记图形的默认尺寸。

CString类的Format函数类似C中的printf。

1
2
3
4
5
6
7
8
CBitmap m_bitmap;//头文件全局变量

m_bitmap.LoadBitmap(IDB_BITMAP1);
GetMenu()->GetSubMenu(0)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);

Ctring str;
str.Format("x = %d , y = %d", GetSystemMetrics(SM_CXMENUCHECK), GetSystemMetrics(SM_CYMENUCHECK));
MessageBox(str);

5、禁用菜单项

CMenu类:UINT EnableMenuItem( UINT nIDEnableItem, UINT nEnable );

nEnable:

MF_BYCOMMAND Specifies that the parameter gives the command ID of the existing menu item. This is the default.

MF_BYPOSITION Specifies that the parameter gives the position of the existing menu item. The first item is at position 0.

MF_DISABLED Disables the menu item so that it cannot be selected but does not dim it.

MF_ENABLED Enables the menu item so that it can be selected and restores it from its dimmed state.

MF_GRAYED Disables the menu item so that it cannot be selected and dims it.

常把MF_GRAYEDMF_DISABLED放在一起使用。

发现不能正常禁用?

// NOTE: m_bAutoMenuEnable is set to FALSE in the constructor of

// CMainFrame so no ON_UPDATE_COMMAND_UI or ON_COMMAND handlers are

// needed, and CMenu::EnableMenuItem() will work as expected.

即在CMainFrame构造函数中添加m_bAutoMenuEnable = FALSE;

6、移除和装载菜单

CWnd类:BOOL SetMenu( CMenu* pMenu );CMenu* pMenu 指向一个新的菜单对象,若值为NULL,则移除当前菜单。

1
2
3
4
5
6
7
8
9
/*移除菜单*/
SetMenu(NULL);

/*装载菜单资源*/
Cmenu menu;//1、框架类成员变量2、若是局部变量,结尾一定要加Detach函数

menu.LoadMenu(IDR_MAINFRAME);
SetMenu(&menu);
menu.Detach();//menu是局部变量的情况,将菜单句柄与菜单对象分离

四、MFC菜单命令更新机制

菜单项状态的维护依赖于CN_UPDATE_COMMAND_UI消息,该消息智能用于菜单项,不能用于永久显示的顶级菜单(即弹出式菜单)。

在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间添加宏以捕获消息

1
2
3
4
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)//自动添加的
ON_WM_CREATE()
ON_UPDATE_COMMAND_UI(ID_FILE_NEW, &CMainFrame::OnUpdateFileNew)
END_MESSAGE_MAP()
1
2
3
4
5
6
7
void CMainFrame::OnUpdateEditCut(CCmdUI* pCmdUI)//消息响应函数
{
// TODO: 在此添加命令更新用户界面处理程序代码
pCmdUI->Enable();
}
/*CCmdUI类决定一个菜单项是否可以使用(Enable),是否有标记(SetCheck),改变菜单项文本(SetText)*/
/*CCmdUI类有成员变量m_nID保存对象ID和m_nIndex保存对象索引*/

如果只是利用禁用菜单项,菜单项变灰禁用,但工具栏按钮依然能够使用。而如果使用命令更新机制,则都不能使用。

五、快捷菜单

(上下文菜单、右键菜单)

为CView类添加一个WM_CONTEXTMENU消息处理函数,当鼠标右键单击窗口时,就会调用该函数。

显示快捷菜单函数: BOOL TrackPopupMenu( UINT nFlags, int x, int y, CWnd* pWnd, LPCRECT lpRect = NULL );

1
2
3
4
5
6
7
8
9
10
void CmenuView::OnContextMenu(CWnd* pWnd, CPoint point)
{
// TODO: 在此处添加消息处理程序代码
CMenu popmenu;
popmenu.LoadMenu(IDR_POPMENU1); //读取资源
popmenu.GetSubMenu(0)->TrackPopupMenu(
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, point.x, point.y, this);
}
/*
TPM_LEFTALIGN定位弹出菜单,使其左侧与 x 指定的坐标对齐。*/

六、动态菜单操作

1、针对子菜单的动态操作

1、把一个新的子菜单添加到菜单栏末尾。CMenu类:BOOL AppendMenu ( UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL);

CMenu类:CreateMenu()创建一个弹出菜单,并与CMenu对象关联

1
2
3
4
CMenu m_menu;//成员变量

m_menu.CreateMenu();
GetMenu()->AppendMenu(MF_POPUP, (UINT)m_menu.m_hMenu, "TEST");

2、两个子菜单中间插入子菜单。CMenu类: BOOL InsertMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL );

用法类似AppendMenu

3、删除子菜单或菜单项。CMenu类:BOOL DeleteMenu( UINT nPosition, UINT nFlags );

1
2
3
4
//删除子菜单
GetMenu()->DeleteMenu(0,MF_BYPOSITION );
//删除菜单项
GetMenu()->GetSubMenu(1)->DeleteMenu(0,MF_BYPOSITION );

2、针对菜单项的动态操作

1、插入菜单项同样使用AppendMenu和InsertMenu

1
2
3
m_menu.AppendMenuA(MF_STRING, 111, "hello");//往弹出菜单TEST中添加菜单项
GetMenu()->GetSubMenu(0)->AppendMenu(MF_STRING, 113, "hello1");//往文件子菜单末尾添加菜单项
GetMenu()->GetSubMenu(0)->InsertMenu(1, MF_BYPOSITION | MF_STRING , 112 , "VC编程");

3、为 动态添加的菜单项 添加响应函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//在头文件resource.h中创建菜单资源ID
#define IDM_HELLO 111

//源文件中
m_menu.AppendMenuA(MF_STRING, IDM_HELLO, "hello");

//遵循消息映射机制:增加3处代码实现消息响应
//1、在响应这个菜单命令的类中添加响应函数原型,位于声明消息映射宏之上,(第11行所示)
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnHello();
DECLARE_MESSAGE_MAP()

//2、在源文件的消息映射表中添加消息映射,消息映射宏是ON_COMMAND宏,(第17行所示)
//注意:消息映射表中,代码结尾不加分号
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_COMMAND(IDM_HELLO,OnHello)
END_MESSAGE_MAP()

//3、添加消息响应函数定义
void CMainFrame::OnHello()
{
MessageBox("Hello");
}

第七章 对话框(一)

1、基本知识

控件:通常作为对话框的子窗口创建

对话框的种类:

模态对话框:对话框显示时,程序暂停执行,直到关闭对话框。

非模态:显示对话框时,可以执行其他任务。

2、创建和显示

CDialog对话框资源类

1.新建一个MFC类与该对话框关联:双击新建的对话框。

新建对话框资源后有两个按钮,功能一样(关闭对话框),返回值不同。

OK IDOK OnOK

Cancel IDCANCEL OnCancel

2.**DoDataExchange()**:完成对话框数据的交换和校验。

3.在视类中

CDialog::DoModal创建并显示模态对话框

CDialog::EndDialog关闭模态对话框

3、创建动态按钮

1.视类→工具箱→对话框编辑器 创建一个按钮。

2.为该按钮添加功能

类向导→选择该对象对应的BN_CLICKED(按钮被单击的消息)消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在CTestDlg类添加
private:
CButton m_btn;//动态按钮对象
BOOL isbtncreate;//判断动态按钮是否被创建,在构造函数中初始化为0

void CTestDlg::OnClickedBtnAdd()
{
// TODO: 在此添加控件通知处理程序代码

//动态创建按钮
//static BOOL isbtncreate = 0;

if (isbtncreate == 0)//判断动态按钮是否被创建,
{
//CButton::Create创建按钮
m_btn.Create(_T("new"), WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, CRect(0, 0, 100, 100), this, 123);
isbtncreate = 1;
}
else
{
m_btn.DestroyWindow();//销毁窗口
isbtncreate = 0;
}
}

初始化变量的两种方法

1.创建成员变量,并在构造函数中初始化;

2.创建静态局部变量同时初始化。

4、对话框控件

1.静态文本控件

IDC_STATIC不能响应消息,可通过修改ID来响应消息

CWnd::GetDlgItem获取对话框子控件的指针

CWnd::GetWindowText获取窗口文本

CWnd::SetWindowText设置窗口文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CTestDlg::OnClickedNumber1()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
if (GetDlgItem(IDC_NUMBER1)->GetWindowText(str),str == "数值1")
//逗号表达式,返回最后一个表达式的值
{
GetDlgItem(IDC_NUMBER1)->SetWindowText("Number1");
}
else
{
GetDlgItem(IDC_NUMBER1)->SetWindowText("数值1");
}
}

最后在属性中选中通知(Notify)选项,否则不能向其父窗口发送鼠标事件。

2.编辑框控件

🌟GetDlgItem()获取编辑框框窗口对象的指针

1.法一:**GetDlgItem→Get(Set)WindowText**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CTestDlg::OnClickedBtnAdd()
{
// TODO: 在此添加控件通知处理程序代码

//编辑框控件法一: CWnd::GetDlgItem()->Get(Set)WindowText();获取(设置)指定窗口文本

int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];

GetDlgItem(IDC_EDIT1)->GetWindowText(ch1, 10);//(指向存放字符串内存的指针,最大字符数目)
GetDlgItem(IDC_EDIT2)->GetWindowText(ch2, 10);
num1 = atoi(ch1);//atoi字符串->数值
num2 = atoi(ch2);
num3 = num1 + num2;
_itoa_s(num3, ch3, 10);//itoa数值->字符串,这里的10代表10进制
GetDlgItem(IDC_EDIT3)->SetWindowText(ch3);//将ch3中的字符串显示在编辑框中

}

2.法二:**Get(Set)DlgItemText**

=GetDlgItem+Get(Set)WindowText

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CTestDlg::OnClickedBtnAdd()
{
// TODO: 在此添加控件通知处理程序代码

//编辑框控件法二:CWnd::Get(Set)DlgItemText()获取指定控件上的文本
int num1 = 0, num2 = 0, num3 = 0;
char ch1[10], ch2[10], ch3[10];

GetDlgItemText(IDC_EDIT1, ch1,10);
GetDlgItemText(IDC_EDIT2, ch2,10);//=GetDlgItem + GetWindowText

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;
_itoa_s(num3, ch3, 10);

SetDlgItemText(IDC_EDIT3, ch3);

}

改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CTestDlg::OnClickedBtnAdd()
{
// TODO: 在此添加控件通知处理程序代码

//编辑框控件法二:CWnd::Get(Set)DlgItemText()获取指定控件上的文本
int num1 = 0, num2 = 0, num3 = 0;
// char ch1[10], ch2[10], ch3[10];
CString str1, str2, str3;
GetDlgItemText(IDC_EDIT1, str1);
GetDlgItemText(IDC_EDIT2, str2);
num1 = atoi(str1);
num2 = atoi(str2);
num3 = num1 + num2;
// str3 = (CString)num3;
//错误 C2440 “类型强制转换” : 无法从“int”转换为“CString”
//Format是CString类的一个成员函数,它通过格式操作使任意类型的数据转换成一个字符串。
str3.Format("%d",num3);
SetDlgItemText(IDC_EDIT3, str3);

}

3.CWnd::Get(Set)DlgItemInt返回指定控件的文本并将其转换为一个整数值。

1
2
3
4
5
6
7
8
9
10
void CTestDlg::OnClickedBtnAdd()
{
int num1 = 0, num2 = 0, num3 = 0;

num1 = GetDlgItemInt(IDC_EDIT1);
num2 = GetDlgItemInt(IDC_EDIT2);
num3 = num1 + num2;

SetDlgItemInt(IDC_EDIT3, num3);
}

4.控件与整型变量相关联(注意DoDataExchange和UpdateData一起用)

步骤:项目→类向导→成员变量→IDC_EDIT1→添加变量→类别(值)→变量名称→变量类型→其他(最大值、最小值等)

系统为我们创建的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  /*1、头文件定义*/
// 编辑框与数值变量相关联
int m_num1;
int m_num2;
int m_num3;

/*2、构造函数初始化*/
CTestDlg::CTestDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_DIALOG1, pParent)
//数值变量初始化
, m_num1(0)
, m_num2(0)
, m_num3(0)
{
}

/*3、DoDataExchange将对话框控件与类成员变量相关联,要配合UpDateDate使用*/
void CTestDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);

//控件与数值变量关联
DDX_Text(pDX, IDC_EDIT1, m_num1);//DDX_前缀用于数据交换
DDV_MinMaxInt(pDX, IDC_EDIT1, 0, 100);//设定数值范围,DDV_数据校验
DDX_Text(pDX, IDC_EDIT2, m_num2);
DDX_Text(pDX, IDC_EDIT3, m_num3);

}

自己编写的代码:

1
2
3
4
5
6
7
8
9
  void CTestDlg::OnClickedBtnAdd()
{
//法四:控件与数值变量相关联

UpdateData();//和DoDataExchange配合使用,参数为TRUE(缺省):函数获取对话框数据
m_num3 = m_num1 + m_num2;
UpdateData(FALSE);//参数为FALSE:以变量的值来初始化对话框控件

}

自动消息提示框:

5.控件与控件变量相关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
系统自动添加

/*1、头文件定义*/
//编辑框与控件变量相关联
CEdit m_edit1;
CEdit m_edit2;
CEdit m_edit3;


/*2、DoDataExchange将对话框控件与类成员变量相关联,要配合UpDateDate使用*/
void CTestDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);

//控件与控件变量关联
DDX_Control(pDX, IDC_EDIT1, m_edit1);
DDX_Control(pDX, IDC_EDIT2, m_edit2);
DDX_Control(pDX, IDC_EDIT3, m_edit3);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
手动添加

//法五:控件与控件变量相关联

int num1 = 0, num2 = 0, num3 = 0;
char ch1[10], ch2[10], ch3[10];

m_edit1.GetWindowText(ch1, 10);
m_edit2.GetWindowText(ch2, 10);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;
_itoa_s(num3, ch3, 10);

m_edit3.SetWindowText(ch3);

6.SendMessage()发送消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  int num1 = 0, num2 = 0, num3 = 0;
char ch1[10], ch2[10], ch3[10];
::SendMessage(GetDlgItem(IDC_EDIT1)->m_hWnd, WM_GETTEXT, 10, (LPARAM)ch1);//获取编辑框窗口对象的指针
::SendMessage(m_edit2.m_hWnd, WM_GETTEXT, 10, (LPARAM)ch2);//第一个参数直接利用控件变量获取句柄
//Windows系统中,获取文本的消息是WM_GETTEXT,将系统指定窗口的文本复制到调用者提供的一个缓存中
//wParam指定复制的字符数,lParam保存窗口文本的缓存地址

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;
_itoa_s(num3, ch3, 10);

m_edit3.SendMessage(WM_SETTEXT, 0, (LPARAM)ch3);
//WM_SETTEXT设置窗口文本的消息,lParam指定设置窗口文本的字符串地址

7.SendDlgItemMessage()直接给对话框的子控件发送消息

1
2
3
4
5
6
7
8
9
10
11
12
13
//法七:

int num1 = 0, num2 = 0, num3 = 0;
char ch1[10], ch2[10], ch3[10];
SendDlgItemMessage(IDC_EDIT1, WM_GETTEXT, 10, (LPARAM)ch1);
SendDlgItemMessage(IDC_EDIT2, WM_GETTEXT, 10, (LPARAM)ch2);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;
_itoa_s(num3, ch3, 10);

SendDlgItemMessage(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)ch3);

补充:

1
2
3
4
5
6
7
8
 //设置复选内容
SendDlgItemMessage(IDC_EDIT3, EM_SETSEL, 1, 3);
// EM_GETSEL获得编辑框中的复选内容
//EM_SETSEL设置……,EM表示Edit Control Message

//wParam接收复选内容开始位置,lParam ……结束位置.
//wParam=0,lParam=-1,表示所有文本
m_edit3.SetFocus();//设置光标

5、对话框的伸缩

CWnd::SetWindowPos

BOOL SetWindowPos (const CWnd* pWndInsertAfter, int x, int y, int cx, int cy, UINT nFlags)// 设置窗口位置、大小、z次序等

CWnd::GetDlgItem

This method retrieves a pointer to the specified control or child window in a dialog box or other window. The pointer returned is usually cast to the type of control identified by nID.
CWnd GetDlgItem (int nID *)const;

CWnd::SetDlgItemText

void SetDlgItemText(int nID, LPCTSTR lpszString);// 将nID中的文本设置为lpszString

CWnd::GetWindowRect

void GetWindowRect(LPRECT lpRect)const;// 获得窗口尺寸

CRect::IsRectNull

CRect::IsRectEmpty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void CTestDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
if (GetDlgItemText(IDC_BUTTON1,str),str == "收缩<<")
{
SetDlgItemText(IDC_BUTTON1, "括展>>");
}
else
{
SetDlgItemText(IDC_BUTTON1, "收缩<<");
}

static CRect rectlarge;//原对话框尺寸
static CRect rectsmall;//切割后的尺寸
if (rectlarge.IsRectNull())//判断矩形区域是否为空
{
CRect rectseparator;//分割线的尺寸
GetDlgItem(IDC_SEPARATOR)->GetWindowRect(rectseparator);//获得分割线的尺寸
GetWindowRect(rectlarge);//获得原对话框的尺寸
rectsmall.top = rectlarge.top;
rectsmall.left = rectlarge.left;
rectsmall.right = rectlarge.right;
rectsmall.bottom = rectseparator.bottom;//将对话框底部的尺寸更改
}
if (str == "收缩<<")
{
SetWindowPos(NULL, 0, 0, rectsmall.Width(), rectsmall.Height(), SWP_NOMOVE | SWP_NOZORDER);//设置窗口的位置和大小
}
else
{
SetWindowPos(NULL, 0, 0, rectlarge.Width(), rectlarge.Height(), SWP_NOMOVE | SWP_NOZORDER);
}
}

6、输入焦点的传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void CTestDlg::OnBnClickedOk()//注释掉默认的OnOK
{
GetNextDlgTabItem(GetFocus())->SetFocus();//按照TAB顺序循环查找【格式】->【TAB顺序】
// CDialog::OnOK();
}

WNDPROC prevproc;//窗口过程类型,接收先前的窗口过程。窗口过程函数是全局函数,里面的函数不能再用CWnd的成员函数,只能用API函数
LRESULT CALLBACK WinEnterProc//windowproc函数
(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
if (uMsg == WM_CHAR && wParam == 0x0d)
{
// ::SetFocus(::GetNextWindow(hwnd, GW_HWNDNEXT));
// ::SetFocus(::GetWindow(hwnd, GW_HWNDNEXT));
::SetFocus(::GetNextDlgTabItem(::GetParent(hwnd), hwnd, 0));//SetFocus设置焦点,GetFocus得到焦点
//GetNextWindow、GetWindow获得下一个窗口的句柄
//GetNextDlgTabItem按照TAB顺序转移焦点
// return 1;
}
else//按下的按键不是回车,让先前的窗口过程来处理
{
return prevproc(hwnd,uMsg,wParam,lParam);
}
}

BOOL CTestDlg::OnInitDialog()//对话框及其子控件创建完成将要显示之前调用。虚函数。

CDialog::OnInitDialog();

// TODO: 在此添加额外的初始化
//SetWindowLong修改指定窗口的属性,本例修改已指定的过程函数
prevproc = (WNDPROC)SetWindowLong(GetDlgItem(IDC_EDIT1)->m_hWnd, GWL_WNDPROC/*设置新的窗口过程地址*/, (LONG)WinEnterProc/*新的窗口过程地址*/);

return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}

7、默认按钮的说明

收缩按钮设置为默认按钮,即在其属性对话框中选择default button选项,就不会再由CTestDlg类的OnOK函数来响应。
当用户按下回车键时,windows将查看对话框中是否存在默认按钮, 如果没有默认按钮,就会调用虚拟的OnOK函数, 即没有默认ok按钮。这个默认OnOK函数的ID是IDOK。而不是IDC_OK.

第八章 对话框2

1、逃跑按钮

/逃跑按钮创建过程
1、创建新类(专注于逃跑功能)
存储地址的变量
OnMouseMove()
2、CTestDlg.h控件与控件变量关联,即创建了新类的对象
3、CTestDlg.cpp的OnInitDialog()中两个对象存储对方首地址
4、鼠标移动OnMouseMove()接收WM_MOUSEMOVE消息,执行程序
/

2、属性表单的创建

一个属性表单是由一个或多个属性页组成,属性页对应MFC中的CPropertyPage类,一个属性页的标题就是选项卡的名称。

步骤:

1、创建属性页

2、添加控件

组框Group Box

单选按钮Radio Button

复选框Check Box

列表框控件List Box

静态文本控件Static Box

组合框Combo Box

simple,能输入,总是显示列表框

dropdown,能输入只有单击下拉列表后,列表框才弹出

droplist,只读,只能从下来列表选择内容

3、为对话框资源创建新类

1.创建新类步骤:参考1,在“添加MFC类”中添加类名与ID号一致,基类中选择相应的类型

属性页CPropertyPage

属性表单CPropertySheet

2.在属性表单(CPropSheet)中,创建三个属性页对象,并在构造函数中初始化。用AddPage将属性页添加进属性表单,构造函数指明了2中实例化对象的方法(ID/字符串)

CPropSheet::AddPage将属性页添加到属性表单中

OnPropertysheet::DoModal()创建模态属性表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  /*定义*/
//自定义成员变量
PROP1 m_prop1;
PROP2 m_prop2;
PROP3 m_prop3;

/*初始化*/
CPropSheet::CPropSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)//用ID号来构造属性表单
{
//初始化属性页
AddPage(&m_prop1);//CPropSheet::AddPage将属性页添加到属性表单中
AddPage(&m_prop2);
AddPage(&m_prop3);
}

CPropSheet::CPropSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption, pParentWnd, iSelectPage)//用字符串来构造属性表单
{
AddPage(&m_prop1);
AddPage(&m_prop2);
AddPage(&m_prop3);
}

3.在视类中创建菜单命令响应函数,用字符串实例化属性表单对象,并用DoModal创建模态属性表单

1
2
3
4
5
6
7
void CpropView::OnPropertysheet()
{
// TODO: 在此添加命令处理程序代码
CPropSheet propsheet("属性表单");//使用字符串构造属性表单对象
// propsheet.SetWizardMode();//创建向导样式时添加
propsheet.DoModal();//创建模态属性表单
}

3、向导的创建

1、更改向导底部按钮CPropertySheet::SetWizardButtons

1
2
3
4
5
6
7
8
9
10
BOOL PROP1::OnSetActive()//右键PROP1类->类向导->虚函数->OnSetActive->编辑代码
{
// TODO: 在此添加专用代码和/或调用基类
CPropertySheet* psheet = (CPropertySheet*)GetParent();//属性页的父窗口属性表单,并将CWnd*转换为CPropertySheet*
psheet->SetWizardButtons(PSWIZB_NEXT);//CPropertySheet::SetWizardButtons设置对话框上的按钮

return CPropertyPage::OnSetActive();
}

PROP2,PROP3过程类似,SetWizardButtons参数不同。

1、设置第一个属性页

将第一个单选按钮设置为Group属性以添加成员变量。第一个单选按钮设置为Group后,之后的按钮和这个按钮属于同一组,直到遇到下一个Group。同一组内的控件关联的成员变量的值依次为1、2、3…

单击【下一步】,调用OnWizardNext()虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*PROP1.h*/
int m_occupation;

/*PROP1.cpp*/
//构造函数
PROP1::PROP1()
: CPropertyPage(IDD_PROP1)
, m_occupation(-1)
{
}

//数据交换
void PROP1::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
DDX_Radio(pDX, IDC_RADIO1, m_occupation);//单选按钮和成员变量之间的数据交换
}

//判断
LRESULT PROP1::OnWizardNext()//单击【下一步】,调用OnWizardNext()虚函数
{
// TODO: 在此添加专用代码和/或调用基类
UpdateData();//系统通过调用UpdateData()来调用DoDataExchange以完成控件与变量的数据交换
//TRUE从控件得到成员变量的值。 FAUSE用成员变量的值初始化控件
if (m_occupation == -1)//用户没有选择职业
{
MessageBox("请选择职业!");
return -1;//OnWizardNext()返回0,进入下一个属性
//返回-1,禁止属性页变更
}
if (m_workaddress == "" )//工作地点为空
{
MessageBox("请选择工作地点!");
return -1;
}
return CPropertyPage::OnWizardNext();//进入下一个页面
}
1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL PROP1::OnInitDialog()
{
CPropertyPage::OnInitDialog();

// TODO: 在此添加额外的初始化
CListBox* listbox = (CListBox*)GetDlgItem(IDC_LIST1);
listbox->AddString("北京");//CListBox::AddString添加字符串到列表框
listbox->AddString("天津");
listbox->AddString("上海");

return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}

2、设置第二个属性页

操作类似第一个属性页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LRESULT PROP2::OnWizardNext()
{
// TODO: 在此添加专用代码和/或调用基类
UpdateData();
if (m_football || m_basketball || m_volleyball || m_swim)
{
return CPropertyPage::OnWizardNext();
}
else
{
MessageBox("请选择那你的兴趣爱好!");
return -1;//返回-1,禁止属性页变更
}
}

3、设置第三个属性页

CComboBox::AddString向组合框的列表框中添加字符串选项

CComboBox::SetCurSel选择列表框中的一个字符串,并将其显示在该组合框的编辑框中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL PROP3::OnInitDialog()
{
CPropertyPage::OnInitDialog();
//组合框控件由一个编辑框和一个列表框组成
//CComboBox::AddString向组合框的列表框中添加字符串选项
//CComboBox::SetCurSel(set current selection)选择列表框中的一个字符串,并将其显示在该组合框的编辑框中
CComboBox* combobox = (CComboBox*)GetDlgItem(IDC_COMBO1);
combobox->AddString("1000元以下");
combobox->AddString("1000元-2000元");
combobox->AddString("2000元-3000元");
combobox->AddString("3000元以上");//取消属性框中的排序功能以关闭自动排序
combobox->SetCurSel(0);

return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}

CComboBox::GetCurSel得到当前选项的索引值

CComboBox::GetLBText得到对应索引值的文本并存储在字符串中

1
2
3
4
5
6
7
8
9
10
11
/*提取用户的薪资选项,并存储在m_strsalary中*/
BOOL PROP3::OnWizardFinish()
{
// TODO: 在此添加专用代码和/或调用基类
int index;
CComboBox* combobox = (CComboBox*)GetDlgItem(IDC_COMBO1);
index = combobox ->GetCurSel();//得到当前选项的索引值
combobox->GetLBText(index, m_strsalary);//得到对应索引值的文本并存储在字符串m_strsalary中

return CPropertyPage::OnWizardFinish();
}

4、将结果在视类显示

memset初始化数组

Invalidate();//让视类窗口无效,从而引起重绘,然后在OnDraw中完成信息的输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
  /*propView.h*/
//自定义成员变量
int m_ioccupation; //职业
CString m_strworkaddress;//工作地点
BOOL m_blike[4];//爱好
CString m_strsalary;//薪资水平



/*propView.cpp*/
//构造函数中初始化
CpropView::CpropView() noexcept
{
// TODO: 在此处添加构造代码
m_ioccupation = -1; //职业,对于一个组,未选为-1
m_strworkaddress = "";//工作地点
memset(m_blike , 0 , sizeof(m_blike));//爱好,将m_blike内存的sizeof(m_blike)个字节设为0
m_strsalary = "";//薪资水平
}


/*一般情况下,CPropertySheet 类的 DoModal 函数的返回值是 IDOK 或 IDCANCEL。
但是如果属性表单已经被创建为向导了,那么该函数的返回值将是 ID_WIZFINISH 或 IDCANCEL.
因此,在程序中应该对属性表单对象的 DoModal 函数的返回值进行判断,
如果返回的是【完成】按钮的 ID:ID_WIZFINISH,那么才进行输出处理。*/
void CpropView::OnPropertysheet()//菜单项响应函数
{
// TODO: 在此添加命令处理程序代码
CPropSheet propsheet("属性表单");//使用字符串构造属性表单对象
propsheet.SetWizardMode();//设置向导样式
// propsheet.DoModal();//创建模态属性表单
if (ID_WIZFINISH == propsheet.DoModal())//属性表单被创建向导,DoModal()返回ID_WIZFINISH
{
m_ioccupation = propsheet.m_prop1.m_occupation;//向导.属性页.属性页成员
m_strworkaddress = propsheet.m_prop1.m_workaddress;
m_blike[0] = propsheet.m_prop2.m_football;
m_blike[1] = propsheet.m_prop2.m_basketball;
m_blike[2] = propsheet.m_prop2.m_volleyball;
m_blike[3] = propsheet.m_prop2.m_swim;
m_strsalary = propsheet.m_prop3.m_strsalary;
Invalidate();//让视类窗口无效,从而引起重绘,然后在OnDraw中完成信息的输出
}
}


//窗口重绘
void CpropView::OnDraw(CDC* pDC)
{
CpropDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CFont font;
font.CreatePointFont(100, "宋体",pDC);//创建字体
CFont* pfont;
pfont = pDC->SelectObject(&font);//将字体选入设备描述表,并保存旧字体

//职位
CString position;
position = "你的职业:";
switch (m_ioccupation)
{
case 0:
position += "程序员";
break;
case 1:
position += "系统工程师";
break;
case 2:
position += "项目经理";
break;
}
pDC->TextOut(0, 0, position);//将文本展示在视类窗口

TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);

//工作地点
CString workplace;
workplace = "你的工作地点:";
workplace += m_strworkaddress;
pDC->TextOut(0, tm.tmHeight, workplace);

//兴趣爱好
CString hobby;
hobby = "你的爱好:";
if (m_blike[0])
hobby += "足球 ";
if (m_blike[1])
hobby += "篮球 ";
if (m_blike[2])
hobby += "排球 ";
if (m_blike[3])
hobby += "游泳 ";
pDC->TextOut(0, tm.tmHeight*2, hobby);

//薪资
CString salary;
salary = "你的薪资水平:";
salary += m_strsalary;
pDC->TextOut(0, tm.tmHeight*3, salary);

pDC->SelectObject(pfont);
}

第九章 定制应用程序外观

4、工具栏编程ToolBar

1.在工具栏上添加、删除按钮

创建按钮:按钮和菜单项用一样的ID

按钮之间的分隔符:用鼠标把按钮向右拖动一段距离

删除按钮:将按钮脱出工具栏

2.创建工具栏

分析系统创建的工具栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmainframe头文件中
CToolBar m_wndToolBar;

cmainframe.cpp中
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
......
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("未能创建工具栏\n");
return -1; // 未能创建
}

....
// TODO: 如果不需要可停靠工具栏,则删除这三行
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);//工具栏对象的成员函数EnableDocking,表示工具栏对象可以停靠
//CBRS_ALIGN_ANY允许停靠在任意位置
EnableDocking(CBRS_ALIGN_ANY);//CFrameWnd对象的EnableDocking成员函数,表示主框架窗口可以被停靠
DockControlBar(&m_wndToolBar);//工具栏停靠在主框架窗口上

创建自定义工具栏(参照系统工具栏)

1.新建ToolBar资源

2.构造ToolBar对象

3.调用Create函数创建工具栏,并将其与ToolBar对象关联

4.LoadToolBar加载工具栏

5.设置停靠位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
头文件  
CToolBar m_newtoolbar;//2.构造ToolBar对象

源文件
if (!m_newtoolbar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_RIGHT | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_newtoolbar.LoadToolBar(IDR_TOOLBAR1))
//3.调用Create函数创建工具栏,并将其与ToolBar对象关联
//4.LoadToolBar加载工具栏
{
TRACE0("未能创建工具栏\n");
return -1; // 未能创建
}

//5.设置停靠位置
m_newtoolbar.EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_newtoolbar);

改进方法CFrameWnd::ShowControlBar隐藏或显示指定的控制条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CMainFrame::OnViewNewtoolbar()
{
/*
if (!m_newtoolbar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_RIGHT | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_newtoolbar.LoadToolBar(IDR_TOOLBAR1))
{
TRACE0("未能创建工具栏\n");
return -1; // 未能创建

m_newtoolbar.EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_newtoolbar);
*/

ShowControlBar(&m_newtoolbar, !m_newtoolbar.IsWindowVisible(), 1);
}


void CMainFrame::OnUpdateViewNewtoolbar(CCmdUI* pCmdUI)
{
// 在菜单项前添加标记,见第六章
pCmdUI->SetCheck(m_newtoolbar.IsWindowVisible());
}

5、状态栏StatusBar

步骤:

1.在资源视图String Table中添加响应的ID

2.在源文件中将响应的ID添加进indicator(状态行指示器)中

系统创建的状态栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  CStatusBar        m_wndStatusBar;//定义默认状态栏对象

static UINT indicators[] =
{
ID_SEPARATOR, // 状态行指示器
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
// IDS_TIMER,
// IDS_PROGRESS,
};

//OnCreate()中
if (!m_wndStatusBar.Create(this))//创建状态栏对象
{
TRACE0("未能创建状态栏\n");
return -1; // 未能创建
}
m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));//设置状态指示器(数组设置状态栏内容,数组元素个数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在OnTimer()中添加
//在状态栏显示动态变化的系统时间
CTime time;//接收系统时间
CString str;//系统时间文本
CClientDC dc(this);//调用GetTextExtent()
CSize sz;//接收时间字符串大小
int indicatorindex = 0;//indicator索引

time = CTime::GetCurrentTime();//获取系统时间保存进time中
str = time.Format("%H:%M:%S");//将time中的时间格式化
indicatorindex = m_wndStatusBar.CommandToIndex(IDS_TIMER);//获取ID在indicator中的索引
sz = dc.GetTextExtent(str);//获取文本显示宽度

m_wndStatusBar.SetPaneInfo(indicatorindex, IDS_TIMER, SBPS_NORMAL, sz.cx);//设置状态栏右侧小窗口属性
m_wndStatusBar.SetPaneText(indicatorindex, str);//将str显示到indicator数组对应的小窗口

6、进度栏

CProgressCtrl::Create//进度栏类CProgressCtrl

BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);// 创建进度栏

CProgressCtrl::SetPos

int SetPos( int nPos);// 设置进度栏当前进度

CStatusBar::GetItemRect

void GetItemRect(int nIndex,LPRECT lpRect)const;// 得到状态栏小窗格大小

🌟补充消息响应过程

1.头文件声明(定义)函数afx_msg LRESULT OnProgress(WPARAM, LPARAM);

2.消息与函数关联ON_MESSAGE(UM_PROGRESS, &OnProgress)

3.实现函数LRESULT CMainFrame::OnProgress(WPARAM wParam, LPARAM lParam)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CMainFrame::OnPaint()//窗口大小发生变化->窗口重绘->重新获得rect
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CFrameWnd::OnPaint()

CRect rect;
m_wndStatusBar.GetItemRect(5, &rect);//获得窗格大小并储存在rect中
if (!m_progress.m_hWnd)//如果句柄没有值,说明对象没有创建
{
m_progress.Create(WS_VISIBLE | WS_CHILD | PBS_SMOOTH, rect, &m_wndStatusBar, 123);//创建进度栏,WS_CHILD:The window is a child window.
}
else
{
m_progress.MoveWindow(rect);//移动窗格,The CRect object or RECT structure that specifies the new size and position.
}
m_progress.SetPos(50);//设置进度
}

7、在状态栏上显示鼠标当前位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CstyleView::OnMouseMove(UINT nFlags, CPoint point)
//视类中捕获鼠标移动的消息WM_MOUSEMOVE
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CString str;
str.Format("x = %d y = %d",point.x, point.y);//格式化鼠标位置
((CMainFrame*)GetParent())->m_wndStatusBar.SetWindowText(str);
/*上述代码中,首先格式化鼠标当前位置的信息。然后为了把该信息显示在
状态栏的第一个窗格上,需要获取状态栏对象。而状态栏对象是在框架类窗口中定义的,同时,
框架类窗口是视类窗口的父窗口,因此在视类对象中通过调用 GetParent 函数就可以得到
视类的父窗口,即框架窗口。因为该函数返回的是一个CWnd 类型的指针,而这里需要的
是 CMainFrame 类型的指针,所以需要进行一个转换。然后利用框架窗口对象去调用该对
象内部的状态栏成员变量:m_wndStatusBar,以得到状态栏对象,*/

/*因为上述代码用到了框架类的类型,所以要在头文件中包含框架类头文件*/

CView::OnMouseMove(nFlags, point);
}

十、绘图控制

1、简单绘图

CPen pen

SelectObject

GetStockObject

FromHandle

SetPixel

Rectangle

Ellipse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*头文件*/
//自定义成员变量
private:
UINT m_nDrawType;//标记用户选择那种图形
CPoint m_ptOrigin;//保存鼠标左键按下的点

/*构造函数*/
CGraphicView::CGraphicView() noexcept
{
// TODO: 在此处添加构造代码
m_nDrawType = 0;//初始化绘图图形类型
m_ptOrigin = 0;
}

/*视类消息处理程序*/
void CGraphicView::OnDot()//点
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 1;
}


void CGraphicView::OnLine()//直线
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 2;
}


void CGraphicView::OnRectangle()//矩形
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 3;
}


void CGraphicView::OnEllipse()//椭圆
{
// TODO: 在此添加命令处理程序代码
m_nDrawType = 4;
}


void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOrigin = point;//保存鼠标左键按下的位置

CView::OnLButtonDown(nFlags, point);
}


void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)//鼠标左键抬起,实现绘图功能
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);//绘图操作首先要有DC对象
CPen pen(PS_SOLID, 10, RGB(255,0,0));//线条的颜色由DC中的画笔颜色确定,构造CPen对象并指定其颜色
dc.SelectObject(&pen);//将画笔选入设备描述表
//加载资源用LoadBitmap等,CDC对象用SelectObject(&xxx)
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));//GetStockObject调用NULL_BRUSH创建透明画刷
//CBrush的静态成员变量FromHandle将画刷句柄转化为指向画刷对象的指针
//FromHandle的参数是HBRUSH类型,强制类型转化
dc.SelectObject(pBrush);//将画刷选入设备描述表

switch (m_nDrawType)
{
case 1:
dc.SetPixel(point, RGB(255,0,0));//在指定的点设置一个像素
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin,point));//利用CRect对象绘制矩形
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));//绘制椭圆
default:
break;
}
CView::OnLButtonUp(nFlags, point);
}

注意将CDC对象加入设备miaoshub;

注意静态成员函数的调用方式:要声明是哪个类的。

2、线宽线型(设置对话框)

1、将设置对话框创建一个新的对话框类

2、在主菜单添加设置菜单项并设置消息响应函数

3、控件关联无符号整型变量,View类创建同类型变量以保存其值

1
2
3
4
5
6
7
8
9
10
11
12
13
void CGraphicView::OnSetting()
{
// TODO: 在此添加命令处理程序代码
CSettingDlg dlg;//实例化设置对话框对象
dlg.m_nLineWidth = m_nLineWidth;//保持之前设置过的线宽
dlg.m_nLineStyle = m_nLineStyle;//保持之前设置过的线型
if (IDOK == dlg.DoModal())
{
m_nLineWidth = dlg.m_nLineWidth;//保存新设置的值
m_nLineStyle = dlg.m_nLineStyle;//保存设置的线型,WINGDI.h定义了一些符号常量,包括线型
//本例设置的对话框的线型排序正好按照WINGDI.h的顺序定义
}
}

3、颜色对话框

CColorDialog默认颜色对话框类了解(m_cc、CHOOSECOLOR、rgbResult、Flags (CC_RGBINIT))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void CGraphicView::OnColor()
{
// TODO: 在此添加命令处理程序代码
CColorDialog colordlg;//CColorDialog颜色对话框类
colordlg.m_cc.Flags |= CC_RGBINIT;//设置颜色对话框初始选择的颜色,需要设置该对话框的CC_RGBINIT标记
/*实际上,当在创建 CColorDialog 对象 dlg 时,它的数据成员 m_cc 中的 Flags 成员已经具有了一些初始的默认标记。
当我们将 CC_RGBINTT 标记直接赋给 Flags 成员时,就相当于将 Flags 成员初始默认的标记都去掉了。
这里不能给 Flags 标记直接赋值,应利用或操作(|)将CC_RGBINIT 标记与 Flags 先前的标记组合起来。*/
colordlg.m_cc.rgbResult = m_ctr;//保持之前设置过的颜色
if (IDOK == colordlg.DoModal())//创建颜色对话框
{
m_ctr = colordlg.m_cc.rgbResult;//CColorDialog类有一个结构体类型的变量m_cc
//CHOOSECOLOR结构体的rgbResult变量保存了用户选择的颜色
//将用户选择的颜色保存在m_ctr中
}
}

4、字体对话框

DeleteObject释放这个字体资源

CFont::CreateFontIndirect利用lpLogFont指向的LOGFONT结构体中的一些特征初始化CFont对象

Invalidate()引起窗口重绘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
头文件
CFont m_font;//字体对象
CString m_strFontName;//保存所选字体的名称

源文件
void CGraphicView::OnFont()
{
// TODO: 在此添加命令处理程序代码
CFontDialog fontdlg;//字体对话框对象
if (IDOK == fontdlg.DoModal())
{
if (m_font.m_hObject)
/*利用CGdiobject 对象的数据成员 m_hObject 来判断 m_font 对象是否已经与某个字体资源相关联了,
该变量保存了与CGdiObject对象相关联的Windows GDI 的资源句柄。
如果已经有关联了,则调用 DeleteObject 释放这个字体资源*/
{
m_font.DeleteObject();
}
m_font.CreateFontIndirect(fontdlg.m_cf.lpLogFont);//CFont::CreateFontIndirect利用lpLogFont指向的LOGFONT结构体中的一些特征初始化CFont对象
m_strFontName = fontdlg.m_cf.lpLogFont->lfFaceName;//lfFaceName保存了字体名字
Invalidate();
/*窗口的客户区无效意味着需要重绘,例如,如果一个被其它窗口遮住的窗口变成了前台窗口,
那么原来被遮住的部分就是无效的,需要重绘。这时Windows会在应用程序的消息队列中放置WM_PAINT消息。
MFC为窗口类提供了WM_PAINT的消息处理函数OnPaint,OnPaint负责重绘窗口。
视图类有一些例外,在视图类的OnPaint函数中调用了OnDraw函数,实际的重绘工作由OnDraw来完成。*/
/*系统会在多个不同的时机发送WM_PAINT消息:
当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口时,等等,
这些动作都是由系统管理的,应用只是被动地接收该消息,在消息处理函数中进行绘制操作;
大多数的时候应用也需要能够主动引发窗口中的绘制操作,比如当窗口显示的数据改变的时候,
这一般是通过InvalidateRect和 InvalidateRgn函数来完成的。*/
}
}

5、示例对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void CSettingDlg::OnChangeLineWidth()
//对编辑框控件中的文本改变时,会向父窗口发送EN_CHANGE消息
{

Invalidate();//让窗口重绘以在OnPaint中完成示例线条的绘制
}


void CSettingDlg::OnClickedRadio1()
//单击Radio Button时,按钮会向对话框发送BN_CLICKED消息
{
Invalidate();
}

void CSettingDlg::OnClickedRadio2()
{
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}

void CSettingDlg::OnClickedRadio3()
{
// TODO: 在此添加控件通知处理程序代码
Invalidate();
}


void CSettingDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialog::OnPaint()
UpdateData();//当控件与一个成员变量相关联时,如果想让控件上的值反应到成员变量上,必须调用UpdateData()
CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
dc.SelectObject(&pen);

CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
/*要想在组框中绘图,那么首先要得到组框的矩形区域范围。
这可以通过调用GetDlgltem 函数来得到指向组框窗口对象的指针,
然后利用 GetWindowRect 函数获得组框窗口矩形区域的大小,参数是指向CRect或RECT结构体的变量,接收屏幕坐标
需要提醒读者注意的是,这里不能直接调用 GetWindowRect 函数,否则得到的将是对话框的矩形区域大小。*/
ScreenToClient(&rect);//得到矩形区域的大小后,将其原点从屏幕坐标转化为客户坐标(即将应用程序坐标转化为设置对话框坐标)

dc.MoveTo(rect.left+20,rect.top+rect.Height()/2);//组框左上角的y值+矩形区域高度的一半,即将线条移动到示例窗口中间
dc.LineTo(rect.right - 20, rect.top + rect.Height() / 2);
}

6、改变对话框及其控件的背景和文本WM_CTLCOLOR

1、改变对话框及其控件的背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//头文件声明
CBrush m_brush;//画刷

//构造函数初始化
m_clr = RGB(255,0,0);
m_brush.CreateSolidBrush(RGB(0,0,255));//利用CreateSolidBrush函数将m_brush初始化画刷颜色

//消息响应函数
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)//每一个控件(对话框和子控件)在绘制时都发送WM_CTLCOLOR消息
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

// TODO: 在此更改 DC 的任何特性

// TODO: 如果默认的不是所需画笔,则返回另一个画笔
if (IDC_LINE_STYLE == pWnd->GetDlgCtrlID())//判断是否为线型组框
{
pDC->SetTextColor(RGB(255,0,0));//设置文本颜色为红色
pDC->SetBkMode(TRANSPARENT);//将控件上的文字背景设置为透明
return m_brush;//如果想要改变背景颜色,只需要自定义一个画刷,然后让OnColor函数返回这个画刷句柄即可
}
if (IDC_LINE_WIDTH == pWnd->GetDlgCtrlID())
{
pDC->SetTextColor(RGB(255, 0, 0));
// pDC->SetBkMode(TRANSPARENT);
pDC->SetBkColor(RGB(255, 0, 0));//设置窗口背景色
return m_brush;
}
return hbr;
}

2、改变控件的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//头文件声明
private:
CFont m_font;//改变控件上的文本字体

//构造函数初始化
m_font.CreatePointFont(200,"华文行楷");//初始化字体

//消息响应函数
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)//每一个控件(对话框和子控件)在绘制时都发送WM_CTLCOLOR消息
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

// TODO: 在此更改 DC 的任何特性

// TODO: 如果默认的不是所需画笔,则返回另一个画笔
if (IDC_TEXT == pWnd->GetDlgCtrlID())
{
pDC->SelectObject(&m_font);//将字体选入设备描述表->改变字体
}
return hbr;
}

3、改变按钮的背景色及文本

7、位图的显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL CGraphicView::OnEraseBkgnd(CDC* pDC)//擦除背景时,系统发送WM_ERASEBKGND消息
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BITMAP1);
BITMAP bmp;//定义BITMAP结构体变量
bitmap.GetBitmap(&bmp);//CBitmap::GetBitmap用位图信息填充BITMAP结构体

CDC dcCompatible;//创建兼容DC
dcCompatible.CreateCompatibleDC(pDC);//兼容DC(源DC)和当前DC兼容

dcCompatible.SelectObject(&bitmap);//将位图选入兼容DC,从而确定兼容DC显示表面的大小

CRect rect;
GetClientRect(&rect);//获得目的DC客户区大小
// pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);//BitBlt()将源DC的位图1:1复制到目的DC
pDC->StretchBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0,bmp.bmWidth,bmp.bmHeight, SRCCOPY);
//StretchBlt复制位图并实现拉伸或压缩;bmp.bmWidth,bmp.bmHeight存储源矩形的宽度和高度
return TRUE;//已经擦除过窗口背景了,返回非零值
// return CView::OnEraseBkgnd(pDC);//将窗口背景擦除
}

//构造函数初始化