目录:
Win32:
1. Win32 简介
2. 注册窗口类
3. 创建窗口
4. 注册窗口类时的附加数据缓冲区
5. 显示窗口
6. 消息循环
7. 消息的分类
8. 消息队列
9. 消息
10. 菜单
11. 资源的使用(菜单资源/图标资源/光标资源/字符串资源/加速键资源/绘图资源)
12. 坐标系
13. 文字的绘制
14. 对话框窗口
15. 控件
16. Windows 库程序
17. Windows 文件系统
18. 内存管理
19. Windows 进程
20. Windows 线程
------------------------------
MFC:
21. MFC 的概念和作用
22. 第一个MFC程序 // 程序的执行过程(程序启动机制) (MFC第一大机制) 和 窗口创建机制 (MFC第二大机制)
23. MFC的消息映射机制 (MFC第三大机制)
24. MFC消息分类
25. MFC的菜单
26. 工具栏
27. 状态栏
28. MFC的视图窗口
29. MFC 运行时类信息机制 (MFC第四大机制)
30. 动态创建机制 (MFC第五大机制)
31. 切分窗口
32. MFC的文档
33. 单文档视图构架程序
34. 多文档视图构架程序
35. MFC的绘图画刷都有哪些类型
36. MFC文件的操作
37. 序列化
38. 对象的序列化 (MFC 第六大机制)
39. MFC对话框
40. 控件操作
41. MFC控件介绍
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Begin
一. Win32 简介
windows 32位系统编程
主要内容:
1> windows 编程基础
2> windows 消息 和 消息机制
3> windows 绘图 和 字体
4> windows 控件
5> windows 资源管理
6> windows 文件处理
7> windows 内存管理
8> windows 的进程 和 线程
windows 编程基础:
windows 应用程序的类型:
1> 控制台程序 Console
DOS 程序, 本身没有窗口,通过 Windows DOS 窗口执行
2> 窗口程序
拥有自己的窗口,可以与用户交互
3> 库程序
存放代码,数据的程序,执行文件可以从中取出代码执行和获取数据,
包括:
静态库程序
扩展名 .LIB 在执行文件执行时把代码复制到执行文件中;
动态库程序
扩展名 .DLL 程序在编译时会保存动态库中的函数的地址(内存中的地址),
执行时会根据保存的地址在内存中找对应的函数执行,
因此动态库程序会进入内存,也就是为什么动态库程序有入口函数;
入口函数对比:
Win32 控制台应用程序的入口函数是: main()
Win32 窗口程序的入口函数是: WinMain()
静态库没有入口函数(无法执行,进不了内存);
动态库的入口函数是: DllMain() 动态库里的程序可以借助于调用者进内存 但动态库不是一个可执行文件,
我们使用动态库的时候记录的地址是内存中的地址,如果动态库都不进内存那么调用者保存的地址就无意义;
文件的存在方式:
控制台程序,窗口程序 ---> .exe 文件
动态库程序 ----> .dll 文件
静态库程序 ----> .lib 文件
执行方式
控制台程序 ---- 在DOS窗口内执行
窗口程序 ---- 拥有自己的窗口在自己的窗口内执行
动态库程序 ---- 本身无法执行,由可执行程序或其它的DLL调用
静态库程序 ---- 不存在执行,代码会嵌入到可执行文件或DLL等中;
Windows 开发工具和库
开发工具:
Visual Studio C++
VC的编译工具:
编译器 CL.EXE 将源代码编译成目标代码
链接器 LINK.EXE 将目标代码,库链接 生成最终文件
资源编译器 RC.EXE 将资源编译,最终通过链接器存入最终文件(.rc)
Windows 库和头文件
Windows 库
kernel32.dll --- 提供了核心的 API, 例如进程,线程,内存管理等;
user32.dll --- 提供了窗口,消息等API
gdi32.dll --- 绘图相关的API
头文件
windows.h --- 所有windows头文件的集合
windef.h --- windows 数据类型
winbase.h --- kernel32的API
wingdi.h --- gdi32的API
winuser.h --- user32的API
winnt.h --- UNICODE字符集支持
窗口程序的入口函数才是 WinMain()
int WINAPI WinMain(
HINSTANCE hInstance, //当前程序的实例句柄,能找到本进程所在的内存(可以通过句柄找到内存但句柄不是指针)
HINSTANCE hPrevInstance, //废弃参数(16位操作系统有用),当前程序前一个实例句柄,一般为NULL;
LPSTR lpCmdLine, //命令行参数字符串, char*
int nCmdShow //窗口的显示方式(最大化/最小化/原样)
);
hPrevInstance ---- Win32 下, 一般为 NULL
注:
窗口是否存在不是以该窗口是否显示为准,而是以在内存中是否有该窗口的数据为准;
MessageBox() 一般是用来弹出提示框的,是一个阻塞函数,原型是:
int MessageBox(
HWND hWnd, //父窗口句柄,没有即为 NULL
LPCTSTR lpText,//提示框内部要显示的内容, char*
LPCTSTR lpCaption, //显示在标题栏中的文字, char*
UINT uType //提示框中的 按钮(通过 "|" 按位或) 图标 显示类型, UINT(unsigned int)
); //返回点击的按钮ID
uType的值有:
MB_ABORTRETRYIGNORE ----- 包含3个按钮( 终止 重试 忽略 )
MB_CANCELTRYCONTINUE ----- 包含3个按钮( 取消 再试一次 继续 )
MB_HELP ----- 包含1个按钮( 帮助 )
MB_OK ----- 包含1个按钮( 确定 )
MB_OKCANCEL ----- 包含2个按钮( 确定 取消 )
MB_RETRYCANCEL ----- 包含2个按钮( 重试 取消 )
MB_YESNO ----- 包含2个按钮( 是 否 )
MB_YESNOCANCEL ----- 包含3个按钮( 是 否 取消 )
MB_ICONEXCLAMATION / MB_ICONWARNING ----- 惊叹号图标 "!"图标
MB_ICONINFORMATION / MB_ICONASTERISK ----- 小写字母 "i" 图标
MB_ICONQUESTION ----- 问号图标 "?"图标
MB_ICONSTOP / MB_ICONERROR / MB_ICONHAND ----- 错误图标 "X"图标
返回值:
IDABORT ---- 终止按钮被点击
IDCANCEL ---- 取消按钮被点击
IDCONTINUE ---- 继续按钮被点击
IDIGNORE ---- 忽略按钮被点击
IDNO ---- 取消(否)按钮被点击
IDOK ---- 确定按钮被点击
IDRETRY ---- 重试按钮被点击
IDTRYAGAIN ---- 再次尝试按钮被点击
IDYES ---- 确定按钮被点击
注:
在Windows下如果一个遇到一个阻塞函数要问两个问题:
1> 该函数什么情况下会阻塞
2> 该函数什么情况下解除阻塞
规律:
在Windows平台下如果一个数据类型是以大写"H"开头基本上就能确定它是一个句柄;
HINSTANCE ----- 当前程序实例句柄
HWND ----- 窗口句柄
句柄:
windows中句柄有很多种,但每种句柄所找到的内存所保存的数据是不一样的;
如:
HINSTANCE ---- 当前程序实例句柄,找到的是就是当前进程所占的内存;
HWND ---- 窗口句柄,找到的内存保存的是关于窗口的数据;
扩:
VC++ 6.0 里面编译如果出现 XXXX.pch 打不开 或 找不到 这个错误 不用理会这是 VC++6.0 的bug 不用理会
一个程序的入口函数是谁来决定?
是由操作系统内核的加载器来决定的;
编译和连接的功能就是把高级语言翻译成二进制的机器语言;
一个程序如果有入口函数就代表它可以被CPU执行;
编译器 链接器
.c/.cpp ------> 目标文件 -------> .exe /.dll/ .lib
编写窗口程序的步骤:
1> 定义WinMain函数
2> 定义窗口处理函数(自己定义,处理消息) ---> WindowProc()
3> 注册窗口类(向操作系统内核写数据) ---> RegisterClass()
4> 创建窗口(申请一块内存,向内存中扔关于窗口的数据,在内存中创建窗口) ---> CreateWindow()
5> 显示窗口(根据内存中的数据,在显示器中绘制出来) ---> ShowWindow() / UpdateWindow()
6> 消息循环(提取(消息队列)/翻译/派发消息(谁处理消息就把消息派发给谁)) ---> GetMessage()/TranslateMessage()/DisptachMessage()
7> 消息处理
注:
窗口销没销毁 和 程序退没退出 是没有关系的;
资源的使用:
windows下 .c 文件经过 CL.EXE 编译后生成 .obj 文件, .obj 文件里面保存 .c 编译后的二进制数据
当程序中要使用到资源的时候就需要使用 .rc 资源脚本文件
.rc资源脚本文件的书写格式:
数字ID 资源的类型 资源
如:
100 ICON small.ico
RC.EXE 可以编译 rc 文件, 编译后生成 .RES 文件, .RES 文件里面保存了 .rc 编译后的二进制数据
最后再通过LINK.EXE连接两个文件.obj和.res文件(如果.obj中使用了库此时还要连接所需的库)生成最终的.exe可执行文件;
编译连接程序的过程:
CL.EXE
.c/.cpp ------> .obj
\ LINK.EXE
| ---------> .exe
RC.EXE /
.rc ---------> .res
NMAKE
Makefile的一个解释执行的工具,根据Makefile文件中的定义,编译和链接程序,最终生成文件;
Makefile(后缀为 .mak)
定义编译和链接等操作的脚本文件(把项目的处理命令写入),一般对整个项目进行处理;
书写格式:
依赖行: 依赖项
命令行
命令行
...
如:
HELLO(依赖行): (xxxx(依赖项))
CL.EXE /c Hello.c //命令行
RC.EXE HELLOWND.RC //命令行
LINK.EXE Hello.obj Hello.res user32.lib //命令行
Makefile文件中可以有多个依赖行,但如果一个Makefile文件中有多个依赖行,默认只执行第一个依赖行,其它依赖行不执行;
如果想执行其它依赖行,在执行 NMAKE 命令的时候需要在后面加参数
如:
HELLO:
CL.EXE /c Hello.c
RC.EXE Helloc.rc
LINK.EXE Hello.obj Hello.res user32.lib
CLEAN:
del *.obj
del *.res
del *.exe
NMAKE /f Hello.mak CLEAN // "/f"是有选择的执行依赖行, 选择谁就在 后面写上依赖行的名字即可
如在Makefile文件中我们把 CLEAN 依赖行, 放在 HELLO 依赖行的":"后面,
则在执行的时候会先执行 HELLO 后面的依赖项也就是 CLEAN , 执行完以后才会执行 HELLO 依赖行后面的 命令行;
如:
HELLO: CLEAN1
CL.EXE /c Hello.c
RC.EXE Helloc.rc
LINK.EXE Hello.obj Hello.res user32.lib
CLEAN:
del *.obj
del *.res
del *.exe
NMAKE Hello.mak //这样就会先执行 CLEAN 然后再执行 HELLO
执行步骤:
NMAKE Hello.mak
执行方式
NMAKE 首先找到第一个依赖行,检查依赖行的依赖项,
如果发现依赖项,首先执行依赖项里的命令行,
如果没有依赖项则执行自己的命令行;
字符编码: 按年代推进
ASC --- 7位代表一个字符,一共128个字符
'A' ---- 65 ---- 0x41
'a' ---- 97 ---- 0x61
0 ---- 48 ---- 0x30
ASCII --- 8位代表一个字符,一共256个字符
CODEPAGE(代码页,不在扩内存的情况下字符的处理方法,每种代码页ID代表一种语言,936是汉语代码页ID)
MBCS(编码族,很多分支)
DBCS ---- 单双字节混合编码(字母1个字节,汉字2个字节),计算机主流编码方式,但先天有缺陷
如:
A 我 是 程 序 员 // A 是占一个字节,汉字占2个字节
01 0203 0405 0607 0809 0A0B //这样会正确的拿到数据,但计算机不能确定是1个字节还是2个字节
如果是: 0102 0304 0506 0708 090A 0B 这样就会出现乱码
UNICODE(编码族,很多分支)
全部按两个字节来编码
如:
A 我 是 程 序 员
0001 0203 0405 0607 0809 0A0B
数据类型:
宽字节字符:
wchar_t 每个字符占2个字节(使用的是UNICODE编码方式)
char 每个字符占1个字节或2个字节(使用的是 DBCS编码方式)
wchar_t 实际是 unsigned short 类型,定义时需要增加"L",目的是告诉编译器按照双字节编译字符串,采用UNICODE编码;
使用时需要使用支持 wchar_t 的函数操作宽字节字符串;
如:
wchar_t* pwszText = L"Hello wchar";//此行的"L"作用是通知编译后面的字符串按2个字节进行编码
wprintf(L"%s\n", pwszText);//此行的"L"作用是通知编译器后面的字符串按2个字节解码
wcslen(pwsaText); //结果是 11 因为 wcslen()求的是字符串的字符个数,不是字符串所占的字节数,strlen()也是一样
归纳:
所有操作 char 类型的函数,都不要套用在 wchar_t 类型上,
但凡有操作 char 类型的函数,都有一个对应的操作 wchar_t 类型的另一个函数;
扩:
在不清楚到底使用那一种字符编码的时候可以使用预定义宏来处理
如:
#define WIDECHAR
void T_char(){
#ifdef WIDECHAR
wchar_t* pszText = L"hello";
wprintf(L"%s\n",pszText);
#else
char* pszText = "hello";
printf("单:%s\n",pszText);
#endif
}
上面是按照 双字节编码
void T_char(){
#ifdef WIDECHAR
wchar_t* pszText = L"hello";
wprintf(L"%s\n",pszText);
#else
char* pszText = "hello";
printf("单:%s\n",pszText);
#endif
}
#define WIDECHAR
上面是按照 单字节编码, #ifdef / #ifndef 都是本文件内具有向上溯源性因此上面是按照 单字节编码;
UNICODE字符打印
printf() 打印 char/char*
wprintf() 打印 wchar_t 但绝大多数的汉字使用wprintf()都无法打印,因为wparing()对UNICODE字符打印支持不完善;
在Windows下使用WriteConsole() API打印UNICODE字符,可以打印UNICODE和DBCS编码,
在使用该函数前定义了 UNICODE 宏则该函数打印的就是UNICODE编码,如果没有则打印DBCS编码
WriteConsole()函数原型:
BOOL WriteConsole(
HANDLE hConsoleOutput, // 标准输出句柄,通过 GetStdHandle()可以拿到
CONST VOID* lpBuffer, // 准备输出内容的缓冲区
DWORD nNumberOfCharsToWrite, //准备输出内容的长度(字符的个数)
LPDWORD lpNumberOfCharsWritten,//用来接收实际输出内容的长度
LPVOID lpReserved //备用,给NULL即可
);
Windows下的句柄都是用来找到内存的,但它不是指针,但有3个例外:
1> 标准输出句柄
2> 标准输入句柄
3> 标准错误句柄
通过GetStdHandle()可以拿到 标准 输入/输出/错误 的3个句柄;
GetStdHandle()函数原型:
GetStdHandle(
DWORD nStdHandle //想要获取的句柄
);
nStdHandle的取值:
STD_INPUT_HANDLE ---- 标准输入句柄
STD_OUTPUT_HANDLE ---- 标准输出句柄
STD_ERROR_HANDLE ---- 标准错误句柄
返回获取到的相应句柄
二. 注册窗口类
窗口类是包含了窗口的各种参数信息的数据结构,
每个窗口都具有自己的窗口类,窗口都是基于窗口类创建,
每个窗口类都具有一个名称,创建窗口前必须把窗口类注册到系统;
窗口类的分类
1> 系统窗口类
系统已经定义好的窗口类,所有应用程序都可以直接使用
2> 应用程序全局窗口类
由用户自己定义,当前应用程序所有模块都可以使用
比如:
有一个大的进程里面有10个子进程,如果其中一个子进程注册了一个全局窗口类,
其余9个子进程都可以使用这个全局窗口类创建窗口;
3> 应用程序局部窗口类
由用户自己定义,当前应用程序中本模块可以使用
注:
全局窗口类和局部窗口类,不建议使用全局窗口类,因为全局窗口类会增加程序的冗余性(程序中模块和模块之间牵扯太多)
全局窗口类实现的功能局部窗口类都能实现;
程序设计的时候尽量使用搭积木的模式,拆掉其中一块木头整个积木不会垮掉;
窗口程序创建的步骤和使用到的函数:
1> 定义WinMain入口函数
2> 定义窗口处理函数, 函数名自定义
3> 注册窗口类,定义一个 WNDCLASS 结构体 然后依次为该结构体赋值,最后通过 RegisterClass()把该结构体写入操作系统内核中;
4> 创建窗口, 使用 CreateWindow()函数先在内存中申请一块空间,然后把需要创建的窗口的信息填上,最后返回申请内存的首地址;
5> 显示窗口, 使用 ShowWindow()显示窗口 / UpdateWindow()更新窗口
6> 消息循环, GetMessage()从消息队列中抓消息, TranslateMessage()翻译消息, DisptachMessage()派发消息给消息处理函数;
7> 消息处理
应用程序窗口类的注册
RegisterClass()/RegisterClassEx() 两个窗口类注册函数功能大体一样, 带 Ex 的称为加强版,加强版就使用 WNDCLASSEX 结构体
RegisterClass()的原型:
ATOM RegisterClass(const WNDCLASS* lpWndClass /* 窗口类的数据 */);
注册成功后,返回一个数字标识,返回0表示注册失败;
RegisterClassEx()的原型:
ATOM RegisterClassEx(const WNDCLASSEX* lpwcx /* 窗口类的数据 */);
注册成功后,返回一个数字标识,返回0表示注册失败;
扩:
ATOM == WORD == unsigned short
加强版 WNDCLASSEX 结构体的成员有: 去掉加强版 WNDCLASSEX 结构体的第一个成员和最后一个成员 就是普通版的 WNDCLASS 结构体
typedef struct_WNDCLASSEX{
UINT cbSize; //结构体的大小 sizeof()求大小,因为结构体涉及到对齐和补齐
UINT style; //窗口类的风格
WNDPROC lpfnWndProc; //窗口处理函数
int cbClsExtra; //窗口类的附加数据 buff 的大小,0表示不开缓冲区,赋值的数据占4个字节也就是说可以赋值指针,
所有基于该窗口类创建出来的窗口都共享该缓冲区
int cbWndExtra; //窗口的附加数据 buff 的大小,0表示不开缓冲区,赋值的数据占4个字节也就是说可以赋值指针,
不共享,哪个窗口写的数据只有该窗口才能获取出数据来
HINSTANCE hInstance; //当前模块的实例句柄
HICON hIcon; //窗口的大图标, NULL 表示使用默认的大图标
HCURSOR hCursor; //鼠标的光标设置,NULL 表示使用默认的鼠标光标,在注册窗口类里设置的鼠标光标,只对窗口客户区有效
HBRUSH hbrBackground; //绘制窗口背景的画刷句柄,背景颜色指的是窗口主体的颜色,一个窗口的主体是指窗口客户区;
LPCTSTR lpszMenuName; //窗口菜单的资源ID字符串,如果不在此挂菜单也可以在创建窗口的时候再挂菜单,
在此挂资源菜单基于该窗口类创建的窗口都有该资源菜单,NULL 表示使用默认菜单
LPCTSTR lpszClassName; //窗口类的名称
HICON hIconSm; //窗口的小图标,窗口左上角的图标,NULL 表示使用默认的小图标
}WNDCLASSEX,*PWNDCLASSEX;
注:
把上面 WNDCLASSEX 或 WNDCLASS 结构体的所有数据通过 RegisterClass() 写入操作系统内核中就叫注册窗口类;
如:
RegisterClassEx(&msg);
扩:
窗口类附加数据缓冲区 和 窗口附加数据缓冲区 赋值的数据是占4个字节,指针也是占4个字节也就是说可以赋值指针,这样附加什么数据都可以了;
窗口类附加数据缓冲区 和 窗口附加数据缓冲区 的区别:
区别:
窗口类附加数据缓冲区: 所有基于该窗口类创建出来的窗口都共享的缓冲区
SetClassLong()向窗口类附加数据缓冲区里写数据,GetClassLong()从窗口类附加数据缓冲区里读数据
窗口附加数据缓冲区: 窗口自己私有的缓冲区,即便是基于同一个窗口类创建出来的窗口相互之间也不共享,哪个窗口写的主句只有该窗口才能获取出数据来
SetWindowLong()向窗口附加数据缓冲区里写数据,GetWindowLong()从窗口附加数据缓冲区里读数据
应用程序全局窗口类和局部窗口类注册:
应用程序全局窗口类的注册,需要在窗口类的风格中增加 CS_GLOBALCLASS
如:
WNDCLASSEX wce = {0};
wce.style = CS_GLOBALCLASS|...;
应用程序局部窗口类的注册,不需要添加 CS_GLOBALCLASS风格;
Windows下窗口类的风格有:
1> CS_GLOBALCLASS //应用程序全局窗口类标志状态
2> CS_BYTEALIGNCLIENT //窗口客户区的水平位置8的倍数对齐,如果不是8的整数倍则系统会微调成8的整数倍
3> CS_BYTEALIGNWINDOW //窗口的水平位置8的倍数对齐,如果不是8的整数倍则系统会微调成8的整数倍
4> CS_HREDRAW //当窗口水平变化时,窗口重新绘制
5> CS_VREDRAW //当窗口垂直变化时,窗口重新绘制
6> CS_CLASSDC //该类型的窗口,都是有同一个绘图(DC)设备
7> CS_PARENTDC //该类型的窗口,使用它的父窗口的绘图(DC)设备
8> CS_OWNDC //该类型的窗口,每个窗口都使用自己的绘图(DC)设备
9> CS_SAVEBITS //允许窗口保存成图片(位图),提高窗口的绘图效率,但是耗费内存资源
10> CS_DBLCLKS //允许窗口接收鼠标双击
11> CS_NOCLOSE //窗口没有关闭按钮 或 关闭按钮不可用
注:
窗口客户区是指窗口的主体区域,也就是窗口的内部;
窗口客户区的水平位置指窗口内部的水平位置;
窗口的水平位置指的是窗口标题栏的水平位置;
为什么我们在创建窗口的时候 第一个参数要填写注册窗口类的名称,倒数第二个参数要填写当前程序实例句柄?
答:
窗口类的查找流程:
1> 系统(CreateWindow()函数内部)根据传入的窗口类名称,在应用程序局部窗口类中查找,
如果找到则执行 "2>", 如果没有找到则执行"3>"
第一步 在应用程序局部窗口类中比对传入的窗口类名称
2> 比较局部窗口类与创建窗口时传入的 HINSTANCE(句柄,也就是倒数第二个参数) 变量,
如果发现相等,说明要创建的窗口和注册的窗口类是在同一进程模块里,则直接创建窗口,并返回该内存的句柄;
如果不相等就继续执行 "3>"
第二步 应用程序局部窗口类名称比对上了 再比对 传入的句柄,
比对 HINSTANCE(句柄)的目的是为了辨别 注册的窗口类是全局窗口类还是局部窗口类;
如果是局部窗口类就只能比对成功后才能创建窗口;
如果是全局窗口类就不需要比对了,因为全局窗口类自身支持一个进程模块注册其它进程使用;
3> 根据传入的窗口类名称,在应用程序全局窗口类中比对查找,
如果找到则执行 "4>" 如果没找到则执行"5>"
第三步 在应用程序全局全局窗口类中比对传入的窗口类名称
4> 如果根据传入的窗口类名称在全局窗口类中比对上了,则使用找到的全局窗口类的信息创建窗口并返回该内存的句柄;
第四步 在全局窗口类中比对上了窗口类的名称,则根据全局窗口类的信息创建窗口并返回该内存的句柄;
5> 根据传入的窗口类名称,在系统窗口类中查找,如果找到则根据系统窗口类的信息创建窗口并返回,否则创建窗口失败;
第五步 在 局部窗口类 / 全局窗口类 中都没有比对上 就只能比对 系统窗口类了,如果有则创建,没有则创建失败;
注:
创建窗口时传入的倒数第二个参数 HINSTANCE(句柄) 的目的是为了辨别 注册的窗口类是全局窗口类还是局部窗口类,
如果是局部窗口类,就只有比对成功了才能使用注册的窗口类创建窗口;
如果是全局窗口类就不需要比对,因为全局窗口类自身就支持一个进程模块注册,其它进程模块使用;
窗口类相关 API
RegisterClass() / RegisterClassEx() 是把 WNDCLASS / WNDCLASSEX 结构体写入操作系统内核中 这就是 注册窗口类
有3种窗口类:
1> 局部窗口类 //用户自己定义,只能在本进程模块内创建窗口,注册窗口类的时候没有 CS_GLOBALCLASS 窗口类风格
2> 全局窗口类 //用户自己定义,本进程模块注册,其它进程模块中可以使用,
注册窗口类的时候窗口类的风格 必须添上 CS_GLOBALCLASS
3> 系统窗口类 //由系统定义,可以直接使用
GetClassInfo() 获取某个窗口类的信息
UnregisterClass() 卸载窗口类,也就是把 RegisterClass() / RegisterClassEx() 写入操作系统内核的数据给删除掉;
例子:
BOOL Register(LPSTR lpClassName, WNDPROC wndProc){
WNDCLASSEX wce = {0};
wce.cbSize = sizeof(wce); // WNDCLASSEX 结构体的大小,因为涉及结构体的对齐和补齐
wce.cbClsExtra = 4; //窗口类附加数据缓冲区大小一般是4的倍数因为要存的数据至少占4个字节是 long类型,0表示不开
wce.cbWndExtra = 4; //窗口附加数据缓冲区一般是4的倍数因为要存的数据至少占4个字节是long类型 0表示不申请
wce.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); //窗口客户区的背景色
wce.hCursor = NULL; //鼠标光标,NULL表示使用系统默认的鼠标光标
wce.hIcon = NULL; //大图标,NULL使用默认的
wce.hIconSm = NULL; //小图标,NULL使用默认的
wce.hInstance = g_hInstance; //当前程序实例句柄
wce.lpfnWndProc = wndProc;//窗口处理函数
wce.lpszClassName = lpClassName; //窗口类名称
wce.lpszMenuName = NULL; //菜单, null表示使用默认菜单,有填写是填写菜单的资源ID,不是菜单的资源ID
wce.style = CS_HREDRAW|CS_VREDRAW; //窗口的风格
ATOM nAtom = RegisterClassEx(&wce); //把上面的结构体写入操作系统内核就叫注册窗口类
if(nAtom == 0){
return FALSE;
}
return TRUE;
}
三. 创建窗口
在windows平台下可以创建窗口的函数有:
1> CreateWindow() --- 标准版,比加强版少一个参数,
2> CreateWindowEx() --- 加强版,比标准版多一个参数,多了第一个参数,把加强版第一个参数去掉就是标准版;
原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽,0表示默认
LPCTSTR lpClassName, //已经注册的窗口类名称
LPCTSTR lpWindowsName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄,如果不在此挂菜单也可以在注册窗口类的时候挂菜单,在此挂资源菜单是该窗口特有的,其它窗口没有该资源菜单;
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时的附带信息
); 创建成功返回窗口句柄
为什么我们在创建窗口的时候 第二个参数要填写注册窗口类的名称,倒数第二个参数要填写当前程序实例句柄?
答:
窗口类的查找流程:
1> 系统(CreateWindow()函数内部)根据传入的窗口类名称,在应用程序局部窗口类中查找,
如果找到则执行 "2>", 如果没有找到则执行"3>"
第一步 在应用程序局部窗口类中比对传入的窗口类名称
2> 比较局部窗口类与创建窗口时传入的 HINSTANCE(句柄,也就是倒数第二个参数) 变量,
如果发现相等,说明要创建的窗口和注册的窗口类是在同一进程模块里,则直接创建窗口,并返回该内存的句柄;
如果不相等就继续执行 "3>"
第二步 应用程序局部窗口类名称比对上了 再比对 传入的句柄,
比对 HINSTANCE(句柄)的目的是为了辨别 注册的窗口类是全局窗口类还是局部窗口类;
如果是局部窗口类就只能比对成功后才能创建窗口;
如果是全局窗口类就不需要比对了,因为全局窗口类自身支持一个进程模块注册其它进程使用;
3> 根据传入的窗口类名称,在应用程序全局窗口类中比对查找,
如果找到则执行 "4>" 如果没找到则执行"5>"
第三步 在应用程序全局全局窗口类中比对传入的窗口类名称
4> 如果根据传入的窗口类名称在全局窗口类中比对上了,则使用找到的全局窗口类的信息创建窗口并返回该内存的句柄;
第四步 在全局窗口类中比对上了窗口类的名称,则根据全局窗口类的信息创建窗口并返回该内存的句柄;
5> 根据传入的窗口类名称,在系统窗口类中查找,如果找到则根据系统窗口类的信息创建窗口并返回,否则创建窗口失败;
第五步 在 局部窗口类 / 全局窗口类 中都没有比对上 就只能比对 系统窗口类了,如果有则创建,没有则创建失败;
如:
HWND CreateMain(LPSTR lpClassName, LPSTR lpWndName){
HMENU hMenu = LoadMenu(g_hInstance,MAKEINTRESOURCE(IDR_MENU1));//这里就可以获取到资源的句柄,然后在 CreateWindowEx()函数的倒数第3个参数添上
HWND hWnd = CreateWindowEx(0,lpClassName,
lpWndName,WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,hMenu/*NULL*/,g_hInstance,NULL);
return hWnd;
}
注:
创建窗口时传入的倒数第二个参数 HINSTANCE(句柄) 的目的是为了辨别 注册的窗口类是全局窗口类还是局部窗口类,
如果是局部窗口类,就只有比对成功了才能使用注册的窗口类创建窗口;
如果是全局窗口类就不需要比对,因为全局窗口类自身就支持一个进程模块注册,其它进程模块使用;
窗口的基本风格有:
WS_BORDER //窗口的边界风格,如果创建的窗口有该风格会有一圈黑色的比较细的边界线
WS_CAPTION //窗口的标题栏风格
WS_CHILD //窗口的子窗口风格,如果有该风格说明创建的是子窗口 此时必须在后面把该窗口的父窗口添上;
WS_CHILDWINDOW //同上
WS_CLIPCHILDREN //裁剪窗口使用
WS_CLIPSIBLINGS //裁剪窗口使用
WS_DISABLED //窗口不可用(窗口点不动,拖不动)
WS_DLGFRAME //对话框窗口的边框
WS_GROUP //分组风格
WS_HSCROLL //水平滚动条风格
WS_ICONIC //窗口创建之初处于最小化状态 和 WS_MINIMIZE
WS_MAXIMIZE //窗口创建之初处于最大化状态
WS_MAXIMIZEBOX //创建的窗口有最大化按钮
WS_MINIMIZE //窗口创建之初处于最小化状态 和 WS_ICONIC
WS_MINIMIZEBOX //创建的窗口有最小化按钮
WS_OVERLAPPED //交叠窗口(交叠窗口必须具备 标题栏 和 边框)
WS_OVERLAPPEDWINDOW //复合风格包含了: WS_OVERLAPPED/WS_CAPTION/WS_SYSMENU/WS_THICKFRAME/WS_MINIMIZEBOX/WS_MAXIMIZEBOX 和 WS_TILEDWINDOW 风格一样
WS_POPUP //弹出式窗口一般用于对话框
WS_POPUPWINDOW //复合风格包含了: WS_BORDER/ WS_POPUP/WS_SYSMENU; WS_CAPTION 和 WS_POPUPWINDOW 风格必须是可见的
WS_SIZEBOX //创建的窗口有一个改变大小的边界
WS_SYSMENU //系统菜单风格(点击左上角图标弹出的菜单就叫系统菜单)
WS_TABSTOP //支持 Tab 键顺序,一般用于控件窗口
WS_THICKFRAME //比较细的边框 和 WS_SIZEBOX 一样鼠标拖拽可以改变边框的大小
WS_TILED //有标题栏
WS_TILEDWINDOW //复合风格和 WS_OVERLAPPEDWINDOW 一样
WS_VISIBLE //窗口初始状态就可见,一般用于子窗口,主窗口不用它,主窗口使用 ShowWindow()
WS_VSCROLL //垂直滚动条风格
Windows平台下有两项重要的技术:
1> 回调函数
2> 钩子(加强版回调)函数
在定义函数的时候如果在 函数返回值 和 函数名 之间 有 CALLBACK 就说明该函数是一个回调函数, 如果我们定义的函数是给系统调用则必须把 CALLBACK 宏添加上
如:
LRESULT CALLBACK WndProc(xxxxx){}
CALLBACK 和 APIENTRY 是一样的 都是回调声明, 它们对应的值 是 __stdcall 是一种调用约定
扩:
_stdcall包含两个意思
1. 按照C风格传递参数即参数从右自左压入堆栈,函数内部正好从左自右读出参数
2. 函数执行清除堆栈,即调用函数是堆栈中压入了参数占用了位置,这些位置将有调用函数负责清空。
如果参数个数未知,函数在编译期就不知到底要在堆栈里清除多少个字节,这样_stdcall就不能用了,
这样的函数如sprintf函数,就使用PASCAL调用约定了,函数不负责清除堆栈。不过这些事情均有编译器代劳
子窗口的创建:
1> 创建时要设置父窗口句柄
2> 创建风格要增加 WS_CHILD|WS_VISIBLE 风格
移动窗口的位置和改变窗口的大小
BOOL MoveWindow(
HWND hWnd, //窗口句柄
int x, //水平坐标
int y, //垂直坐标
int nWidth, //宽
int nHeight, //高
BOOL bRepaint //是否擦除原来绘制的窗口 true 擦除, false 不擦除
);
扩:
只要见到一个数据类型是以 "LP" 开头就可以确定该数据类型是一个指针类型,把"LP"去掉就是它所指向的类型
如:
LPVOID --- 就是 void*
例子:
HWND CreateMain(LPSTR lpClassName, LPSTR lpWndName){
HWND hWnd = CreateWindowEx(0,lpClassName,
lpWndName,WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,NULL,g_hInstance,NULL);
return hWnd;
}
四. 注册窗口类时的附加数据缓冲区
窗口类附加数据缓冲区的用法:
1> 申请缓冲区
注册窗口类时给 cbClsExtra 成员赋值 --- 一般赋4的倍数因为要存的数据至少是4个字节,0表示不开缓冲区
2> 写入数据
SetClassLong() --- 向窗口类附加数据缓冲区里写数据
原型:
DWORD SetClassLong(
HWND hWnd, //窗口句柄
int nIndex, //字节索引号,表示从哪个字节开始存数据,索引号从0开始
LONG dwNewLong //要存入的数据,是 long(占4个字节)类型
);
3> 读取数据
GetClassLong() --- 从窗口类附加数据缓冲区里读数据
原型:
DWORD GetClassLong(
HWND hWnd, //窗口句柄
int nIndex //字节索引号,从哪个字节开始读
);返回值获取读取的数据
窗口附加数据缓冲区的用法:
1> 申请缓冲区
注册窗口类时给 cbWndExtra 成员赋值 --- 一般赋4的倍数因为要存的数据至少是4个字节,0表示不开缓冲区
2> 写入数据
SetWindowLong() --- 向窗口附加数据缓冲区写数据
原型:
DWORD SetWindowLong(
HWND hWnd, //窗口句柄
int nIndex, //字节索引号,表示从哪个字节开始存数据,索引号从0开始
LONG dwNewLong //要存入的数据,是 long(占4个字节)类型
);
3> 读取数据
GetClassLong() --- 从窗口附加数据缓冲区里读数据
原型:
DWORD GetWindowLong(
HWND hWnd, //窗口句柄
int nIndex //字节索引号,从哪个字节开始读
);返回值获取读取的数据
扩:
窗口类附加数据缓冲区 和 窗口附加数据缓冲区 赋值的数据是占4个字节,指针也是占4个字节也就是说可以赋值指针,这样附加什么数据都可以了;
窗口类附加数据缓冲区 和 窗口附加数据缓冲区 的区别:
区别:
窗口类附加数据缓冲区: 是所有基于该窗口类创建出来的所有窗口共享的缓冲区
窗口附加数据缓冲区: 是窗口自己私有的缓冲区,即便是基于同一个窗口类创建出来的窗口相互之间也不共享
五. 显示窗口
ShowWindow(hWnd, SW_SHOW); //显示窗口
hWnd ---> 窗口句柄,通过窗口句柄可以找到保存窗口信息的内存,内存中有窗口的数据如:窗口的大小,宽高 等
然后根据获取的信息在显示器上绘制窗口图形
UpdateWindow(hWnd); //刷新
例子:
void Display(HWND hWnd){
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);
}
六. 消息循环
程序执行机制: 所有的程序可分为两种执行机制
过程驱动 --- 程序的执行过程是按照预定好的顺序执行
事件驱动 --- 程序的执行无序,用户可以根据需要随机触发相应的事件
Win32窗口程序就是采用事件驱动方式执行,也就是消息机制; windows平台下微软定义了 1024 个消息
当系统通知窗口工作时,就采用消息的方式通过 DispatchMessage() 调用窗口的窗口处理函数;
消息的组成(windows平台下) 必须下面6个部分组成 :
窗口句柄 --- 告诉我们是哪个窗口的消息,消息一诞生就应该知道是哪个窗口的消息
消息ID --- 不能重复的数字就是ID,微软定义了1024个消息,每个消息都分配了一个唯一的ID,告诉我们是那种消息
消息的两个参数(两个附带信息) --- 不同的消息附带的信息不一样
消息产生的时间 --- 一般不关心,但它也是消息不可缺少的一部分
消息产生时的鼠标位置 --- 一般不关心,但它也是消息不可缺少的一部分
Windows和UC下的消息:
UC下的消息主要功能是 IPC(进程间通信), 至少由2部分组成(定义消息ID(key),消息的信息)
Windows下的消息的功能不是 IPC(进程间通信), 严格规定由6部分组成;
MSG结构体原型:
typedef struct tagMSG{
HWND hwnd; //窗口句柄
UINT message; //消息ID
WPARAM wParam; //消息的附带信息
LPARAM lParam; //消息的附带信息
DWORD time; //消息产生的时间
POINT pt; //消息产生时的鼠标位置
}MSG, *PMSG;
MSG 结构体的成员就是消息的6个组成部分;
GetMessage()是把抓到消息放入 MSG 结构体中
TranslateMessage()是翻译抓到的消息
DispatchMessage()派发消息
消息处理的流程:
消息队列 //存放消息
↓
GetMessage() //在消息队列里面抓消息
↓
TranslateMessage() //翻译消息
↓
DispatchMessage() //调用消息处理函数
↓
消息处理函数
DispatchMessage() --- 派发消息(调用窗口处理函数)
原型:
LRESULT DispatchMessage(
CONST MSG* lpmsg; //要派发的消息
)
功能:
将消息派发到该消息所属窗口的窗口处理函数上;
DispatchMessage()是怎么派发消息的?
流程:
DispatchMessage()的参数是 MSG 消息结构体的地址,MSG 消息结构体里面第一个成员是窗口句柄;
根据窗口句柄找到存储窗口信息(也就是使用CreateWindowEx()创建窗口时填写的信息) 的哪块内存,
在CreateWindowEx()创建窗口的时候填写的信息中有关于该窗口的窗口类名称 和 当前程序实例句柄,
拿到窗口类名称 和 当前程序实例句柄 就会去操作系统内核中匹配:
先用窗口类名称去应用程序局部窗口类区匹配,如果匹配成功则继续匹配当前程序实例句柄,如果全都匹配成功,就可以拿到注册窗口类的信息;
如果在应用程序局部窗口类区没有匹配上则去应用程序全局窗口类区匹配窗口类名称如果匹配成功就拿到注册窗口类的信息
(不需要再匹配当前程序实例句柄,因为全局窗口类可以一个进程注册其它进程使用),
如果没有匹配成功,就去应用程序系统窗口类里面去匹配,如果匹配成功就拿到注册窗口类的信息,如果没有匹配上则获取失败
匹配规则参见 窗口类的查找流程
拿到匹配的窗口类信息,就能获取该窗口类注册时填写的信息,在填写的信息中就有窗口的处理函数地址也就是窗口处理函数名,
拿到窗口处理函数的地址就调用该函数处理消息;
调用我们自己定义的窗口处理函数时需要传参数: 其实传的参数就是 抓到的消息的前4个成员
LRESULT CALLBACK WndProc(
HWND hWnd, //窗口句柄 也是 MSG 结构体中的第一个成员 msg.hwnd
UINT msg, //消息的ID 也是 MSG 结构体中的第二个成员 msg.message
WPARAM wParam, //消息的附带信息 也是 MSG 结构体中的第三个成员 msg.wParam
LPARAM lParam //消息的附带信息 也是 MSG 结构体中的第四个成员 msg.lParam
);
当系统通知窗口工作时,就采用消息的方式通过DispatchMessage()派发给窗口的窗口处理函数;
DefWindowProc(hWnd,msg,wParam,lParam) 这是把我们不关系的消息扔给操作系统做默认处理
每个窗口都必须具有 窗口处理函数 : 窗口处理函数就是在处理窗口的消息
LRESULT CALLBACK WindowProc(
HWND hwnd, //窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //消息附带信息
LPARAM lParam //消息附带信息
);
当系统通知窗口时,DispatchMessage()会调用窗口处理函数,同时将 窗口句柄/消息ID/两个消息附带信息 传递给窗口处理函数 (也就是 MSG 结构体的前4个成员)
在窗口处理函数中,不处理的消息,可以使用 DetWindowProc() 函数做系统默认处理;
一个窗口只能有一个窗口处理函数,同一个窗口处理函数可以被多个窗口使用;
GetMessage() 获取本进程的消息
GetMessage()函数从系统获取消息,将消息从系统中移除,阻塞函数,当系统无消息时,GetMessage()会阻塞等候下一条消息
原型:
BOOL GetMessage(
LPMSG lpMsg, //存放获取到的消息BUFF(用来保存 GetMessage()抓到的消息) "LP"表示该类型是指针类型, 把"LP"去掉就是它所指向的类型
HWND hWnd, //窗口句柄,填NULL表示抓本进程的所有消息,如果填写的是某个窗口句柄,那么GetMessage()就只能抓指定窗口的消息不能抓其它窗口的消息
UINT wMsgFilterMin, //获取消息的最小ID, 0表示只要是属于本进程的消息不管消息ID多小统统抓取
UINT wMsgFilterMax //获取消息的最大ID, 0表示只要是属于本进程的消息不管消息ID多大统统抓取
);
lpMsg -- 当获取到消息后,将消息的参数存放到 MSG 结构中;
hWnd -- 获取到hWnd所指定窗口的消息,是限制抓取窗口的范围
wMsgFilterMin 和 wMsgFilterMax 消息的最大和最小 ID ,是限制消息的抓取范围,
微软定义了 1024个消息,都是通过消息ID来区别,消息ID就是非负的整数,所有是有大有小的;
返回值: 抓的消息是 WM_QUIT 则返回 0(FALSE), 如果抓的消息不是 WM_QUIT 则返回 非0
PeekMessage()函数以查看的方式从系统获取消息,可以不将消息从系统移除,非阻塞函数,当系统无消息时,返回 FALSE , 这时就可以做空闲处理
原型:
BOOL PeekMessage(
LPMSG lpMsg, // 用来保存 PeekMessage()抓到的消息, "LP"表示该类型是指针类型,把"LP"去掉就是它指向的类型
HWND hWnd, //窗口句柄,填 NULL 表示抓本进程所有的消息,如果填写的是某个窗口句柄,那么PeekMessage()就只能抓指定窗口的消息不能抓其它窗口的消息
UINT wMsgFilterMin,//获取消息的最小ID,0表示只要是属于本进程的消息不管消息ID多小统统抓取
UINT wMsgFilterMax,//获取消息的最大ID,0表示只要是属于本进程的消息不管消息ID多大统统抓取
UINT wRemoveMsg // 移不移除
); 返回值告诉我们有没有消息
wRemoveMsg的取值:
FM_NOREMOVE(不移除消息,表示只查看消息不抓消息)
RM_REMOVE(移除消息,表示抓消息)
任何一个窗口只要产生了消息,哪个消息都会进消息队列,GetMessage()函数就是从消息队列中抓消息的;
CPU的处理非常快,如果只使用 GetMessage()函数等把消息都处理完以后就处于阻塞状态,
这样CPU就处于空闲状态就是浪费资源,因此一般我们先通过 PeekMessage()函数查看是否有无信号;
PeekMessage() 可以抓消息也可以不抓消息,一般我们不用它来抓消息,一般用它来查看消息;
TranslateMessage() 翻译消息,将按键消息翻译成字符消息(只翻译可见字符的按键消息如: A-Z, a-z)
原型:
BOOL TranslateMessage(){
CONST MSG* lpMsg; //要翻译的消息地址
}
功能:
检查消息是否是按键消息,如果不是按键消息,不做任何处理,继续执行;
如果发现是按键消息,再判断是否是可见字符按键被按下产生的按键消息,还是不是可见字符被按下产生的按键消息,
如果不是可见字符的按键按下产生的按键消息,不做任何处理继续执行,
如果是可见字符按下产生的按键消息 TranslateMessage() 则调用 PostMessage() 发送字符消息(WM_CHAR) 到系统消息队列里,
然后再由系统消息队列转发到程序的消息队列里;
TranslateMessage()内部处理流程:
TranslateMessage(&nMsg){ //翻译消息
nMsg是抓到的消息的结构体,第二个成员是消息的ID,根据消息的ID判断是否是按键消息
if(nMsg.message != WM_KEYDOWN){
//如果不是按键消息直接返回
return ...;
}
如果是按键消息,再判断消息是不是可见字符被按下产生的按键消息,如果是 上下左右 这种按键被按下产生的按键消息就不翻译,
通过 nMsg.wParam(虚拟键码值)判断是否为可见字符按键被按下产生的按键消息,还是不可见字符被按下产生的按键消息;
if(不是可见字符按键被按下产生的按键消息){
//如果不是可见字符按键被按下,直接返回
return ...;
}else{
如果是可见字符按键被按下产生的按键消息就翻译消息,
翻译消息首先要确定大写锁定键是否处于打开状态,判断大小写键盘灯是否打开,然后 发送 WM_CHAR 消息
if(打开){
如果大写锁定键处于打开状态,发送 WM_CHAR 消息,并把字符的大写ASC码发送
PostMessage(nMsg.hwnd,WM_CHAR,0x.. , ...);
}else{
如果大写锁定键没有处于打开状态,发送 WM_CHAR 消息,并把字符的小写ASC码发送
PostMessage(nMsg.hwnd,WM_CHAR,0x.. , ...);
}
}
}
GetMessage()只负责抓消息,处理消息是通过我们自己定义的消息处理函数,如果我们没有调用消息处理函数则由编译器调用;可以把消息循环注释掉来测试;
发送消息
说一个函数能发消息,那是因为该函数调用了 SendMessage() 或 PostMessage() 中的其中一个函数来发送消息;
SendMessage() -- 发送消息不进系统队列,直接调用窗口处理函数,会等待消息处理的结果
PostMessage() -- 发送消息到系统消息队列里,消息发送后立刻返回,不等待消息的执行结果,
系统消息队列会把消息转发到程序消息队列里,最后再由 GetMessage()从程序消息队列里抓取该消息;
原型:
LRESULT SendMessage/PostMessage (
HWND hWnd, //消息发送的目的窗口
UINT Msg, //消息ID
WPARAM wParam, //消息参数
LPARAM lParam //消息参数
);
PostMessage() 发送的消息是进入系统消息队列,然后从系统消息队列里转发到程序消息队列里,最后再通过 GetMessage() 从程序消息队列里抓取发送的消息;
一个消息由6部分组成:
1> 窗口句柄 --- 是哪个窗口的消息
2> 消息ID --- 不能重复的数字就是ID,每个ID代表一个对应的消息,Windows下面有1024个消息
3> 消息的2个附带信息(wParam/lParam)
4> 消息产生的时间
5> 消息产生时的鼠标位置
扩:
调试程序的手段,可以添加一个DOS窗口,把程序要打印的信息打印到DOS窗口上;
在main()函数前面添加一个函数:
AllocConsole(); 它的功能就是给窗口程序添加一个附带的DOS窗口
通过GetStdHandle()可以拿到 标准 输入/输出/错误 的3个句柄;
GetStdHandle()函数原型:
HANDLE GetStdHandle(
DWORD nStdHandle //想要获取的句柄
);
nStdHandle的取值:
STD_INPUT_HANDLE ---- 标准输入句柄
STD_OUTPUT_HANDLE ---- 标准输出句柄
STD_ERROR_HANDLE ---- 标准错误句柄
返回获取到的相应句柄
如:
HANDLE g_hOutput = 0;
g_hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
打印信息:
可以使用 MessageBox(); 但MessageBox()会阻塞;
原型:
int MessageBox(
HWND hWnd, //父窗口句柄,没有即为 NULL
LPCTSTR lpText,//提示框内部要显示的内容, char*
LPCTSTR lpCaption, //显示在标题栏中的文字, char*
UINT uType //提示框中的 按钮(通过 "|" 按位或) 图标 显示类型, UINT(unsigned int)
); //返回点击的按钮ID
还可以用 WriteConsole(); 不会阻塞
原型:
BOOL WriteConsole(
HANDLE hConsoleOutput, // 标准输出句柄,通过 GetStdHandle()可以拿到
CONST VOID* lpBuffer, // 准备输出内容的缓冲区
DWORD nNumberOfCharsToWrite, //准备输出内容的长度(字符的个数)
LPDWORD lpNumberOfCharsWritten,//用来接收实际输出内容的长度
LPVOID lpReserved //备用,给NULL即可
);
GetMessage()终极解释:
1. 在程序(线程)消息队列查找消息,如果队列有消息,检查消息是否满足指定条件(HWND,ID范围)不满足条件就不会取出消息,否则从队列取出消息返回;
2. 如果程序(线程)消息队列没有消息,GetMessage()向系统消息队列要属于本程序的消息,如果系统队列的当前消息属于本程序,系统会将消息转发到程序消息队列
3. 如果系统消息队列也没有消息,GetMessage()会检查所有窗口的需要重新绘制的区域,如果发现有需要绘制的区域,
GetMessage()会使用PostMessage()发送 WM_PAINT 消息到系统消息队列,然后转发最后GetMessage()再从程序的消息队列里取得消息返回处理;
4. 如果GetMessage()函数检查了本进程所有的窗口,都没有重新绘制区域,会去检查定时器如果有到时间的定时器,
GetMessage()使用PostMessage()发送 WM_TIMER 到系统消息队列里,然后转发到程序的消息队列,GetMessage()抓取消息再返回处理执行
5. 如果GetMessage()检查了本进程所有的定时器,没有到时的定时器,GetMessage()就做整理程序的资源,内存等等
6. 如果做完资源整理还没有消息,GetMessage()会继续等候下一条消息,PeekMessage()会返回FALSE此时可以做空闲处理;
GetMessage()阻塞流程:
GetMessage() -> 在程序消息队列抓消息,如果有,则消息匹配符合条件的消息进行处理,如果没有 -> 向系统消息队列要消息,如果有,系统消息队列转发消息到
程序消息队列里,GetMessage()抓取处理,如果系统消息队列里没有消息 -> 检查本进程所有窗口看有没有需要重绘的,如果有GetMessage()发送WM_PAINT绘图消息
到系统消息队列里,然后转发最后GetMessage()在程序消息队里里抓取,如果没有可绘制的窗口 -> GetMessage()检查本进程所有的定时器,如果有到时间的定时器
GetMessage()发送 WM_TIMER 消息到系统消息队列里然后转发然后再抓取,如果没有到时间 -> GetMessage()做资源整理,做完资源整理如果还没有消息 ->
GetMessage()阻塞继续等待下一条消息,PeekMessage()返回Flase此时可以做空闲处理;
注:
GetMessage()如果获取到是WM_QUIT函数会返回FALSE
SendMessage()
发送消息到指定窗口的窗口处理函数,并等候对方(窗口处理函数)将消息处理,然后等候消息的执行结果,用于非队列消息的发送;
PostMessage()
将消息放到系统消息队列中,不等待立刻返回,用于队列消息的发送,无法获知消息是否被对方处理;
例子:
void Message(){
MSG nMsg = {0};
while(GetMessage(&nMsg,NULL,0,0)){
TranslateMessage(&nMsg);
DispatchMessage(&nMsg);
}
}
七. 消息的分类
消息分为4大类:
1> 系统消息 --- 消息ID范围是 0 - 0x03FF (也就是 1024)
由系统定义好的消息,可以在程序中直接使用
2> 用户自定义消息 ---- 消息ID范围 0x0400 ~ 0x7FFF 之间
#define WM_MYMESSAGE WM_USER+n ( 0x0400+n n的取值范围是 31743 )
由用户自己定义,满足用户自己的需求,由用户自己发出消息,并响应处理;
自定义消息宏: WM_USER
3> 应用程序消息 --- 消息ID 范围 0x8000 - 0xBFFF
程序之间通讯时使用的消息,一般用于底层驱动编程;
应用程序消息宏: WM_APP
4> 系统注册消息 --- 消息ID范围 0xC000 - 0xFFFF
在系统注册并生成相应消息,然后可以在各个程序中使用这个消息
注:
所谓消息 其实就是 不重复的数字(ID), 通过 PostMessage() 或 SendMessage() 发送给消息处理函数,
在消息处理函数中根据消息ID 再对消息做不同的处理;
八. 消息队列
消息队列
用于存放消息的一个队列,消息在队列中先入先出,所有窗口程序都具有消息队列,程序可以从队列中获取消息
消息队列的类型
系统消息队列 -- 由系统维护的队列,存放系统产生的消息,非常庞大因为要存所有进程的消息, 如鼠标,键盘等消息
程序消息队列 -- 属于每一个应用程序(线程)的消息队列,由应用程序(线程)维护;
除特殊消息外,所有程序的消息都先进系统消息队列,然后操作系统每隔一段时间会把各程序的消息转发到该程序的程序消息队列中;
转发过程:
消息的第一个成员是窗口句柄,根据窗口句柄找到保存窗口的数据的内存(也就是创建窗口时填写的信息),
该内存中有一条数据是当前程序实例句柄,根据当前程序实例句柄就能找到该程序所在的内存,再把消息保存到该内存中(也就是程序的消息队列里);
消息队列的流程
1> 当鼠标,键盘产生消息时,会将消息存放到系统消息队列
2> 系统会根据存放的消息,找到对应窗口的消息对列
3> 将消息投递到程序的消息队列中
消息和消息队列
根据消息和消息队列之间的使用关系,将消息分成两类:
1> 队列消息 -- 消息的发送和获取,都是通过消息队列完成
2> 非队列消息 -- 消息的发送和获取,是直接调用消息的窗口处理完成
一个消息是队列消息还是非队列消息取决于程序员,程序员让它进队列它就是队列消息,不让它进队列它就是非队列消息
队列消息 -- 消息发送后,首先放入系统的消息队列,然后通过消息循环,从程序的消息队列当中获取
GetMessage() -- 从程序的消息队列中获取消息(永远只能从程序的消息队列中抓消息,不能直接从系统的消息队列中抓消息);
PostMessage() -- 将消息投递到系统的消息队列,系统消息队列会把消息转发到程序消息队列里;
常见队列消息: WM_PAINT,键盘,鼠标,定时器 WM_QUIT必须进队列
非队列消息 -- 消息发送时,首先查找消息接收窗口的窗口处理函数,直接调用处理函数,完成消息;
SendMessage() -- 直接将消息发送给窗口的处理函数,并等候处理结果;
常见消息: WM_CREATE,WM_SIZE 等
大部分消息既可以走队列也可以不走队列,但 WM_CREATE 绝对不会走队列,因为 WM_CREATE 的产生时间是在创建窗口成功显示窗口之前,
如果 WM_CREATE 进队列了 GetMessage()就不能把它抓出来,因为 ShowWindow()在 GetMessage()前执行;
WM_SIZE 窗口大小发生变化时产生的消息,第一次窗口从无到有会发生一次窗口大小变化会产生该消息这个时候绝对不会走队列,
如果此时进对了GetMessage()不能把它抓出来,因为ShowWindow()在GetMessage()前执行,但后面的 WM_SIZE 就可以进队列了;
WM_QUIT 就必须进队列,因为GetMessage()只能从系统消息队列中抓消息;
为什么不是所有的消息都直接通过 SendMessage()函数发送不进队列而是让 PostMessage()函数发送让消息进入队列
因为如果所有消息不进队列直接调用消息处理函数就不能保证先入队列的消息先执行,这样就全乱套了;
为什么不让GetMessage()函数直接从系统消息队列中抓消息,还要弄一个程序消息队列,让GetMessage()函数只能从程序的消息队列中抓消息
因为CPU出了派发消息汗有很多其它事情要做,如果让每个程序的GetMessage()直接到系统消息队列中去抓消息,那么CPU就非常忙做不了其它事情,降低效率;
CPU是每隔一段时间转发一次消息;
九. 消息
Windows平台下的常用消息
拿到1个消息要问:
1> 该消息什么时候产生(产生时间)
2> 该消息的用途(用来做什么)
3> 该消息的附带信息(能告诉我们什么信息)
Windows平台下消息是由6个部分组成:
1> 窗口句柄(哪个窗口的消息)
2> 消息ID
3> 消息的两个附带信息
4> 消息产生的时间
5> 消息产生时鼠标的位置
GetMessage()只负责抓消息,处理消息是通过我们自己定义的消息处理函数,如果我们没有调用消息处理函数则由编译器调用;可以把消息循环注释掉来测试;
消息分为4大类:
1> 系统消息 --- ID范围是 0 - 0x03FF (也就是 1024)
由系统定义好的消息,可以在程序中直接使用
2> 用户自定义消息 ---- ID范围 0x0400 - 0x7FFF (31743)
#define WM_MYMESSAGE WM_USER+n(WM_USER的值就是 0x0400, WM_USER+n 等价于 0x0400+n 的和 < 0x7FFF 也就是小于 31743)
由用户自定义,满足用户自己的需求,由用户自己发出消息,并响应处理;
自定义消息宏: WM_USER 值是 0x0400
3> 应用程序消息 --- ID 范围 0x8000 - 0xBFFF
程序之间通讯时使用的消息,一般用于底层驱动编程;
应用程序消息宏: WM_APP
4> 系统注册消息 --- ID范围 0xC000 - 0xFFFF
在系统注册并生成相应消息,然后可以在各个程序中使用这个消息
规律:
消息如果是我们发,一般情况下就不用我们处理,由系统处理,相反如果是系统发的消息一般情况下就要由我们来处理;
在 smdn 里面如果一个消息的介绍中有由 SendMessge() 发送,一般情况下该消息就不需要我们处理,由系统处理;
在 smdn 里面如果一个消息的介绍中有由 CALLBACR 修饰的东西, 一般情况下该消息由系统发送需要我们自己处理该消息;
注:
所谓消息 其实就是 不重复的数字(ID), 通过 PostMessage() 或 SendMessage() 发送给消息处理函数,
在消息处理函数中根据消息ID 再对消息做不同的处理;
窗口相关消息:
WM_DESTROY ---- 窗口被销毁时的消息,无消息参数,常用于 在窗口被销毁之前,做相应的善后处理,例如 资源,内存 等
窗口销毁是以保存窗口数据的那块内存释没释放为准;
产生时间: 窗口被销毁时产生
用途: 在窗口销毁前做资源的回收
附带信息: 无
WM_SYSCOMMAND ---- 系统命令消息,当点击窗口的最大化,最小化,关闭等命令时,收到这个消息,常用在窗口关闭时,提示用户处理;
点 最大化 最小化 关闭 都会产生该消息,但具体是点的什么地方产生的该消息,我们不知道,因此需要消息的附带信息;
wParam 具体点击的位置,
SC_CLOSE ---- 点击关闭
SC_MAXIMIZE ---- 点击最大化窗口
SC_MINIMIZE ---- 点击最小化窗口
lParam 鼠标的位置( LOWORD 低两个字节,水平位置 / HIWORD 高两个字节,垂直位置)
点击窗口右上角的关闭按钮不能产生 WM_DESTROY 消息,点击关闭按钮会产生 WM_SYSCOMMAND 消息,
WM_SYSCOMMAND 消息可以通过 DefWindowProc()做默认处理,它会销毁窗口,在销毁窗口时才就会产生 WM_DESTROY 消息
取低两个字节的数据使用"&",取高两个字节的数据使用">>"
如:
long a = 1234456789987654;
低两个字节: a & 0x0000FFFF
高两个字节: a >> 16 (按位右移 高位大部分补0,但不排除某些编译器补1因此保险期间右移后再做一次 "&")
可以使用"&"和">>"取数据但也可以直接使用微软为我们提供了两个宏,LOWORD(低两个字节) HIWORD(高两个字节)
如:
int x = LOWORD(lParam); //这样就取出 低两个字节
int y = HIWORD(lParam); //这样就取出 高两个字节
产生时间: 当点击窗口的最大化,最小化,关闭等命令时,产生该消息
用途: 常用于窗口关闭时,提示用户处理
附带信息: wParam(具体点击的位置), lParam(鼠标的位置)
WM_CREATE ---- 在窗口创建成功还未显示之前,产生这个消息,常用于初始化窗口的参数,资源等等,包括创建子窗口等;
产生该消息是 窗口已经创建只是没有显示出来而已,就是在 CreateWindowEx() 与 ShowWindow() 之间
产生时间: 窗口创建成功还未显示之前,产生该消息
用途: 常用于初始化窗口的参数,资源 和 创建子窗口等
附带信息: wParam(不使用), lParam( CREATESTRUCT 结构体的指针,保存了CreateWindowEx()中的12个参数 )
CREATESTRUCT 结构体的原型 和 CreateWinodEx()函数的参数一样,只是参数位置相反
typedef struct tagCREATESTRUCT {
LPVOID lpCreateParams; //创建窗口时的附带信息
HINSTANCE hInstance; //应用程序实例句柄
HMENU hMenu; //窗口菜单句柄
HWND hwndParent; //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上
int cy; //窗口的高度,如果写的是 CW_USEDEFAULT 则使用默认的坐标
int cx; //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y; //窗口左上角垂直坐标位置, 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int x; //窗口左上角水平坐标位置, 如果写的是 CW_USEDEFAULT 则使用默认的坐标
LONG style; //窗口的基本风格
LPCTSTR lpszName; //窗口标题栏的名字
LPCTSTR lpszClass; //已经注册的窗口类名称
DWORD dwExStyle; //窗口的扩展风格,大部分是指窗口是否支持文件拖拽
} CREATESTRUCT, *LPCREATESTRUCT;
CreateWindowEx()的原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽
LPCTSTR lpClassName, //已经注册的窗口类名称
LPCTSTR lpWindowsName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时附加参数
); 创建成功返回窗口句柄
WM_SIZE ---- 窗口的大小发生变化时产生该消息,但有个特例也会产生该消息就是第一次显示窗口时,
因为窗口从无到有也相当于窗口大小在变化,会收到WM_SIZE消息,
常用于窗口大小变化后,调整窗口内各个部分的布局,如果主窗口大小变化了里面的子窗口不改变则非常难看
WPARAM(窗口大小变化的原因)
LPARAM(变化窗口客户区的大小: LOWORD(变化后的宽度),HIWORD(变化后的高度))
产生时间: 窗口的大小发生变化后,产生该消息,还有一个特例也会产生该消息就是第一次显示窗口时,因为窗口从无到有也相当于窗口大小在变化
用途: 用于窗口大小变化后,调整窗口内各个部分的布局;
附带信息:
WPARAM --- 窗口大小变化的原因
LPARAM --- 变化窗口客户区的大小
LOWORD(低字节,编号后的宽度)
HIWORD(高字节,变化后的高度)
如:
int nWidth = LOWORD(lParam);
int nHight = HIWORD(lParam);
WM_QUIT ---- 用于结束消息循环, wParam(PostQuitMessage()函数参数是什么 wParam 就是什么), lParam(不使用)
当GetMessage()收到这个消息后,会返回 FALSE 结束 while 处理,退出消息循环;
WM_QUIT 永远进不了我们定义的窗口处理函数,因为 WM_QUIT 不需要我们处理;
产生时间: 当我们调用 PostQuitMessage()函数时,操作系统会自己发送 WM_QUIT 消息
用途: 结束消息循环
附带信息: wParam(PostQuitMessage()函数参数是什么 wParam 就是什么), lParam(不使用)
WM_QUIT 消息必须进系统消息队列,但永远进不了程序的窗口处理函数,
因为不进队列GetMessage()结束不了,GetMessage()函数抓到GetMessage()就返回0
GetMessage()函数返回0,就执行不了DispatchMessage()函数,执行不了DispatchMessage()函数就调用不了窗口处理函数;
绘图相关消息:
WM_PAINT ---- 绘图消息,
当窗口需要从新绘制的时候,由GetMessage()发送 WM_PAINT 到系统消息队列里,然后再从系统队列里转发到程序消息队列里,GetMessage()在抓取
创建窗口成功到第一次显示会产生该消息, 变大,最大化,被遮挡到从新显示 都会产生该消息,
变小,最小化不会产生该消息,但如果我们在注册窗口类的时候窗口的风格 添加了 CS_HREDRAW|CS_VREDRAW 在窗口大小发生变化时会强制重绘
窗口无效区域 : 需要重新绘制的区域就叫 窗口无效区域
BOOL InvalidateRect(
HWND hWnd, //窗口句柄,哪个窗口需要重绘
CONST RECT* lpRect, //区域的矩形坐标信息,是一个结构体有4个成员 上下左右,如果填 NULL 则整个窗口都重绘
BOOL bErase //重绘前是否先擦除原来的图形 true 擦除 false 不擦除
);
在程序中,如果需要绘制窗口,调用此函数声明窗口无效区域
InvalidateRect()函数不能发送 WM_PAINT 消息,InvalidateRect()函数只是声明窗口需要重绘,
GetMessage()当检查到有窗口需要重绘时会发送 WM_PAINT 消息;
产生时间: 当GetMessage()无消息可抓,检查窗口是否需要绘制的时候产生,
窗口创建成功到第一次显示,窗口被遮挡到第一次显示,窗口变大,
想随时随地产生WM_PAINT消息调用 InvalidateRect()函数声明为窗口无效区域
变小(注册窗口类的时候需要添加强制重绘的风格 CS_HREDRAW|CS_VREDRAW)
用途: 专职用途用来绘制图形的消息
附带信息: 两个附带信息都不使用
绘图的步骤:
1> 开始绘图处理
定义一个 PAINTSTRUCT 结构体;
PAINTSTRUCT ps = {0};
HDC BeginPaint(
HWND hWnd; //绘图窗口句柄,在哪个窗口里面画图
LPPAINTSTRUCT lpPaint //用来保存绘图的相关参数,
//"LP"表示是一个指针 去掉 "LP" 剩下的就是指向的类型 PAINTSTRUCT(是一个结构体)
);返回绘图设备句柄 HDC
2> 绘图,需要用到 BeginPaint()的返回值 绘图设备句柄
3> 结束绘图处理
BOOL EndPaint(
HWND hWnd, //绘图窗口
CONST PAINTSTRUCT* lpPaint //绘图参数的指针 PAINTSTRUCT 结构体的地址
);
以上代码必须放在 WM_PAINT 消息的处理中调用,
绘图步骤必须放在绘图消息里面写;
注:
Y轴下为正,X轴右为正
Windows系统下如果想要绘图,绘图的步骤必须放在绘图消息(WM_PAINT)里面绘制
InvalidateRect()函数不能发送 WM_PAINT 消息 WM_PAINT 消息 是 GetMessage()空闲的时候发送 WM_PAINT 消息
如:
PAINTSTRUCT ps = {0};
HDC hc = BeginPaint(hWnd,&ps);
Ellipse(hc,x,y,x+50,y+50);
EndPaint(hWnd,&ps);
键盘相关消息:
键盘可能产生的消息有:
1> WM_KEYDOWN //按键被按下时产生,按着不放会连续产生很多 WM_KEYDOWN 消息,由键盘驱动发送
2> WM_KEYUP //按键被放开时产生,只在按键放开的瞬间产生该消息
3> WM_SYSKEYDOWN //系统键按下时产生 比如 ALT, F10
4> WM_SYSKEYUP //系统键放开时产生 比如 ALT, F10
以上消息的附带信息都一样:
WPARAM --- 按键的 Virtual Key(虚拟键码),按键对应的数值,但虚拟键码值无法区分大小写;
LPARAM --- 按键的参数, 例如 按下的次数
5> WM_CHAR //字符消息(由TranslateMessage()发送)
WM_CHAR 消息的附带信息:
WPARAM --- 输入的字符(ASC字符编码),用ASC可以区分大小写;
LPARAM --- 按键的相关参数
TranslateMessage()内部发送 WM_CHAR 消息的流程:
TranslateMessage(&nMsg){ //翻译消息
nMsg是抓到的消息的结构体,第二个成员是消息的ID,根据消息的ID判断是否是按键消息
if(nMsg.message != WM_KEYDOWN){
//如果不是按键消息直接返回函数
return ...;
}
如果是按键消息,再判断消息是不是可见字符的按键消息,如果想 上下左右 这种按键消息就不翻译,
通过 nMsg.wParam(虚拟键码值)判断是否为可见字符的按键被按下
if(不是可见字符按键被按下){
return ...;
}else{
如果是可见字符按键被按下就翻译消息,
翻译消息首先要确定大写锁定键是否处于打开状态
if(打开){
如果大写锁定键处于打开状态,发送 WM_CHAR 消息,并把字符的大写ASC码发送
PostMessage(nMsg,WM_CHAR,0x.. , ...);
}else{
如果大写锁定键没有处于打开状态,发送 WM_CHAR 消息,并把字符的小写ASC码发送
PostMessage(nMsg.hwnd,WM_CHAR,0x.. , ...);
}
}
}
鼠标相关消息:
我们一般分为3大类(微软没有分)
1> 基本鼠标消息
2> 双击消息
3> 滚轮消息
1> 基本鼠标消息:
WM_LBUTTONDOWN //鼠标左键按下,只会在鼠标左键按下的瞬间产生该消息,
WM_LBUTTONUP //鼠标左键抬起,只会在鼠标左键抬起的瞬间产生该消息
WM_RBUTTONDOWN //鼠标右键按下
WM_RBUTTONUP //鼠标右键抬起
WM_MOUSEMOVE //鼠标移动消息
WM_LBUTTONDOWN 和 WM_LBUTTONUP 消息的附带信息
wParam:
MK_CONTROL ---- 按下/松开鼠标左键的同时还按着Ctrl键
MK_MBUTTON ---- 按着/松开鼠标左键的同时还按着中间键
MK_RBUTTON ---- 按着/松开鼠标左键的同时还按着鼠标右键
MK_SHIFT ---- 按着/松开鼠标左键的同时还按着Shift键
MK_LBUTTON ---- 按下鼠标左键的同时还按着鼠标左键
注:
要判断是否 按着上面的键 需要通过 "&" 来判断,不能直接使用 "==" 判断,因为 wParam 的值是 Ctrl 键标志 和 鼠标左键/右键的标志 之和
如:
if(wParam & MK_CONTROL){
WriteConsole(g_hOutput,szText,strlen(szText),NULL,NULL);
}
lParam: 鼠标当前的位置,是窗口客户区坐标系(窗口内部坐标系)
LOWORD(lParam) 鼠标的 水平位置
HIWORD(lParam) 鼠标的 垂直位置
鼠标消息使用:
一般情况下鼠标按下/抬起是成对出现的,但如果是双击鼠标就不成对出现: 鼠标单击 -> 鼠标弹起 -> 鼠标后双击 -> 鼠标弹起
在鼠标移动过程中,会根据移动速度产生一系列的 WM_MOUSEMOVE 消息(鼠标移动消息),
移动慢产生的 WM_MOUSEMOVE 消息要比移动慢产生的 WM_MOUSEMOVE 消息多,因为鼠标驱动捕获次数有时间断,
比如 1 秒鼠标驱动捕获5次,移动快就只能捕获到5次鼠标移动,如果移动慢就会多次捕获鼠标消息;
2> 双击消息:
WM_LBUTTONDBLCLK //鼠标左键双击
WM_RBUTTONDBLCLK //鼠标右键双击
消息的附带信息:
wParam:
MK_CONTROL ---- 双击鼠标左键/右键的同时还按着Ctrl键
MK_MBUTTON ---- 双击鼠标左键/右键的同时还按着中间键
MK_RBUTTON ---- 双击鼠标左键/右键的同时还按着鼠标右键
MK_SHIFT ---- 双击鼠标左键/右键的同时还按着Shift键
MK_LBUTTON ---- 双击鼠标左键/右键的同时还还按着鼠标左键
lParam: 鼠标当前的位置,是窗口客户区坐标系(窗口内部坐标系)
LOWORD(lParam) 鼠标的 水平位置
HIWORD(lParam) 鼠标的 垂直位置
注:
使用时需要在注册窗口类的时候添加 CS_DBLCLKS 风格
消息产生顺序以 WM_LBUTTONDBLCLK 为例:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
要判断是否 按着上面的键 需要通过 "&" 来判断不能直接使用 "==" 判断,因为 wParam 的值是 Ctrl 键标志 和 鼠标左键/右键的标志 之和
如:
if(wParam & MK_CONTROL){
WriteConsole(g_hOutput,szText,strlen(szText),NULL,NULL);
}
3> 滚轮消息(需要把windows系统文件版本提升至4.0及其以上版本)
WM_MOUSEWHEEL //鼠标滚轮消息
消息附带信息:
wParam:
LOWORD --- 其它按键的状态
HIWORD --- 滚轮的偏移量(基于当前光标位置滚动的距离),是120的倍数,通过正负值表示滚动方向
正: 向前滚动
负: 向后滚动
LPARAM: 鼠标当前的位置,但是 lParam 传过来的是 屏幕坐标系(显示器的左上角)
LOWORD --- X 坐标
HIWORD --- Y 坐标
使用通过偏移量,获取滚轮的方向和倍数
4> WM_MOUSEMOVE 鼠标移动消息 :
产生时间: 当鼠标有移动,WM_MOUSEMOVE 消息就会连续不断的产生,
鼠标移动时除了会产生 WM_MOUSEMOVE 消息外还会连续不断产生 WM_SETCURSOR 消息(是在鼠标被捕获的情况下才会连续不断的产生WM_SETCURSOR)
消息附带信息:
wParam: 该参数告诉我们各个虚拟键有没有被按下,可能包含多个值
MK_CONTROL --- Ctrl 键被按下
MK_LBUTTON --- 鼠标左键被按下
MK_MBUTTON --- 鼠标中间被按下
MK_RBUTTON --- 鼠标右键被按下
MK_SHIFT --- Shift 键被按下
MK_XBUTTON1 --- 第一个 X 按钮被按下
MK_XBUTTON2 --- 第二个 X 按钮被按下
lParam: 是一个32位整数型参数
LOWORD(lParam) --- 低 16 位表示鼠标相对于客户区左上角的 X 坐标 也就是鼠标的x位置
HIWORD(lParam) --- 高 16 位表示鼠标相对于客户区左上角的 Y 坐标 也就是鼠标的y位置
定时器消息
可以在程序中设置定时器,当到达时间间隔时,GetMessage()空闲时会使用PostMessage()向系统消息队列发送一个 WM_TIMER 消息,
定时器的精度是毫秒(毫秒和秒的进制是1000),但是准确度很低,例如设置时间间隔为 1000ms 但是会在非 1000毫秒到达;
不会提前只会延后产生该消息因为GetMessage()空闲的时候会检查已经到了时间的定时器,如果有才发送 WM_TIMER 消息;
此定时器主要是用来刷新界面的,如果对时间准确度要求比较高的是使用其它的定时器;
消息的附带信息
WPARAM --- 已经到时间的定时器ID
LPARAM --- 定时器处理函数的指针,如果是NULL 则调用窗口的窗口处理函数,
这个参数是给DispatchMessage()函数看的,DispatchMessage()函数根据这个参数决定调用哪个函数,
是调用窗口处理函数还是调用我们自己定义的定时器处理函数根据这个参数来决定;
定时器处理函数原型:
void CALLBACK TimerProc(){ //CALLBACK 表示这个函数为回调函数
HWND hWnd, //窗口句柄
UINT uMsg, //定时器消息的ID
UINT_PTR idEvent, //定时器ID
DWORD dwTime //当前寄存器的系统时间
}
定时器的使用步骤:
1> 创建定时器
UINT SetTimer(
HWND hWnd,//要设置定时器的窗口句柄,当定时器到时间后会产生WM_TIMER消息,但WM_TIMER消息由谁处理?因此要告诉系统谁处理WM_TIMER消息(备用)
UINT nIDEvent, //定时器ID 范围 0~2^32
UINT uElapse, //时间间隔 单位 ms(毫秒)
TIMERPROC lpTimerFunc //定时器处理函数指针,如果为NULL则使用本窗口的窗口处理函数来处理定时器消息,如果不为NULL 则调用自定义函数处理
); 创建成功返回非0;
当 lpTimerFunc 为NULL使用窗口处理函数,做为定时器处理函数;
当 lpTimerFunc 不为NULL使用定时器处理函数处理定时器消息
2> 消息处理 WM_TIMER
3> 关闭定时器
BOOL KillTimer(
HWND hWnd, //定时器窗口句柄
UINT uIDEvent //定时器ID
);
附:
消息就是一个非负的数字,本身不代任何信息,但我们规定用这个数字对应具体是现象,不同的数字做的使其不一样,像函数一样如返回1表示成功 0表示失败 相同道理;
在Windows平台下只要看见以 "LP" 开头的类型那么它必定是指针类型,只要把 "LP" 去掉就是它所指向的类型;
在Windows平台下看注释的时候只要看见 "xxxx的处理函数" 该函数一定是程序员定义由系统调用;
Ellispse(): 专门用来画圆的函数
原型:
BOOL Ellipse(
HDC hdc, //绘图设备句柄 DC 句柄(BeginPaint()函数的返回值)
int nLeftRext, //左上角X坐标
int nTopRext, //左上角Y坐标
int nRightRect, //右下角X坐标
int nBootomRext //右下角Y坐标
);
GetClientRect()获取窗口客户区大小,可以得到窗口的边界信息
原型:
BOOL GetClientRect(
HWND hWnd, //窗口句柄,要获取哪个窗口的大小
LPRECT lpRext //返回窗口边界信息(窗口的大小),LPRECT 以 "LP" 开头是一个指针,指向的是 RECT 的结构体
);
RECT 结构体原型: 保存矩形框信息的结构体
typedef struct _RECT{
LONG left; //左
LONG top; //上
LONG right; //右
LONG bottom; //下
} RECT, * PRECT;
使用:
RECT rc = {0};
GetClientRect(hWnd, &rc);
菜单相关消息:
WM_COMMAND 消息:
当菜单的风格填写为 MF_STRING 时被点击后会产生 WM_COMMAND 消息, 加速键也能产生 WM_COMMAND 消息
WM_COMMAND 消息的两个附带信息:
WPARAM:
HIWORD --- 如果来自于菜单项高两个字节为0,如果是加速键为1,对于控件是消息的通知码
LOWORD --- 低两个字节是被点击控件的ID
LPARAM --- 来自于菜单项 LPARAM 为NULL,对于控件为控件窗口的句柄
根据 WM_COMMAND 消息的附带信息 WPARAM 拿到菜单ID 然后根据菜单ID对应的数字再进行消息的处理
WM_INITMENUPOPUP 消息:
在菜单被激活但是未显示,窗口会收到这个消息
两个附带信息:
WPARAM ---- 即将显示菜单的句柄
LPARAM ---- LOWORD 低两个字节,是被点击顶层菜单项位置(索引就是从0开始的位置)
HIWORD 高两个字节,即将显示菜单是否为窗口菜单
窗口菜单: 顶层菜单/系统菜单 属于窗口菜单,弹出式菜单不属于窗口菜单;
WM_SYSCOMMAND 消息:
系统菜单项被点击时发送 WM_SYSCOMMAND 消息(普通菜单项被点击是发送 WM_COMMAND 消息):
WPARAM 的 LOWORD(低两个字节)是 被点击菜单项的ID
图标相关消息:
WM_SETICON 消息:
WM_SETICON 消息是由我们发送系统处理, WM_SETTCON 消息是通过 SendMessage() 发送消息,一般是在创建窗口之后 手动发送该消息;
只有两个函数能发送消息:
SendMessage() --- 发送消息不进系统队列,直接调用窗口处理函数,会等待消息处理的结果
PostMessage() --- 发送消息到系统消息队列里,消息发送后立刻返回,不等待消息的执行结果,系统消息队列会把消息转发到程序消息队列里;
原型:
LRESULT SendMessage/PostMessage (
HWND hWnd, //消息发送的目的窗口
UINT Msg, //消息ID
WPARAM wParam, //消息参数,在发送图标消息的时候 wParam 是填写图标的类型 是改大图标还是改小图标要告诉系统;
LPARAM lParam //消息参数,在发送图标消息的时候 lParam 是填写要改成那一个图标的句柄;
);
wParam 的取值:
wParam 填写 ICON_BIG ,要修改大图标
wParam 填写 ICON_SMALL ,要修改小图标
大图标在 Alt+Tab 里面,小图标在窗口的标题栏里面(系统菜单那一行);
系统菜单 --- 最大化/最小化/关闭 那一行的菜单
顶层菜单 --- 文件/编辑/帮助 那一行
弹出式菜单 --- 鼠标右键, 点击 文件/编辑/帮助 出现的菜单
WM_SETCURSOR 消息: 和 默认处理函数( DefWindowProc() )有冲突
SetCursor() 函数必须在 WM_SETCURSOR 消息里面调用
产生时间: 在不捕获鼠标的前提下,当鼠标有移动,WM_SETCURSOR 消息就会连续不断的产生,
鼠标移动时除了会产生 WM_SETCURSOR 消息外还会连续不断产生 WM_MOUSEMOVE 消息
用法: 专门用来改光标
消息附带信息:
wParam --- 当前使用的光标句柄
lParam --- LOWORD(lParam) 低两个字节 鼠标光标的活动区域(Hit-Test code)
HTCLIENT(表示鼠标光标在窗口客户区活动)
HTCAPTION(表示鼠标光标在非客户区活动)
HIWORD(lParam) 高两个字节 当前鼠标消息ID,在鼠标移动过程中有没有点鼠标左键右键这类的;
在鼠标没有被捕获的情况下,鼠标移动会连续不断的产生 WM_SETCURSOR 消息,如果鼠标被捕获了就不会发出该消息;
注:
当使用 SetCursor() 函数修该了鼠标光标,要避免调用 DefWindowProc()函数,因为 DefWindowProc() 函数会把我们修改的光标还原成默认光标;
十. 菜单
菜单可以看成是一个容器,里面装的是菜单项;
菜单的分类:
1> 窗口顶层菜单 (窗口最上面的一行里面有: 文件/编辑/帮助 ...)
2> 弹出式菜单 (鼠标右键 或 点击 文件/编辑/帮助... 弹出的菜单)
3> 系统菜单 (最大化最小化关闭那一行的菜单)
HMENU 类型是菜单句柄用来保存菜单相关的数据,菜单每一项有相应的ID;
窗口的顶层菜单
创建顶层菜单
HMENU CreateMenu(void);//创建成功返回菜单句柄
增加菜单项
BOOL AppendMenu( //以追加的方式添加菜单项 InsertMenu()以插入方式添加菜单项
HMENU hMenu; //菜单句柄
UINT uFlags; //菜单项风格,有三种基本风格必选其一,其它的风格可选可不选;
UINT uIDNewItem, //菜单项ID 或 下拉菜单的句柄,如果第二个参数填的风格是MF_POPUP则就填写下拉菜单的句柄,其余的添ID但如果是分割线可以不添ID
LPCTSTR lpNewItem //菜单项名称
);
uFlags 菜单项风格:
MF_BITMAP //具备该风格,能给菜单项添加图片
MF_CHECKED //具备该风格,菜单项可以勾选
MF_DISABLED //具备该风格,菜单项不可用,Win7下和MF_GRAYED一样变成灰色不可用
MF_ENABLED //具备该风格,菜单项可用
MF_GRAYED //具备该风格,菜单项变灰色不可用
MF_MENUBARBREAK //具备该风格,菜单项换列 菜单项过多时建议换列
MF_MENUBREAK //具备该风格,菜单项换列 菜单项过多时建议换列
MF_OWNERDRAW //具备该风格,自绘菜单项,自绘时要处理 WM_MEASUREITEM 和 WM_DRAWITEM 消息
MF_POPUP //菜单项三基本风格之一,具备该风格,点击菜单项弹出下拉菜单,不需要填菜单项ID,但需要填写下拉菜单的句柄 \
MF_SEPARATOR //菜单项三基本风格之一,具备该风格,菜单项就变成分割线,不需要填写菜单项ID | 必选其一
MF_STRING //菜单项三基本风格之一,具备该风格,菜单项被点击会发出 WM_COMMAND 消息 /
MF_UNCHECKED //具备该风格,菜单项非勾选
当 uFlags 的风格填写为 MF_STRING 时菜单项被点击会函数会发送 WM_COMMAND 消息
WM_COMMAND 消息的两个附带信息:
WPARAM:
HIWORD --- 如果来自于菜单项高两个字节为0,如果是加速键为1,对于控件是消息的通知码
LOWORD --- 低两个字节是被点击菜单项的ID
LPARAM --- 来自于菜单项 LPARAM 为NULL,对于控件为控件窗口的句柄
当顶层菜单的菜单项被点击产生 WM_COMMAND 消息根据 WM_COMMAND 消息的附带信息 WPARAM 拿到菜单ID 然后根据菜单ID对应的数字再进行消息的处理
顶层菜单本身被点击可以通过 WM_INITMENUPOPU 消息来判断哪个被点击了,在右键弹起到菜单显示中间产生 WM_INITMENUPOPUP 消息;
消息的6个组成部分:
1> 窗口句柄
2> 消息 ID
3> 消息的两个附带信息 WPARAM LPARAM
4> 消息产生的时间
5> 消息产生时的鼠标位置
把顶层菜单挂到窗口上(菜单就相当于容器,里面装菜单项)
BOOL SetMenu(
HWND hWnd, //窗口句柄
HMENU hMenu //菜单句柄
);
弹出式菜单
创建弹出式菜单
HMENU CreatePopupMenu(void); //创建成功返回菜单句柄
把弹出式菜单添加到顶层菜单里 或在 给弹出式菜单添加菜单项
BOOL AppendMenu(
HMENU hMenu, //菜单句柄
UINT uFlags, //菜单项风格, 如果填写的是 MF_POPUP 风格, uIDNewItem 参数就填写弹出式菜单的句柄,不再填写菜单项ID了
UINT_PTR uIDNewItem, //弹出式菜单的句柄 或 菜单项ID
LPCTSTR lpNewItem //菜单项名称
);
菜单项被点击 产生 WM_COMMAND 消息
AppendMenu()是以追加的方式添加菜单项,可以给任何菜单添加菜单项可以给顶层和弹出式菜单添加菜单项,
还有一个添加菜单项的函数 InsertMenu() 是以插入的方式添加菜单项;
DeleteMenu()什么菜单项都能删除,包括 系统菜单项,弹出式菜单项,顶层菜单项;
菜单项的状态
在增加菜单项(调用 AppendMenu())的时候可以设置菜单项的初始状态,
如果想在程序运行当中修改菜单项的状态可以使用菜单API(函数)修改状态:
CheckMenuItem() //设置菜单项勾选非勾选状态
EnableMenuItem() //设置菜单项可用不可用以及灰色不可用状态
CheckMenuItem()函数原型: 设置菜单项勾选非勾选状态
DWORD CheckMenuItem(
HMENU hMenu, //菜单句柄
UINT uIDCheckItem, //菜单ID 或 菜单项的位置(索引就是以0为基准的位置), 是填写菜单ID还是填写位置根据第三个参数决定
UINT uCheck // 设置修改的状态 通过设置 uIDCheckItem 来确定是填写菜单项的ID 还是 菜单项的位置
);
uCheck 的取值: 4个取值,只能填写两个,两两排斥
如果 uCheck 填写的是 MF_BYCOMMAND , uIDCheckItem 参数必须填写菜单ID
如果 uCheck 填写的是 MF_BYPOSITION , uIDCheckItem 参数必须填写菜单项的位置,以0为基准的位置
如果 uCheck 填写的是 MF_CHECKED 表示菜单项勾选
如果 uCheck 填写的是 MF_UNCHECKED 表示菜单项非勾选
菜单项的位置从0开始,但必须是同一菜单里面的菜单项,不同菜单的菜单项不影响;
EnableMenuItem()函数原型: 设置菜单项可以不可用或灰色不可用
BOOL EnableMenuItem(
HMENU hMenu, //菜单句柄
UINT uIDEnableItem, //菜单项的ID 或 菜单项的位置(索引就是以0为基准的位置),是填写菜单的ID还是填写位置根据第三个参数决定
UINT uEnable //设置修改的状态 和 设置 uIDEnableItem 是填写菜单项的ID 还是 菜单项的位置
);
uEnable 的取值: 5个取值,只能填写两个,相互排斥
如果 uEnable 填写的是 MF_BYCOMMAND , uIDEnableItem 参数必须填写菜单项ID
如果 uEnable 填写的是 MF_BYPOSITION , uIDEnableItem 参数必须填写菜单项位置
如果 uEnable 填写的是 MF_DISABLED 菜单项不可用,WIN7及其以上系统效果和 MF_GRAYED 一样
如果 uEnable 填写的是 MF_ENABLED 菜单项可用
如果 uEnable 填写的是 MF_GRAYED 菜单项灰色不可用
系统菜单
1> 获取系统菜单
HMENU GetSystemMenu(
HWND hWnd, //窗口句柄
BOOL bRevert //重置选项
); 如果 bRevert 为 FALSE 返回获取到的系统菜单句柄,否则返回 0;
bRevert 的取值:
TRUE --- 删除旧菜单,恢复到默认的系统菜单,不返回系统菜单的句柄;
FALSE --- 返回当前系统菜单的句柄
注:
如果 bRevert 为 FALSE 返回获取到的系统菜单句柄, GetSystemMenu()函数返回当前系统菜单的句柄;
如果 bRevert 为 TRUE 不返回系统菜单的句柄,GetSystemMenu()函数返回 0 ;
2> 增加系统菜单
AppendMenu() //添加菜单项,追加的方式, InsertMenu()以插入方式添加菜单项
原型:
BOOL AppendMenu(
HMENU hMenu, //菜单句柄
UINT uFlags, //菜单项风格
UINT_PTR uIDNewItem, //菜单的ID
LPCTSTR lpNewItem //菜单项名称
);
DeleteMenu() //删除菜单项,什么菜单项都能删除
原型:
BOOL DeleteMenu(
HMENU hMenu, //菜单句柄
UINT uPosition, //菜单项ID 或 菜单项位置, 是填写菜单项ID还是菜单项位置取决于第三个参数
UINT uFlags //决定第二个参数是填写 菜单项ID 还是 菜单项位置
);
uFlags 的取值:
如果 uFlags 填写 MF_BYCOMMAND , uPosition 就必须填写菜单项ID
如果 uFlags 填写 MF_BYPOSITION , uPosition 就必须填写菜单项位置
注:
在删除菜单项的时候,要注意把第一个菜单项删除后第二个菜单项会自动向前移动成为新的第一个菜单项,也就是说被删除的菜单项会影响后面菜单项的位置;
3> WM_SYSCOMMAND 消息:
系统菜单项被点击时发送 WM_SYSCOMMAND 消息(普通菜单项被点击是发送 WM_COMMAND 消息):
WPARAM 的 LOWORD(低两个字节)是 被点击菜单项的ID
上下文菜单(右键菜单) Context Menu
1> 创建右键菜单
右键菜单是一个弹出式菜单,使用 CreatePopupMenu() 函数创建,一般是在右键弹出的时候才弹出菜单,
在右键弹起到菜单显示中间也会产生 WM_INITMENUPOPUP 消息;
CreatePopupMenu()函数原型:
HMENU CreatePopupMenu(void); //创建成功返回菜单句柄
窗口菜单包括:
系统菜单,顶层菜单 弹出式菜单不属于窗口菜单;
2> 直接显示弹出式菜单
BOOL TrackPopupMenu(
HMENU hMenu, //弹出式菜单句柄
UINT uFlags, //显示方式
int x, //水平位置,屏幕坐标系
int y, //垂直位置,屏幕坐标系
int nReserved, //预留参数,给0即可
HWND hWnd, //处理菜单消息的窗口句柄,当点击弹出式菜单里的菜单项发生的消息交给此参数指明的窗口处理函数处理
CONST RECT* prcRect //NULL,忽略,过去此参数有用,现在已经不使用了, RECT 是保存矩形框信息的一个结构体
); TrackPopupMenu()是一个阻塞函数,但有菜单项被点击TrackPopupMenu()函数就会返回,会返回右键菜单弹出式菜单中被点击的菜单项的ID
uFlags 的取值: 显示方式
如果 uFlags 填写的是 TPM_CENTERALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单水平方向居中
如果 uFlags 填写的是 TPM_LEFTALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单水平方向的左边
如果 uFlags 填写的是 TPM_RIGHTALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单水平方向的右边
如果 uFlags 填写的是 TPM_BOTTOMALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单垂直方向的底部
如果 uFlags 填写的是 TPM_TOPALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单垂直方向的顶部
如果 uFlags 填写的是 TPM_VCENTERALIGN 当右键菜单弹出式菜单弹出的时候鼠标的位置处于弹出式菜单垂直方向的居中
如果 uFlags 填写的是 TPM_LEFTBUTTON 当右键菜单弹出式菜单弹出的时候只能使用鼠标的左键去点击菜单项
如果 uFlags 填写的是 TPM_RIGHTBUTTON 当右键菜单弹出式菜单弹出的时候使用鼠标的左右键点击菜单项都可以
如果 uFlags 填写的是 TPM_RETURNCMD 如果不填写这个风格,右键菜单弹出式菜单的菜单项被点击会发出 WM_COMMAND 消息,
如果填写这个风格右键菜单弹出式菜单的菜单项被点击后就不会发出 WM_COMMAND 消息
TrackPopupMenu()是一个阻塞函数,但有菜单项被点击TrackPopupMenu()函数就会返回,会返回右键菜单弹出式菜单中被点击的菜单项的ID
有这个为基础就可以在 TrackPopupMenu() 函数后面直接处理菜单项被点击
原因有两个:
1> TrackPopupMenu()函数是阻塞函数,如果右键菜单弹出式菜单不点击就不会返回
2> 右键菜单的菜单项被点击 TrackPopupMenu()函数会返回被点击的菜单项ID
有了被点击的菜单项ID 就能根据被点击的菜单项ID做不同的处理了
作用:
如果有两个菜单,菜单里面的菜单项ID一样此时就可以用这个风格来区分处理;
注:
上下文菜单(右键菜单)一般是在右键弹起时再弹出菜单,也就是说在处理 WM_RBUTTONUP 消息的时候再创建右键菜单
但 WM_RBUTTONUP 消息的附带信息 lParam 是鼠标的位置但这个位置是窗口内部坐标系 而右键菜单需要的是 屏幕坐标系的位置,因此需要做转换;
ClientToScreen() //把窗口客户区坐标系下的坐标转换成屏幕坐标系下的坐标
原型:
BOOL ClientToScreen(
HWND hWnd, // 窗口句柄
LPPOINT lpPoint // 是一个输入/输出参数 [IN/OUT] ,借这个参数输入数据然后再借这个参数把数据传出,
这里输入窗口客户区坐标系下的坐标,传出屏幕坐标系下的坐标
"LP"是一个指针,把"LP"去掉就是指向的类型 POINT(点) , POINT 是一个结构体,里面有两个成员 x和y坐标
);
ScreenToClient() //把屏幕坐标系下的坐标转换成窗口客户区坐标系下的坐标
原型:
BOOL ScreenToClient(
HWND hWnd, // 窗口句柄
LPPOINT lpPoint // 是一个输入/输出参数 [IN/OUT] , 借这个参数输入数据然后再借这个参数把数据传出,
这里输入屏幕坐标系下的坐标,传出窗口客户区坐标系下的坐标
"LP"是一个指针,把"LP"去掉就是指向的类型 POINT(点) , POINT 是一个结构体,里面有两个成员 x和y坐标
);
POINT 结构体的使用:
POINT pt = {0};
pt.x = LOWORD(lParam); // pt 结构体中的 x 保存一个坐标
pt.y = HIWORD(lParam); // pt 结构体中的 y 保存一个坐标
两个坐标合起来就可以确定一个点的位置了
扩:
RECT 结构体原型: 保存矩形框信息的结构体
typedef struct _RECT{
LONG left; //左
LONG top; //上
LONG right; //右
LONG bottom; //下
} RECT, * PRECT;
3> 菜单的处理
鼠标右键弹起
WM_RBUTTONUP 鼠标右键弹起发出的消息,但它的坐标系是窗口坐标系坐标,而我们需要的是屏幕坐标系下的坐标,因此需要做转换
ClientToScreen() 窗口客户区坐标系下的坐标 转换成 屏幕坐标系下的坐标
ScreenToClient() 屏幕坐标系下的坐标 转换成 窗口客户区坐标系下的坐标
菜单被激活但还未被显示
WM_INITMENUPOPUP 菜单被激活但还未被显示发出的消息
WM_CONTEXTMENU 鼠标右键弹起产生该消息, 在 WM_RBUTTONUP 消息之后产生,专门用来处理右键菜单的;
WParam --- 右键点击的窗口句柄
LPARAM --- LOWORD X坐标,屏幕坐标系
HIWORD Y坐标,屏幕坐标系
WM_CONTEXTMENU 消息是在 WM_RBUTTONUP 消息之后产生
总:
菜单相关:
CreateMenu(void); //创建顶层菜单
CreatePopupMenu(void); //创建弹出式菜单
GetSystemMenu( HWND hWnd, /*窗口句柄*/ ,BOOL bRevert /*重置选项*/); //获取系统菜单
CreatePopupMenu(void); //创建右键菜单
TrackPopupMenu(.......); //显示弹出式菜单
AppendMenu(.......); //增加菜单项,InsertMenu()以插入方式添加菜单项
DeleteMenu(.......); //删除菜单项
十一. 资源的使用
资源相关
资源脚本文件: *.rc 文件
编译器: RC.EXE
回顾:
.c/.cpp --- CL.EXE ---> .obj \ .c/.cpp 程序的代码
| --- LINK.EXE ---> .exe
.rc --- RC.EXE ---> .res / .rc 资源脚本文件
菜单资源的使用:
1> 添加菜单资源
2> 加载菜单资源
2.1> 在注册窗口类时设置菜单资源
我们添加菜单资源后拿到的是菜单的数字ID,但注册窗口类时要我们填写的是字符串形式的菜单ID,因此我们需要转换,
MAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
如:
MAKEINTRESOURCE(IDR_MENU1);
使用时: 除了把数字形式的资源ID 转换成 字符串形式的资源ID 还需要导入 "resource.h" 头文件
"resource.h" 头文件是在我们保存 .rc 的时候生成的头文件,里面是菜单项对应的数字,还包含有 IDR_MENU1 ...
2.2> 添加菜单资源,设置到窗口
HMENU LoadMenu(
HINSTANCE hInstance, //当前程序实例句柄
LPCTSTR lpMenuName //要添加资源的资源ID,是字符串形式的资源
//使用 IDMAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
);返回 HMENU 的句柄
如果是使用 CreateMenu() 函数创建菜单 它的返回值就是 菜单句柄, 但如果不是使用函数创建的菜单而是使用资源创建的菜单,
则需要到内存里找菜单所占的那块内存,我们的资源保存在 .rc 文件中 虽然经过 RC.EXE 编译生成 .res 文件 但也是保存在硬盘上面的,
没有加载到内存里面,但 .obj(.c/.cpp编译后的文件) 和 .res 文件 通过连接生成 .exe 文件 .exe 文件就可以进内存,
进内存变成进程就有进程的实例句柄,就能找到 .exe 程序在内存中所占的内存,然后在再哪个内存区域中找菜单资源所占的内存,
再把菜单资源添加到窗口里,就相当于把硬盘上的菜单添加到窗口里面了,这是在程序运行之间完成的操作;
挂菜单资源 可以在 注册窗口类 或 创建窗口 时挂菜单资源
但有区别:
如果是在注册窗口类的时候挂的菜单资源,那么所有基于该窗口类创建的窗口都会有该菜单资源,
如果是在创建窗口的时候挂的菜单资源,那么就只有该窗口特有的资源菜单,其它窗口没有该菜单资源;
图标资源的使用:
1> 添加资源
注意图标的大小,一个图标文件中可以有多个不同大小的图标
2> 获取图标资源
HICON LoadIcon(
HINSTANCE hInstance, //当前程序实例句柄
LPCTSTR lpIconName //要添加资源的资源ID,是字符串形式的资源ID
//使用 MAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
); 成功返回 HICON (图标) 的句柄
我们的资源保存在 .rc 文件中 虽然经过 RC.EXE 编译生成 .res 文件 但也是保存在硬盘上面的,
没有加载到内存里面,但 .obj(.c/.cpp编译后的文件) 和 .res 文件 通过连接生成 .exe 文件 .exe 文件就可以进内存,
进内存变成进程就有进程的实例句柄,就能找到 .exe 程序在内存中所占的内存,然后再那哪个内存区域中找图标资源所占的内存,
在注册窗口类的时候在设置大图标和小图标时把图标资源添加到窗口类里面,就相当于把硬盘上的图标添加到了窗口类里面;
这是在程序运行之间完成的操作;
如果是在注册窗口类里面添加的大小图标资源,那么所有基于该窗口类创建的窗口大小图标都一样,共用同一个图标资源;
3> 设置大小图标的方式
3.1> 注册窗口类
在注册窗口类的时候可以设置大小图标,但如果在注册窗口类的时候设置大小图标,
那么基于该窗口类创建的所有窗口大小图标都一样,共用同一个图标资源;
3.2> WM_SETICON 消息
如果想不同的窗口使用不同的大小图标,可以借助于 WM_SETICON 消息,
WM_SETICON 消息是由我们发送系统(其实就是系统的默认处理 DefWindowProc() )处理,
WM_SETTCON 消息是通过 SendMessage() 发送消息,一般是在创建窗口之后 手动发送该消息;
需要我们自己发的消息包括:
WM_SETICON WM_QUIT 和 自定义消息
只有两个函数能发送消息:
SendMessage() --- 发送消息不进系统队列,直接调用窗口处理函数,会等待消息处理的结果
PostMessage() --- 发送消息到系统消息队列里,消息发送后立刻返回,不等待消息的执行结果,系统消息队列会把消息转发到程序消息队列里;
原型:
LRESULT SendMessage/PostMessage (
HWND hWnd, //消息发送的目的窗口
UINT Msg, //消息ID
WPARAM wParam, //消息参数,在发送图标消息的时候 wParam 是填写图标的类型 是改大图标还是改小图标要告诉系统;
LPARAM lParam //消息参数,在发送图标消息的时候 lParam 是填写要改成那一个图标的句柄;
);
wParam 的取值:
wParam 填写 ICON_BIG ,要修改大图标
wParam 填写 ICON_SMALL ,要修改小图标
大图标在 Alt+Tab 里面,小图标在窗口的标题栏里面(系统菜单那一行);
系统菜单 --- 最大化/最小化/关闭 那一行的菜单
顶层菜单 --- 文件/编辑/帮助 那一行
弹出式菜单 --- 鼠标右键, 点击 文件/编辑/帮助 出现的菜单
4> 绘制
DrawIcon() --- 在窗口客户区中绘制一个后缀为 .ico 的图标
原型:
BOOL DrawIcon(
HDC hDC, //绘图设置的句柄 ( BeginPaint() 的返回值)
int x, //水平坐标
int y, //垂直坐标
HICON hIcon //要绘制的图标的句柄
);
光标资源的使用
1> 添加光标的资源,光标资源其实也就是图片,但光标图片必须是 .cur 为后缀的图片才能做光标图片
还有一种光标资源 .ani 为后缀的光标叫动画光标, .ani 光标里面保存的不是图片是保存的一段视频流;
光标的大小默认是 32x32 像素的图片,每个光标有 HotSpot(热点), 也就是说每个光标由 1000多个点构成,但每个光标只有一个热点;
热点就是鼠标点击东西能生效的哪个点;
2> 获取光标资源的句柄
HCURSOR LoadCursor(
HINSTANCE hInstance, //当前程序实例句柄
LPCTSTR lpCursorName //要添加光标资源的资源ID,是字符串形式的资源ID
//使用 MAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
); 返回 HCURSOR (鼠标光标的句柄)
可以在注册窗口时设置鼠标的光标,但在注册窗口类的时候设置的鼠标光标只对窗口客户区有效,离开窗口客户区鼠标又恢复成系统默认光标;
LoadCursor()函数能加载静态的光标资源,但硬盘上的动态光标不像静态光标那样有资源因此就不能再使用 LoadCursor() 来加载硬盘上的动态光标
LoadCursorFromFile()函数可以从硬盘上加载光标
原型:
HCURSOR LoadCursorFromFile(
LPCTSTR lpFileName //带盘符的光标文件的全路径名
); 返回该光标的句柄
3> 设置光标
如果想在程序运行过程中更改鼠标的光标,可以使用 SetCursor() 设置光标
原型:
HCURSOR SetCursor(
HCURSOR hCursor //新光标的句柄
);返回原来的光标句柄
WM_SETCURSOR 消息: 和 默认处理函数( DefWindowProc() )有冲突
SetCursor() 函数必须在 WM_SETCURSOR 消息里面调用
产生时间: 在不捕获鼠标的前提下,当鼠标有移动,WM_SETCURSOR 消息就会连续不断的产生,
鼠标移动时除了会产生 WM_SETCURSOR 消息外还会连续不断产生 WM_MOUSEMOVE 消息
用法: 专门用来改光标
消息附带信息:
wParam --- 当前使用的光标句柄
lParam --- LOWORD(lParam) 低两个字节 鼠标光标的活动区域(Hit-Test code)
HTCLIENT(表示鼠标光标在窗口客户区活动)
HTCAPTION(表示鼠标光标在非客户区活动)
HIWORD(lParam) 高两个字节 当前鼠标消息ID,在鼠标移动过程中有没有点鼠标左键右键这类的;
在鼠标没有被捕获的情况下,鼠标移动会连续不断的产生 WM_SETCURSOR 消息,如果鼠标被捕获了就不会发出该消息;
注:
当使用 SetCursor() 函数修该了鼠标光标,就要避免调用 DefWindowProc()函数,因为 DefWindowProc() 函数会把我们修改的光标还原成默认光标;
所以如果在子类中 调用了 SetCursor() 函数 就不要再去调用基类的 SetCursor()(参见 MFC单文档视图框架 )
练习:
鼠标在窗口左边圆球 鼠标移动 过了中间线 变成 方型,要用以下的函数:
GetCursorPos() 获取鼠标光标的位置(屏幕坐标系下的位置)
原型:
BOOL GetCursorPos(
LPPOINT lpPoint //返回光标的位置, "LP" 是一个指针,把 "LP"去掉就是所指向的类型,该类型是 POINT 类型,
//指针返回的是 屏幕坐标系下鼠标光标的位置
);
GetClientRect() 获取窗口的边界信息(窗口客户区坐标系下的位置)
原型:
BOOL GetClientRect(
HWND hWnd, //窗口句柄,要获取哪个窗口的大小
LPRECT lpRext //返回窗口边界信息(窗口的大小),LPRECT 以 "LP" 开头是一个指针,指向的是 RECT 的结构体
);
RECT 结构体原型: 保存矩形框信息的结构体
typedef struct _RECT{
LONG left; //左
LONG top; //上
LONG right; //右
LONG bottom; //下
} RECT, * PRECT;
使用:
RECT rc = {0};
GetClientRect(hWnd, &rc);
ClientToScreen() //把窗口客户区坐标系下的坐标转换成屏幕坐标系下的坐标
原型:
BOOL ClientToScreen(
HWND hWnd, // 窗口句柄
LPPOINT lpPoint // 是一个输入/输出参数 [IN/OUT] ,借这个参数输入数据然后再借这个参数把数据传出,
这里输入窗口客户区坐标系下的坐标,传出屏幕坐标系下的坐标
"LP"是一个指针,把"LP"去掉就是指向的类型 POINT(点) , POINT 是一个结构体,里面有两个成员 x和y坐标
);
ScreenToClient() //把屏幕坐标系下的坐标转换成窗口客户区坐标系下的坐标
原型:
BOOL ScreenToClient(
HWND hWnd, // 窗口句柄
LPPOINT lpPoint // 是一个输入/输出参数 [IN/OUT] , 借这个参数输入数据然后再借这个参数把数据传出,
这里输入屏幕坐标系下的坐标,传出窗口客户区坐标系下的坐标
"LP"是一个指针,把"LP"去掉就是指向的类型 POINT(点) , POINT 是一个结构体,里面有两个成员 x和y坐标
);
扩:
POINT 结构体的使用:
POINT pt = {0};
pt.x = LOWORD(lParam); // pt 结构体中的 x 保存一个坐标
pt.y = HIWORD(lParam); // pt 结构体中的 y 保存一个坐标
两个坐标合起来就可以确定一个点的位置了
字符串资源
用途: 如果用户要求我们实现中英文两版程序,就可以使用字符串资源;
原理: 在程序中但凡用到字符串的地方都不要使用固定的字符串字面常量值,
如果使用字符串字面常量值如果用户要求字符串更改就要需要修改代码这样就违背了程序的可改性;
软件工程的可改性: 在不改动代码的大前提下,程序想怎么变就怎么变;
类似于宏替换
1> 添加字符串资源
添加字符串表,在表中增加字符串
2> 加载字符串资源
int LoadString(
HINSTANCE hInstance, //当前程序实例句柄
UINT uID, //字符串ID
LPTSTR lpBuffer, //用来保存字符串ID对应的字符串的缓冲区
int nBufferMax //字符串 缓冲区 的大小
); 成功返回字符串的真正长度,失败返回0
加速键资源的使用(快捷键)
菜单和加速键本质上是没有一点关系的,但菜单中都有和加速键对应的菜单项,因此一般情况下 加速键都和菜单绑定使用;
1> 建立加速键表资源,增加命令 ID 对应的加速键
2> 加载加速键
2.1 加载加速键表
HACCEL LoadAccelerators(
HINSTANCE hInstance, //当前程序实例句柄
LPCTSTR lpTableName //加速键表资源ID,是字符串形式的资源ID
// 使用 MAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
); 返回加速键表句柄
2.2 翻译加速键消息
原型:
int TranslateAccelerator(
HWND hWnd, //处理消息的窗口句柄
HACCEL hAccTable, //加速键表句柄
LPMSG lpMsg //具体的消息,是一个指针 去掉 "LP"就是执行的类型 MSG
); TranslateAccelerator() 函数会判断是否是加速键, 如果是加速键返回非零, 如果不是加速键返回0;
如果是加速键 TranslateAccelerator() 函数会调用 PostMessage() 发 WM_COMMAND 消息, 到系统消息队列里,
然后再由系统消息队列转发到程序的消息队列里;
TranslateAccelerator()函数的执行流程:
TransateAccelerator(hWnd,hAccel,&nMsg){ //翻译消息
//首先判断 nMsg 消息是不是键盘消息,如果不是键盘消息说明没有按键被按下,那肯定不是加速键
if(nMsg.message != WM_KEYDOWN/WM_SYSKEYDOWN){ //nMsg.message 消息ID
return 0; //如果不是加速键返回0
}
//如果是按键消息就要判断是不是加速键被按下了,WM_KEYDOWN 的 WPARAM 的附带信息是虚拟键码值根据虚拟键码值来判断
通过 nMsg.wParam(虚拟键码值)可以获知是哪个按键被按下 如 Ctrl + M ,但 WM_KEYDOWN 的键码值只有一个,怎么能传两个按键的键码值呢?
永远没有两个按键同时按下的事,因为键盘驱动只会一个一个的接收按键,一定是一个先一个后,
当按键被按下时键盘驱动会先把按下的按键的状态用一个标志量记录下来,如果在一个时间内(非常短)没有按其它按键标志量就作废
从而执行被按的按键对应的操作,如果短时间内有新的按键被按下就会和之前标志量保存的按键组合在一起再等待,
以此内推,也就是所谓的组合键;
知道按的键是那些 但不知道按的那些键是不是加速键,这个时候就该第二个参数上场了,第二个参数是 hAccel 加速键表的句柄,
拿到加速键表的句柄就能找到加速键所占的内存,在把知道的按键去加速键的内存中去匹配查找(匹配 值那一列看有没有),
if(如果没有匹配上){
//就意味着 按的键不是加速键
return 0;
}
if(如果匹配上){
//匹配上就是加速键
PostMessage(nMsg.hWnd,WM_COMMAND,低两个字节为加速键的资源ID高两个字节为1,NULL); //发送 WM_COMMAND 消息
return 非0;
}
}
TranslateAccelerator()函数是放在消息循环里面,一定是放在 GetMessage()函数后面,因为GetMessage()负责抓消息,
没有消息 TranslateAccelerator() 函数的第三个参数就没法填,但TranslateAccelerator()函数是放在 TranslateMessage()函数的后面还是前面呢?
推测: 如果放在 TranslateMessage()函数后面就先执行TranslateMessage()函数当确定是加速键的时候会有可见字符的情况,
那么 TranslateMessage()函数会判断是否为大小写并会调用 PostMessage() 发送 WM_CHAR 消息显然这些操作都是多余的因为我们不关心这些
因此 TranslateAccelerator() 函数应该放在 TranslateMessage()函数前面,
这样就可以通过 TranslateAccelerator()函数的返回值判断,如果是加速键返回非零,如果不是加速键返回0
这样就可以不执行 TranslateMessage()函数内部
2.3 在WM_COMMAND中相应消息,消息参数
WPARAM:
HIWORD 高两个字节为1 表示是 TranslateAccelerator()函数发的 WM_COMMAND 消息 是加速键消息, 为0 表示是 菜单
LOWORD 低两个字节 为命令ID,菜单项命令ID和加速键命令ID统称为命令ID,也就是具体项的ID
LPARAM ---- 如果 WM_COMMAND 消息来自加速键, LPARAM 为 NULL
菜单项被点击也会发送 WM_COMMAND 消息但参数的附带信息不一样:
WM_COMMAND 消息的两个附带信息:
WPARAM:
HIWORD --- 如果来自于菜单项高两个字节为0,如果是加速键为1,对于控件是消息的通知码
LOWORD --- 低两个字节是被点击菜单项的ID
LPARAM --- 来自于菜单项 LPARAM 为NULL,对于控件为控件窗口的句柄
根据 WM_COMMAND 消息的附带信息 WPARAM 拿到菜单ID 然后根据菜单ID对应的数字再进行消息的处理
菜单 WPARAM 高两个字节为0,加速键高两个字节为1
加速键和菜单项没有任何关系,不过我们可以把加速键和菜单项的命令ID写一样,这样就可以复用代码,这样不需要分别判断命令ID了;
注:
资源ID 和 命令ID
在使用 LoadXXXX() 函数添加资源的时候使用的是 资源ID
在处理的时候比对资源的具体菜单项是使用 命令ID
如:
switch(......){
case 命令ID:
......
break;
}
资源ID 是通过该资源ID能找到资源所占的哪块内存,
命令ID 是资源ID对应的哪块内存中具体的每一项的ID;
翻译消息:
所谓翻译消息其实就是通过发送消息来翻译消息
目前有两个:
1> TranslateMessage() 翻译消息,将按键消息翻译成字符消息(只翻译可见字符的按键消息如: A-Z, a-z)
原型:
BOOL TranslateMessage(){
CONST MSG* lpMsg; //要翻译的消息地址
}
功能:
检查消息是否是按键消息,如果不是按键消息,不做任何处理,继续执行;
如果发现是按键消息,再判断是否是可见字符按键被按下产生的按键消息,还是不是可见字符被按下产生的按键消息,
如果不是可见字符的按键按下产生的按键消息,不做任何处理继续执行,
如果是可见字符按下产生的按键消息 TranslateMessage() 则使用 PostMessage() 发送字符消息(WM_CHAR)到系统消息队列里,
然后再由系统消息队列转发到程序的消息队列里;
TranslateMessage()内部处理流程:
TranslateMessage(&nMsg){ //翻译消息
nMsg是抓到的消息的结构体,第二个成员是消息的ID,根据消息的ID判断是否是按键消息
if(nMsg.message != WM_KEYDOWN){
//如果不是按键消息直接返回
return ...;
}
如果是按键消息,再判断消息是不是可见字符被按下产生的按键消息,如果是 上下左右 这种按键被按下产生的按键消息就不翻译,
通过 nMsg.wParam(虚拟键码值)判断是否为可见字符按键被按下产生的按键消息,还是不可见字符被按下产生的按键消息;
if(不是可见字符按键被按下产生的按键消息){
//如果不是可见字符按键被按下,直接返回
return ...;
}else{
如果是可见字符按键被按下产生的按键消息就翻译消息,
翻译消息首先要确定大写锁定键是否处于打开状态,判断大小写键盘灯是否打开,然后 发送 WM_CHAR 消息
if(打开){
如果大写锁定键处于打开状态,发送 WM_CHAR 消息,并把字符的大写ASC码发送
PostMessage(nMsg.hwnd,WM_CHAR,0x.. , ...);
}else{
如果大写锁定键没有处于打开状态,发送 WM_CHAR 消息,并把字符的小写ASC码发送
PostMessage(nMsg.hwnd,WM_CHAR,0x.. , ...);
}
}
}
2> TranslateAccelerator() 翻译加速键消息
原型:
int TranslateAccelerator(
HWND hWnd, //处理消息的窗口句柄
HACCEL hAccTable, //加速键表句柄
LPMSG lpMsg //具体的消息
); TranslateAccelerator() 函数会判断是否是加速键, 如果是加速键返回非零, 如果不是加速键返回0;
如果是加速键 TranslateAccelerator() 函数会调用 PostMessage() 发 WM_COMMAND 消息, 到系统消息队列里,
然后再由系统消息队列转发到程序的消息队列里;
TranslateAccelerator()函数的执行流程:
当我们按下按键时 产生 WM_KEYDOWN 按键消息, 再由 GetMessage() 抓取 然后在 while(){} 循环中 执行 TransateAccelerator()
TransateAccelerator(hWnd,hAccel,&nMsg){ //翻译消息
//首先判断 nMsg 消息是不是键盘消息,如果不是键盘消息说明没有按键被按下,那肯定不是加速键,用来区分按键消息以为的消息
if(nMsg.message != WM_KEYDOWN/WM_SYSKEYDOWN){ //nMsg.message 消息ID
return 0; //如果不是加速键返回0
}
//如果是按键消息就要判断是不是加速键被按下了,WM_KEYDOWN 的 WPARAM 的附带信息是虚拟键码值根据虚拟键码值来判断
通过 nMsg.wParam(虚拟键码值)可以获知是哪个按键被按下 如 Ctrl + M ,但 WM_KEYDOWN 的键码值只有一个,怎么能传两个按键的键码值呢?
永远没有两个按键同时按下的事,因为键盘驱动只会一个一个的接收按键,一定是一个先一个后,
当按键被按下时键盘驱动会先把按下的按键的状态用一个标志量记录下来,如果在一个时间内(非常短)没有按其它按键标志量就作废
从而执行被按的按键对应的操作,如果短时间内有新的按键被按下就会和之前标志量保存的按键组合在一起再等待,
以此内推,也就是所谓的组合键;
知道按的键是那些 但不知道按的那些键是不是加速键,这个时候就该第二个参数上场了,第二个参数是 hAccel 加速键表的句柄,
拿到加速键表的句柄就能找到加速键所占的内存,在把知道的按键去加速键的内存中去匹配查找(匹配 值那一列看有没有),
if(如果没有匹配上){
//就意味着 按的键不是加速键
return 0;
}
if(如果匹配上){
//匹配上就是加速键
PostMessage(hWnd,WM_COMMAND,低两个字节为加速键的资源ID高两个字节为1,NULL); //发送 WM_COMMAND 消息
return 非0;
}
}
TranslateAccelerator()函数是放在消息循环里面,一定是放在 GetMessage()函数后面,因为GetMessage()负责抓消息,
没有消息 TranslateAccelerator() 函数的第三个参数就没法填,但TranslateAccelerator()函数是放在 TranslateMessage()函数的后面还是前面呢?
推测: 如果放在 TranslateMessage()函数后面就先执行TranslateMessage()函数当确定是加速键的时候会有可见字符的情况,
那么 TranslateMessage()函数会判断是否为大小写并会调用 PostMessage() 发送 WM_CHAR 消息 和 执行 DispatchMessage() 调用消息处理函数,
显然这些操作都是多余的因为我们不关心这些,因此 TranslateAccelerator() 函数应该放在 TranslateMessage()函数前面,
这样就可以通过 TranslateAccelerator()函数的返回值判断,如果是加速键返回非零,如果不是加速键返回0 再确定是否执行 TranslateMessage()函数
绘图资源
绘图设备 因为缩写 DC (Device Context) 外号: 绘图上下文,绘图描述表
HDC --- DC 句柄,表示绘图设备, BeginPaint()函数能返回绘图设备句柄
GDI --- Windows graphics device interface Win32提供的绘图API, GDI编程特指 Windows 下的绘图编程
Windows系统下如果想绘制图形绘图的步骤只能在 WM_PAINT 消息中编写;
绘图的步骤:
1> 开始绘图处理
定义一个 PAINTSTRUCT 结构体;
PAINTSTRUCT ps = {0};
HDC BeginPaint(
HWND hWnd; //绘图窗口,在哪个窗口里面画图
LPPAINTSTRUCT lpPaint //用来保存绘图的相关参数,
//"LP"表示是一个指针 去掉 "LP" 剩下的就是指向的类型 PAINTSTRUCT(是一个结构体)
);返回绘图设备句柄 HDC
2> 绘图,需要 BeginPaint()函数的返回值, 绘图设备句柄
3> 结束绘图处理
BOOL EndPaint(
HWND hWnd, //绘图窗口
CONST PAINTSTRUCT * lpPaint //绘图参数的指针 BeginPaint()函数返回值
);
以上代码必须放在 WM_PAINT 消息的处理函数中,绘图步骤必须放在绘图消息里面写;
注:
Y轴下为正,X轴右为正
Windows系统下如果想要绘图,绘图的步骤必须放在绘图消息(WM_PAINT)里面编写;
InvalidateRect()函数只是声明窗口需要重绘不能发送 WM_PAINT 消息 WM_PAINT 消息 是 GetMessage()空闲的时候发送 WM_PAINT 消息
颜色的表示:
计算机使用 红 绿 蓝 为三原色, 有了这三种颜色可以调配出各种各样的颜色
R ---- 0~255 红 占1个字节
G ---- 0~255 绿 占1个字节
B ---- 0~255 蓝 占1个字节
RGB 都为0 是黑色,都为255 是白色,黑色到白色之间有 2的24次方种颜色;
每一个点颜色是3个字节 3*8 = 24 位保存 0-2^24-1 0到2的24次方减1
16位: 低5位(红色配比),中间5位(绿色配比),高6位(蓝色配比) 二维图像编程不用考虑透明度;
32位: 低8位(红色配比),第二8位(绿色配比),第三8位(蓝色配比),最高8位(代表透明度多少,全为0代表不透明,8位全是1代表全透明)
三维图现象编程要考虑两点:
1> 如何降低CPU的使用率
2> 特殊材质的仿真
颜色的使用:
COLORREF --- 实际 DWORD(unsigned long)4个字节,可以用来保存一个颜色的值 例如: COLORREF nColor = 0; //nColor 4个字节全都是0 表示黑色
设置颜色值使用 RGB 宏 如: RGBA nColor = RGB(0,255,0); // 第一个0表示红色配比,255表示绿色配比,最后一个0表示蓝色配比
获取 颜色的 值:
GetRValue()返回红色配比
GetGValue()返回绿色配比
GetBValue()返回蓝色配比
例如:
BYTE nRed = GetRValue(nColor); //BYTE 相当于 unsigned char 无符号char 类型
RGB 宏只能做二维图像编程,因为只有 3个参数没有透明度, 所以三维图像编程是使用 RGBA 宏来设定颜色;
绘制点:
GetPixel()获取指定点的颜色
原型:
COLORREF GetPixel(
HDC hdc, //绘图设备句柄
int nXPos, //点的 x 坐标
int nYPos //点的 y 坐标
); 返回指定点的颜色
SetPixel()设置指定点的颜色
原型:
COLORREF SetPixel(
HDC hdc, //绘图设备句柄
int x, //点的 x 坐标
int y, //点的 y 坐标
COLORREF crColor //设置的颜色
);
绘制线: 线包括 直线,弧线
MoveToEx(): 移动窗口的当前点 到 MoveToEx()函数指定的点上,窗口的默认当前点是在零零点上,
MoveToEx()功能是把窗口的当前点从零零点移动到指定的点上,并把指定的点当做新的窗口当前点; 画图的起点位置
原型:
BOOL MoveToEx(
HDC hdc, //绘图设备句柄 DC 句柄
int x, //指定点的 X 坐标
int y, //指定点的 Y 坐标
LPPOINT lpPoint //原来当前点的坐标,可以不接收 填 NULL
);
扩:
窗口的当前点:
窗口的当前点可以认为是窗口的一种属性,任何一个窗口本身都具备该属性,在默认情况下窗口的当前点是在零零点上
LineTo: 从窗口的当前点到 LineTo()函数指定的点绘制一条直线,并把 LineTo()函数指定的点设置成窗口的当前点
原型:
BOOL LineTo(
HDC hdc, //绘图设备句柄 DC 句柄
int nXEnd, //指定点的 X 坐标
int nYEnd //指定点的 Y 坐标
);
封闭图形: 能够被画刷填充的图形才能叫封闭图形,如果是用一条条直线封闭的图形不是封闭图形;
Rectangle(): 绘制直角矩形的函数
原型:
BOOL Rectangle(
HDC hdc, //绘图设备句柄 DC 句柄
int nLeftRect, //矩形左上角 X 坐标
int nTopRect, //矩形左上角 Y 坐标
int nRinghtRect, //矩形右下角 X 坐标
int nBottomRect //矩形右下角 Y 坐标
);
RoundRect(): 绘制圆角矩形的函数
原型:
BOOL RoundRect(
HDC hcd, //绘图设备句柄 DC 句柄
int nLeftRect, //矩形左上角 X 坐标
int nTopRect, //矩形左上角 Y 坐标
int nRightRect, //矩形右下角 X 坐标
int nBottomRect, //矩形右下角 Y 坐标
int nWidth, //圆弧线的宽度
int nHeight //圆弧线的高度
);
注:
如果想切圆,弧的宽度和高度就是矩形的边长,如果弧的宽度和高度超过矩形的边长则按最大弧切,也能把圆切出来;
Ellispse(): 专门用来画圆的函数
原型:
BOOL Ellipse(
HDC hdc, //绘图设备句柄 DC 句柄(BeginPaint()函数的返回值)
int nLeftRext, //圆左上角X坐标
int nTopRext, //圆左上角Y坐标
int nRightRect, //圆右下角X坐标
int nBootomRext //圆右下角Y坐标
);
Arc(): 专门绘制弧线,是从起点坐标逆时针到终点坐标取弧
原型:
BOOL Arc(
HDC hdc, //绘图设备句柄 DC 句柄
int nLeftRect, //矩形左上角 X 坐标
int nTopRect, //矩形左上角 Y 坐标
int nRightRect, //矩形右下角 X 坐标
int nBottomRect, //矩形右下角 Y 坐标
int nXStartArc, //弧线起点 X 坐标
int nYStartArc, //弧线起点 Y 坐标
int nXEndArc, //弧线终点 X 坐标
int nYEndArc //弧线终点 Y 坐标
);
Arc()函数默认是从起点坐标逆时针到终点坐标取弧,但可以通过 SetArcDirection() 函数修改取弧规则:
SetArcDirection(): 修改取弧规则
原型:
int SetArcDirection(
HDC hdc, //绘图设备句柄 DC 句柄
int ArcDirection // AD_COUNTERCLOCKWISE 逆时针默认就是逆时针, AD_CLOCKWISE 顺时针
);
Pie(): 绘制扇形
原型:
BOOL Pie(
HDC hdc, //绘图设备句柄 DC 句柄
int nLeftRect, //矩形左上角 X 坐标
int nTopRect, //矩形左上角 Y 坐标
int nRightRect, //矩形右下角 X 坐标
int nBottomRect, //矩形右下角 Y 坐标
int nXRadial1, //扇形第一条半径端点的 X 坐标
int nYRadial1, //扇形第一条半径端点的 Y 坐标
int nXRadial2, //扇形第二条半径端点的 X 坐标
int nYRadial2 //扇形第二条半径端点的 Y 坐标
);
GDI绘图对象: 画笔
画笔的作用:
设置线条的 颜色,线型,线粗
HPEN ----- 画笔句柄
画笔的使用步骤:
1> 创建画笔
HPEN CreatePen(
int fnPenStyle, //画笔的样式 包括 实心线 虚线 点线 等
int nWidth, //画笔的粗细
COLORREF crColor //画笔的颜色, COLORREF 实际 DWORD(unsigned long)4个字节,可以用来保存一个颜色的值
// 例如: COLORREF nColor = 0;
//设置颜色的值使用 RGB 宏 如: RGB(255,0,0);
); 创建成功返回画图句柄
fnPenStyle 的取值:
PS_SOLID --- 画笔画出的是 实心的,可以支持多个像素宽也就是说第二个参数的粗细随便填写,如果是其它画笔样式就只能是一个像素宽
PS_DASH --- 画笔画出的是 虚线 nWidth 必须 <= 1
PS_DOT --- 画笔画出的是 点线, 线是由一个个点组成的 nWidth 必须 <=1
PS_DASHDOT --- 画笔画出的是 点划线 nWidth 必须 <=1
PS_DASHDOTDOT --- 画笔画出的是 点 点 划线 nWidth 必须 <=1
PS_NULL --- 画笔不能画图
PS_INSIDEFRAME --- 由 椭圆, 矩形, 圆角矩形, 扇图 以及 弧 等生成的封闭对象框时,画线宽度向内扩展,如指定的准确 RGB 颜色不存在,
就进行抖动处理
注:
如果 CreatePen() 函数的第一个参数 fnPenStyle 值是 PS_SOLID(实心画笔) 第二个参数 nWindth(画笔的粗细) 随便设置
如果 CreatePen() 函数的第一个参数 fnPenStyle 值不是 PS_SOLID(实心画笔) 第二个参数 nWindth(画笔的粗细) 只能是 <=1
如果 CreatePen() 函数的第二个参数 nWidth 的值 >1 则第一个不管是什么样式的画笔 通通按实心画笔处理,
因为微软没有实现其它画笔样式的像素宽,在微软推出的 GDI+的绘图库中就实现了其它画笔样式的像素宽;
虚线画出的图形也算封闭图形,虚线是因为画笔的原因,跟它是不是封闭图形没关系;
2> 将画笔应用到 绘图设备中: 使用 SelectObject()
绘图设备本身也有画笔
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 画笔或画刷的句柄, HGDIOBJ 数据类型是 GDI绘图对象句柄,画笔是属于它的一种它兼容 HPEN 画笔句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中的画笔或画刷的句柄
画笔/画刷 都属于 GDI 绘图对象 因此都可以通过 HGDIOBJ(绘图对象句柄)接收;
3> 绘图
如果想绘制图形,绘图的步骤只能在 WM_PAINT 消息中编写;
4> 取出 绘图设备 中的 画笔
将原来的画笔或画刷的句柄使用 SelectObject() 函数放入绘图设备中,就会将我们创建的 画笔 或 画刷 句柄返回
5> 释放画笔
BOOL DeleteObject(
HGDIOBJ hObject // GDI 绘图对象句柄,画笔句柄 GDI编程指的是Windows下的绘图编程
);
只能 删除不被 绘图设备 使用的画笔,所以在释放前,必须将画笔从 绘图设备中取出
GDI绘图对象: 画刷
画刷的作用:
给封闭图形填充 颜色 或 图案
HBRUSH --- 画刷句柄
画笔/画刷 都属于 GDI 绘图对象 因此都可以通过 HGDIOBJ(绘图对象句柄)接收;
画刷的使用步骤:
1> 创建画刷
CreateSolidBrush(): 创建实心画刷,可以给封闭图形填充单一的颜色
原型:
HBRUSH CreateSolidBrush(
COLORREF crColor //实心画刷的颜色
); 返回实心画刷的句柄
CreateHatchBrush(): 创建阴影画刷,可以给封闭图形填充一组阴影线
原型:
HBRUSH CreateHatchBrush(
int fnStyle, //阴影画刷阴影线的样式
COLORREF clrref //阴影线的颜色
);
fnStyle 参数的取值:
HS_BDIAGONAL ----- 45度向上,水平阴影
HS_CROSS ----- 水平和垂直交叉阴影
HS_DIAGCROSS ----- 45度交叉阴影
HS_FDIAGONAL ----- 45度向下,水平阴影
HS_HORIZONTAL ----- 水平阴影
HS_VERTICAL ----- 垂直阴影
CreatePatternBrush(): 创建位图画刷,可以给封闭图形填充以 .bmp 为后缀的图片
原型:
HBRUSH CreatePatternBrush(
HBITMAP hbmp //位图句柄
); 返回创建的 位图画刷 句柄
2> 将画刷应用到 绘图设备 中: 使用 SelectObject()
绘图设备也有画刷(默认颜色为白色)
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 画刷或画刷的句柄, HGDIOBJ 数据类型是 GDI绘图对象句柄,画刷是属于它的一种它兼容HBRUSH画刷句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中画刷或画笔的句柄
画笔/画刷 都属于 GDI 绘图对象 因此都可以通过 HGDIOBJ(绘图对象句柄)接收;
3> 绘图
如果想在 Windows 平台下绘制图形,绘图的步骤必须在 WM_PAINT(绘图消息)的处理中编写
4> 将画刷从 绘图设备 中取出
将原来的画笔或画刷的句柄使用 SelectObject() 函数放入绘图设备中,就会将我们创建的 画笔 或 画刷 句柄返回
5> 删除画刷
BOOL DeleteObject(
HGDIOBJ hObject // GDI 绘图对象句柄,画笔或画刷的句柄, GDI编程指的是Windows下的绘图编程
);
只能 删除不被 绘图设备 使用的画笔或画刷,所以在释放前,必须将画笔或画刷从 绘图设备中取出
透明画刷:
如果窗口的背景是张图片,填充什么颜色都不好看,但我们可以填充透明色;
设置透明色可以使用 RGBA 宏但 RGBA 宏是一般用于三维图像编程,
二维图像编程里面没有透明度这一概念因此在二维图像编程里面使用 RGBA 宏也不起作用;
在二维图像编程里面可以使用 GetStockObject()函数从操作系统里面获取绘图设备比如: 画刷 画笔 字体 等
GetStockObject(): 专门从操作系统里面获取 绘图设备 相关的设备 如: 画刷 画笔 字体 等等
原型:
HGDIOBJ GetStockObject(
int fnObject //想要获取的绘图设备
); 返回想要获取的绘图设备句柄
fnObject 参数的取值:
画刷:
BLACK_BRUSH ------ 黑色画刷
DKGRAY_BRUSH ------ 暗灰色画刷
DC_BRUSH ------ 纯色画刷,默认为白色,可以使用 SetDCBrushColor()函数修改颜色
GRAY_BRUSH ------ 灰色画刷
HOLLOW_BRUSH ------ 空画刷(透明画刷),相当于 NULL_BRUSH
NULL_BRUSH ------ 空画刷(透明画刷),相当于 HOLLOW_BRUSH
LTGRAY_BRUSH ------ 亮灰色画刷
WHITE_BRUSH ------ 白色画刷
如果不想使用画刷来填充,fnObject 参数就填写 NULL_BRUSH 表示使用空画刷(或者说是使用透明画刷)
画笔:
BLACK_PEN ------ 黑色画笔
DC_PEN ------ 纯色画笔,默认为白色,可以使用 SetDCPenColor()函数修改颜色
WHITE_PEN ------ 白色画笔
字体:
ANSI_FIXED_FONT ------ 在Windwos中为固定间距(等宽)系统字体
ANSI_VAR_VONT ------ 在Windows中为变间距(比列间距)系统字体
DEVICE_DEFAULT_FONT ------ 在Windows中为设备相关字体
DEFAULT_GUI_FONT ------ 用户界面对象缺省字体,如: 菜单 和 对话框
OEM_FIXED_FONT ------ 初始化相关固定间距(等宽)字体
SYSTEM_FONT ------ 系统字体,在默认情况下,系统使用系统字体绘制菜单,对话框控制和文本
SYSTEM_FIXED_FONT ------ 固定间距(等宽)系统字体,该对象仅提供给兼容 16位 Windows 版本
DEFAULT_PALETTE ------ 默认调色板,该调色板由系统调色板中的静态色彩组成
注:
GetStockObject()函数获取的绘图设备不需要 DeteleObject() 删除绘图设备
取反色就是把 红 绿 蓝 三种颜色在内存中的数值(0到255)按位取反透明色除外,透明色没有反色,透明色取反还是透明色;
GDI 绘图对象: 位图 既是资源的一种 也是 GDI绘图对象
位图常识: 所有的图片分类大体上分为以下两种
光栅图形: 计算机和生活中见到图形大部分是光栅图形 如: .bmp .jpg .gif 等,
光栅图形的共同点在硬盘中都记录了图像每一个点的颜色等信息,一个点的颜色占4个字节,因此占的硬盘空间和大很浪费空间,
.jpg 为后缀的图片一般要比 .bmp 的图片占的空间要小,因为 .jpg 是通过压缩算法来保存点的颜色的而 .bmp 是没有任何压缩的
矢量图形: 一般在实验室中见到的图形是矢量图形如细胞那种图片一般就是矢量图形,
占的硬盘空间比较小颜色很单一,为什么占硬盘空间比较小?
是因为矢量图形在硬盘上不会记录每一个点的颜色,它里面记录的是图形的算法和绘图的指令
HBITMAP ---- 位图句柄
位图 既是资源的一种 也是 GDI绘图对象
位图的使用步骤:
1> 在资源中添加位图资源
2> 从资源中加载位图资源使用 LoadBitmap() 函数
LoadBitmap():
原型:
HBITAMP LoadBitmap(
HINSTANCE hInstance, //当前程序实例句柄
LPCTSTR lpBitampName //要添加资源的资源ID,是字符串形式的资源ID
//使用 MAKEINTRESOURCE()宏 可以将任何数字形式的资源ID转换成字符串形式的资源ID;
); 成功返回 HBITMAP(位图) 的句柄
如果是通过代码创建的位图可以通过当前程序实例句柄找到该位图所占的哪块内存区域从而拿到该位图,
但我们是通过资源的方式创建的位图是保存在硬盘上的,就算我们通过 RC.EXE 编译生成 .res 文件 也是在硬盘上面没有加载到内存里面,
但 .obj(.c/.cpp 编译后的文件) 和 .res 通过连接生成 .exe 文件 执行.exe 文件就可以进内存变成进程,就可以拿到进程的实例句柄,
有进程的实例句柄就能找到 .exe 程序在内存中所占的内存,然后再在改内存区域中找位图资源所占的内存,这样就能拿到位图资源的句柄,
拿到位图资源的句柄就能对位图进行相关操作了;
3> 创建一个与 当前绘图设备(DC) BeginPaint()返回的绘图句柄就是当前绘图设备 相匹配的 内存绘图设备(DC)(内存绘图设备,内存 DC)
当前绘图设备是指在窗口中绘图(控制的是显存), 内存绘图设备是指在内存虚拟区域中绘图(控制的是内存)
HDC CreateCompatibleDC(
HDC hdc, //当前绘图设备 句柄(显存中的地址),可以为 NULL(使用屏幕 绘图设备)
); 返回创建好的 内存绘图设备虚拟区域的 句柄
CreateCompatibleDC() 作用: 创建一个内存绘图设备,同时在内存中申请和当前绘图设备(在显存中)在中一样大小的内存区域
4> 将位图放入匹配的内存虚拟绘图设备中 使用 SelectObject() 函数
虽然把位图放入匹配的内存虚拟绘图设备中但我们看不见,因为在内存中只有,把该位图加载到窗口里我们才能见到;
真正的图形是在内存虚拟区域中绘制的,只是在窗口中呈现出来而已;
好比照相机一样,真正的图形是在胶卷上(好比内存虚拟绘图设备),我们看到的相片是把胶卷上的图形呈现在相纸上(好比窗口中看见的图形)
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 画笔或画刷 位图 的句柄, HGDIOBJ 数据类型是 GDI绘图对象句柄,画笔是属于它的一种它兼容 HPEN 画笔句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中的画笔或画刷的句柄
SelectObject()函数在此的功能: 把位图所在的内存区域的首位地址放在绘图对象句柄里;
5> 成像: 把内存虚拟区域中已经画好的位图加载到显存里然后在窗口里呈现出来 使用 BitBlt() 函数
在窗口中成像,真正图形是在内存虚拟绘图设备区域里面绘制的,只是在窗口中呈现出来;
成像就好比照相机一样,真正的图形是在胶卷上(好比内存虚拟绘图设备),我们看到的相片是把胶卷上的图形呈现在相纸上(好比窗口中看见的图形)
BitBlt(): 类似于 memcopy() 是1:1呈现出来,不会放大也不会缩小
原型:
BOOL BitBlt(
HDC hdcDest, //需要在那里呈现图像, 目的地的 绘图设备 句柄
int nXDest, //目的地 左上角 X 坐标
int nYDest, //目的地 左上角 Y 坐标
int nWidth, //目的地的 宽度 开辟多大区域来成像
int nHeight, //目的地的 高度 开辟多大区域来成像
HDC hdcSrc, //图像来自于那里, 源 绘图设备 句柄
int nXSrc, //从源图像哪个位置开始拷贝图像 源左上角 X 坐标
int nYSrc, //从源图像哪个位置开始拷贝图像 源左上角 Y 坐标
DWORD dwRop //成像方法 指的是 在从源图像拷贝到窗口的时候,图像里某些点的颜色进行修改,
//这样就可以把不关心的部分和关心的部分形成对比
);
dwRop 参数的取值:
BLACKNESS --- 成像时把图像里面的颜色值通通设置成0也就是全黑
SRCCOPY --- 原样成像
NOTSRCCOPY --- 取反色成像
参数还有很多 参见 www.baidu.com -> BitBlt
BitBlt()函数的功能是: 根据保存位图在内存中的首位地址,找到位图,然后把该位图资源每个点的值一次性拷贝到当前绘图设备中(也就是显存中),
然后根据显存中保存的值点亮显示器上的发光二极管;
6> 取出位图设备 使用 SelectObject() 函数
之前把位图放到哪个绘图设备就从哪个绘图设备里面取出, 这里我们是把位图放入了 内存虚拟绘图设备,所以只能从内存虚拟绘图设备里取出
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 画笔或画刷的句柄, HGDIOBJ 数据类型是 GDI绘图对象句柄,画笔是属于它的一种它兼容 HPEN 画笔句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中的画笔或画刷的句柄
7> 释放位图 使用 DeleteObject()
BOOL DeleteObject(
HGDIOBJ hObject // GDI 绘图对象的句柄, 画笔句柄 GDI编程指的是Windows下的绘图编程
);
只能 删除不被 绘图设备 使用的画笔,所以在释放前,必须将画笔从 绘图设备中取出
8> 释放内存虚拟区域的绘图设备 使用 DeleteDC()
BOOL DeleteDC(
HDC hdc //要释放的内存虚拟绘图设备的句柄
);
扩:
使用 GetObject()获取位图信息
标准的绘图设备就是显卡,显卡主要是操作显存的,
当前绘图设备 hdc 保存的是 显存里要操作区域的首尾地址
内存绘图设备 mHdc 保存的是 内存里要操作的首尾地址
CreateCompatibleDC()就是通过当前绘图设备里操作区域的大小来申请相同大小的内存虚拟绘图设备
绘图的思路:
添加位图资源 -> 找到位图资源 -> 在内存中申请一块和当前绘图设备(显存中)一样大小的内存区域
-> 把找到的位图资源放到申请的内存中 -> 从内存中把位图资源拷贝到当前绘图设备中(显存中) -> 根据显存中的位图资源在窗口中成像
除了 BitBlt()函数 1:1 成像 还有 StretchBlt() 缩小/放大 成像
原型:
BOOL StretchBlt(
HDC hdcDest, //需要在那里成像, 目的地的 绘图设备 句柄
int nXOriginDest, //目的地 左上角 X 坐标
int nYOriginDest, //目的地 左上角 Y 坐标
int nWidthDest, //目的地的 宽度 开辟多大区域来成像
int nHeightDest, //目的地的 高度 开辟多大区域来成像
HDC hdcSrc, //图像来自于那里, 源 绘图设备 句柄
int nXOriginSrc, //从源图像哪个位置开始拷贝图像 源左上角 X 坐标
int nYOriginSrc, //从源图像哪个位置开始拷贝图像 源左上角 Y 坐标
int nWidthSrc, //源 绘图设备的 宽
int nHeightSrc, //源 绘图设备的 高
DWORD dwRop //成像方法 指的是 在从源图像拷贝到窗口的时候,图像里某些点的颜色进行修改,
//这样就可以把不关心的部分和关心的部分形成对比
);
GetObject()获取绘图设备信息: 可以动态获取 画笔/画刷/位图 等信息
原型:
int GetObject(
HGDIOBJ hgdiobj, //要获取绘图对象的句柄 如: 画笔句柄/画刷句柄/位图句柄 等
int cbBuffer, //缓冲区的大小, 根据 绘图对象的不同 填写不同的大小
LPVOID lpvObject //缓冲区的首地址
);
如:
第一个参数如果填写位图的句柄,缓冲区就开 BITMAP 结构体 的大小
如:
BITMAP bmpInfo = {0};
GetObject(bBmp,sizeof(bmpInfo),&bmpInfo);
BITMAP 结构体原型:
typedef struct tagBITMAP {
LONG bmType; //位图类型,必须为0
LONG bmWidth; //位图宽度
LONG bmHeight; //位图高度
LONG bmWidthBytes; //每一行像素所在的字节数
WORD bmPlanes; //颜色平面数
WORD bmBitsPixel; //像素的位数
LPVOID bmBits; //位图内存指针
} BITMAP, *PBITMAP;
十二. 坐标系
坐标系分类:
外围设备坐标系(以显示器为例)
显示器以像素为单位,以设备左上角为原点, X 右为正, Y 下为正的坐标系
1> 屏幕坐标希 --- 以当前屏幕左上角为原点坐标系
2> 窗口坐标系 --- 以窗口左上角为原点坐标系
3> 客户区坐标系 --- 以窗口的客户区左上角为原点的坐标系
逻辑坐标系: 计算机内部是用逻辑坐标系,调用的函数填写的参数都是逻辑坐标系
在GDI绘图中,都是使用逻辑坐标系绘图,逻辑坐标系本身没有单位但可以设置坐标系单位,是一个对应关系 如: 1个逻辑单位对应几个像素
如果没有指定对应关系则使用默认对应关系 1个逻辑单位 对应 1个像素
设备的单位是根据具体的设备来定的,逻辑单位是计算机内部使用的单位,逻辑单位和外围设备单位是有对应关系的;
坐标系映射
映射模式:
逻辑坐标系 和 设备坐标系单位之间映射关系,设备坐标系的单位是由设备决定,大小固定,逻辑坐标系的单位,可以通过程序设置;
int SetMapMode(
HDC hdc, // 绘图设备句柄
int fnMapMode // 映射模式
); 返回 旧的映射模式
fnMapMode 映射模式的取值有:
MM_TEXT ---- 默认关系, 1个逻辑单位 对应 1个像素 X 轴右为正,Y 轴下为正
以下5个模式有一个共同点: X 轴右为正, Y 轴上为正
MM_HIENGLISH ---- 1个逻辑单位 对应 0.001 英寸
MM_LOENGLISH ---- 1个逻辑单位 对应 0.01 英寸
MM_HIMETRIC ---- 1个逻辑单位 对应 0.01 毫米
MM_LOMETRIC ---- 1个逻辑单位 对应 0.1 毫米
MM_TWIPS ---- 1个逻辑单位 对应 1/1440 英寸(打印机常用)
自定义模式:
MM_ISOTROPIC ---- 1个逻辑单位 对应 自定义比列 ,如果设置的 X轴 和 Y轴 不是同比列的时候 按 X轴 比列来设置 Y轴比列则失效
MM_ANISOTROPIC ---- X 轴一个逻辑单位 对应 自定义1, Y 轴一个逻辑单位 对应 自定义2
自定义模式需要使用 SetWindowExtEx() 和 SetViewportExtEx() 函数来设置 逻辑比列 和 设备比列
X轴 和 Y轴 的方向也可以自定义
正数:
X轴 右为正 Y轴 下为正
负数:
X轴 左为正 Y轴 上为正
SetWindowExtEx(): 设置 逻辑的比例
原型:
BOOL SetWindowExtEx(
HDC hdc, //绘图设备 句柄
int nXExtent, //逻辑的 X 比列
int nYExtent, //逻辑的 Y 比列
LPSIZE lpSize //返回原来的 比例 ,一般为 NULL
);
SetViewportExtEx(): 设置 设备的比列
原型:
BOOL SetViewportExtEx(
HDC hdc, //绘图设备 句柄
int nXExtent, //设备的 X 比列
int nYExtent, //设备的 Y 比列
LPSIZE lpSize //返回原来的 比列, 一般为 NULL
);
如:
SetWindowExtEx(hdc, 1, 1, NULL); //设置逻辑的比列
SetViewportExtEx(hdc, 2, 3, NULL); //设置设备的比列
通过上面的设置得到 逻辑单位 和 设备单位 的对应关系是:
X轴的比列是 1个逻辑单位 对应 2个设备单位
Y轴的比列是 1个逻辑单位 对应 3个设备单位
例子:
int nOldMode = SetMapMode(hdc,MM_ANISOTROPIC);
SetWindowExtEx(hdc,1,1,NULL);
SetViewportExtEx(hdc,2,3,NULL);
十三. 文字的绘制
TextOut(): 将文字绘制在指定坐标位置(功能非常薄弱: 不能换行,必须指定绘制的位置,没有任何的对齐方式)
原型:
BOOL TextOut(
HDC hdc, // 绘图设备句柄
int nXStart, // 字符串的开始位置 X 坐标
int nYStart, // 字符串的开始位置 Y 坐标
LPCTSTR lpString, // 要绘制的字符串,此字符串不必为以\0结束的,因为cbString中指定了字符串的长度
int cbString // 字符串的长度
);
DrawText(): 绘制字符串 功能比 TextOut()函数功能要强
原型:
int DrawText(
HDC hdc, //绘图设备句柄
LPCTSTR lpString, //要绘制的字符串
int nCount, //字符串的长度
LPRECT lpRect, //绘制文字的矩形框信息的结构体
UINT uFormat //绘制的方式
);
uFormat 参数的取值有:
DT_BOTTOM ---- 绘制的字符串靠矩形区底部绘制
DT_CENTER ---- 字符串在矩形区域水平方向居中绘制
DT_END_ELLIPSIS ---- 如果绘制的字符串过长多余的部分以省略号代替
DT_LEFT ---- 水平方向字符串靠矩形区域左边绘制
DT_NOCLIP ---- 如果绘制的字符串太长矩形区域容纳不了,默认情况下会把多余的部分截取掉,但如果加上此绘制方式多余的部分不会被截取一样显示
DT_RIGHT ---- 水平方向字符串靠矩形区域右边绘制
DT_SINGLELINE ---- 单行绘制,不会换行
DT_TOP ---- 字符串靠矩形区域顶端绘制
DT_VCENTER ---- 字符串在矩形区域垂直方向居中绘制
DT_WORDBREAK ---- 多行绘制字符串
DT_NOPREFIX ---- 转义字符针对"&",具体参加 帮助文档
注:
垂直居中和垂直靠底部只适用于单行文字绘制,如果是多行则无效,多行模式下字符串只能靠顶端绘制;
扩:
RECT 结构体原型: RECT 是用来创建矩形框的结构体
typedef struct _RECT{
LONG left; //左
LONG top; //上
LONG right; //右
LONG bottom; //下
} RECT, * PRECT;
使用:
RECT rc = {0};
GetClientRect(hWnd, &rc);
ExtTextOut(): 功能比 DrawText() 函数功能强
原型:
BOOL ExtTextOut(
HDC hdc, //绘图设备 句柄
int X, //绘制字符串的 X 位置
int Y, //绘制字符串的 Y 位置
UINT fuOptions, //输出选项,目前已经没用 给0即可
const RECT* lprc, //输出的矩形框,废弃参数 给NULL即可
LPCTSTR lpString, //要绘制的字符串
UINT cbCount, //字符串的长度
CONST INT* lpDx //字符间距的数组
);
ExtTextOut()函数指定间距的时候是按字节去指定的,如果是汉字(占两个字节)就需要两个对应的间距,可以补0;
如:
char szExtText[] = "A中国人民";
int nDis[] = {10,0,20,0,30,0,40,0};
ExtTextOut(hdc,100,300,0,NULL,szExtText,strlen(szExtText),nDis);
SetTextColor(): 设置字符串的颜色
原型:
COLORREF SetTextColor(
HDC hdc, //绘图设备 句柄
COLORREF crColor //要设置字体的颜色
); 返回字体原来的颜色
设置颜色值使用 RGB 宏 如:
RGBA nColor = RGB(0,255,0); // 第一个0表示红色配比,255表示绿色配比,最后一个0表示蓝色配比
SetBkColor(): 更改字符串的背景颜色,字符串默认背景是白色的并且该函数只适用于不透明模式
原型:
COLORREF SetBkColor(
HDC hdc, //绘图设备句柄
COLORREF crColor //要设置的背景颜色
); 返回字符串原来的背景颜色
SetBkMode(): 设置字符串背景模式
原型:
int SetBkMode(
HDC hdc, //绘图设备 句柄
int iBkMode //设置字符串背景模式(透明 或 不透明)
);
iBkMode 参数的取值有:
OPAQUE --- 字符串背景不透明,默认是不透明
TRANSPARENT --- 字符串背景透明
注:
如果设置字符串背景为透明, SetBkColor()函数(更改字符串的背景颜色)就失效了;
字体相关:
Windows平台下常用的字体格式为 TureType 字体,
如果想更改字体,那么就必须先创建一个字体,Windows 中字体都是根据系统现有的字体文件(C:\Windows\Fonts)来创建,不是想当然的想创建什么字体就创建什么字体
字体和画笔画刷一样也属于 GDI 绘图对象的一种,而且字体的使用步骤和画笔画刷一个套路
字体名 --- 表示字体类型,如果想使用该字体,字体的名字不要看字体文件名,而是打开字体文件看它的第一行是什么就是什么字体;
HFONT --- 字体句柄
字体使用步骤:
1> 创建字体
HFONT CreateFont{
int nHerght, //字体高度
int nWidth, //字体宽度, 一般给0, 给0不是说字体就非常小,而是 给0后系统会根据 高度匹配一个比较合适的字体宽度,也可以自己填写
int nEscapement, //字符串倾斜角度,字符串整体倾斜
int nOrientation, //字符旋转角度,字符串以 X 轴为轴心向里或向外旋转(要涉及 Z 轴及空间),在二维图形编程中没有 Z 轴因此设不设置没影响
int fnWeight, //字体的粗细
DWORD fdwItalic, //斜体, 非0 及斜体, 0 不斜体
DWORD fdwUnderline, //字符下划线, 非0 及下划线, 0 没有下划线
DWORD fdwStrikeOut, //删除线, 非0 带删除键, 0 不带删除键
DWORD fdwCharSet, //字符集(GB2312_CHARSET 该字符集 基本上包含了所有汉字)
DWORD fdwOutputPrecision, //输出精度, 废弃 给0即可
DWORD fdwClipPrecision, //剪切精度, 废弃 给0即可
DWORD fdwQuality, //输出质量, 废弃 给0即可
DWORD fdwPitchAndFamily, //匹配字体,给0也可以创建但速度要慢点,创建字体会在我们提供的字体文件中依次匹配,如果填写的匹配信息匹配速度更快
LPCTSTR lpszFace, //字体名称, 字体名称是要打开字体文件看它的第一行
}; 返回创建字体的句柄
2> 把字体应用到 绘图设备中 使用 SelectObject() 函数
原型:
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 字体画笔或画刷的句柄,
//HGDIOBJ 数据类型是 GDI绘图对象句柄,画笔是属于它的一种它兼容 HPEN 画笔句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中的画笔或画刷的句柄
3> 绘制文字 使用 TextOut()/DrawText()/ExtTextOut()
TextOut(): 将文字绘制在指定坐标位置(功能非常薄弱: 不能换行,必须指定绘制的位置,没有任何的对齐方式)
原型:
BOOL TextOut(
HDC hdc, // 绘图设备句柄
int nXStart, // 字符串的开始位置 X 坐标
int nYStart, // 字符串的开始位置 Y 坐标
LPCTSTR lpString, // 要绘制的字符串,此字符串不必为以\0结束的,因为cbString中指定了字符串的长度
int cbString // 字符串的长度
);
DrawText(): 绘制字符串 功能比 TextOut()函数功能要强
原型:
int DrawText(
HDC hdc, //绘图设备句柄
LPCTSTR lpString, //要绘制的字符串
int nCount, //字符串的长度
LPRECT lpRect, //绘制文字的矩形框,创建矩形框使用 RECT 结构体
UINT uFormat //绘制的方式
);
uFormat 参数的取值有:
DT_BOTTOM ---- 绘制的字符串靠矩形区底部绘制
DT_CENTER ---- 字符串在矩形区域水平方向居中绘制
DT_END_ELLIPSIS ---- 如果绘制的字符串过长多余的部分以省略号代替
DT_LEFT ---- 水平方向字符串靠矩形区域左边绘制
DT_NOCLIP ---- 如果绘制的字符串太长矩形区域容纳不了,默认情况下会把多余的部分截取掉,
但如果加上此绘制方式多余的部分不会被截取一样显示
DT_RIGHT ---- 水平方向字符串靠矩形区域右边绘制
DT_SINGLELINE ---- 单行绘制,不会换行
DT_TOP ---- 字符串靠矩形区域顶端绘制
DT_VCENTER ---- 字符串在矩形区域垂直方向居中绘制
DT_WORDBREAK ---- 多行绘制字符串
DT_NOPREFIX ---- 转义字符针对"&",具体参加 帮助文档
注:
垂直居中和垂直靠底部只适用于单行文字绘制,如果是多行则无效,多行模式下字符串只能靠顶端绘制;
扩:
RECT 结构体原型: RECT 是用来保存矩形框信息的结构体
typedef struct _RECT{
LONG left; //左
LONG top; //上
LONG right; //右
LONG bottom; //下
} RECT, * PRECT;
使用:
RECT rc = {0};
GetClientRect(hWnd, &rc);
ExtTextOut(): 功能比 DrawText() 函数功能强
原型:
BOOL ExtTextOut(
HDC hdc, //绘图设备 句柄
int X, //绘制字符串的 X 位置
int Y, //绘制字符串的 Y 位置
UINT fuOptions, //输出选项,目前已经没用 给0即可
const RECT* lprc, //输出的矩形框,废弃参数 给NULL即可
LPCTSTR lpString, //要绘制的字符串
UINT cbCount, //字符串的长度
CONST INT* lpDx //字符间距的数组
);
ExtTextOut()函数指定间距的时候是按字节去指定的,如果是汉字(占两个字节)就需要两个对应的间距,可以补0;
如:
char szExtText[] = "A中国人民";
int nDis[] = {10,0,20,0,30,0,40,0};
ExtTextOut(hdc,100,300,0,NULL,szExtText,strlen(szExtText),nDis);
4> 从 绘图设备 中取出 字体 使用 SeletObject()
原型:
HGDIOBJ SelectObject(
HDC hdc, //绘图设备句柄, 应用到那里就填写哪个的句柄
HGDIOBJ hgdiobj //GDI 绘图对象句柄, 字体画笔或画刷的句柄,
//HGDIOBJ 数据类型是 GDI绘图对象句柄,画笔是属于它的一种它兼容 HPEN 画笔句柄
); 返回原来的 GDI 绘图对象的句柄
注意保存原来 绘图设备 当中的画笔或画刷的句柄
5> 删除字体 使用 DeleteObject()
原型:
BOOL DeleteObject(
HGDIOBJ hObject // GDI 绘图对象句柄,字体,画笔,画刷 的句柄 GDI编程指的是Windows下的绘图编程
);
只能 删除不被 绘图设备 使用的 字体,画笔,画刷 所以在释放前,必须将 字体,画笔,画刷从 绘图设备中取出
十四. 对话框窗口
大到全屏界面,小到一个控件都属于窗口;
对话框是一个特殊的窗口: 说特殊是因为在处理窗口的时候与普通窗口处理不一样
普通窗口 ---- 普通窗口在处理消息的时候遵循的规则是: 我们自己定义的函数 调用 缺省处理函数
如我们自己定义的窗口处理函数 WndProc(...){ .... return DefWindowProc() }
对话框窗口 ---- 对话框窗口在处理消息的时候遵循的规则是: 缺省处理函数 调用 我们自己定义的处理函数
对话框的分类: 所有见过的对话框都分为以下两类
模式对话框 ---- 当对话框显示时,会禁止本进程的其它窗口和用户进行交互操作(因为它会阻塞)
无模式对话框 ---- 在对话框显示后,本进程的其它窗口同样可以和用户进行交互操作
注: 模式对话框 和 无模式对话框 单从外观看是看不出来的,除非用鼠标去点;
模式对话框基本使用:
1> 模式对话框窗口处理函数(真正的窗口处理函数是系统默认的,但系统默认的处理函数可以调用我们自己定义的函数)
原型:
int CALLBACK WindowProc(
HWND hwnd, //窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //消息附带信息
LPARAM lParam //消息附带信息
);
2> 注册窗口类(可选,基本不使用),现在的操作系统已经把对话框的窗口类已经注册好了,
谁注册窗口类,谁负责实现窗口处理函数,并且在微软程序员在实现对话框处理函数的时候在函数内部回去调用我们自己的定义的函数,
如果微软程序员不调用我们自己定义的函数我们就参与不了窗口处理,因此微软程序员在实现对话框窗口处理函数的时候会调用我们自己定义的函数
要调用我们自己定义的函数:
第一: 我们需要把该函数定义出来
第二: 要把我们定义的函数交给操作系统
模式对话框的使用:
对话框窗口自定义处理函数(并非真正对话框处理函数,真正的对话框处理函数是操作系统定义的缺省对话框处理函数)
INT CALLBACK DialogProc( //函数名是可以修改的
HWND hwndDlg, //对话框窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //消息参数
LPARAM lParam //消息参数
);
返回 true 表示 DialogProc() 函数中处理了这个消息, 缺省处理函数则不需要再处理了
返回 false 表示 DialogProc() 函数中未处理这个消息, 则交给缺省处理函数处理
我们自己定义的函数: 函数名 参数名 都是可以该的,但参数个数和类型不能修改;
普通窗口点击关闭按钮如果我们不处理则执行 DefWindowProc()函数执行默认处理,会销毁窗口,在销毁窗口时会产生 WM_DESTROY 消息,
在 WM_DESTROY 消息中 我们可以使用 PostMessage()函数 向消息队列中发送 WM_QUIT 消息 用来结束消息循环;
但在模式对话框窗口中点击关闭按钮系统默认处理函数不会销毁窗口,因此需要我们在自定义函数中做对应的处理来关闭模式对话框;
3> 创建模式对话框: 使用 DialogBox() 函数创建模式对话框
原型:
INT DialogBox(
HINSTANCE hInstance, //应用程序实例句柄
LPCTSTR lpTemplate, //对话框模版资源的数字ID
HWND hWndParent, //对话框父窗口
DLGPROC lpDialogFunc //自己定义的函数
); 返回值由 EndDialog()函数的第二个参数指定
DialogBox()函数是一个阻塞函数,只要程序执行到该函数模式对话框显示出来该函数就阻塞,这也是为什么模式对话框显示时其它窗口不能和用户进行交互;
只要模式对话框消失就解除阻塞,使用 EndDialog()函数可以销毁对话框并解除阻塞,,还能指定 DialogBox()函数的返回值;
DialogBox()函数中通过第一个和第二个参数就可以在当前程序所在的内存中找到对话框所占的那块内存;
并且 在 DialogBox() 内部 会调用 Loadxxxxx()函数加载对话框资源;
4> 对话框的关闭 使用 DndDialog()函数
原型:
BOOL EndDialog(
HWND hDlg, //要关闭的对话框窗口句柄
INT nResult //设置 DialogBox()函数的返回值
);
EndDialog()函数功能: DialogBox()函数是一个阻塞函数,EndDialog()函数可以销毁对话框窗口并且解除阻塞,还能指定 DialogBox()函数的返回值;
关闭模式对话框,只能使用 EndDialog,不能使用 DestroyWindow()等函数
普通窗口点击关闭按钮如果我们不处理则执行 DefWindowProc()函数执行默认处理,会销毁窗口,在销毁窗口时会产生 WM_DESTROY 消息,
在 WM_DESTROY 消息中 我们可以使用 PostMessage()函数 向消息队列中发送 WM_QUIT 消息 用来结束消息循环;
但在模式对话框窗口中点击关闭按钮系统默认处理函数不会销毁窗口,因此需要我们在自定义函数中做对应的处理来关闭模式对话框;
DestroyWindow()函数专门用来销毁窗口,只要能拿到窗口的句柄就可以销毁窗口,
原型:
BOOL DestroyWindow(
HWND hWnd, //要销毁窗口的句柄
);
注:
但在模式对话框中不能使用因为 DestroyWindow()只能销毁模式对话框,不能解除阻塞;
5> 模式对话框的消息
模式对话框的消息除了 WM_INITDIALOG 消息不一样,其它的消息都一样,普通窗口在创建成功显示之前产生的消息是 WM_CREATE 而对话框是 WM_INITDIALOG
WM_INITDIALOG ---- 对话框创建之后显示之前,通知对话框窗口处理函数,可以完成对 对话框的初始化相关的操作;
无模式对话框的使用:
1> 无模式对话框窗口处理函数(真正的窗口处理函数是系统默认的,但系统默认的处理函数可以调用我们自己定义的函数)
原型:
int CALLBACK WindowProc(
HWND hwnd, //窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //消息附带信息
LPARAM lParam //消息附带信息
);
无模式对话框在处理消息的时候和模式对话框是一样的,真正才消息处理函数是系统默认的处理函数,但我们可以自己定义一个处理函数给默认处理函数调用;
2> 创建无模式对话框使用函数: CreateDialog()
原型:
HWND CreateDialog(
HINSTANCE hInstance, //应用程序实例句柄
LPCTSTR lpTemplate, //模版资源ID
HWND hWndParent, //父窗口
DLGPROC lpDialogFunc //自定义函数
); 非阻塞函数,创建成功返回窗口句柄
CreateDialog()函数功能只是在内存中把无模式对话框创建出来但不能显示,因此如果想显示无模式对话框需要使用 ShowWindow()函数
ShowWindow()函数原型:
BOOL ShowWindow(
HWND hWnd, //要显示的窗口句柄
int nCmdShow //指定窗口如何显示,具体参见帮助文档
);
3> 关闭无模式对话框
关闭时使用 DestroyWindow() 销毁窗口,不能使用 EndDialog()函数来关闭无模式对话框
EndDialog()函数只能将无模式对话框隐藏,不能销毁;
在无模式对话框创建成功到显示之前产生的消息是 WM_INITDIALOG 并不是 WM_CREATE 消息;
对话框 和 普通窗口的区别:
1> 创建
模式对话框使用 DialogBox()函数创建,并且 DialogBox()函数是阻塞函数
无模式对话框使用 CreateDialog()函数创建, CreateDialog()函数是非阻塞函数
普通窗口使用 CreateWindow()/CreateWindowEx() 函数创建
2> 窗口处理函数
无模式对话框/模式对话框 的窗口处理函数是 系统默认的窗口处理函数,在默认窗口处理函数中可以调用我们自定义的函数
普通窗口的处理函数 调用我们自定义的窗口处理函数,配合系统缺省窗口处理函数 DefWindowProc()
3> 窗口消息
普通窗口创建成功到显示之前产生 WM_CREATE 消息
对话框窗口创建成功到显示之前产生 WM_INITDIALOG 消息
4> 窗口关闭
模式对话框只能使用 EndDialog()函数, EndDialog()函数会销毁模式对话框并解除 DialogBox()函数导致的阻塞
无模式对话框/普通窗口 使用 DestroyWindow()函数销毁 无模式对话框/普通窗口, EndDialog()只能隐藏无模式对话框不能销毁无模式对话框
十五. 控件
控件也属于窗口,但控件一般不作为主窗口出现,一般是作为子窗口出现,因此也叫子控件;
控件的注册窗口类由系统注册,并且相对应的窗口处理函数也由系统完成了(谁注册谁写窗口处理函数)
创建子控件时不需要注册窗口类(系统已经都注册好了),直接使用 CreateWindow()/CreateWindowEx() 创建该类的窗口(大到全屏界面小到一个控件都属于窗口),
子控件创建时每个控件都具有一个ID号(占用窗口的菜单句柄哪个参数来设置控件的 ID,需要强转成 HMENU )
子控件的处理函数中不像模式对话框的处理函数一样,模式对话框的处理函数中可以调用我们自定义的函数参与对话框的处理,子控件中不会调用自定义函数参与控件的处理
但我们可以通过消息来完成程序和子控件之间的交互:
控件的窗口消息 ---- 使用 SendMessage()向控件发送消息,如果想 获取控件的信息 或想 设置控件状态 都是控件的窗口消息,我们向控件发送消息
控件的通知消息 ---- 控件有相应的事件发生后,会向所在的父窗口发送通知消息(绝大部分都 WM_COMMAND 消息) 父窗口可以根据通知消息的通知码做相应的处理
通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
回顾:
SendMessage() -- 发送消息不进系统队列,直接调用窗口处理函数,会等待消息处理的结果
PostMessage() -- 发送消息到系统消息队列里,消息发送后立刻返回,不等待消息的执行结果,
系统消息队列会把消息转发到程序消息队列里,最后再由 GetMessage()从程序消息队列里抓取该消息;
静态框相关
静态框常用于显示文字和图标等,窗口类名称 "STATIC"
文字静态框 ---- 显示文字
图像静态框 ---- 显示图像,如果想显示图标需要设置 SS_ICON(显示 .ICO 为后缀的图标)/SS_BITMAP(显示 .bmp 为后缀的图片) 风格并且只能二选一
静态框的使用
1> 创建静态框
CreateWindow()/CreateWindowEx()
1> CreateWindow() --- 标准版,比加强版少一个参数,
2> CreateWindowEx() --- 加强版,比标准版多一个参数,多了第一个参数,把加强版第一个参数去掉就是标准版;
原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽,0表示默认
LPCTSTR lpClassName, //已经注册的窗口类名称,创建静态框时窗口类由系统已经注册好了的,直接使用即可
LPCTSTR lpWindowsName, //窗口标题栏的名字, 在创建文字静态框时是文字静态框里面要显示的文字,
//图像静态框是要显示的图片数字ID并且要在数字ID前加"#"
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄,如果创建图像静态框那么窗口菜单句柄要设置成图像ID (需要强制转换为 HMENU 类型)
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时附带信息
); 创建成功返回窗口句柄
创建时窗口类名称要填写成 "STATIC"
如果要创建图像静态框需要设置 SS_ICON(显示 .ICO 为后缀的图标)/SS_BITMAP(显示 .bmp 为后缀的图片) 风格并且只能二选一
如果是图像静态框,在创建静态框时窗口标题栏的名字参数中要填写需要显示的图片的数字ID(在资源头文件中查看并且还要在数字ID前面加上"#")
如:
CreateWindowEx(0,"STATIC","#101", ...... );
如:
文字静态框
CreateWindowEx(0,"STATIC","创建文字静态框时,此选项就是要显示在文字静态框中的文字",WS_CHILD|WS_VISIBLE|SS_NOTIFY,
100,100,200,30,hWnd,(HMENU)1001,g_hInstance,NULL);
图像静态框
CreateWindowEx(0,"STATIC","#1002",WS_CHILD|WS_VISIBLE|SS_ICON|SS_NOTIFY,
350,100,0,0,hWnd,(HMENU)1002,g_hInstance,NULL);
SS_NOTIFY 是为了静态库被点击后向父窗口发 WM_COMMAND 消息而添加上的
2> 静态框的窗口消息
SendMessage() 发送消息到控件 例如: STM_SETICON 设置 .ICO 图像
SendMessage() 函数有4个参数,在向控件发送消息时各个参数要填写的值如下:
原型:
LRESULT SendMessage(
HWND hWnd, //控件的窗口句柄
UINT Msg, //要发送消息的ID
WPARAM wParam, //要修改成什么样的图像的句柄
LPARAM lParam //无用,必须为0
);
只要是 获取控件信息 或者 设置控件状态 就一定是 控件窗口消息, 只要是 控件窗口信息 就是我们调用 SendMessage()函数向控件发送消息
GetDlgItem()函数可以通过控件ID拿到控件句柄
原型:
HWND GetDlgItem(
HWND hDlg, //控件父窗口句柄
int nIDDlgItem //控件的ID
); 返回控件对应的句柄
3> 静态框的通知消息
如果想静态框被点击后发送 WM_COMMAND 消息,需要在创建静态框时增加 SS_NOTIFY 风格,否则静态框被点击后不会发送 WM_COMMAND 消息
静态框的通知消息通过 WM_COMMAND 消息传递
附:
WM_COMMAND 不局限于静态框,像 按钮 编辑框 列表框 组合框 有事件发送也是发的是 WM_COMMAND 附带信息也是如下:
WM_COMMAND 消息的附带信息
wParam:
LOWORD(低两个字节) --- 菜单项/加速键/控件 的ID
HIWORD(高两个字节) --- 对于菜单项为0,
对于加速键为1,
对于控件是 Notify-Code(通知码),通过通知码可以知道控件发生了什么事情
lParam:
对于菜单项,加速键 为 NULL
对于控件 为控件窗口句柄
按钮控件相关:
根据按钮的风格,将按钮分成4类:
1> 下压式按钮风格 : BS_PUSHBUTTON/BS_DEFPUSHBUTTON 二选一 没什么区别 任填
2> 分组框按钮风格: BS_GROUPBOX 窗体中的一圈线
3> 复选框按钮风格: BS_CHECKBOX/BS_AUTOCHECKBOX 复选框 勾选/非勾选, 带 AUTO 由系统维护,不带 AUTO 则需要程序员写代码控制
BS_3STATE/BS_AUTO3STATE 复选框3种状态(勾选/非勾选/灰色勾选) 带 AUTO 的由系统维护, 不带 AUTO 则需要程序员写代码控制
4> 单选框按钮风格: BS_RADIOBUTTON/BS_AUTORADIOBUTTON 带 AUTO 由系统维护,不带 AUTO 则需要程序员写代码控制勾选状态
窗口类名称: BUTTON 已经由系统注册好了的
扩:
如果一个风格是以 WS 开头表示它是 Windows 的风格,可以适用于任何一个窗口
如果一个更改是以 BS 开头表示它是 BUTTON 的风格,只适用于 BUTTON 的风格
下压式按钮的使用:
1> 创建按钮
CreateWindow()/CreateWindowEx()
1> CreateWindow() --- 标准版,比加强版少一个参数,
2> CreateWindowEx() --- 加强版,比标准版多一个参数,多了第一个参数,把加强版第一个参数去掉就是标准版;
原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽,0表示默认
LPCTSTR lpClassName, //已经注册的窗口类名称,创建静态框时窗口类由系统已经注册好了的,直接使用即可
LPCTSTR lpWindowsName, //窗口标题栏的名字, 如果是按钮则显示在按钮的内部
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄,如果创建图像静态框那么窗口菜单句柄要设置成图像ID (需要强制转换为 HMENU 类型)
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时的附带信息
); 创建成功返回按钮句柄
使用 CreateWindowEx()函数创建下压式按钮时需要在:
窗口类名称填写成 "BUTTON"
窗口基本风格添加上 BS_PUSHBUTTON/BS_DEFPUSHBUTTON 风格(二选一,没区别选谁都可以)
2> 窗口消息, 只要想设置/获取窗口的信息就是窗口的窗口消息,窗口消息是程序员调用 SendMessage() 向控件发消息
3> 通知消息,通知码是: BN_CLICKED , BN_CLICKED 通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
控件有相应的事件发生后,会向所在的父窗口发送通知消息(绝大部分都 WM_COMMAND 消息) 父窗口可以根据通知消息的通知码做相应的处理
通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
分组框按钮的使用:
常用于界面上的控件分组显示,提高界面友好性(把功能相近的控件圈起来,让用户好找些)
CreateWindow()/CreateWindowEx() 函数创建分组框, 窗口标题栏的名字换成在分组框上想显示的文字
需要在创建分组框时在基本风格添加上 BS_GROUPBOX 风格
复选框按钮的使用:
风格和创建
使用 CreateWindow()/CreateWindowEx() 函数创建时窗口类名称填写成"BUTTON",
基本风格需要添上如下风格:
2种状态的复选框:
BS_CHECKBOX --- 点击选择时,需要自己写代码控制 勾选/非勾选状态
BS_QUTOCHECKBOX --- 点击选择时,系统自动维护 勾选/非勾选状态
3种状态的复选框:
BS_3STATE --- 点击选择时,需要自己写代码控制 勾选/灰色勾选/非勾选
BS_AUTO3STATE --- 点击选择时,系统自动维护 勾选/灰色勾选/非勾选
窗口消息: 如果想 设置或者获取 控件的状态 是窗口消息, 窗口消息由程序员调用 SendMessage() 向控件发消息
SendMessage() 函数有4个参数,在向控件发送消息时各个参数要填写的值如下:
原型:
LRESULT SendMessage(
HWND hWnd, //控件的窗口句柄
UINT Msg, //要发送消息的ID
WPARAM wParam, //要设置的复选框的状态
LPARAM lParam //无用,必须为0
);
获取复选框的勾选状态
BM_GETCHECK
wParam/lParam ---- 都没用必须为0
复选框当前状态通过 SendMessage()函数的返回值获取, SendMessage()函数直接调用窗口处理函数,不让消息进系统消息队列
设置复选框的勾选状态
BM_SETCHECK
wParam --- 要更改的具体状态
lParam --- 没用,必须为0
wParam 的取值:
BST_CHECKED --- 勾选
BST_INDETERMINATE --- 灰色勾选, 3种状态的才有,2种状态的没有此选项
BST_UNCHECKED --- 非勾选
通知消息: 控件有相应的事件发生后,会向所在的父窗口发送通知消息(绝大部分都 WM_COMMAND 消息) 父窗口可以根据通知消息的通知码做相应的处理
通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
WM_COMMAND 消息的附带信息:
wParam:
LOWORD(低两个字节) --- 菜单项/加速键/控件 的ID
HIWORD(高两个字节) --- 对于菜单项为0,
对于加速键为1,
对于控件是 Notify-Code(通知码),通过通知码可以知道控件发生了什么事情
BN_CLICKED 控件被点击的通知码
lParam:
对于菜单项,加速键 为 NULL
对于控件 为控件窗口句柄
单选框按钮
风格 和 创建
使用 CreateWindow()/CreateWindowEx() 函数创建单选框按钮,
创建时窗口类名称要填写成"BUTTON",基本风格上添加 BS_RADIOBUTTON/BS_AUTORADIOBUTTON 风格(二选一,带 AUTO 由系统维护)
窗口消息: 如果想 设置或者获取 控件的状态 是窗口消息, 窗口消息由程序员调用 SendMessage() 向控件发消息
SendMessage() 函数有4个参数,在向控件发送消息时各个参数要填写的值如下:
原型:
LRESULT SendMessage(
HWND hWnd, //控件的窗口句柄
UINT Msg, //要发送消息的ID
WPARAM wParam, //要设置的单选框的状态
LPARAM lParam //无用,必须为0
);
注意:
单选框维护时出了要维护被点击单选框的状态,还要维护同组其它单选框的状态,同一时间内只能选择一个单选框的状态;
获取单选框的选择状态
BM_GETCHECK
设置单选框的选择状态
BM_SETCHECK
wParam 的取值:
BST_CHECKED --- 勾选
BST_UNCHECKED --- 非勾选
通知消息: 控件有相应的事件发生后,会向所在的父窗口发送通知消息(绝大部分都 WM_COMMAND 消息) 父窗口可以根据通知消息的通知码做相应的处理
通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
WM_COMMAND 消息的附带信息:
wParam:
LOWORD(低两个字节) --- 菜单项/加速键/控件 的ID
HIWORD(高两个字节) --- 对于菜单项为0,
对于加速键为1,
对于控件是 Notify-Code(通知码),通过通知码可以知道控件发生了什么事情
BN_CLICKED 控件被点击的通知码
lParam:
对于菜单项,加速键 为 NULL
对于控件 为控件窗口句柄
注意:
单选框分组,在创建单选框的时候可以使用 WS_GROUP 风格分组,从当前具有 WS_GROUP 风格的单选框,到下一个 WS_GROUP 风格单选框之前,为1组单选框
扩:
多态按钮: 多态按钮的本质就是有两种状态,但是外形却像下压式按钮(两种状态的有: 2种状态的复选框,单选框)
创建多态按钮还是使用 CreateWindow()/CreateWindowEx() 函数创建
创建时需要添加上 BS_AUTOCHECKBOX/BS_CHECKBOX(表示它是一个2种状态的复选框 勾选/非勾选) 和 BS_PUSHLIKE(表示外形像一个下压式的按钮)
多态按钮有2种状态: 点击时 下压 但不会弹起 再次点击则弹起 这两种状态
如:
CreateWindowEx(0,"BUTTON","复选框模式的多态按钮",WS_CHILD|WS_VISIBLE|BS_AUTOCHECKBOX|BS_PUSHLIKE,
50,380,100,40,hWnd,(HMENU)1011,g_hInstance,NULL);
CreateWindowEx(0,"BUTTON","单选框模式的多态按钮",WS_CHILD|WS_VISIBLE|BS_AUTORADIOBUTTON|BS_PUSHLIKE,
50,400,100,40,hWnd,(HMENU)1012,g_hInstance,NULL);
CreateWindowEx(0,"BUTTON","单选框模式的多态按钮",WS_CHILD|WS_VISIBLE|BS_AUTORADIOBUTTON|BS_PUSHLIKE,
50,460,200,40,hWnd,(HMENU)1013,g_hInstance,NULL);
编辑框控件相关:
从风格可以将编辑框控件分成以下类: 非官方分类
单行编辑框 ---- 只能处理一行文字
多行编辑框 ---- 可以显示多行文字
密码编辑框 ---- 创建编辑框时需要添加上ES_PASSWORD风格,加此风格后输入的文字以"*"代替显示不以明码显示,但注意密码编辑框只适用于单行编辑框;
编辑框控件的使用:
1> 创建编辑框控件
窗口类名称: EDIT
使用 CreateWindow()/CreateWindowEx() 创建编辑框控
CreateWindow()/CreateWindowEx()
1> CreateWindow() --- 标准版,比加强版少一个参数,
2> CreateWindowEx() --- 加强版,比标准版多一个参数,多了第一个参数,把加强版第一个参数去掉就是标准版;
原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽,0表示默认
LPCTSTR lpClassName, //已经注册的窗口类名称,创建静态框时窗口类由系统已经注册好了的,直接使用即可
LPCTSTR lpWindowsName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄,如果创建图像静态框那么窗口菜单句柄要设置成图像ID (需要强制转换为 HMENU 类型)
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时的附带信息
); 创建成功返回窗口句柄
使用 CreateWindow()/CreateWindowEx() 创建编辑框控件,创建控件时窗口类名称要填写成 "EDIT",
窗口标题栏名称填写编辑框要显示的内存,窗口菜单句柄填写 编辑框控件的ID(需要强转成 HMENU 类型)
编辑框常用风格:
ES_AUTOHSCROLL --- 自动水平滚动(没有条),加此风格一行里可以随便输入不会输入到末尾就不能输入的情况了
ES_AUTOVSCROLL --- 自动垂直滚动(没有条),加此风格在回车敲到底部不会出现不能再敲回车的情况了
ES_CENTER --- 编辑框中的字符串水平居中显示
ES_LEFT --- 编辑框中的字符串水平靠左显示
ES_RIGHT --- 编辑框中的字符串水平靠右显示
ES_MULTILINE --- 多行显示,加此风格就可以敲回车了
ES_PASSWORD --- 加此风格编辑框中的字符串用"*"代替显示不再以明码显示,但需要注意密码编辑框只适用于单行编辑框
ES_READONLY --- 加此风格编辑框中的内容只能读不能写
ES_NUMBER --- 加此风格就只能输入数字键,不能在输入字符键
WS_HSCROLL --- 水平滚动条风格,加此风格就可以使用鼠标滚轮左右移动
WS_VSCROLL --- 垂直滚动条风格,加此风格就可以使用鼠标滚轮上下移动
WS 开头的风格说明它是 Windows 窗口的风格,也就是说在 Windows 下任何一个窗口都可以使用该风格;
注:
创建的编辑框默认没有边界线的要想知道该编辑框多大需要加上 WS_BORDER 风格, WS_BORDER 是在窗口外显示一圈黑色的边界线
一般的编辑框是凹进去的,如果想达到该效果需要在 扩展风格处加上 WS_EX_CLIENTEDGE 风格
如:
CreateWindowEx(WS_EX_CLIENTEDGE,"EDIT","hello",WS_CHILD|WS_VISIBLE|WS_BORDER|ES_AUTOVSCROLL|ES_AUTOHSCROLL|ES_MULTILINE,
0,0,200,200,hWnd,(HMENU)1001,g_hInstance,NULL);
扩:
为什么密码编辑框一般都是单行编辑框,因为如果是多行编辑框就可以敲回车,但那个回车是不是属于密码的一部分就不好规定了
因此密码编辑框一般都是单行编辑框
2> 编辑框的窗口消息: 只要想 获取/设置 控件信息 就都是窗口消息,只要是窗口消息就是 程序员 调用 SendMessage() 函数向 控件发消息
原型:
LRESULT SendMessage(
HWND hWnd, //控件的窗口句柄
UINT Msg, //要发送消息的ID
WPARAM wParam, //要设置的单选框的状态
LPARAM lParam //无用,必须为0
);
WM_SETTEXT --- 该消息能 更改/设置 编辑框的文本内容
消息的附带信息:
wParam:
不使用,必须为0
lParam:
要 设置/更改 文本内容的缓冲区首地址
WM_GETTEXT --- 该消息能 获取 编辑框的文本内容
消息的附带信息:
wParam:
用来保存编辑框文本缓冲区的大小
lParam:
保存编辑框文本缓冲区的首地址
WM_GETTEXTLENGTH --- 该消息能 获取 编辑框文本内容的长度
消息的附带信息:
wParam/lParam 都没用 都为0, 通过 SendMessage() 函数的返回值返回编辑框文本内容的长度
WM_SETFONT --- 该消息能 更改 编辑框文本内容的字体
消息的附带信息:
wParam --- 要设置成的字体
lParam --- 重绘标识,1表示采用新的字体
如:
HWND hEdit = CreateWindowEx(WS_EX_CLIENTEDGE,"EDIT",
NULL,WS_BORDER|WS_VISIBLE|ES_AUTOHSCROLL|ES_AUTOVSCROLL|
WS_CHILD|WS_HSCROLL|WS_VSCROLL|ES_MULTILINE,
0,0,200,200,hWnd,(HMENU)1003,g_hInstance,NULL);
HFONT hFont = CreateFont(30,0,0,0,900,0,0,0,GB2312_CHARSET,0,0,0,0,"黑体");
SendMessage(hEdit,WM_SETFONT,(WPARAM)hFont,1);
WM_CTLCOLOREDIT --- 只要编辑框控件不是只读或不可用的,在编辑框控件需要重绘的时候会向其父窗口发送 WM_CTLCOLOREDIT 消息
如果要处理该消息必须返回一个画刷句柄,操作系统会根据该画刷句柄设置编辑框的背景颜色
编辑框控件和按钮控件一样都有自己的消息处理函数,控件是通过控件的消息处理函数调用我们定义的函数让我们参与控件消息的处理,
而编辑框控件是通过消息让我们参与控件的处理,在编辑框控件的处理函数中也会处理 WM_PAINT 绘图消息,
处理完以后再给其父窗口 发送 WM_CTLCOLOREDIT 消息,我们就可以在父窗口的消息处理函数里面处理该消息
WM_CTLCOLOREDIT 的两个附带信息:
wParam --- 传过来的是编辑框控件内的绘图设备句柄
lParam --- 静态控件的句柄
练习:
简易记事本
"保存" --- 将编辑框的内容写入 "D:/my.txt"
"打开" --- 将 "D:/my.txt" 文件中内容显示到编辑框控件中
编辑框控件 和 主窗口 实时保持一样大小
标C文件读写:
fopen -- 打开
fclose -- 关闭
fwrite -- 写
fread -- 读
fseek -- 更改文件指针
ftell -- 获取文件大小
GetDlgItem()函数可以通过控件ID拿到控件句柄
原型:
HWND GetDlgItem(
HWND hDlg, //控件父窗口句柄
int nIDDlgItem //控件的ID
); 返回控件对应的句柄
3> 编辑框的通知消息
EN_CHANGE 当编辑框内的文字被修改,EN_CHANGE 通知码会依附于 WM_COMMAND 消息 发送给父窗口
利用该消息可以达到只要编辑框的内容被修改可以在窗口标题栏上添加标志加以区分
SetWindowText() 函数 可以设置窗口标题栏的信息
原型:
BOOL SetWindowText(
HWND hWnd, //需要设置窗口的句柄
LPCTSRT lpString //标题栏要显示的信息
);
组合框控件相关
组合框的分类:
1> 简单组合框 --- 风格是 CBS_SIMPLE
2> 下拉式组合框 --- 可以用键盘输入也可以用鼠标选择, 风格是 CBS_DROPDOWN
3> 下拉列表式组合框 --- 只能用鼠标从选项中选择不能用键盘输入, 风格是 CBS_DROPDOWNLIST
组合框窗口类 --- COMBOBOX
组合框的使用
1> 创建组合框
使用 CreateWindow()/CreateWindowEx()函数创建,
创建时窗口类名称填写成 "COMBOBOX" ,窗口菜单句柄填写成组合框的ID(需要强转),组合框控件没有标题栏此项设置了也不会显示在组合框控件中
原型:
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格,大部分是指窗口是否支持文件拖拽,0表示默认
LPCTSTR lpClassName, //已经注册的窗口类名称,创建静态框时窗口类由系统已经注册好了的,直接使用即可
LPCTSTR lpWindowsName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int y, //窗口左上角垂直坐标位置 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nWidth, //窗口的宽度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
int nHeight, //窗口的高度 如果写的是 CW_USEDEFAULT 则使用默认的坐标
HWND hWndParent, //窗口的父窗口句柄,如果创建的是主窗口则填NULL,如果是子窗口则需要把父窗口的句柄填写上;
HMENU hMenu, //窗口菜单句柄,如果创建组合框控件窗口菜单句柄填写成组合框控件的ID (需要强制转换为 HMENU 类型)
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时的附带信息
); 创建成功返回窗口句柄
组合框的窗口消息: 要设置或获取都是窗口的窗口消息,只要是窗口消息就是程序员通过 SendMessage()发送消息给组合框
2> 组合框选项的添加
首先通过 GetDlgItem()函数(通过控件ID获取控件句柄)
如:
HWND hSimple = GetDlgItem(hWnd, 1001);
通过 SendMessage()要发送的消息:
CB_ADDSTRING: 追加
wParam --- 不使用
lParam --- 每个选项的文本内容(字符串指针)
CB_INSERTSTRING: 插入
wParam --- 选项的索引,也就是要插入的位置 以0开始的位置
lParam --- 每个选项的文本内容(字符串指针)
3> 组合框选项的删除, 要设置或获取是组合框的窗口消息,只要是窗口消息就是程序员通过 SendMessage()发送消息给组合框
首先通过 GetDlgItem()函数(通过控件ID获取控件句柄)
如:
HWND hSimple = GetDlgItem(hWnd, 1001);
通过 SendMessage()要发送的消息:
CB_DELETESTRING --- 删除指定项,一次只能删除一项
wParam --- 指明准备删除的选项的索引(索引都是从 0 开始类似于数组下标)
lParam --- 没用
CB_RESETCONTENT --- 清除所有项
wParam --- 没用必须为0
lParam --- 没用必须为0
4> 获取和设置组合框选择项(组合框的选项,被选的项可以有N个,但当前选择项只能有1个)
获取
CB_GETCURSEL --- 获取当前选择项索引,当前选择项的索引是通过 SendMessage()函数返回值获取
wParam/lParam --- 没用
SendMessage()原型:
LRESULT SendMessage(
HWND hWnd, //消息发送的目的窗口
UINT Msg, //消息ID
WPARAM wParam, //消息参数
LPARAM lParam //消息参数
);
设置(可以设置默认选择项)
CB_SETCURSEL --- 设置当前被选择项
wParam --- 要设置成选择项的选项索引(以0开始的位置)
lParam --- 没用必须为0
获取/设置是组合框的窗口消息,窗口消息是程序员调用 SendMessage()函数发送给组合框窗口处理函数,
5> 匹配查找,拿着一个字符串到我们的选项中去匹配查找看有没有匹配的
根据字符串,查找选择项,从选择项的起始字符查找包含的字符串
CB_FINDSTRING --- 非精确匹配查找也就是说查找的内容并不一定完全匹配(可以不全但不能错),
wParam --- 从哪个选项开始匹配,但如果想从 0 哪个选项开始匹配 要填写 -1,索引号是从 0 开始
wParam只是指明从哪个选项索引开始,所有的选项都会参与进来匹配,指明wParam匹配索引号是为了匹配的效率;
lParam --- 要参与匹配的字符串
匹配的结果通过 SendMessage()函数的返回告知,如果匹配上返回匹配选项的索引号,如果匹配失败返回 " CB_ERR " (CB_ERR宏的值是 -1 )
SendMessage(
HWND hWnd, //窗口句柄
UINT Msg, //消息ID
WPARAM wParam, //从哪个索引开始匹配
LPARAM lParam //要匹配的字符串
);匹配结果通过返回值返回
CB_FINDSTRINGEXACT --- 精确匹配查找的字符串(给的字符串必须合某个选项完全一致才能匹配上)
wParam --- 从哪个选项开始匹配,但如果想从 0 哪个选项开始匹配 要填写 -1,索引号是从 0 开始
wParam只是指明从哪个选项索引开始,所有的选项都会参与进来匹配,指明wParam匹配索引号是为了匹配的效率;
lParam --- 要参与匹配的字符串
CB_SELECTSTRING --- 查找并设置成当前被选择项(非精确匹配查找,可以不全但不能出错)
如果查找到则把查找到的选项设置成当前选择项
6> 获取选项的文本内容
CB_GETLBTEXTLEN --- 获取选项的文本长度
wParam --- 要获取的选项的索引
lParam --- 没用给0即可
CB_GETLBTEXT --- 获取选项的文本内容(只能获取鼠标选择的文本内容,不能获取键盘输入的文本内容)
wParam --- 要获取的选项的索引
lParam --- 用来保存获取到的选项的文本内容的缓冲区
获取输入的字符串
WM_GETTEXT --- 功能是: 只要是窗口就能获取该窗口的文本内容(包括鼠标选择和键盘输入的文本内容),因为这是 WM 开始是 Window Message
wParam --- 用来保存获取到的窗口文本内容缓冲区的大小
lParam --- 用来保存获取到的文本缓冲区的首地址
7> 选项的附加数据
在每个选项中,可以添加自定义的附加数据
CB_SETITEMDATA --- 将附加数据保存到指定选项
wParam --- 要设置附加数据选项的索引
lParam --- 具体设置什么附加数据
CB_GETITEMDATA --- 获取指定选项的附加数据
wParam --- 要获取附加数据选项的索引,附加数据通过 SendMessage()函数的返回值获取
lParam --- 没用,给0即可
SendMessage(
HWND hWnd, //窗口句柄
UINT Msg, //消息ID
WPARAM wParam,
LPARAM lParam
);
组合框的通知消息: 控件有相应的事件发生后,会向所在的父窗口发送通知消息(绝大部分都 WM_COMMAND 消息) 父窗口可以根据通知消息的通知码做相应的处理
通知码是依附于 WM_COMMAND 消息附带信息 wParam 的 HIWORD(高两个字节) 发送给父窗口的
WM_COMMAND 消息的两个附带信息:
WPARAM:
HIWORD --- 如果来自于菜单项高两个字节为0,如果是加速键为1,对于控件是消息的通知码
LOWORD --- 低两个字节是被点击菜单项的ID
LPARAM --- 来自于菜单项 LPARAM 为NULL,对于控件为控件窗口的句柄
根据 WM_COMMAND 消息的附带信息 WPARAM 拿到菜单ID 然后根据菜单ID对应的数字再进行消息的处理
通知码:
CBN_SELCHANGE --- 当组合框的选择项发生变化后, CBN_SELCHANGE 通知码会依附于 WM_COMMAND 消息发送给它的父窗口
CBN_EDITCHANGE --- 当组合框用键盘输入发生变化后, CBN_EDITCHANGE 通知码会依附于 WM_COMMAND 消息发送给它的父窗口
下拉列表式组合框永远不会给我们发送 CBN_EDITCHANGE 通知码,因为下拉列表式组合框不能使用键盘输入
列表框控件
列表框就是: 组合框去掉输入框剩下的就是列表框
分类:
单列列表框 --- 创建单列列表框时,默认就是单列列表框
多列列表框 --- 创建多列列表框时,需要加 LBS_MULTCOLUMN 风格
使用列表框控件
1> 创建列表框
列表框的窗口类名称: LISTBOX
列表框窗口消息:
2> 添加选项
通过 SendMessage()发送窗口消息
LB_ADDSTRING --- 追加
LB_INSERTSTRING --- 按指定位置添加
3> 删除选项
通过 SendMessage() 发送窗口消息
LB_DELETESTRING --- 删除指定项
LB_RESETCONTENT --- 清空所有项
4> 获取/设置 选择项
通过 SendMessage()发送窗口消息
LB_GETCURSEL --- 获取选择项的索引
LB_SETCURSEL --- 设置选择项
5> 匹配查找
通过 SendMessage() 发送窗口消息
LB_FINDSTRING --- 非精确匹配查找,可以不全,但不能错
LB_FINDSTRINGEXACT --- 精确查找,给定的字符必须和其中一个选项完全一致
LB_SELECTSTRING --- 非精确匹配查找并将匹配到的选项设置成当前选择项
6> 获取选项文本内容
通过 SendMessage() 发送窗口消息
LB_GETTEXT --- 获取文本框某个选项的文本内容
LB_GETTEXTLEN --- 获取文本框某个选项的文本内容的长度
7> 列表框的附加数据
通过 SendMessage() 发送窗口消息
LB_SETITEMDATA --- 设置选项的附加数据
LB_GETITEMDATA --- 获取选项的附加数据
窗口的通知消息:
LB_SELCHANGE --- 当选择项发生变化后, LB_SELCHANGE 通知码会依附于 WM_COMMAND 消息的附加数据发送到父窗口的处理函数中
十六. Windows 库程序
Windows库 和 头文件
Windows 库
kernel32.dll --- 提供了核心的 API, 例如进程,线程,内存管理等;
user32.dll --- 提供了窗口,消息等API
gdi32.dll --- 绘图相关的API
头文件
windows.h --- 所有windows头文件的集合
windef.h --- windows 数据类型
winbase.h --- kernel32的API
wingdi.h --- gdi32的API
winuser.h --- user32的API
winnt.h --- UNICODE字符集支持
静态库程序:
静态库不能进内存,运行时不存在,
静态库的源代码会被连接到可执行文件或者动态库中,
静态库属于目标程序的归档,
静态库文件扩展名: .LIB
UC静态库文件名是: .a
UC静态库是把各个 .o(目标文件)整合压缩到 .a 文件里面
静态库特点:
1> 运行时不存在
2> 连接到可执行文件或者动态库中
3> 目标程序的归档
C语言静态库:
C静态库的创建
1> 建项目
2> 添加库程序,源文件使用 .c 为后缀的文件
C静态库路径设置
1> 项目的 属性设置里面设置库的路径
2> 可以使用 pragma 关键字设置路径
如:
#pragma comment(lib,"../Debug/静态库.lib") //告诉链接器到那里去抓静态库的源代码 .lib文件里面存的就是静态库的源代码
C静态库的调用
建立一个C文件,可以在文件中直接使用C库函数,不需要头文件,C编译器只是根据库函数名称,在库中找到对应的函数代码,进行连接
如:
#include
#pragma comment(lib,"../Debug/静态库.lib")
int main(int argc,char* argv[]){
int sum, sub;
sum = Clib_add(4,6);
sub = Clib_sub(5,3);
printf("%d %d\n",sum,sub);
return 0;
}
C++语言的静态库:
C++静态库的建立
1> 建立项目
2> 添加库程序,源文件使用 .CPP为后缀的文件
C++静态库路径设置
1> 项目的 属性设置里面设置库的路径
2> 可以使用 pragma 关键字设置路径
如:
#pragma comment(lib,"../Debug/静态库.lib") //告诉链接器到那里去抓静态库的源代码 .lib文件里面存的就是静态库的源代码
C++静态库的调用
在静态库中写函数的实现代码,在调用程序中会提示没有声明,要求我们导入头文件,但静态库没有头文件这时我们可以在调用函数前手动写出函数声明,
这样编译就不会报错了,但链接时还是会报错,这时就只需要把 静态库里的源代码导入即可,把静态库的路径告诉链接器,则自动去静态库里抓源代码
如:
#include "stdio.h"
int CPPlib_add(int,int); // 为了编译时不报错
int CPPlib_sub(int,int); // 为了编译时不报错
#pragma comment(lib,"../Debug/C++静态库.lib") //把静态库里的源代码导入
int main() {
int sum = CPPlib_add(5,4);
int sub = CPPlib_sub(5, 4);
printf("%d %d \n", sum, sub);
return 0;
}
在C++环境中使用 C的静态库,库中函数原型定义要增加 extern "C" 按C语言的方式生成函数调用名
如:
#include "stdio.h"
int CPPlib_add(int,int);
int CPPlib_sub(int,int);
#pragma comment(lib,"../Debug/C++静态库.lib")
extern "C" {
int Clib_add(int, int);
int Clib_sub(int, int);
}
#pragma comment(lib,"../Debug/静态库.lib")
int main() {
int sum = CPPlib_add(5,4);
int sub = CPPlib_sub(5, 4);
printf("%d %d \n", sum, sub);
sum = Clib_add(4,6);
sub = Clib_sub(10,2);
printf("%d %d \n", sum, sub);
return 0;
}
注:
"\" 在windows下面有时候是"\\"才能表示路径
"/" 只用写一个就可以表示路径,UC也用"/"
在VS中写 C程序 添加 -> 新建项 -> win32 -> win32控制台应用程序 -> ... -> 源文件右键 -> 添加 -> 实用工具 -> 文本文件(.txt) -> 把 .txt 变成 .c
动态库程序:
动态库程序能执行,但它并不能自己进内存,动态库必须依附于调用它的程序才能进内存,
进内存后一旦运行动态库它就是独立的进程脱离调用它的进程,运行时就独立存在,
动态库的源文件不会被连接到可执行文件或其它动态库中,
动态库文件扩展名: .DLL
UC的动态库文件名是: .so
动态库特点:
1> 动态库一旦执行起来后就独立存在(动态库只能依附于调用它的进程进入内存执行的)
2> 动态库中的源代码不会链接到执行程序(链接到程序中的知识动态库中函数的地址)
3> 使用动态库时必须先加载动态
因为我们要调用动态库中的函数但我们程序中保存的只是这个动态库函数在内存中的地址,真正的源代码在硬盘上保存的因此必须要把动态库加载到内存
动态库与静态库比较:
1> 由于静态库是将代码嵌入到程序中,当多个程序使用时,会有多份代码,会导致代码体积增大,
动态库的代码只需要在内存中存在一份,其它程序通过函数地址使用,所以程序代码体积小
2> 静态库发生变化后,新的代码需要重新链接嵌入到执行程序中
动态库发生变化后,如果库中函数的定义(或地址)未变化,则使用 .DLL 的程序不需要重新链接
动态库的创建
1> 建立项目
2> 添加库程序
3> 库程序导出(导出库函数的文件头) 只有把动态库的文件头导出来其它程序才能调用动态库中的函数(UC中的位置无关码)
动态库经过编译链接后会生成 .dll 文件,在 .dll 文件的开始部分(文件头)会生成一个索引(包括 最终函数调用名/函数偏移地址/函数编号 等)
只要把动态库的文件头导出给调用者,调用者就可以知道动态库里的信息从而进行调用
如果不导出动态库,那怕编译生成 .dll 文件(动态库的源代码的确也在 .dll 文件中)但其它程序也调用不了
怎么查看动态库是否导出:
VC++6.0是使用 Dependency Walker 工具
VS2015是 ???
动态库的函数
实现动态库的函数
库函数的导出: 2种方式 导出只能导出 最终函数调用名/函数编号/该lib文件配套的dll是谁 不能导出函数的偏移地址,偏移地址在动态库的文件头中
导出动态库的文件头:
1> 在动态库函数前面加上 _declspec(dllexport)导出动态库文件头里的信息
编译链接后就会生成一个 .lib 文件,里面保存的就是从动态库文件头里导出来的信息
信息包括:
1> 最终函数调用名
2> 函数编号
3> 该lib文件配套的dll是谁
如:
_declspec(dllexport)int CPPdll_add(int a, int b){
return a+b;
} //导入动态库中文件头的信息
注意:
动态库编译链接后,也会有 LIB 文件, 是作为动态库函数映射使用,与静态库不完全相同,
静态库的 .lib 文件里面存的是静态库的源代码, 动态库的 .lib 文件里面存的是: 最终函数调用名/函数编号/该lib文件配套的dll是谁
_declspec(dllexport)导出动态库文件头中的信息: 最终函数调用名/函数编号/该lib文件配套的dll是谁
_declspec(dllimport)导入动态库文件头中的信息: 最终函数调用名/函数编号/该lib文件配套的dll是谁
#pragma comment(lib,"../Debug/动态库.lib")
//告诉链接器哪个文件里面保存了动态库的信息
_declspec(dllimport) 和 #pragma comment() 的区别
_declspec(dllimport) 导入的是动态库文件头中的具体信息: 最终函数调用名/函数编号/该lib文件配套的dll是谁
#pragma comment() 告诉链接器保存动态库文件头信息的文件在那里
2> 模块定义文件 .def
创建动态库(.cpp)文件 然后创建 .def 文件
右键工程名 -> 添加 -> 新建项 -> Visual C++ -> 代码 -> 模块定义文件(.def)
.def文件的编写
一个def文件必须有两个部分:LIBRARY 和 EXPORTS
让我们先看一个基本的.def文件稍后我将解析:
LIBRARY libdll.dll ;dll 文件的文件名
DESCRIPTION "描述信息" ;描述信息,此行可以不要
EXPORTS
lib_add @1 ;要导出的函数
lib_sub @2 ;
第一行,"LIBRARY" 是一个必需的部分。它告诉链接器(linker)如何命名你的DLL一般和工程名相同。
下面被标识为 "DESCRIPTION" 的部分并不是必需的,但是我喜欢把它放进去。
该语句将字符串写入 .rdata 节[据 MSDN],它告诉人们谁可能使用这个DLL,这个DLL做什么或它为了什么(存在)也可以写版本号。
再下面的部分标识为 "EXPORTS"是另一个必需的部分;这个部分使得该函数可以被其它应用程序访问到并且它创建一个导入库。
当你生成这个项目时,不仅是一个.dll文件被创建,而且一个文件扩展名为.lib的导出库也被创建了。
除了前面的部分以外,这里还有其它四个部分标识为: NAME, STACKSIZE, SECTIONS, 和 VERSION。
我将不再在本文中涉及这些内容,但是如果你在Internet上搜索,
我想你将找到一些东西(译注: MSDN2003上对模板定义文件各部分内容有详尽解释,请参阅)。
另外,一个分号(;)开始表示一个注解
.def文件路径的设置
项目属性 -> 链接器 -> 输入 -> 模块定义文件
.def文件的作用:
通俗解释: 在VC++中,生成DLL可以不使用.def文件。只需要在VC++的函数定义前要加 __declspec(dllexport)修饰就可以了。
但是使用__declspec(dllexport)和使用.def文件的DLL是有区别的:
如果 DLL是提供给VC++用户使用的,你只需要把编译DLL时产生的.lib提供给用户,它可以很轻松地调用你的DLL。
但是如果你的DLL是供其他程序如 VB、delphi,以及.NET 用户使用的,那么会产生一个小麻烦。
因为VC++对于 __declspec(dllexport)声明的函数会进行名称转换,
如:
__declspec(dllexport) int __stdcall IsWinNT() 会转换为 IsWinNT@0,
这样你在VB中必须这样声明: Declare Function IsWinNT Lib "my.dll" Alias "IsWinNT@0" () As Long
@的后面的数由于参数类型不同而可能不同。这显然不太方便。
所以如果要想避免这种转换,就要使用.def文件方式。
EXPORTS后面的数可以不给,系统会自动分配一个数。
对于VB、PB、 Delphi用户,通常使用按名称进行调用的方式,这个数关系不大,
但是对于使用.lib链接的VC程序来说,不是按名称进行调用,而是按照这个数进行调用的,所以最好给出。
例子:
我们用VC6.0制作一个dll,不使用.def文件,在头文件中这样写:
#ifndef LIB_H
#define LIB_H extern "C" int _declspec(dllexport)add(int x,int y);
#endif
动态库的使用
隐式链接(让动态库程序运行起来的过程不需要程序员负责), 动态库是在程序启动时就被加载,当DLL不存在,程序无法启动
1> 使用隐式链接时尽量在库函数声明时加上 _declspec(dllimport)导入动态库中文件头的信息
如:
_declspec(dllimport)int CPPdll_add(int,int);
如果库函数使用的是C语言格式导出,需要在函数声明时增加 extern "C" 按C语言的方式生成函数调用名
2> 导入动态库的 LIB 文件 : #pragma comment(lib, ".../.../xxx.lib") 告诉链接器保存动态库文件头信息的文件在那里
3> 在程序中使用函数
4> 隐式链接的情况, DLL 可以存放的路径:
4.1> 与可执行文件(.exe文件)同一个目录下,建议使用
4.2> 当前工作目录,发布版本是没有此路径的
4.3> Windows 目录
4.4> Windows/System32目录
4.5> Windows/System
4.6> 环境变量PATH指定目录,不建议使用此路径因为用户的路径和我们的PATH有可能是不一样的
显示链接(让动态库程序运行起来的过程需要程序员自己负责)
1> 定义函数指针类型 使用 typedef 关键字
2> 加载动态库(让动态库程序进内存并返回动态库在内存中的首地址), 动态库只在使用 LoadLibrary()函数才会被加载
HMODULE LoadLibrary(
LPCTSTR lpFileName //动态库文件名(必须和 最终的 .exe 在同一个文件)或带盘符的全路径名
);//返回 DLL 的实例句柄 ( HINSTANCE ) 加载到内存里的首地址
3> 获取函数绝对地址 : 通过动态库首地址和函数的偏移地址
FARPROC GetProcAddress(
HMODULE hModule, //DLL实例句柄 动态库加载到内存里的首地址,每次加载在内存中的地址是不一样的
LPCSTR lpProcName //要获取函数的名称,我们通过 _declspec(dllexport)导出的地址就是这个偏移地址,是固定的偏移地址
); 成功返回函数的绝对地址
注:
要获取函数的名称 一定要是内存中真正的函数调用名 如果是 C++ 编译器编译的 此处就要填写 换名后的调用名
如:
DLL_SUB mySub = (DLL_SUB)GetProcAddress(hDll,"?CPPdll_sub@@YAHHH@Z");
4> 使用动态库函数
5> 卸载动态库
BOOL FreeLibrary(
HMODULE hModule //DLL的实例句柄
);
VS更改库的输出路径:
动态库:属性-配置属性-常规-输出目录
静态库:属性-配置属性-链接器-高级-导入库
动态库总结:
理解动态库只需要解决两个问题:
1> 怎么制作动态库 --- 怎么导出动态库
1. _declspec(dllexport) 函数声明导出
2. 增加 .def 为后缀的文件 模块定义文件导出
2> 怎么调用动态库 --- 怎么加载动态库
1. _declspec(dllimport) #pragma comment(lib,"...") 隐式链接
2. LoadLibrary() GetProcAddress() FreeLibrary() 显示链接
在动态库里面封装的任何一个东西如果想让其它程序使用都必须先导出动态库里的东西其它程序才能使用否则其它程序无法使用动态库中的东西
动态库文件头导出:
1> 函数声明导出: 在函数声明前面加上 _declspec(dllexport) 就可以将函数的偏移地址导到 dll 文件的文件头中
如果是 C++ 编译器, dll 文件头中记录的是 换名之后的函数名以及对应的偏移地址
动态库的函数声明导出会生成一个 .lib 文件,
动态库的 .lib 文件中仅仅记录的是换名之后的函数名和它对应的编号及配套的dll文件是谁
2> 模块定义文件导出: 将函数的偏移地址导到 dll 文件的文件头中
即便是C++编译器, dll文件头中记录的是未换名的函数名以及对应的偏移地址
动态库的 .lib 文件中仅仅记录的是换名之后的函数名和它对应的编号及配套的dll文件是谁
3> 原样导出, 此方式需要 导出和导入都要加 extern "C" 关键字
如:
extern "C" __declspec(dllexport)int MyFunction(){
...
}
extern "C" _declspec(dllimport)int Dadd(int,int);
DLL 中类的使用
如果想导出 DLL 中的类 一般使用 声明函数的方式导出,不用模块文件导出因为模块文件导出类中的函数时要写类中函数换名后的函数名
虽然使用声明函数的方式导出DLL中的类也会换名 但我们调用的时候可以使用 类的对象 去调用类中的函数
在类名称前增加 _declspec(dllexport) 定义
如:
class _declspec(dllexport)CMath{
...
};
通常类中要使用预编译开关切换类的导入导出定义,因为我们要把定义类的哪个头文件给用户,我们在写类的时候是要导出而用户在使用时是导入
如:
#ifdef DLLCLASS_EXPORTS
#define EXT_CLASS _declspec(dllexport)
#else
#define EXT_CLASS _declspec(dllimport)
#endif
class EXT_CLASS CMath{
...
}
使用动态库:
1> 隐式链接 -- 操作系统的加载器负责让动态库进内存,链接器负责到 lib 文件中抓函数的编号,
程序执行起来后操作系统加载器负责拿着编号到 dll 文件头中查询函数的偏移地址
#pragma commect(lib,"..../../xxx.lib") //告诉链接器保存着动态库信息的 .lib 文件在那里
_declspec(dllimport)int xxxx(); //导入动态库的信息让加载器拿着编号去 dll 文件中去查询
2> 显示链接 -- 程序员调用 LoadLibrary()函数让动态库进内存并返回动态库在内存中的首地址
程序员调用 GetProcAddress()函数, 在该函数内部通过函数名 到 DLL 文件的文件头中查询函数的偏移地址
拿到偏移地址在配合首地址就能返回函数的绝对地址从而对函数进行调用
如:
GetProcAddress(hDll,"?CPPdll_add@@YAHHH@Z");
两种链接方式对比:
1> 在库函数的定义不变情况下:
隐式链接,由于库函数地址是在程序编译链接时设置,所以当动态库变化后,使用程序需要重新编译链接
显示链接,由于库函数地址是在程序执行时,动态的从库中查询,所以库变化后,不需要重新编译链接
隐式链接是先到 lib 文件中找函数导出来的对应编号,再拿编号到DLL文件的文件头里去匹配查找,如果函数的编号发生改变则需要重新编译链接
显示链接是用函数名直接到DLL文件的文件头里去匹配查找,编号发生改变不需要重新编译链接
2> 动态库加载
隐式链接, 动态库是在程序启动时就被加载,当 DLL 加载不成功或不存时则自身的程序也无法启动
显示链接, 动态库只在使用 LoadLibrary()函数,才会被加载,不管 DLL 存在与否自身程序是可以启动的
注:
库文件一般 和 最终的 exe 文件在同一目录下 编译链接才不会报错
隐式链接的时候一定不要忘记告诉 链接器 保存函数编号和配套的dll文件是谁的 lib文件在那里,
如:
#pragma comment(lib,"../Debug/XXXXX.lib")
如果不告诉链接器则会报错:
error LNK2019: 无法解析的外部符号 "__declspec(dllimport) public: int __thiscall CMath::Add(int,int)"
(__imp_?Add@CMath@@QAEHHH@Z),该符号在函数 _main 中被引用
动态库的入口函数:
动态库的入口函数并不是动态库必须得有的可以不写,如果不写操作系统会默认添加一个入口函数,如果写了入口函数操作系统则不再提供
动态库的入口函数和传统的入口函数功能不同:
传统的入口函数功能:
1> 运行在主线程下
2> 但程序执行起来后第一个执行的函数就是 入口函数
3> 在入口函数里面 执行 return 语句就表示入口函数执行完毕
4> 在整个程序执行期间入口函数只会被执行一次
动态库的入口函数功能:
1> 入口函数不是动态库必须得有的
2> 如果不写操作系统会自动添加一个入口函数,如果写了操作系统则不会再提供入口函数
3> 动态库的入口函数一般用来做 动态库内部的初始化 或 做善后处理
4> 动态库的入口函数在动态库执行期间有可能被调用多次
5> 动态库的加载或卸载时动态库的入口函数会被调用
如:
Loadlibrary() 或 FreeLibrary() 时 动态库的入口函数会被调用
动态库入口函数原型:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, //动态库实例句柄
DWORD fdwReason, //动态库被调用的原因
LPVOID lpvReserved, //预留参数,暂时没启用
);
返回 TRUE 让动态库加载成功, 返回 FALSE 让动态库加载失败
fdwReason的取值:
DLL_PROCESS_ATTACH ---- 进程加载动态库时调用了 DllMain()
DLL_PROCESS_DETACH ---- 进程卸载动态库时调用了 DllMain()
DLL_THREAD_ATTACH ---- 线程加载动态库时调用了 DllMain()
DLL_THREAD_DETACH ---- 线程卸载动态库时调用了 DllMain()
十七. Windows 文件系统
标C的 fopen()/fclose() 函数在不同的系统平台下调用的底层函数不同
在UC下 fopen()/fclose() 调用的是 open()/close()
在Windows下 fopen()/fclose() 调用的是 CreateFile()/CloseHandle()
硬盘上鼠标右键设置的属性在程序里面是修改不了的
1> 创建或打开Windows文件 // #include
HANDLE CreateFile(
LPCTSTR lpFileName, //想打开文件的路径(带盘符的全路径) 或 想在硬盘上那个路径下创建文件(带盘符的全路径)
DWORD dwDesiredAccess, //访问权限, 读/写
DWORD dwShareMode, //共享方式,在我操作该文件期间希望其它程序以什么方式打开该文件,最安全的方式的读方式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全属性,默认为 NULL, 在Windows平台下只要见到安全属性通通给 NULL
DWORD dwCreationDisposition, //创建或打开方式, 告诉操作系统是创建文件还是打开文件
DWORD dwFlagsAndAttributes, //文件属性, 在创建或打开文件的时候是否增加什么属性
HANDLE hTemplateFile //文件句柄模版, 如果使用 CreateFile()操作硬盘文件则给 NULL 全双工 如果是操作外围设备则不一定为 NULL 有可能是半双工
);
成功返回文件句柄
dwDesiredAccess 访问权限取值:
GENERIC_READ --- 读模式
GENERIC_WRITE --- 写模式
0 --- 以质询方式访问,如果是访问外围设备可以先用此方式,看一看外围设备是否存在,如果设备不存在会抛出异常信息
dwShareMode 共享方式取值:
FILE_SHARE_DELETE --- 在我操作该文件期间允许其它程序删除该文件
FILE_SHARE_READ --- 在我操作该文件期间只允许其它程序读该文件
FILE_SHARE_WRITE --- 在在操作该文件期间只允许其它程序写该文件
dwCreationDisposition 创建 或 打开文件
创建文件:
CREATE_NEW --- 创建新文件,如果文件已经存在则创建失败
CREATE_ALWAYS --- 创建文件,如果该文件已经存在则删除后重新创建
打开文件:
OPEN_EXISTING --- 打开文件,如果该文件存在则打开,如果该文件不存在则打开失败
OPEN_ALWAYS --- 打开文件,该文件存在则打开,如果该文件不存在则创建然后在打开
TRUNCATE_EXISTING --- 打开文件,打开的同时清空文件, 不常用
dwFlagsAndAttributes 文件属性取值:
FILE_ATTRIBUTE_ARCHIVE --- 普通文件属性
FILE_ATTRIBUTE_HIDDEN --- 隐藏属性
FILE_ATTRIBUTE_NORMAL --- 不增加属性
FILE_ATTRIBUTE_OFFLINE --- 离线属性
FILE_ATTRIBUTE_READONLY --- 只读属性
FILE_ATTRIBUTE_SYSTEM --- 系统文件属性 以 .sys 为后缀的文件
FILE_ATTRIBUTE_TEMPORARY --- 临时文件属性
2. 写数据
BOOL WriteFile(
HANDLE hFile, //文件句柄
LPCVOID lpBuffer, //要写数据的缓冲区
DWORD nNumberOfButesToWrite, //准备写的数据有多长
LPDWORD lpNumberOfBytesWritten, //返回实际写入数据的长度
LPOVERLAPPED lpOverlapped //默认为 NULL 表示同步传输, 在数据没有写到硬盘上会阻塞不会返回,
也不是永远为 NULL 我们可以设置一个类似信号的东西当执行 WriteFile()函数后
不管有没有把数据写到硬盘上立即返回,
当数据全部写入硬盘后系统会通过我们设置的信号给我们发送一个东西告诉我们数据写入硬盘已经写完了
);
3. 读数据
BOOL ReadFile(
HANDLE hFile, //文件句柄
LPVOID lpBuffer, //用来存放读到数据的缓冲区
DWORD nNumberOfBytesToRead, //准备读多少字节的数据
LPDWORD lpNumberOfBytesRead, //实际读了多少字节的数据
LPOVERLAPPED lpOverlapped //默认为 NULL 表示同步传输, 在没有把数据读完之前会阻塞不返回
也不是永远为 NULL 我们可以设置一个类似信号的东西当执行 ReadFile()函数后
不管有没有把数据读完立即返回,
当数据全部读完后系统会通过我们设置的信号给我们发送一个东西告诉我们数据已经读完了
);
调用成功,返回非0
调用不成功,返回为0
4. 关闭文件: 其中包括文件、文件映射、进程、线程、安全和同步对象等
BOOL CloseHandle(
HANDLE hObject //文件句柄
);
5. 获取文件长度
DWORD GetFileSize(
HANDLE hFile, //想要获取大小的文件句柄
LPDWORD lpFileSizeHigh //返回文件长度的高32位
);
返回值是文件长度的低32位
返回值是 DWORD 类型 最大能能表示 2^32=4G 大小 但windows系统下超4G的文件很多,所以 GetFileSize()函数是用两个 32位的数来保存一个文件的大小,
lpFileSizeHigh 如果等于1则表示该文件已经超4G了,这个参数是高32位,
返回值返回1表示 1个字节
6. 文件指针
DWORD SetFilePointer(
HANDLE hFile, //要移动文件的文件句柄
LONG lDistanceToMove, //要移动位置的偏移量的低32位
PLONG lpDistanceToMovehigh //返回实际偏移量的高32位 有高32位就可以移动到 4G 以上的位置 1表示4G
DWORD dwMoveMethod //偏移的相对位置3个取值: FILE_BEGIN(文件头) FILE_CURRENT(当前位置) FILE_END(文件尾)
);
返回实际偏移量的低32位
7. 文件相关操作
CopyFile --- 拷贝文件
原型:
BOOL CopyFile(
LPCTSTR lpExistingFileName, //源文件路径(被复制的文件)
LPCTSTR lpNewFileName, //目的地路径
BOOL bFailIfExists //目的地已经存在是否覆盖(true 不覆盖, false 覆盖)
);
成功返回 非0, 失败返回 0
如:
CopyFile("F:/file.txt","F:/1/file.txt",TRUE);
DeleteFile --- 删除文件
原型::
BOOL DeleteFile(
LPCTSTR lpFileName // 待删除文件的路径
);
成功返回非0, 失败返回0
MoveFile --- 移动文件,如果目的地已经有该文件 MoveFile()直接失败
原型:
BOOL MoveFile(
LPCTSTR lpExistingFileName, //要移动文件的路径
LPCTSTR lpNewFileName //目的地路径
);
成功返回 非0, 失败返回0
8. 文件遍历
1. 查找指定路径下的第一个文件
HANDLE FindFirstFile(
LPCTSTR lpFileName, //指明要查找文件的路径
LPWIN32_FIND_DATA lpFindFileData //保存找到文件的信息
);
返回查找句柄, 查找句柄里面保存的是当前找到第几个文件
LPWIN32_FIND_DATA是以LP开头表示是一个指针指向 WIN32_FIND_DATA 结构体
WIN32_FIND_DATA结构体原型:
typedef struct _WIN32_FIND_DATA{
DWORD dwFileAttributes; //文件的属性(只读/隐藏 ... )
FILETIME ftCreationTime; //文件的创建时间
FILETIME ftLastAccessTime; //文件最后一次访问时间
FILETIME ftLastWriteTime; //文件最后一修改时间
DWORD nFileSizeHigh; //文件大小的高32位
DWORD nFileSizeLow; //文件大小的低32位
DWORD dwReserved0; //备用
DWORD dwReserved1; //备用
TCHAR cFileName[MAX_PATH]; //文件的名称
TCHAR cAlternateFileName[14]; //过去标识文件的名称16位操作系统之前用的,现在没用了
}WIN32_FIND_DATA,* PWIN32_FIND_DATA;
dwFileAttributes 文件属性包括:
FILE_ATTRIBUTE_ARCHIVE // 存档文件
FILE_ATTRIBUTE_COMPRESSED // 压缩文件
FILE_ATTRIBUTE_DIRECTORY // 目录文件
FILE_ATTRIBUTE_ENCRYPTED // 加密文件
FILE_ATTRIBUTE_HIDDEN // 隐藏文件
FILE_ATTRIBUTE_NORMAL // 普通文件
FILE_ATTRIBUTE_OFFLINE // 离线文件
FILE_ATTRIBUTE_READONLY // 只读文件
FILE_ATTRIBUTE_REPARSE_POINT // 超链接或快捷方式文件
FILE_ATTRIBUTE_SPARSE_FILE // 稀疏文件
FILE_ATTRIBUTE_SYSTEM // 系统文件
FILE_ATTRIBUTE_TEMPORARY // 临时文件
2. 获取指定路径下的下一个文件
BOOL FindNextFile(
HANDLE hFindFile, //查找句柄
LPWIN32_FIND_DATA lpFindFileData //保存找到文件的信息
);
找到返回 TRUE, 没有找到返回 false
3. 关闭查找
BOOL FindClose(
HANDLE hFindFile //查找句柄
);
十八. 内存管理
地址空间
程序中可以寻址的最大范围,对于 32 位操作系统,地址空间范围为 0-4G-1(2^32),地址空间越大,相对程序的编写就会更容易
地址空间的划分
用户地址空间 0 - 2G( 小于 7FFFFFFF 的地址)
存放用户的程序和数据
用户空间的代码是不能访问内核空间的数据和代码
1> 空指针区(NULL区,0-64k) 系统将地址小于 64k指针,都认为是空指针
2> 用户区
3> 临近内核区的64k的地址叫禁入区(0x7FFEFFFF - 0x7FFFFFFF)也是不能用的,内核也不能用;
内核地址空间 2G - 4G 大于 7FFFFFFF 的地址
存放内核的代码和数据,例如系统驱动,内核空间代码是可以访问用户空间
UC的内存管理是:
低3G是用户可以使用的内存,高1G是内核使用的地址,
Windows 是低 2G 属于用户使用的地址,高2G属于内核使用的地址
区域: 专门针对内存的 和 地址没有关系
区域就是连续的一块真实内存,区域的大小一般为 64K 或者 64K的倍数,每个区域都有自己的状态:
1> 空闲: 没有被使用
2> 私有: 被预定的区域但我们不能使用
3> 映像: 里面存放的是代码 (代码段)
4> 映射: 里面存放的是数据,不具备常属性的(全局区)
物理内存:
系统可以使用的实际内存,CPU可以直接访问的内存
硬盘交换文件(虚拟内存)
将硬盘文件虚拟成内存使用(pagefile.sys文件),查看 pagefile.sys 文件时文件的大小是该交换文件最多能写多少数据,并不是该文件大小,比较特殊;
CPU如果要访问虚拟内存数据,必须将虚拟内存数据放到物理内存,CPU只能直接访问内存不能直接访问硬盘;
我们在申请内存的时候是申请的物理内存,还是在使用硬盘的交换文件这个是由计算机决定的,计算机根据我们申请内存里存放的数据是否使用频繁决定
如果常用则使用物理内存,如果不常用则使用硬盘交换文件;
注:
Windows下的虚拟内存是把硬盘上的一个文件当做内存来使用,
UC下的虚拟内存是指一个进程只要启动系统会默认给它虚拟出一个4G的内存,这个4G个内存必须和真正的物理内存进行映射才能使用;
32位系统共4G个地址,64位系统共 17179869184G
内存页: 是针对地址,不是真实存在的是一种内存管理方式
系统管理内存的最小单位,内存页大小为 4K (一个内存页有 4k 个内存地址),每个内存页有自己的权限
我们申请内存的时候,映射是一次映射 4k个内存地址也就是给一整页映射
页目表
指针地址一共32位(0 - 31)
页目表就相当于UC下的虚拟内存
31--------22 21---------12 11---------0
↓ ↓ ↓
↓ ↓ 0~11 页内偏移地址 2^12 == 4K == 4096个编号
↓ ↓
↓ 12~21 10位放页表 2^10 == 1024个编号
↓
22~31 10位放页目 2^10 == 1024个编号
空间一共有多少层由最高10位决定也就是由页目决定, 1024
每一层有多少个内存页由中间10位决定也就是由页表决定, 1024
每个内存页有多少个地址,由最低12位决定也就是页内偏移地址决定 4096
如:
0000000001 0000000001 000000000000
↓ ↓ ↓
↓ ↓ 此12位表示每个内存页有多少个地址,这里表示是第二个内存页上的第一个地址(从0开始)
↓ 此10位表示每一层有多少个内存页,这里表示第2个内存页(从0开始)
此10位表示有多少层,这里是第2层(从0开始)
计算机默认单位为 Byte(字节)
1Byte == 8bit
1KB == 1024 Byte 千字节
1k * 1k == 1m
1m * 4k == 4G
CPU从内存获取数据的过程
CPU会根据给它的地址在物理内存中查找相应的位置,看给它的地址是否和物理内存的某一块内存进行绑定了,
如果发现有和物理内存的某一块内存进行绑定了,就直接取回数据,如果没有和物理内存的某一块内存进行绑定,
CPU会根据地址去虚拟内存中查找相应的位置,看是不是和虚拟内存(硬盘交换分区)的某一块进行绑定,如果没有绑定,那么该地址没有内存空间返回错误,
如果有和硬盘交换分区的某一块进行绑定,就会将该地址所在的内存页,全部置换到物理内存中,
同时将原物理内存数据,存入到虚拟内存中(硬盘交换分区中 pagefile.sys文件 )
将物理内存中的数据返回给使用者
一个内存页里的数据要么全部在硬盘交换分区里面,要么全都在物理内存里面,绝不可能一半在物理内存一半在交换分区里,
因为如果某一块的数据要进内存CPU是会把该地址所在的内存页全部置换到物理内存中;
Windows下内存分配的方式:
虚拟内存分配 --- 适合大内存分配 一般是 1M之上的内存
堆内存分配 --- 适合小内存分配, 一般是 1M 以下的内存 malloc/new
栈内存分配 -- 适合小内存分配, 一般是 1M以下的内存分配,系统自动分配
虚拟内存分配和堆内存分配区别:
虚拟内存是从硬盘置换出来的,堆本身就是内存,程序运行时,可用内存=物理内存+虚拟内存。
虚拟内存一般用文件来保存数据,虚拟内存的出现主要是因为以前内存不够(16M的内存刚出来的时候可是天价啊),磁盘相对便宜一些,
所以聪明的系统设计者就把设计了虚拟内存,在程序运行的时候把那些很久没有被访问过的(可能以后也不会用到)
内存映射到文件里面去(以后需要的时候再读进内存),把内存腾出来给真正需要执行的代码和数据,这样看起来可用内存就比物理内存多了。
虚拟内存分配:
内存分配速度快,大内存效率高,将内存和地址分配分别执行,可以只要地址不要内存,也可以在需要的时候再提交内存,常用于大型电子表格等处理
虚拟内存使用:
申请地址 和 提交内存
LPVOID VirtualAlloc(
LPVOID lpAddress, //申请地址时填写申请地址的首地址一般为 NULL 系统管理, 如果是提交内存,填写给哪个地址提交内存
SIZE_T dwSize, //要申请多少个地址
DWORD flAllocationType, //分配方式
DWORD flProtect //内存访问方式
); 分配成功返回首地址,调用失败,返回NULL 可以通过GetLastError()函数来获取错误信息
分配方式:
MEM_COMMIT --- 连地址带内存都申请
MEM_RESERVE --- 只申请地址,分配之后只返回地址,内存空间不生成,要使用内存必须再提交
访问方式:
PAGE_READONLY --- 该区域为只读,如果应用程序试图访问区域中的页的时候,将会被拒绝访问
PAGE_EXECUTE --- 区域包含可被执行的代码,试图读写该区域的操作将被拒绝
PAGE_EXECUTE_READ --- 区域包含可执行代码,应用程序只能读该区域
PAGE_READWRITE --- 区域不可执行代码,应用程序可以读写该区域
PAGE_EXECUTE_READWRITE --- 区域可以执行代码,应用程序可以读写该区域
PAGE_GUARD --- 区域第一次被访问时进入一个 STATUS_GUARD_PAGE 异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限
PAGE_NOACCESS --- 任何访问该区域的操作将被拒绝
PAGE_NOCACHE --- RAM中的页映射到该区域时将不会被微处理器缓存(cached)
注:
PAGE_GUARD 和 PAGE_NOCHACHE 标志可以和其他标志合并使用以进一步制定页的特征,PAGE_GUARD标志指定了一个防护页(guard page),
即当一个页被提交时会因第一次被访问而产生一个 one-shot异常,接着取得制定的访问权限,
PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存,这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块;
使用
释放:
BOOL VirtualFree(
LPVOID lpAddress, //把那个地址对应的内存释放掉 或者 指明要释放的地址
SIZE_T dwSize, //释放多少个地址或释放多少个字节,可以写0 表示之前申请多少个地址就释放多少个地址
DWORD dwFreeType //释放方式
);
释放方式:
MEM_DECOMMIT --- 只释放和地址有映射关系的内存,地址本身不释放会成为野指针
MEM_RELEASE --- 连地址带内存都释放
GlobalMemoryStatus()函数可以查看当前计算机内存以及地址的使用情况:
原型:
VOID GlobalMemoryStatus(
LPMEMORYSTATUS lpBuffer //通过 MEMORYSTATUS 结构体返回内存及地址的使用情况
);
MEMORYSTATUS结构体原型:
typedef struct _MEMORYSTATUS {
DWORD dwLength; // 结构体的大小,跟结构体的对齐和补齐有关
DWORD dwMemoryLoad; // 物理内存使用率是百分比
SIZE_T dwTotalPhys; // 物理内存一共有多大
SIZE_T dwAvailPhys; // 剩余物理内存还有多少
SIZE_T dwTotalPageFile; // 硬盘交换分区的总大小(虚拟内存的总大小)
SIZE_T dwAvailPageFile; // 硬盘交换分区剩余大小(虚拟内存剩余大小)
SIZE_T dwTotalVirtual; // 一共有多少个地址
SIZE_T dwAvailVirtual; // 剩余多少个地址
} MEMORYSTATUS, *LPMEMORYSTATUS;
堆内存分配:
适合分配小内存,一般是小于1M的内存,一般每个程序都有自己的堆,默认大小为1M,会根据使用情况进行调整;
堆是内核中的一个结构,堆内存就是由堆结构维护的内存
堆结构里面包括:
首地址,尾地址 以及其它信息 这里的首地址尾地址是内核空间的高2G的地址,上层程序员是不能使用的(上层程序员只能使用低2G的地址)
在Windows系统下1个进程启动就默认维护3-5个堆结构,每个堆结构都维护一块内存,我们使用的 new 或 malloc()函数都是从默认堆结构维护的内存里分配内存的
在UC下一个进程开启不会默认开启堆内存
堆的使用:
获取堆的信息
GetProcessHeap() --- 获取程序的第一个堆结构
原型:
HANDLE GetProcessHeap(VOID); //返回本进程第一个堆结构的句柄
GetProcessHeaps() --- 获取程序中所有的堆
原型:
建立一个数组
DWORD GetProcessHeaps(
DWORD NumberOfHeaps, //数组元素个数
PHANDLE ProcessHeaps //数组首地址,用数组来接收所有堆结构的句柄
); 返回当前进程堆结构的实际个数
创建堆: 在操作系统内核中创建一套堆结构,同时根据初始化大小申请内存,该堆结构用来维护申请的内存
HANDLE HeapCreate(
DWORD flOptions, //创建选项
SIZE_T dwInitialSize, //初始化大小,要维护多大的内存
SIZE_T dwMaximumSize //最大值,一般给0不设置堆的上限值
); //成功返回堆句柄,通过堆句柄可以找到保存堆结构的内存,拿到堆结构就能找到申请的内存
创建选项取值:
HEAP_GENERATE_EXCEPTIONS --- 堆没有创建成功抛出异常
HEAP_NO_SERIALIZE --- 创建的堆支持物理不连续,win98之前如果申请内存不加此选项一般都申请不下来,因为没有那么大连续的内存
将堆结构维护的内存 和 本进程地址空间建立映射(进程启动默认有4个G的虚拟地址空间进程映像)
LPVOID HeapAlloc(
HANDLE hHeap, //堆结构句柄
DWORD dwFlags, //分配方式
SIZE_T dwBytes //分配大小,该值不能大于创建堆设置的最大值(HeapCreate()函数的第3个参数) 给0是本进程所有地址就和内存映射
); //成功返回本进程建立映射的首地址
分配方式取值:
HEAP_GENERATE_EXCEPTIONS --- 如果建立失败会抛出异常,而不是返回 NULL,异常值有可能是 STATUS_NO_MEMORY表示获得的内存容量不足 ...
HEAP_NO_SERIALIZE --- 不使用连续的内存
HEAP_ZERO_MEMORY --- 对已经建立映射的内存做初始化为0的操作
为什么要建立映射:
因为堆结构里面的首地址,尾地址是内核空间的高2G的地址,上层程序员是不能使用的(上层程序员只能使用低2G的地址)
使用内存
解除映射(将堆结构维护的内存 和 本进程地址空间解除映射)
BOOL HeapFree(
HANDLE hHeap, //堆句柄
DWORD dwFlags, //释放方式,没用给0即可
LPVOID lpMem //释放地址
); 将本进程的地址 和 堆结构维护的内存 解除映射关系
释放内存后其他进程也不能使用刚才堆结构维护的那块内存因为释放内存只是单纯的把堆结构维护的内存和本进程地址空间解除映射,
堆结构并没有销毁因此其它进程不能使用这块内存
销毁堆结构
BOOL HeapDestroy(
HANDLE hHeap //堆句柄
); 将内核中的堆结构删除,同时将堆结构维护的内存彻底释放
堆内存分配流程:
UC下 new --> malloc --> brk/sbrk --> mmap/munmap
Windows下 new --> malloc --> HeapAlloc(和本进程建立映射) --> VirtualAlloc(申请地址和提交内存,该函数才是真正建立映射的函数)
malloc 底层调用的是 HeapAlloc()函数,HeapAlloc()是将堆结构维护的内存和本进程地址空间(4G个虚拟地址空间,进程映像)建立映射
但一般我们在调用 malloc 时没有在内核中建立堆结构维护内存,那么在malloc调用 HeapAlloc()函数时是和那块内存建立映射的呢?
之前讲过Windows系统平台下进程启动操作系统会默认开启3-5个堆内存,malloc就是用默认开启的堆内存调用HeapFree()函数和本进程地址建立映射的;
malloc是怎么拿到系统默认开启的堆内存呢?
在malloc()函数内部调用 GetProcessHeap()/GetProcessHeaps()就能获取内核中系统默认开启的堆结构
栈内存:
栈内存 --- 每个线程都具有自己的栈,默认大小 1M 一般是系统维护栈
Windows提供了 _alloca, 可以在栈上分配内存
内存映射文件
将文件映射成内存来使用,当使用内存时,就是在使用文件,主要用来实现进程间通信
为什么不直接使用硬盘文件来完成进程间通信呢?
因为如果直接拿硬盘上的文件来做进程间通信的媒介时效率非常慢
比如用硬盘文件做媒介要10个小时才能完成但如果使用内存映射文件只需要1个小时就能完成
内存映射文件和堆一样也是内核中的一个结构
内存映射文件的使用
1> 创建或打开文件 CreateFile()
原型:
HANDLE CreateFile(
LPCTSTR lpFileName, //想打开文件的路径(带盘符的全路径) 或 想在硬盘上那个路径下创建文件(带盘符的全路径)
DWORD dwDesiredAccess, //访问权限, 读/写
DWORD dwShareMode, //共享方式,在我操作该文件期间希望其它程序以什么方式打开该文件,最安全的方式的读方式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全属性,默认为 NULL, 在Windows平台下只要见到安全属性通通给 NULL
DWORD dwCreationDisposition, //创建或打开方式, 告诉操作系统是创建文件还是打开文件
DWORD dwFlagsAndAttributes, //文件属性, 在创建或打开文件的时候是否增加什么属性
HANDLE hTemplateFile //文件句柄模版, 如果使用 CreateFile()操作硬盘文件则给 NULL 全双工
//如果是操作外围设备则不一定为 NULL 有可能是半双工
);成功返回硬盘文件的句柄
2> 在内核中创建内存映射文件结构
HANDLE CreateFileMapping(
HANDLE hFile, //硬盘文件的句柄 或 NULL
LPSECURITY_ATTRIBUTES lpAttributes, //安全属性,没用 给NULL 即可
DWORD flProtect, //访问方式
DWORD dwMaximumSizeHigh, //内存映射文件大小的高32位
DWORD dwMaximumSizeLow, //内存映射文件大小的低32位
LPCTSTR lpName //为内存映射文件命名,可以为 NULL 如果为NULL就无法实现进程间通信了
); 创建成功返回内存映射文件的句柄 或 申请的和本进程建立映射的内存的句柄
hFile如果填写的是硬盘文件的句柄, CreateFileMapping()的功能是把硬盘文件进行扩容,根据 dwMaximumSizeHigh 和 dwMaximumSizeLow 确定扩充大小
如果 hFile 填写为 NULL 则 CreateFileMapping()函数就不是在给硬盘文件扩容,而是在申请内存,
dwMaximumSizeHigh 和 dwMaximumSizeLow 决定申请内存的大小,申请内存后还和本进程地址空间建立映射 最后再返回建立映射的句柄
注:
dwMaximumSizeHigh 和 dwMaximumSizeLow 两个参数指明的大小是用在文件句柄对应的文件上的,
比如 dwMaximumSizeHigh 和 dwMaximumSizeLow 两个参数确定了大小为 1M 那么 hFile 对应的文件大小就会变成 1M
但一个文件的大小取决于文件里面存放多少数据, hFile是刚刚建立的文件怎么回有1M的呢,
这就要调用像UC里面的truncate()/ftruncate()函数修改文件大小(扩大小就往文件里面填0),
这里就是往hFile对应的文件里填1M个0这样hFile对应文件的大小就是1M了
3> 将硬盘文件和本进程地址空间建立映射 或 获取刚刚申请内存的某个部分的地址
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //内存映射文件句柄
DWORD dwDesiredAccess, //访问模式
DWORD dwFileOffsetHigh, //偏移量的高 32位
DWORD dwFileOffsetLow, //偏移量的低 32位
SIZE_T dwNumberOfBytesToMap //映射的字节大小, 填写0 表示从硬盘文件指定位置开始一直到文件尾全都和本进程地址空间建立映射
); 成功返回和本进程地址空间建立映射的内存的首地址
访问模式:
FILE_MAP_WRITE --- 读写访问,目标一定是创建文件映射对象与 PAGE_READWRITE 或 PAGE_WRITE 保护,允许读写的文件映射视图
FILE_MAP_READ --- 只读访问,目标一定是创建文件映射对象与PAGE_READWRITE 或 PAGE_READ 保护,允许将文件的只读视图映射
FILE_MAP_ALL_ACCESS --- 所有访问,目标一定是创建文件映射对象 PAGE_READWRITE 保护,允许读写的文件映射视图
FILE_MAP_COPY --- 即写即拷访问,目标一定是创建文件映射对象 PAGE_WRITECOPY 保护,允许即写即拷的文件映射视图
dwFileOffsetHigh 和 dwFileOffsetLow 偏移量指从什么位置开始和本进程地址空间建立映射,填0表示从硬盘文件一开始的位置和本进程地址空间建立映射
并且 dwFileOffsetHigh 和 dwFileOffsetLow 合成的偏移量,必须是区域粒度的整数倍(区域一般是64K 或者 是64K的整数倍 64k == 64*1024 == 65536)
偏移量是确定从什么位置开始映射,最后一个参赛是映射的大小
只要硬盘上的文件和本进程地址空间建立映射了,我们才能像操作内存一样操作硬盘上的文件;
4> 使用内存(实际是写入内存映射文件)
5> 将硬盘文件和本进程地址空间解除映射
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress //和本进程地址空间建立映射的内存的首地址
);
6> 关闭内存映射文件 : 关闭文件: 其中包括文件、文件映射、进程、线程、安全和同步对象等
CloseHandle() 内存映射文件一旦关闭,在内核中就没有了内存映射文件结构了
原型:
BOOL CloseHandle(
HANDLE hObject //内存映射文件句柄
);
7> 关闭文件 : 关闭文件: 其中包括文件、文件映射、进程、线程、安全和同步对象等
CloseHandle()
原型:
BOOL CloseHandle(
HANDLE hObject //硬盘文件句柄
);
进程间通信就是让多线程在同一内核里汇合使用同一内核结构,达到多个进程访问同一数据
Windows下通过内存映射文件实现进程间通信的步骤是:
OpenFileMapping() //通过 内存映射文件名 找到内存映射文件句柄, 拿到内存映射文件句柄就能在内核中找到内存映射文件的结构,
拿到这套结构就能知道硬盘上具体的文件
原型:
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, //访问方式
BOOL bInheritHandle, //继承标识,是否让当前进程的子进程继承 OpenFileMapping()函数的返回值 true(可以继承) false(不能继承)
LPCTSTR lpName //内存映射文件的名字
); 返回内存映射文件的句柄(有了内存映射文件句柄就可以找到内核中内存映射文件的结构,就能找到对应的硬盘文件)
访问方式:
FILE_MAP_WRITE --- 读写访问,目标一定是创建文件映射对象与 PAGE_READWRITE 或 PAGE_WRITE 保护,允许读写的文件映射视图
FILE_MAP_READ --- 只读访问,目标一定是创建文件映射对象与PAGE_READWRITE 或 PAGE_READ 保护,允许将文件的只读视图映射
FILE_MAP_ALL_ACCESS --- 所有访问,目标一定是创建文件映射对象 PAGE_READWRITE 保护,允许读写的文件映射视图
FILE_MAP_COPY --- 即写即拷访问,目标一定是创建文件映射对象 PAGE_WRITECOPY 保护,允许即写即拷的文件映射视图
MapViewOfFile() //将内存映射文件和本进程地址空间建立映射,这样就能想操作内存一样操作硬盘上的文件了
使用内存
UnmapViewOfFile() 将硬盘文件和本进程地址空间解除映射
如果是多进程间通信就不能调用 CloseHandle()函数,因为内存映射文件一旦关闭,在内核中就没有了内存映射文件结构了,其它进程就使用了内存映射文件了
什么是句柄:
Windows系统下的句柄类似于UC下面的文件描述符,是一个非负整数,Windows平台下每个进程启动后操作系统会在 内核中 为每个进程分配一张 句柄表,
句柄表里每一行存放一个地址,该地址是内核使用的地址是高2G的地址,每个地址指向一块内存;
句柄就是句柄表的行索引号(类似于UC的文件描述符,数组的下标)
如果只拿到句柄没有句柄表的地址,就没有办法通过句柄表修改句柄表里每一行对应内存里面的数据,只能通过Windows的API函数修改;
如果Windows系统让我们修改会把指针传给我们,如果Windows系统不让我们修改会把句柄给我们;
句柄类始于UC下的文件描述符,是一个索引号,拿到这个索引号就可以到句柄表里找到对应的数据该数据是一个地址,拿到这个地址就能找到那块保存数据的内存
Windows系统为什么不让我们修改句柄表每一行对应的内存
是因为一般通过句柄和句柄表找到的内存里面存放的数据都很复杂的,并不是单一类型的数据,这些数据都有自己的排布格式,
如果随意让程序员修改就有可能导致打乱整个数据的排布,所以如果要修改句柄表对应内存里的数据一般都是通过 Windows提供的API来修改
十九. Windows 进程
Windwos系统下进程是一个容器,包含程序执行需要的代码,数据,资源等等信息,Windows是多任务操作系统,可以同时执行多个进程;
进程开启只意味着开辟程序的初始内存,进程一起动就有进程映像,主线程开启才意味着程序执行, 进程好比是一辆车,主线程就是车里面的发动机,车是容器里面装发动机;
Windows进程的特点:
1> 每个进程都有自己的ID号
2> 每个进程都有自己的地址空间,进程之间一般是无法访问对方的地址空间
3> 每个进程都有自己的安全属性
4> 每个进程当中至少包含一个线程(主线程)
进程环境信息(进程上下文)
获取和释放 操作系统为我们配置的环境信息(环境信息就是一推的环境变量组合而成的,类似于UC的环境表)
获取操作系统为本进程配置的环境信息
LPVOID GetEnvironmentStrings(VOID); //类似于UC的 GetEnv()
返回环境块信息首地址
释放操作系统为本进程配置的环境信息
BOOL FreeEnvironmentSrings(
LPTSTR lpszEnvironmentBlock //环境块信息首地址
);
获取和设置环境变量:
获取:
GetEnvironmentVariable() 类似于UC的 GetEnv()
原型:
DWORD GetEnvironmentVariable(
LPCTSTR lpName, //想获取环境变量的值
LPTSTR lpBuffer, //用来保存环境变量缓冲区的首地址
DWORD nSize //缓冲区的大小
);
设置:
SetEnvironmentVariable() 类似于UC下的 SetEnv()
原型:
BOOL SetEnvironmentVariable(
LPCTSTR lpName, //环境变量的名字
LPCTSTR lpValue //环境变量的值
);
进程的信息:
获取当前进程的ID
DWORD GetCurrentProcessId(VOID); //返回当前进程的ID,类似于UC的getpid();
获取当前进程的句柄
HANDLE GetCurrentProcess(VOID) //返回当前进程的伪句柄(-1)因为句柄类似于文件描述符是非负整数,可以使用该伪句柄访问该进程的所有操作
注:
所有为 -1的句柄 除了 GetCurrentProcess()拿到的句柄外 其它的 -1的句柄都不能用
创建进程: 启动进程
平时在Windows下创建进程是用鼠标双击 .exe 的执行文件创建进程,是通过系统加载器加载到内存变成进程的
WinExec() -- 早期16位,目前已经废弃
ShellExecute() -- Shell操作
CreateProcess() -- 目前最多使用,类似于UC下的 vfok() + execl族
原型:
BOOL CreateProcess(
LPCTSTR lpApplicationName, //可执行应用程序带盘符的全路径名称
LPTSTR lpCommandLine, //命令行参数,此命令行参数的给要启动程序的入口函数的命令行参数,可以给""
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全属性 没用给 NULL 即可
LPSECURITY_ATTRIBUTES lpThreadAttributes, //主线程安全属性 没用给 NULL 即可
BOOL bInheritHandles, //继承句柄标识,true表示让本进程的子进程继承,false表示不让本进程的子进程继承
DWORD dwCreationFlags, //创建方式,给0即可表示立即启动创建进程
LPVOID lpEnvironment, //环境信息,给要启动的进程配置环境信息,填NULL表示启动进程使用当前程序环境信息
LPCTSTR lpCurrentDirectory, //当前目录,给要启动的进程设置一个工作目录,可以给NULL表示启动的进程和当前进程使用同一个工作目录
LPSTARTUPINFO lpStartupInfo, //返回启动进程的起始信息 STARTUPINFO是一个结构体
LPPROCESS_INFORMATION lpProcessInformation //返回启动进程的 句柄 和 启动进程的ID PROCESS_INFORMATION是一个结构体
PROCESS_INFORMATION pi = {0}; pi.dwThreadId 拿到ID , pi.hProcess 拿到句柄
);
继承标识:
继承标识所继承的是 句柄, true 表示让本进程的子进程继承要操作程序的句柄,false表示不让本进程的子进程继承要操作程序的句柄
PROCESS_INFORMATION结构体原型:
typedef struct_PROCESS_INFORMATION{
HANDLE hProcess; //新进程的句柄
HANDLE hThread; //主线程的句柄
DWORD dwProcessld; //返回一个全局进程标识符,该标识符用于标识一个进程,从进程被创建到终止,该值始终有效
DWORD dwThreadld; //返回一个全局线程标识符,该标识符用于标识一个线程,从线程被创建到终止,该值始终有效
}PROCESS_INFORMATION;
结束进程:
VOID ExitProcess( //只能结束本进程
UINT uExitCode //退出码给系统内核看,UC下面也有进程退出码,
);
BOOL TerminateProcess( //可以结束指定进程
HANDLE hProces, //要结束进程的句柄
UINT uExitCode //进程退出码
);
通过进程ID获取进程句柄
HANDLE OpenProcess(
DWORD dwDesiredAccess, //访问权限
BOOL bInheritHandle, //继承标识,true表示返回的句柄能被当前进程的子进程使用,false表示返回的句柄不能被当前程序的子进程使用
DWORD dwProcessId //要获取句柄的进程ID
); 返回进程句柄
关闭进程句柄:
CloseHandle() 关闭文件: 其中包括文件、文件映射、进程、线程、安全和同步对象等
原型:
BOOL CloseHandle(
HANDLE hObject //文件句柄,用在不同的地方功能不一样
);
注:
CloseHandle()用在进程/线程这里只是把 进程/线程 的句柄置成 -1 ,仅仅只是 进程/线程 的句柄不能用其它该执行还是在执行
CloseHandle()只能 关闭进程/线程 并不能 结束进程/线程,只有 ExitProcess() 或 TerminateProcess() 函数才能结束进程/线程
进程间的等候
等候可等候的句柄有信号
可等候句柄:
如果说一个句柄是可等候句柄,那么该句柄必须具备两种状态
1> 有信号状态
2> 无信号状态
到目前为止只有进程句柄才是可等候句柄,以前学的全是不可等候句柄, 进程在执行过程中进程句柄是无信号状态,当进程结束进程句柄是有信号状态
可以使用 WaitForSingleObject()函数等候可等候句柄,类似于UC下的 wait()
原型:
DWORD WaitForSingleObject(
HANDLE hHandle, //可等候句柄
DWORD dwMilliseconds //最多等候时间(毫秒为单位), 如果填 INFINITE 表示等候时间无限大
);
WaitForSingleObject()是阻塞函数,等候句柄的信号,只在可等候句柄有信号或超出等候时间,才返回,结束等候
二十. Windows线程
Windows线程是可以执行的代码的实例UC也一样,真正被执行的是线程并非进程,进程只负责申请程序的初始内存,只有主线程执行才意味着程序真正被执行;
系统是以线程为单位调度程序,CPU把自己的时间划分为时间片根据时间片的不同依次执行不同的线程,但真正决定执行哪个线程是操作系统决定的并非CPU
CPU只负责执行,操作系统扔什么线程给CPU,CPU就执行什么线程,
操作系统根据自己的算法确定把哪个线程扔给CPU执行,这个算法大概会考虑这些情况: 线程优先级,线程中断与恢复,奖惩措施(CPU两种消耗形式IO消耗/CPU消耗) 等等
一个程序当中可以有多个线程,实现多任务的处理;
Windows线程的特点:
1> 线程都具有1个ID
2> 线程具有自己的安全属性
3> 每个线程都具有自己的栈内存
4> 每个线程都具有自己的寄存器信息
进程多任务和线程多任务:
进程多任务是每个进程都使用私有地址空间,这就是为什么进程之间如果不使用特殊手段无法访问对方的地址空间
线程多任务是进程内的多个线程使用同一个地址空间
线程的调度:
将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程,根据时间片执行不同的线程是操作系统决定的
线程轮询: 线程A --> 线程B --> 线程A ......
注:
不管是线程还是进程在CPU的级别上都是串行,没有并行这一说,我们感觉上是并行那是因为CPU执行速度过快导致的,
双核/多核 CPU 也是按照上面执行方式来,只是如果比较空闲的时候1个CPU能执行的其它CPU就睡眠,当1个CPU忙不过来的时候其它CPU才会被使用起来,
并且CPU之间是有关联的并不是单独存在,如果是单独存在那就是两台电脑并不是双核/多核,
CPU之间还有主次CPU,主CPU忙不过来唤醒次CPU帮忙,主CPU会把某部分操作交给次CPU然后主CPU轮询线程等次CPU执行完把结果返回给主CPU,主CPU再做其它操作 ...
线程的使用:
1> 定义线程处理函数
DWORD WINAPI ThreadProc(
LPVOID lpParameter //创建线程时,传递给线程的参数
);
2> 创建子线程: 主线程就是启动入口函数的线程, UC下面创建线程 pthread_create() 函数创建子线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性,没用 给 NULL 即可
SIZE_T dwStackSize, //线程栈的大小,默认为 1M, 低于1M 按 1M来
LPTHREAD_START_ROUTINE lpStartAddress, //线程处理函数的函数地址,只要见着某某处理函数一般都是我们定义系统调用
LPVOID lpParameter, //传递给线程处理函数的参数
DWORD dwCreationFlags, //线程的创建方式,填0表示创建之后线程立即执行,填CREATE_SUSPENDED表示创建之后线程处于挂起(休眠)状态
LPDWORD lpThreadId //创建成功,返回线程的ID
); 创建成功,返回线程句柄
3> 结束线程
结束指定线程
BOOL TerminateThread(
HANDLE hThread, //要结束线程的句柄
DWORD dwExitCode //线程的退出码
);
结束函数当前的线程
VOID ExitThread(
DWORD dwExitCode //线程退的出码
);
4> 关闭线程句柄
CloseHandle()
原型:
BOOL CloseHandle(
HANDLE hObject //线程句柄,用在不同的地方功能不一样
);
注:
CloseHandle()用在进程/线程这里只是把 进程/线程 的句柄置成 -1 ,仅仅只是 进程/线程 的句柄不能用其它该执行还是在执行
CloseHandle()只能 关闭进程/线程 并不能结束进程/线程,能结束线程的只有 TerminateThread() 和 ExitThread() 两个函数
5> 切换线程的状态: 线程的挂起和执行
挂起:
DWORD SuspendThread(
HANDLE hThread //线程句柄
);
执行:
DWORD ResumeThread(
HANDLE hThread //线程句柄
);
6> 线程的信息
DWORD GetCurrentThreadId(VOID); //获取当前线程的ID
HANDLE GetCurrentThread(VOID); //获取当前线程的句柄
通过指定的线程ID,获取指定的线程句柄
HANDLE OpenThread(
DWORD dwDesiredAccess, //访问权限
BOOL bInheritHandle, //继承标识,true 表示返回的句柄能被当前进程的子进程所使用,false 表示返回的句柄不能被当前进程的子进程所使用
DWORD dwThreadId //要获取句柄的线程ID
);
凡是继承标识都是两种值: true 表示返回的句柄能被当前进程的子进程所使用,false 表示返回的句柄不能被当前进程的子进程所使用
扩:
临界资源和临界区
临界资源:
被多个进程/线程所共享,但一次仅允许一个进程/线程使用的资源就叫临界资源
如:
两个线程同时往dos窗口里面打印东西,两个线程共享dos窗口,但每次只能一个线程占用dos窗口打印东西,这个dos窗口就是临界资源
临界区: 类似于 互斥锁 的东西
每个进程/线程中访问临界资源的那段程序称为临界区
进程进入临界区的调度原则是:
1> 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入
2> 任何时候,处于临界区内的进程不可多于一个,如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待
3> 进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区
4> 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现"忙等"现象
多线程之间为什么会出现交叉执行 printf() 的现象:
当 线程A 执行printf()输出时,如果线程A的执行时间结束,系统会将线程A的相关信息(栈,寄存器)压栈保护,
同时将之前压栈保护的线程B相关的信息恢复,然后执行线程B,线程B继续输出字符,由于线程A正输出字符,线程B会继续输出,画面字符会产生混乱
线程的同步技术
原子锁 \
临界区(段) | 实现线程之间加锁关系
互斥 /
事件 \
信号量 | 实现线程之间同步,多个线程之间协调工作
可等候定时器 /
等候函数
WaitForSingleObject(); //等候单个可等候句柄有信号,类似于UC下的 wait()
原型:
DWORD WaitForSingleObject(
HANDLE hHandle, //可等候句柄
DWORD dwMilliseconds //最多等候时间(毫秒为单位), 如果填 INFINITE 表示等候时间无限大
);
WaitForSingleObject()是阻塞函数,等候句柄的信号,只在可等候句柄有信号或超出等候时间,才返回,结束等候
WaitForMultipleObjects();//等候多个可等候句柄有信号,类似于UC下的 waitpid()
原型:
DWORD WaitForMultipleObjects(
DWORD nCount, //句柄数量,一次等待多少个可等候句柄有信号
CONST HANDLE* lpHandles, //可等候句柄的数组的首地址
BOOL bWaitAll, //等候方式,true 表示所有句柄都有信号,才结束等候, fasle 表示只要任意一个句柄有信号,就结束等候
DWORD dwMilliseconds //等候时间以毫秒为单位, INFINITE 表示等候时间无限大
);
返回值:
如果因时间到了而返回,那返回值是: WAIT_TIMEOUT
如果 等候方式(bWaitAll)是TRUE,那么返回值是: WAIT_OBJECT_0
如果 等候方式(bWaitAll)是FALSE,那么返回值是: 返回值减去 WAIT_OBJECT_0,也就是有信号句柄在句柄数组里面的下标
如果函数失败,则返回: WAIT_FAILD, 这时候你可以用GetLastError()找出失败原因
注:
WAIT_OBJECT_0 是一个宏 WAIT_OBJECT_0 == ((STATUS_WAIT_0 ) + 0 ) == ((DWORD )0x00000000L) == 0
目前为0但为了程序的健壮还是要减 WAIT_OBJECT_0 目的是防止以后微软修改 WAIT_OBJECT_0 的值
线程/进程 句柄是可等候句柄, 线程/进程 在执行过程中是没有信号,线程/进程 一旦执行结束 线程/进程 句柄有信号
原子锁
相关问题: 多个线程对同一个数据进行原子操作,会产生结果丢失
比如:
执行 ++ 运算时,当线程A执行 sum++时,如果线程切换时间正好是在线程A将值保存到 sum 之前,
线程切换到之前压栈保护的线程B继续执行 sum++,那么当线程A再次被切换回来之后,会将原来线程A保存的值保存到 sum上,那么线程B进程的加法操作则被覆盖了
原子锁函数:
InterlockedIncrement(); // 对 ++ 运算符进行 原子锁
原型:
LONG InterlockedIncrement(
LPLONG lpAddend //要做 ++ 操作变量的地址
);
InterlockedDecrement(); //对 -- 运算符进行 原子锁
InterlockedCompareExchange
InterlockedExchange
原子锁不好用,因为每个运算符对应一个原子锁函数,程序员需要记大量的原子锁函数,但原子锁是线程同步技术里面效率是最高的;
原子锁的实现:
直接对数据所在的内存进行操作,并且在任何一个瞬间只能有一个线程访问该内存,也就是对数据所在的内存进行加锁
临界区
原子锁能实现的临界区都能实现,而临界区能实现的原子锁未必都能实现
临界资源: 被多个线程/进程共享,但每次只能有一个进程/线程访问的资源叫临界资源
临界区: 包含访问临界资源的那段代码就叫临界区,并且在同一时间内只能有一个线程/进程进入临界区,其它进程/线程只能等待进入临界区的进程退出临界区
相关问题: printf()输出混乱,多线程情况下同时使用一段代码,临界区可以锁定一段代码,防止多个线程同时使用该段代码
使用临界区:
1> 初始化一个临界区
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区变量, CRITICAL_SECTION 结构体变量的地址
);
2> 进入临界区
添加到被锁定的代码之前
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//临界区变量的地址
);
3> 离开临界区
添加到被锁定的代码之后
VOID LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区变量的地址
);
4> 删除临界区
VOID DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区变量的地址
);
临界区 和 原子锁 的区别
原子锁 --- 只能锁定单挑指令,一般就是些操作符
临界区 --- 可以锁定单行指令,也可以锁定多行指令
互斥(Mutex) 和 UC的互斥量有区别
临界区能决绝的互斥都能解决,但是互斥能解决的临界区未必都能解决
相关的问题: 多线程下代码 或 资源的共享使用
使用互斥:
1> 创建互斥
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性,没用给 NULL 即可
BOOL bInitialOwner, //初始的使用者,是否允许创建互斥的线程就拥有互斥, true 表示允许, false 表示不允许
LPCTSTR lpName //给互斥命名,可以不取
);创建成功返回 互斥句柄
互斥句柄也是可等候句柄,有两种状态(有信号 和 无信号),
当任何一个线程都不拥有互斥的时候互斥句柄有信号,但如果某一个线程拥有互斥的时候互斥句柄没信号
互斥是谁先等谁得到互斥,互斥在没有线程得到的时候互斥句柄有信号,一旦某个线程得到了互斥,互斥句柄就没有信号了
2> 等候互斥
WaitForSingleObject()/WaitForMultipleObjects() 互斥的等候遵循谁先等候谁先获取互斥
如果互斥句柄有信号,通过 等候互斥 的函数把互斥句柄置为无信号 并且 等候互斥函数解除阻塞
如果互斥句柄无信号,等候互斥 的函数 阻塞
WaitForSingleObject(); //等候单个可等候句柄有信号,类似于UC下的 wait()
原型:
DWORD WaitForSingleObject(
HANDLE hHandle, //可等候句柄
DWORD dwMilliseconds //最多等候时间(毫秒为单位), 如果填 INFINITE 表示等候时间无限大
);
WaitForSingleObject()是阻塞函数,等候句柄的信号,只在可等候句柄有信号或超出等候时间,才返回,结束等候
WaitForMultipleObjects();//等候多个可等候句柄有信号,类似于UC下的 waitpid()
原型:
DWORD WaitForMultipleObjects(
DWORD nCount, //句柄数量,一次等待多少个可等候句柄有信号
CONST HANDLE* lpHandles, //可等候句柄的数组的首地址
BOOL bWaitAll, //等候方式,true 表示所有可等候句柄都有信号才结束等候, fasle 表示只要任意一个句柄有信号,就结束等候
DWORD dwMilliseconds //等候时间以毫秒为单位, INFINITE 表示等候时间无限大
);
返回值:
如果因时间到了而返回,那返回值是: WAIT_TIMEOUT
如果 等候方式(bWaitAll)是TRUE,那么返回值是: WAIT_OBJECT_0
如果 等候方式(bWaitAll)是FALSE,那么返回值是: 返回值减去 WAIT_OBJECT_0,也就是有信号句柄在句柄数组里面的下标
如果函数失败,则返回: WAIT_FAILD, 这时候你可以用GetLastError()找出失败原因
注:
WAIT_OBJECT_0 是一个宏 WAIT_OBJECT_0 == ((STATUS_WAIT_0 ) + 0 ) == ((DWORD )0x00000000L) == 0
目前为0但为了程序的健壮还是要减 WAIT_OBJECT_0 目的是防止以后微软修改 WAIT_OBJECT_0 的值
3> 释放互斥
BOOL ReleaseMutex(
HANDLE hMutex //互斥句柄
);
4> 关闭互斥句柄
CloseHandle(); //把互斥句柄置为 -1
互斥 和 临界区的区别
临界区 ---- 用户态,执行效率高,只能在同一个进程中使用
互斥 ---- 内核态,执行效率低,可以通过命名的方式跨进程使用
事件
事件相关问题: 程序之间的通知的问题
事件的使用:
1> 创建事件
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,没用给 NULL 即可
BOOL bManualReset, //事件复位方式(从有信号状态变成无信号状态就叫事件复位), true 手动,false自动
BOOL bInitialState, //事件初始状态, true有信号,false无信号
LPCTSTR lpName //事件命名
); 创建成功返回 事件句柄
事件句柄也是可等候句柄,两种状态,什么时候有信号什么时候没信号程序员决定
复位方式如果是自动复位,其实是在 WaitForSingleObject()/WaitForMultipleObjects()函数内部调用了 ResetEvent()函数
其内部大概流程是:
WaitForSingleObject(g_hEvent,INFINITE){
.... //除了可以等待事件句柄 还可以等待其它可等待的句柄
阻塞代码 //是一个阻塞函数
解除阻塞说明已经等待到了可等待句柄有信号了,然后判断 g_hEvent是否为事件句柄,通过 g_hEvent 句柄找到的那块内存里面保存的数据判断
if(是事件句柄){
通过事件句柄,查看事件的复位方式
if(手动){
//什么都不做
}else if(自动){
ResetEvent(g_hEvent);
}
}
}
2> 等候事件
事件句柄也是可等候句柄,两种状态,什么时候有信号什么时候没信号程序员决定
WaitForSingleObject(); //等候单个可等候句柄有信号,类似于UC下的 wait()
原型:
DWORD WaitForSingleObject(
HANDLE hHandle, //可等候句柄
DWORD dwMilliseconds //最多等候时间(毫秒为单位), 如果填 INFINITE 表示等候时间无限大
);
WaitForSingleObject()是阻塞函数,等候句柄的信号,只在可等候句柄有信号或超出等候时间,才返回,结束等候
WaitForMultipleObjects();//等候多个可等候句柄有信号,类似于UC下的 waitpid()
原型:
DWORD WaitForMultipleObjects(
DWORD nCount, //句柄数量,一次等待多少个可等候句柄有信号
CONST HANDLE* lpHandles, //可等候句柄的数组的首地址
BOOL bWaitAll, //等候方式,true 表示所有可等候句柄都有信号才结束等候, fasle 表示只要任意一个句柄有信号,就结束等候
DWORD dwMilliseconds //等候时间以毫秒为单位, INFINITE 表示等候时间无限大
);
返回值:
如果因时间到了而返回,那返回值是: WAIT_TIMEOUT
如果 等候方式(bWaitAll)是TRUE,那么返回值是: WAIT_OBJECT_0
如果 等候方式(bWaitAll)是FALSE,那么返回值是: 返回值减去 WAIT_OBJECT_0,也就是有信号句柄在句柄数组里面的下标
如果函数失败,则返回: WAIT_FAILD, 这时候你可以用GetLastError()找出失败原因
注:
WAIT_OBJECT_0 是一个宏 WAIT_OBJECT_0 == ((STATUS_WAIT_0 ) + 0 ) == ((DWORD )0x00000000L) == 0
目前为0但为了程序的健壮还是要减 WAIT_OBJECT_0 目的是防止以后微软修改 WAIT_OBJECT_0 的值
3> 触发事件
将事件设置成有信号状态
BOOL SetEvent(
HANDLE hEvent //事件句柄
);
将事件设置成无信号状态(复位事件)
BOOL ResetEvent(
HANDLE hEvent //事件句柄
);
4> 关闭事件
CloseHandle(
HANDLE hEvent //事件句柄
);
小心事件的死锁
信号量
相关的问题: 类似于事件,解决通知的相关问题,但是可以提供一个计数器,可以设置次数 功能和 UC 的信号量差不多,但具体实现有差别
信号量的使用:
1> 创建信号量
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //安全属性,没用给NULL即可
LONG lInitialCount, //初始化信号量数量,依次递减
LONG lMaximumCount, //信号量的最大值,用来限制重新设置信号量计数值的最大上限
LPCTSTR lpName //给信号量命名,可以为NULL
);创建成功返回信号量句柄
信号量句柄也是可等候句柄,当信号量数量不为0的时候信号量句柄有信号,一旦信号量数量等于0的时候信号量句柄没信号
2> 等候信号量
在调用 WaitForSingleObject()/WaitForMultipleObjects()等待函数时每等候通过一次,信号量的信号个数减1,直到信号量个数为0就阻塞
信号量句柄也是可等候句柄,当信号量数量不为0的时候信号量句柄有信号,一旦信号量数量等于0的时候信号量句柄没信号
WaitForSingleObject(); //等候单个可等候句柄有信号,类似于UC下的 wait()
原型:
DWORD WaitForSingleObject(
HANDLE hHandle, //可等候句柄
DWORD dwMilliseconds //最多等候时间(毫秒为单位), 如果填 INFINITE 表示等候时间无限大
);
WaitForSingleObject()是阻塞函数,等候句柄的信号,只在可等候句柄有信号或超出等候时间,才返回,结束等候
WaitForMultipleObjects();//等候多个可等候句柄有信号,类似于UC下的 waitpid()
原型:
DWORD WaitForMultipleObjects(
DWORD nCount, //句柄数量,一次等待多少个可等候句柄有信号
CONST HANDLE* lpHandles, //可等候句柄的数组的首地址
BOOL bWaitAll, //等候方式,true 表示所有可等候句柄都有信号才结束等候, fasle 表示只要任意一个句柄有信号,就结束等候
DWORD dwMilliseconds //等候时间以毫秒为单位, INFINITE 表示等候时间无限大
);
返回值:
如果因时间到了而返回,那返回值是: WAIT_TIMEOUT
如果 等候方式(bWaitAll)是TRUE,那么返回值是: WAIT_OBJECT_0
如果 等候方式(bWaitAll)是FALSE,那么返回值是: 返回值减去 WAIT_OBJECT_0,也就是有信号句柄在句柄数组里面的下标
如果函数失败,则返回: WAIT_FAILD, 这时候你可以用GetLastError()找出失败原因
注:
WAIT_OBJECT_0 是一个宏 WAIT_OBJECT_0 == ((STATUS_WAIT_0 ) + 0 ) == ((DWORD )0x00000000L) == 0
目前为0但为了程序的健壮还是要减 WAIT_OBJECT_0 目的是防止以后微软修改 WAIT_OBJECT_0 的值
3> 释放信号量(给信号量重新设置信号量计数值)
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //信号量句柄
LONG lReleaseCount, //释放数量(重新设置信号量计数值),但不能大于创建信号量时设置的信号量最大值
LPLONG lpPreviousCount //返回释放前原来信号量的数量,可以为NULL
);
4> 关闭信号量句柄
CloseHandle(
HANDLE hSemaphore //信号量句柄
);
到目前为止接触到的可等候句柄有那些: 可以通过 WaitForSingleObject()/WaitForMultipleObjects() 等待函数等候可等候句柄, 有信号才返回否则阻塞
进程句柄 --- 进程执行过程中进程句柄没有信号,一旦进程结束进程句柄有信号
线程句柄 --- 线程执行过程中线程句柄没有信号,一旦线程结束线程句柄有信号
互斥句柄 --- 互斥没有被拥有互斥句柄有信号,一旦被某个进程/线程拥有互斥句柄就没信号
事件句柄 --- 事件句柄是程序员调用函数设置事件句柄有无信号: SetEvent()设置事件句柄有信号 ResetEvent()设置事件句柄无信号
信号量句柄 --- 信号量计数值不为0信号量句柄有信号,信号量计数值等于0信号量句柄无信号
------------------------------------------------------------------ MFC ------------------------------------------------------------------------
二十一. MFC 的概念和作用
1> 什么是MFC
MFC 全称为 Microsoft Foundation Class Library 一般称为 微软基础类库
从硬盘的存在形式来说 MFC 就是一个库(静态库/动态库)
从原理来说 MFC 还是一个程序框架,把该封装的都封装了包括程序的流程都封装了(想知道程序的流程就去看程序的入口函数里面就是整个程序的流程)
MFC 和 Win32的关系
包含关系, MFC 是一个库,库里面封装着上千个类和一些全局函数,这上千个类里面封装着 Win32 的上万个 函数
Win32是基于系统框架编程,MFC是基于程序框架
2> 几个重要的头文件
afx.h ----- 包含了绝大部分 MFC 库中类的声明
afxwin.h ----- 包含了 afx.h 和 windows.h
afxext.h ----- 包含了 关于扩展窗口类的声明 例如: 工具栏,状态栏 等等 大到全屏界面小到控件都算窗口,工具栏和状态栏并非传统意义上的窗口
3> MFC应用程序类型
Win32分为:
控制台应用程序类
窗口应用程序类
库程序类(动态库/静态库 程序)
Win32里面只能调用Win32的库里封装的API函数
kernel32.dll --- 提供了核心的 API, 例如进程,线程,内存管理等;
user32.dll --- 提供了窗口,消息等API
gdi32.dll --- 绘图相关的API
头文件
windows.h --- 所有windows头文件的集合
winbase.h --- kernel32的API
wingdi.h --- gdi32的API
winuser.h --- user32的API
MFC 可以调用 MFC的库也可以调用 Win32的库
使用MFC库制作自己的控制台程序:
相比 Win32 控制台程序 MFC 的控制台程序 多了一个全局变量 CWinApp theApp
入口函数不同于 Win32的入口函数
使用MFC库制作自己的静态库程序
使用MFC库制作自己的动态库程序
可以使用 静态/动态 MFC 库制作自己的规则动态库程序,也可以制作扩展动态库
规则动态库 和 扩展动态库的区别:
规则动态库可以被任何程序调用 如: java程序 C#程序 VB程序 ..., 直接使用库里的功能不能添加新的功能
扩展动态库只能被支持 MFC 的程序调用,在继承库的基础上添加新的功能
使用 MFC 库制作自己的窗口程序
单文档视图框架程序(以前Win32讲的窗口只是单文档视图构架程序)
由下面4个类构成:
CFrameWnd --- 框架窗口类,该类的对象就代表程序运行起来的主窗口(主框架窗口),该类封装关于主框架窗口的操作 如: 改变窗口大小 位置 宽高 ...
CWinApp --- 应用程序类,该类的对象就代表程序的流程,该类的成员函数 负责管理程序的流程
CDocument --- 文档类,该类的对象代表数据,该类封装了关于数据的操作(提取/转换/存储 数据)
CView --- 视图窗口类,该类的对象代表视图窗口,该类的成员函数封装了关于视图窗口的操作 如: 改变 大小/颜色/显示数据 ...
视图窗口是覆盖在主窗口的窗口客户区上的一个没有标题栏也没有边框的窗口(一个窗口有没有标题栏和边框是通过窗口的风格决定的)
多文档视图构架程序(VS这种界面就的多文档视图构架程序)
由下面5个类构成:
CMDIChildWnd --- 子框架窗口类,封装了关于子框架的操作
CMDIFrameWnd --- 主框架窗口类,封装了关于主框架窗口的操作
CWinApp --- 应用程序类,该类的对象就代表程序的流程,该类的成员函数 负责管理程序的流程
CDocument --- 文档类,该类的对象代表数据,该类封装了关于数据的操作(提取/转换/存储 数据)
CView --- 视图窗口类,该类的对象代表视图窗口,该类的成员函数封装了关于视图窗口的操作 如: 改变 大小/颜色/显示数据 ...
视图窗口是覆盖在主窗口的窗口客户区上的一个没有标题栏也没有边框的窗口(一个窗口有没有标题栏和边框是通过窗口的风格决定的)
多文档视图关系:
视图窗口 的父窗口是 子窗口, 子窗口的父窗口是 覆盖在主窗口的窗口客户区上的没有边框没有标题栏的窗口, 最后是主窗口
对话框构架程序(出现的第一个界面就是一个对话框)
由下面2个类构成:
CWinApp --- 应用程序类,负责管理程序的流程
CDialog --- 对话框窗口类,封装了关于对话框窗口的操作
Win32都能实现以上3种窗口程序
4> MFC库中类概述
CObject --- MFC库中绝大部分类的最基类,封装一些最基本的比较通用的东西,细节的东西都是通过子类去封装
CObject封装了3个机制:
运行时类信息机制 --- 程序运行过程中,可以知道对象是不是某个类的对象
动态创建机制 --- 在不知道类名的情况下能把对象创建出来
序列化机制 --- 读写硬盘文件
CCmdTarget --- 消息映射机制的最基类,只有跟它有关系的类才能处理消息
CWinThread/CWinApp --- 应用程序类,主要负责程序的流程, CWinApp类 继承自 CWinThread类
CDocument 及其子类 --- 文档类,负责管理各种数据
Exceptions --- 异常处理类,封装了 MFC 中各种异常情况的处理
CFile 及其子类 --- 文件操作类,封装了关于各种文件的读写等操作
CWnd --- 所有窗口类的最基类
Frame Windows/CFrameWnd --- 框架窗口类,该类对象可以代表主窗口,封装了关于各种框架窗口的操作
Dialog Boxes --- 对话框窗口类,封装了关于各种对话框窗口的操作
Views --- 视图窗口类,封装了关于各种视图窗口的操作
Controls --- 控件窗口类,封装了关于各种控件窗口的操作
CDC 及其子类 --- 封装了各种绘图设备,以及绘图函数
CGdiObject 及其子类 --- 封装了关于各种 GDI 绘图对象的操作
CArrary/CMap/CList及其子类 --- 封装了 C++ 语法中相应的数据结构
非CObject类 --- 对各种结构进行了封装 例如: CPoint(对Point结构体进行封装)/CRect(对矩形区域进行封装)/CTime(对时间结构进行封装)/CString(对字符串封装)
注: 在 MFC 程序里面程序员能见到只有3类函数
1> 在MFC中见到 以 "Afx" 开头的函数就可以确定该函数是 MFC 库中封装的全局函数
2> 在MFC中见到 以 "::" 开头的函数就可以确定该函数是 Win32的 API函数
3> 除去上面两种情况和类的静态成员函数,其它的函数都是类的成员函数,都可以在函数前面补上 this 指针
断点调试:
F9 打断点, F5调试执行(如果有多个断点 F5可以快速执行断点中的代码停到下一断点上), F11进函数内部,F10执行下一行代码
ASSERT() 是断言判断要求 ASSERT后面 "()" 里的条件必须成立,如果"()"里的条件不成立,程序执行到此就报错了不再往下执行了
二十二. 第一个MFC程序
怎么把Win32程序改成MFC程序
1> 配置环境
删除入口函数 WinMain()
因为 入口函数里面是整个程序的流程,MFC要接管程序的流程所以它已经封装了一个入口函数,因此如果我们要使用MFC就得把自己的入口函数删掉
将头文件 "stdafx.h" 内的
项目->属性->配置属性->常规->MFC的使用->在静态库中使用MFC/在动态库中使用MFC
注:
全局变量的执行是在入口函数之前执行的
在C++语法中是先执行基类构造再执行子类构造,析构是先执行子类析构再执行基类析构
怎样达到先执行基类构造在执行子类构造的呢:
是通过 "{" 调用基类的构造函数的
"{}"的功能:
"{" --- 调用基类的构造函数
"}" --- 调用子类的析构函数,在UC下"}"还要做很多事情
2> 书写代码
从 CFrameWnd 类派生了一个自己的框架窗口类 CMyFrameWnd
从 CWinApp 类派生了一个自己的应用程序类 CMyWinApp,并在类中重写了父类的成员虚函数 InitInstance
程序的执行过程(程序启动机制): (MFC第一大机制)
1> 构造 theApp(爆破点)
在父类的构造函数中做了3件事:
将 &theApp 保存到 当前程序线程状态信息中
将 &theApp 保存到 当前程序模块状态信息中
AfxGetApp()/AfxGetThread() 两个MFC的全局函数 返回 &theApp
2> 进入入口函数( WinMain() 主要看程序的流程 )
在MFC中如果想看 WinMain()入口函数可以利用 调用堆栈查看创建窗口和显示窗口的调用关系,因为创建窗口和显示窗口都是在 入口函数中调用的
获取 theApp 对象地址
利用 theApp 调用应用程序类成员虚函数 InitApplication(初始化)
利用 theApp 调用应用程序类成员虚函数 InitInstance(创建并显示窗口)
利用 theApp 调用应用程序类成员虚函数 Run(消息循环)
在Run()函数中 如果没有消息 利用 theApp调用应用程序类成员虚函数 OnIdle(空闲处理)
如果程序退出 利用 theApp调用应用程序类成员虚函数 ExitInstance(做善后处理)
CWinApp类的成员虚函数:
InitApplication --- 初始化函数
InitInstance --- 创建显示窗口
Run --- 内部有消息循环/消息处理
OnIdle --- 空闲处理 virtual BOOL OnIdle(long lCount); // lcount 返回出现了多少此空闲
ExitInstance --- 最后释放资源
注: 以下全是MFC的全局函数直接调用即可
AfxGetInstanceHandle() --- 该全局函数可以拿到 WinMain()的第一个参数,当前程序实例句柄(能找到本进程所在的内存)
AfxGetModuleState() --- 该全局函数可以获取 当前程序模块状态信息
AfxGetModuleThreadState() --- 该全局函数可以获取 当前程序线程状态信息
窗口创建机制 (MFC第二大机制)
1> 加载菜单( LoadMenu() )
2> 调用 CreateEx()函数,设计并注册窗口类,以及创建窗口
在CreateEx()函数中调用 PreCreateWindow()函数设计并注册窗口类
WNDCLASS wndcls;
...
wndcls.lpfnWndProc = DefWindowProc; //DefWindowProc 是窗口处理函数函数名
...
并调用 Win32 API 函数 ::RegisterClass()注册一个窗口类
调用 AfxHookWindowCreate()函数
利用Win32 API 函数 SetWindowsHookEx() 在程序中埋下一个类型为 WH_CBT 的钩子
将自己 new 的框架类对象地址(pFrame)保存到当前程序线程信息中
调用 Win32 API 函数 ::CreateWindowEx()创建窗口,此函数一旦执行成功 WM_CREATE 立即出现, 只要 WM_CREATE 消息一出现就被之前埋下的钩子钩走
钩到钩子处理函数里面去
3> 钩子处理函数
将窗口句柄 和 自己 new 的框架类对象(pFrame)建立一对一的绑定关系,窗口句柄能代表窗口,它俩绑定后 pFrame 对象才能代表窗口
pFrame->m_hWnd = hWnd;
pMap->m_permanentMap[(LPVOID)hWnd] = pFrame;
利用 Win32 的 API 函数 SetWindowLong()将窗口处理函数更改为 AfxWndProc()(这才是真正的窗口处理函数)
消息的处理
1> 当消息产生后进入 AfxWndPrpc()函数处理
2> 找到和 窗口句柄(hWnd) 绑定在一起的框架类对象地址(pFrame) 只有通过对象才能调用类中的成员
3> 利用 pFrame 调用框架类的成员虚函数 WindowProc() 完成消息的处理
扩:
钩子函数,要使用钩子函数首先要埋入钩子, 钩子是 Win32的API函数不属于MFC
HHOOK SetWindowsHookEx(
int idHook, //钩子类型,埋的钩子给什么感兴趣 如: WH_CBT
HOOKPROC lpfn, //钩子处理函数,我们定义系统调用
HINSTANCE hMod, //应用程序实例句柄,可以勾取指定进程的消息,NULL表示勾取所有进程的消息
DWORD dwThreadld //线程ID,可以勾取指定线程的消息,0表示勾取所有线程的消息
);
钩子处理函数原型:
LRESULT CALLBACK CBTProc(
int nCode, //钩子码 如: 如果钩子的类型是 WH_CBT 那么钩子码就是 HCBT_CREATEWND
WPARAM wParam, //消息的附带信息,如: 如果是 HW_CBT 消息 wParam 就是刚刚创建成功的窗口的窗口句柄
LPARAM lParam //消息的附带信息
);
向窗口附加缓冲区写数据 还能更改窗口处理函数
LONG SetWindowLong(
HWND hWnd, //窗口句柄,更改该窗口的窗口处理函数 或是 要往该窗口缓冲区存数据
int nlndex, //如果填写的是 GWL_WNDPROC 就是更改窗口处理函数,如果是填写字节索引号就是往窗口附加缓冲区写数据
LONG dwNewLong //新的窗口处理函数 或者是 要存入窗口缓冲区的数据
);
程序员设计了一个类,根据这个类构造出来的对象,这个对象并不能代表具体的事务,要代表具体的事务必须和该事务的句柄绑定到一起才可以
在Win32中代表一个东西是用句柄去代表的,在MFC中是用对象代表一个东西,但这个对象必须和这个东西的句柄绑定了才能代表这个东西
句柄类似于文件描述符,数组下标 是一个非负整数,表示句柄表里的偏移量,通过句柄可以快速找到句柄表里保存的地址,这个地址是内核的地址
MFC的6大机制:
1> 程序启动机制 ---- 程序的流程(CWinApp类负责)
2> 窗口创建机制 ---- 窗口创建流程( CWinApp类 + CFrameWnd类)
3> 消息映射机制 ---- 不重写 消息处理函数的前提下 处理消息 (必须继承自 CCmdTarget 类)
二十三. MFC的消息映射机制 (MFC第三大机制)
MFC的消息映射机制是指在不重写消息处理函数的前提下依然可以处理消息
原理就是弄一个静态结构体变量,里面有2个成员第一个成员保存其基类的静态结构体变量,第二个成员就是一个静态结构体数组,数组里面保存了消息ID和对应的处理函数
这样就形成一个单向链表,依次遍历就能找到和消息ID对应的消息处理函数
消息映射机制的使用:
如果一个类想利用MFC的消息映射机制处理消息(也就是不重写 WindowProc() 就想处理消息) 则必须具备如下条件:
1> 该类必须继承自 CCmdTarget 类
2> 类内必须添加声明宏: DECLARE_MESSAGE_MAP()
3> 类外必须添加实现宏
BEGIN_MESSAGE_MAP(本类类名,父类类名)
//在此之间使用 ON_MESSAGE() 把要被处理的消息ID 和 该消息对应的消息处理函数 放到 本类静态结构体数组元素中
END_MESSAGE_MAP()
如:
class CMyFrameWnd :public CFrameWnd {
DECLARE_MESSAGE_MAP()
public:
LRESULT OnCreate(WPARAM wParam,LPARAM lParam); //声明消息处理函数
};
BEGIN_MESSAGE_MAP(CMyFrameWnd,CFrameWnd) //CMyFrameWnd 是子类类名 CFrameWnd是基类类名
ON_MESSAGE(WM_CREATE,OnCreate) //ON_MESSAGE() 类似于在建立绑定关系, WM_CREATE 是被处理消息的消息ID, OnCreate 是消息处理函数
END_MESSAGE_MAP()
//实现消息处理函数
LRESULT CMyFrameWnd::OnCreate(WPARAM wParam, LPARAM lParam) {
AfxMessageBox(L"CMyFrameWnd::OnCreate");
return 0;
}
ON_MESSAGE() 宏:
是把要被处理的消息的消息ID 和 该消息对应的消息处理函数 放到本类结构体数组元素中,
类似于把 消息ID 和 消息处理函数建立绑定关系 并且 ON_MESSAGE() 可以把任何消息的消息ID和该消息的处理函数放进本类静态结构体数组元素中
消息映射机制的实现
1> 数据结构
struct AFX_MSGMAP_ENTRY { //静态数组每个元素的类型
UINT nMessage; //消息ID
UINT nCode; //通知码, 只有 WM_COMMAND 来自于控件才有通知码
UINT nlD; //命令ID(控件ID)
UINT nLastlD; //最后一个控件的ID
UINT nSig; //消息处理函数的类型,用来和 后面的联合体的成员进行对比 确定调用的消息处理函数是什么类型
AFX_PMSG pfn; //消息处理函数的地址
};
struct AFX_MSGMAP{ //静态变量的类型
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); //基类的 GetThisMessageMap() 函数指针 PASCAL == _stdcall
const AFX_MSGMAP_ENTRY* lpEntries; //本类静态结构体数组的首地址
}
宏展开的代码
见 MFC消息映射机制.cpp
宏展开各部分的作用:
GetThisMessageMap() --- 静态函数,函数中定义了两个静态成员,最后返回本类静态结构体成员 messageMap 的地址
messageMap --- 静态结构体成员,messageMap 类似于获取链表的首节点,结构体中有2个成员:
1> 保存了基类的 GetThisMessageMap() 函数指针(链表的下一个节点)
2> 保存了本类静态结构体数组的首地址(也就是 _messageEntries[0] 的地址)
_messageEntries[] --- 静态结构体数组,数组中每个元素保存了 消息ID 和 消息处理函数的地址 等信息
GetMessageMap() --- 是一个虚函数,在其内部 调用了GetThisMessageMap()函数,也就是说可以获取本类静态结构体成员 messageMap 的地址(链表首节点)
遍历链表:
子类::GetMessageMap() --> 拿到 &messageMap 判断是否为 NULL 如果 子类名::messageMap.pBaseMap 不为 NULL -->
基类::GetMessagemap() --> 拿到基类中 &messageMap 如果 基类::messageMap.pBaseMap 不为 NULL -->
依次往上推直到遇到 CCmdTarget 类为止因为 CCmdTarget 消息映射机制的最基类,只有跟它有关系的类才能处理消息,它的 messageMap.pBaseMap 成员为 NULL
消息映射机制的执行流程:
1> 获取和窗口句柄绑定在一起的框架类对象 (pFrame)
2> 利用 pFrame 调用宏展开的虚函数 GetMessageMap() 获取本类静态结构体变量的地址(相当与链表头) pMessageMap
3> 利用 pMessageMap 静态结构体变量的第二个成员 获取对应类的静态结构体数组,并在数组中匹配查找 消息ID 和 对应的消息处理函数,找到执行5
4> 如果没有找到, pMessageMap 的第一个成员获取父类的静态结构体变量地址,如果为NULL结束查找 没有找到执行3
5> 利用找到的数组元素的第6个成员保存到函数地址,并调用之,完成消息的处理
二十四. MFC消息分类
MFC消息可分为:
1> Windows的标准消息( 例如: 键盘/鼠标/定时器 ...)
ON_MESSAGE()宏可以把任何消息的消息ID 和 该消息的消息处理函数 放进本类静态结构体数组元素中,
但每个消息都有自己特有的宏负责把自己的消息ID 和 对应的消息处理函数 放进本类静态结构体数组元素中 并且还不带参数
如:
ON_WM_MOUSEMOVE() / ON_WM_CREATE() / ON_WM_PAINT() ...
不带参数 放的消息就是把 ON_WM 去掉, 消息处理函数就是 On + 消息名称如:
ON_WM_MOUSEMOVE() 宏 展开可以看见 放的消息ID是 MOUSEMOVE 该消息的处理函数名是 OnMousemove 函数原型可以到 msdn 查 ON_WM_MOUSEMOVE 宏
2> 自定义消息
消息ID范围 0x0400 ~ 0x7FFF 之间
定义消息:
格式:
#define 消息名称 消息ID (消息ID 使用 WM_USER 宏来定义)
如:
#define WM_MYMESSAGE WM_USER+n ( 0x0400+n n的取值范围是 31743 )
由用户自己定义,满足用户自己的需求,由用户自己发出消息,并响应处理;
用户可以利用 SendMessage() 和 PostMessage() 发送消息
SendMessage() -- 发送消息不进系统队列,直接调用窗口处理函数,会等待消息处理的结果
PostMessage() -- 发送消息到系统消息队列里,消息发送后立刻返回,不等待消息的执行结果,
系统消息队列会把消息转发到程序消息队列里,最后再由 GetMessage()从程序消息队列里抓取该消息;
如果自定义消息也想利用 MFC的消息映射机制来处理消息 可以利用 万能的 ON_MESSAGE() 宏 把消息的消息ID 和 消息处理函数放入本类静态结构体数组元素中
3> 命令消息 (专指 WM_COMMAND )
WM_COMMAND 消息是利用 ON_COMMAND 宏把消息处理函数放入 本类静态结构体数组元素中
控件/加速键/菜单 都能产生 WM_COMMAND 消息
4> 通知消息 ( WM_COMMAND )
ON_通知码
如: 编辑框这种控件,当它的文本内容发生变化会给父窗口发送 WM_COMMAND 消息并附带 EN_CHANGE 通知消息
ON_EN_CHANGE
注:
afx_msg --- 在函数声明前加上 afx_msg 表示该函数是专门用来处理消息的函数
二十五. MFC的菜单
相关问题
WIN32 中谁代表菜单 ---> HMENU 菜单句柄 (句柄是内核中句柄表的下标)
MFC 中谁代表菜单 ---> CMenu 类的对象 ( CMenu 类的对象要代表一个菜单必须 和 菜单的句柄进行绑定才能真的代表一个菜单 )
相关类
CMenu 类封装了关于菜单操作的各种 API 函数,还封装了一个非常重要的成员变量 m_hMenu 菜单句柄
在Win32中关于菜单的相关 API 有:
CreateMenu() --- 创建顶层菜单(窗口最上面的一行里面有: 文件/编辑/帮助 ...)
CreatePopupMenu() --- 创建弹出式菜单 (鼠标右键 或 点击 文件/编辑/帮助... 弹出的菜单)
CheckMenuItem() --- 设置菜单的勾选/非勾选状态
EnableMenuItem() --- 设置菜单可用/不可用以及灰色不可用
AppendMenu() --- 增加菜单项 , InsertMenu()以插入方式添加菜单项
DeleteMenu() --- 删除菜单项
SetMenu() --- 设置菜单项/挂载菜单项
GetSystemMenu() --- 获取系统菜单项
TrackPopupMenu() --- 直接显示弹出式菜单
菜单的使用
1> 添加菜单资源
2> 加载菜单
Win32 有两种方式:
a. 在注册窗口类时挂载菜单,后面基于该窗口类创建出来的窗口都带有这个菜单
b. 在创建窗口的时候挂载菜单,只限于本窗口才有该菜单
MFC 也有两种方式: 因为 CreateWindowEx()函数已经被 MFC 封装了所以只能使用MFC提供的方法了
a. 在调用 Create()函数创建主框架窗口时加载菜单
b. 在框架窗口的 WM_CREATE 消息处理函数中加载菜单
步骤:
1> CMenu menu; //定义 CMenu 菜单对象
2> menu.LoadMenu(); //调用 Win32的 API 函数 LoadMenu()加载菜单并获取菜单句柄,最后再将 菜单句柄 和 CMenu对象 建立进行绑定
3> this->SetMenu(); //调用框架类的 SetMenu() 设置菜单, Win32 中也有一个 SetMenu()函数用来设置菜单
函数内部调用 Win32的API函数 SetMenu() 加载菜单
注意:
在VS中 menu 一定要声明成 静态的不能是局部变量 否则会出现断言错误
或者 在加载完菜单后 用 menu 对象调用它的 Detach() 函数
如:
CMenu menu; //菜单对象
menu.LoadMenu(IDR_MENU1); //IDR_MENU1 菜单ID
SetMenu(&menu);
//或者 ::SetMenu(m_hWnd, menu.m_hMenu) m_hWnd 是在创建窗口里面的钩子函数里面把 窗口句柄和窗口类对象进行绑定的,
// m_hMenu 是利用 menu.LoadMenu() 把菜单句柄和菜单类对象进行绑定的
menu.Detach(); // 也可以把 menu 声明成静态变量就可以不用调用 Detach()函数了
// Detach() 功能是将菜单句柄和菜单对象分开
为什么要把 菜单句柄 和 菜单对象 分开
因为当执行完 OnCreate 之后 menu 这个对象的生命周期就结束了,程序就会自动销毁这个对象和与这个对象想关联的窗口,
所以我们要在menu被销毁之前把与它相关联的窗口通过 Detach() 函数分离开,这样当 menu 被销毁之后 之前加载的菜单不会为销毁
3> 命令消息 ( 专指 WM_COMMAND ) 处理
ON_COMMAND() --- 把WM_COMMAND 消息对应的消息处理函数扔到 静态结构体数组元素中
4> 设置菜单项状态 (菜单项 勾选/非勾选 可用/不可用)
WM_INITMENUPOPUP --- 产生时间是菜单已经被激活即将显示还没有显示时产生,可以在这个消息里面设置菜单的状态,
可以使用 ON_WM_INITMENUPOPUP 把 WM_INITMENUPOPUP 消息的消息处理函数 扔进静态结构体数组元素中
勾选/非勾选:
Win32 中是使用 CheckMenuItem() 设置菜单项勾选/非勾选
MFC中是使用 CMenu::CheckMenuItem()
可用/不可用:
Win32 中使用 EnableMenuItem() 设置菜单项 可用/不可用
MFC中是使用 CMenu::EnableMenuItem() 设置菜单项 可用/不可用
5> 命令消息( 专指 WM_COMMAND 消息 )的处理顺序
先到框架类( CFrame )支脉寻找处理函数如果没有则到应用程序类( CWinApp )中寻找
是由于 CFrameWnd::OnCmdMsg() 函数内部代码的顺序决定 对应这个消息处理的执行先后,通过重写该虚函数来修改执行顺序
6> 上下文(鼠标右键)菜单
Win32中 WM_CONTEXTMENU 专门用来处理鼠标右键菜单
MFC中可以用 ON_WM_CONTEXTMENU 把 WM_CONTEXTMENU 消息对用的处理函数放入静态结构题数组元素中
右键菜单属于弹出式菜单:
可以使用 Win32 的API 函数 TrackPopupMenu() 直接显示弹出式菜单
或者 MFC 中的 CMenu::TrackPopupMenu() 函数直接显示弹出式菜单
下拉菜单其实就是弹出式菜单,是直接通过鼠标右键就显示出来
可以通过 Win32 API 函数 GetSubMenu() 获取某个顶层菜单的下拉菜单(弹出式菜单)
或则通过 CMENU::GetSubMenu() 函数来获取顶层菜单的下拉菜单
GetSubMenu()函数的功能: 获取某一个顶层菜单项的下拉菜单,返回获取到的下拉菜单的句柄
Win32 中 GetSubMenu() 原型:
HMENU GetSubMenu(
HMENU hMenu, // 菜单句柄(想获取哪个菜单的下拉菜单)
int nPos // 菜单项的索引(以0为基准的位置)
); //返回获取到的下拉菜单的句柄
MFC中 GetSubMenu()原型:
CMenu* GetSubMenu( int nPos ) const; //参数就是 要获取菜单项的索引(以0为基准的位置)
返回获取到的下拉菜单对象的地址
扩:
在程序执行过程中如果出现断言错误时,可以按 F5 以调试形式执行程序 等到出现断言错误时 按 "r" 键就可以定位到出现断言错误的代码上
二十六. 工具栏
工具栏是资源的一种,工具栏相当于是装工具的容器
工具栏和菜单栏的区别:
功能上没有什么区别,使用的快捷键有区别,一些常用的功能是放在工具栏上并且以图形表示出来,如:保存,新建 等
一般菜单栏是在工具栏上面
工具栏按钮 和 加速键一样 一般是和菜单项绑定的
注意:
如果利用 Win32 的窗口程序 改成 MFC程序 除了把 WinMain()函数删除
在 "stdafx.h"头文件中除了要把
最后再在 "项目" -> "属性" -> "配置属性" -> "常规" -> "MFC的使用" -> "在静态库中使用 MFC " 或 "在共享 DLL 中使用 MFC"
相关类:
CToolBarCtrl 继承自 CWnd 类, CToolBarCtrl 类对象代表工具栏中某一个工具按钮, 类中封装了关于工具栏控件的操作
CToolBar 继承自 CControlBar 类, CToolBar 类对象就代表 整个工具栏, 类中封装了工具栏的各种操作以及工具栏和框架窗口之间的关系(1.父子关系 2.停靠关系)
使用工具栏
定义 CTollBar 类对象
1> 添加工具栏资源
VS中:
资源文件右键属性 -> Toolbar -> 新建
2> 创建工具栏
一般在主窗口的 WM_CREATE 消息中创建工具栏
调用 CToolBar::CreateEx()(加强版) 或 CToolBar::Create()
原型:
BOOL CreateEx(
CWnd* pParentWnd, //父窗口对象地址
DWORD dwCtrlStyle = TBSTYLE_FLAT, //单个工具的风格, TBSTYLE_FLAT 默认为平滑的
DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP, //整个工具栏容器的风格 如: 鼠标停靠在工具栏的提示信息,大小 等
CRect rcBorders = CRect(0, 0, 0, 0), //设置矩形区域的大小, 前面两个是 起始坐标 左上角 0,0 后面两个0是 宽和高
UINT nID = AFX_IDW_TOOLBAR
);
工具栏的风格:
CBRS_GRIPPER --- 把手或者说是夹子风格,有了该风格工具栏好拖拽(前提是工具栏允许拖拽)
CBRS_SIZE_DYNAMIC --- 可以修改工具栏的形状
TBSTYLE_FLAT --- 工具栏按钮平滑
TBSTYLE_TRANSPARENT --- 工具栏按钮突起
CBRS_TOOLTIPS --- 工具栏可以显示标签 如: 鼠标停靠在工具栏上时会有的提示信息, 在菜单项资源右键属性 "Prompt" 项设置提示信息
可以用 "\n" 分割提示信息, "\n" 前面的提示信息显示在状态栏里面"\n"后面的信息显示在工具栏上面
3> 加载工具栏
调用 CToolBar::LoadToolBar() 把工具栏对象 和 工具栏资源 进行绑定
4> 工具栏的停靠(船坞化: 工具栏是船, 窗口是港口)
CToolBar::EnableDocking() --- 工具栏想停靠的位置
原型:
void EnableDocking(
DWORD dwStyle //想停靠的位置
);
CBRS_ALIGN_TOP --- 停靠在最上边
CBRS_ALIGN_BOTTOM --- 停靠到最下边
CBRS_ALIGN_LEFT --- 停靠在最左边
CBRS_ALIGN_RIGHT --- 停靠在最右边
CBRS_ALIGN_ANY --- 停靠在任意地方
CBRS_FLOAT_MULTI --- 不停靠在窗口上
CFrameWnd::EnableDocking() --- 框架窗口允许停靠的位置
CFrameWnd::DockControlBar() --- 框架窗口确定临时停靠的位置(之所以说是临时是因为这个时候就可以使用鼠标拖拽工具栏了)
原型:
void DockControlBar(
CControlBar* pBar, //工具栏对象地址
UINT nDockBarID= 0, //临时停靠的位置
LPCRECT lpRect = NULL //没什么用
);
nDockBarID 临时停靠的位置:
AFX_IDW_DOCKBAR_TOP --- 临时停靠在最上边
AFX_IDW_DOCKBAR_BOTTOM --- 临时停靠在最下边
AFX_IDW_DOCKBAR_LEFT --- 临时停靠在最左边
AFX_IDW_DOCKBAR_RIGHT --- 临时停靠在最右边
注意:
工具栏想停靠的位置 必须和 框架窗口允许停靠的位置 有交集不然程序会报错
5> 给工具栏的标题栏设置文本信息
调用 CTollBar 类中的 成员函数 SetWindowText() 可以给工具栏的标题栏设置文本信息 其 内部是调用 WIN32的 API 函数 SetWindowText()
6> 工具栏的显示和隐藏
CFrameWnd::ShowControlBar() --- 可以切换工具栏的 显示/隐藏 状态
原型:
void ShowControlBar(
CControlBar* pBar, //想 隐藏/显示 的工具栏对象地址
BOOL bShow, // TRUE(显示) FALSE(隐藏)
BOOL bDelay // 是否延迟操作, TRUE(延迟) FALSE(不延迟) 一般填 FALSE
);
CWnd::IsWindowVisible() --- 可以判断一个窗口是否处于显示状态, 如果返回 TRUE 表示该窗口处于显示状态, FALSE 表示该窗口处于隐藏状态
原型:
BOOL IsWindowVisible( ) const;
练习:
加一个菜单项 "工具栏"
1> 当工具栏处于显示状态时,菜单项勾选,否则菜单项非勾选
2> 菜单项每点击一次,工具栏的 显示/隐藏 状态 切换一次
3> 不要添加任何变量
能发 WM_COMMAND 消息总结:
菜单项被点击
加速键被点击
控件被点击
工具栏按钮被点击
二十七. 状态栏
状态栏不属于资源,但属于窗口, 一般在主窗口的 WM_CREATE 消息里面创建
相关类
CStatusBar --- 其父类是 CControlBar, 该类对象代表整个状态栏,封装了关于状态栏的各种操作
状态栏的使用
1> 创建状态栏
CStatusBar::CreateEx (加强版) / CStatusBar::Create (非加强版)
2> 设置状态栏指示器
CStatusBar::SetIndicators()
原型:
BOOL SetIndicators(
const UINT* lpIDArray, //指示器的ID数组, 数组里面存放各个指示器的ID, 可以使用字符串表来设置指示器ID,因为指示器里面存的就是字符串
int nIDCount //指示器的个数
);
回顾:
用途: 如果用户要求我们实现中英文两版程序,就可以使用字符串资源;
原理: 在程序中但凡用到字符串的地方都不要使用固定的字符串字面常量值,
如果使用字符串字面常量值如果用户要求字符串更改就要需要修改代码这样就违背了程序的可改性;
3> 设置指示器的宽度 和 风格
CStatusBar::SetPaneInfo
原型:
void SetPaneInfo(
int nIndex, //指示器的索引 (索引: 以0开始的位置)
UINT nID, //指示器的ID
UINT nStyle, //指示器的风格
int cxWidth //指示器的宽度
);
4> 设置指示器的文本内容
CStatusBar::SetPaneText()
原型:
BOOL SetPaneText(
int nIndex, //指示器索引(以0开始的位置`)
LPCTSTR lpszNewText, //指示器要设置的文本内容
BOOL bUpdate = TRUE //是否立即更新显示
);
扩:
获取本地计算机时间
SYSTEMTIME st = {0}; //结构体里面有 年月日时分秒 等等
::GetLocalTime(&st); //获取计算机本地时间并保存到 st 结构体中
二十八. MFC的视图窗口
视图窗口的功能:
视图窗口主要是提供了一个用于显示数据的窗口,并和用户进行交互操作
视图相关类
CView 及其子类 --- 继承自 CWnd 类, 该类对象代表一个视图窗口,该类内部封装了关于视图窗口的操作
视图窗口的使用 : 视图窗口的父窗口是主框架窗口,子窗口一般都是在父窗口创建成功但还没有显示的时候创建,也就是在父窗口的 WM_CREATE 消息中创建
1> 从 CView 类派生一个自己的视图类(CMyView), 并必须重写 CView 类中的一个纯虚函数 OnDraw (因为 拥有纯虚函数的类(抽象类)不能创建对象,所以必须重写)
2> 在主框架窗口的 WM_CREATE 消息处理中, new 了一个 视图类(CMyView)的对象(pView),并利用 pView对象 调用 Create()函数完成视图窗口的创建
OnDraw()函数的作用,如果子类没有处理 WM_PAINT 消息,编译器会调用其父类的 OnPaint()函数来处理,
父类的OnPaint()函数会调用子类重写的 OnDraw() 函数处理 WM_PAINT 消息,
OnDraw()函数必须重写且可以处理 WM_PAINT 消息因此一般都是用 OnDraw()函数来处理WM_PAINT消息程序员不会自己再去定义 WM_PAINT 消息处理函数
如果想设置视图窗口和主框架窗口一样大小有两种方式:
1> 在创建主框架窗口的 WM_CREATE 消息处理中 创建视图窗口时最后一个参数视图窗口ID填写成 AFX_IDW_PANE_FIRST 则创建的视图窗口默认与主框架窗口一样大小
2> 在主框架窗口 WM_SIZE 消息处理函数中创建视图窗口时倒数第三个参数设置视图窗口的大小时可以把 WM_SIZE 的附带信息填写上,
那样创建出来的视图窗口也可以和主框架窗口一样大小,因为 WM_SIZE 消息的传过来的附带信息就是主框架窗口的窗口客户区的大小
命令消息(专指 WM_COMMAND 消息) 处理顺序
(View)试图类 --> (Frame)框架类 --> (App)应用程序类
顺序是由 CFrameWnd::OnCmdMsg()函数内部代码执行先后顺序决定的
对象的关系图
CMyWinApp theApp; // (CWinApp)应用程序类对象 AfxGetApp()/AfxGetThread() 两个MFC的全局函数 返回 theApp 的地址
|-> theApp.m_pMainWnd = pFrame; // (CWinApp)应用程序类的成员保存了(CFrameWnd)主框架窗口类对象的地址
|-> pFrame.m_pViewActive = pView; // (CFrameWnd)主框架窗口类的成员保存了(CView)视图窗口类对象的地址
m_pViewActive 是在 OnCmdMsg() 函数中 CView* pView = GetActiveView(); 在GetActiveView()函数中 return m_pViewActive;
m_pViewActive 在 CFrameWnd 类中可以给它赋值,让它保存视图类对象的地址;
如:
CMyView* pView = new CMyView;
pView->Create(NULL, L"", WS_CHILD | WS_VISIBLE | WS_BORDER, CRect(0, 0, x, y), this,/*AFX_IDW_PANE_FIRST*/ 1001);
m_pViewActive = pView;
MFC中框架窗口相当于一个容器,里面存放各种子窗口,视图窗口就是处于窗口客户区上的窗口,用来显示数据和用户交互的窗口
注意:
如果类是继承自 CView 类则必须重写 OnDraw(),但如果继承自 CEditView 类就不必重写 OnDraw() 因为在 CEditView 类中已经重写好了
并且继承自 CEditView 类还支持键盘输入
点了菜单项产生的 WM_COMMAND 消息的传过来的窗口句柄是主窗口的窗口句柄,因为菜单的挂在主窗口上的
二十九. MFC 运行时类信息机制 (MFC第四大机制)
运行时类信息机制是MFC第四大机制
运行时类信息机制的作用:
在程序执行过程中,可以通过这个机制获知 对象 相关类的信息,也就是在程序执行过程中可以通过这个机制获知对象是不是某个类的对象
运行时类信息机制的使用
要使用运行时类信息机制的必要条件:
1> 类必须派生自 CObject 类
2> 类内必须添加声明宏, DECLARE_DYNAMIC() //参数是本类类名
3> 类外必须添加实现宏, IMPLEMENT_DYNAMIC() //参数有两个: 本类类名 父类类名
只要类具备上面3个条件就可以使用 CObject::IsKindOf() 判断 对象 是不是某个类的对象
运行时类信息的实现:
DECLARE_DYNAMIC()声明宏展开后
public:
static const CRuntimeClass classCDog; //是一个静态结构体变量,里面保存了(类名称/类大小/类版本) 运行时类信息
virtual CRuntimeClass* GetRuntimeClass() const; //成员虚函数,可以获取本类静态结构体变量的地址(相当于获取链表的头节点)
出现的 CRuntimeClass 结构体类型
CRuntimeClass 结构体:
struct CRuntimeClass{
LPCSTR m_lpszClassName; // 类名称
int m_nObjectSize; // 类的大小 用 sizeof() 计算大小
UINT m_wSchema; // 类的版本 MFC类的版本基本上是 0xFFFF
CObject* (PASCAL* m_pfnCreateObject)(); // 用于动态创建机制, 运行时类信息机制为NULL
.....
CRuntimeClass* m_pBaseClass; //保存父类静态结构体变量的地址(该成员负责链接链表)
....
CRuntimeClass* m_pNextClass; // 序列化机制,运行时类机制为NULL
};
IMPLEMENT_DYNAMIC(CDog,CAnimal)实现宏展开后 得到: IMPLEMENT_RUNTIMECLASS(CDog, CAnimal, 0xFFFF, NULL, NULL)
所以实现宏还得二次展开: IMPLEMENT_RUNTIMECLASS(CDog, CAnimal, 0xFFFF, NULL, NULL) 宏 得到:
AFX_COMDAT const CRuntimeClass CDog::classCDog = {
"CDog", //类的名称
sizeof(class CDog), //类的大小
0xFFFF, //类的版本
NULL, //用于动态创建机制, 运行时类信息机制为NULL
RUNTIME_CLASS(CAnimal), //RUNTIME_CLASS()宏展开后是((CRuntimeClass*)(&CAnimal::classCAnimal)) 父类的静态结构体变量地址
NULL,
NULL //序列化机制,运行时类机制为NULL
};
CRuntimeClass* CDog::GetRuntimeClass() const {
return RUNTIME_CLASS(CDog);
//RUNTIME_CLASS(CDog)宏展开后是 ((CRuntimeClass*)(&CDog::class##CDog)) == ((CRuntimeClass*)(&CDog::classCDog))
}
调用关系
首先调用 GetRuntimeClass() 虚函数 得到 静态结构体变量的地址, 该静态结构体变量中的 m_pBaseClass 成员保存了其父类的静态结构体变量的地址
这样就可以把子类和父类串起来形成一个链表,子类就是链表头
IsKindOf()函数的执行过程:
1> 利用类对象(yellowdog)调用宏展开的虚函数 GetRuntimeClass() 获取本类静态结构体变量的地址(类似于链表头)
2> 利用 获取到的静态结构体变量的地址和 IsKindOf()函数的参数进行比较,如果相等证明 yellowdog 属于这个类的对象,
3> 不相等则利用 静态结构体变量的 m_pBaseClass 成员拿到其父类的静态结构体变量地址,然后再和 IsKindOf() 函数的参数 进行比对,
只要有一次比对成功则 yellowdog 就属于这个类的对象,依次类推
4> 循环结束一次比对都不成功才证明 yellowdog 类不属于这个类
MFC的运行时类信息其实就是把要判断的类的对象在 IsDerivedFrom() 函数内 和 IsKindOf()函数的参数进行比较,如果匹配成功则要判断的类就是 IsKindOf()参数的对象
如果没有匹配成功则利用要判断类对象获取其父类对象的静态成员再和 IsKindOf()函数的参数进行比较 依次循环
只要有一次成功则 要判断类的对象就是IsKindOf()参数的对象
MFC运行时类信息机制其实就是在比较 静态结构体变量的地址
扩:
RUNTIME_CLASS(CDog) 展开后就是: (CRuntimeClass*)(&CDog::classCDog) 就是获取 CDog 类中的 静态结构体变量 classCDog 的地址
有了 静态结构体变量 classCDog,就可以利用它的 m_pBaseClass 成员 得到其父类的静态结构体变量的地址
也就是说 RUNTIME_CLASS()宏功能就是能得到 括号内 指明的哪个类的静态结构体变量的地址
宏替换 "#" 的替换:
如:
#abcd --> "abcd", 就是用 ""把 # 后面的字符串括起来,就是替换成字符串字面常量值
abc##123 --> abc123, 就是把 ## 两边的字符串拼接起来
三十. 动态创建机制 (MFC第五大机制)
动态创建机制的作用:
在不知道类名的情况下,将类对象创建出来
动态创建机制的使用:
1> 类必须派生 CObject 类
2> 类内必须添加声明宏: DECLARE_DYNCREATE() 参数是: 本类类名
3> 类外必须添加实现宏: IMPLEMENT_DYCREATE() 参数是: 本类类名,父类类名
动态创建机制其实就是动态创建类的对象,当满足上面3个条件之后就可以调用 CRuntimeClass 类的 CreateObject()函数 帮我们创建该类的对象
CRuntimeClass::CreateObject() -- 负责动态创建类的对象,并返回对象的地址
动态创建机制的实现:
1> 宏展开的代码(见MFC动态创建机制宏替换.txt)
2> 宏展开各部分的作用(和运行时类信息机制的区别)
多了一个静态函数 CDog::CreateObject(),该函数内部 new 了一个 CDog的对象
动态创建机制和运行时类信息机制的区别:
CObject* (PASCAL* m_pfnCreateObject)() 在运行时类信息里面为NULL,
而动态创建机制里面不为NULL,里面保存的是新增加的静态函数 CDog::CreateObject() 的地址
m_pfnCreateObject 是一个函数指针
3> CRuntimeClass::CreateObject()的执行过程
1> 利用 CDog 类静态变量 (&CDog::classCDog) 的 m_pfnCreateObject 成员获取新增加的静态函数(CDog::CreateObject)
2> 调用 m_pfnCreateObject 成员保存的函数(CDog::CreateObject),在这个静态函数内部是 new 了一个 CDog 类的对象,并返回对象的地址
RUNTIME_CLASS(CDog) 宏替换出来就是 ((CRuntimeClass*)(&CDog::classCDog)) , CRuntimeClass 是一个结构体, classCDog 是 CRuntimeClass 结构体的变量
这个静态结构体变量是运行时类信息机制的声明宏替换中出现的,该结构体中的 m_pfnCreateObject 成员则保存了声明宏替换中的 CDog::CreateObject()函数地址
在 CDog::CreateObject() 静态函数中就 new 出了 CDog 类的对象并然会 对象地址;
CDog::CreateObject() 和 CRuntimeClass::CreateObject() 的关系:
CRuntimeClass::CreateObject()函数内部调用 CDog::CreateObject() 函数, CDog::CreateObject()函数内部是在 new 对象 并返回对象地址
扩:
在MFC中凡是见到 RUNTIME_CLASS(CDog) 就要猜测 MFC 有可能要创建 RUNTIME_CLASS(CDog) 宏参数的类的对象,
如果在 RUNTIME_CLASS(CDog) 宏后面 调用了 CreateObject()函数就可以确定 MFC 就是在创建 RUNTIME_CLASS(CDog)宏中类的对象
RUNTIME_CLASS(CDog)宏替换成: ((CRuntimeClass*)(&CDog::classCDog))
也就是见到 RUNTIME_CLASS(CDog) 宏就要知道 它是在取 宏参数的类中的静态结构体变量的地址
RUNTIME_CLASS(CDog) 这个宏 在运行时类信息机制里 和 动态创建机制里面都有 目的都是拿到类的结构体静态变量,
在结构体静态变量里面有 成员函数和成员变量,包括父类的结构体静态成员和本类的 m_pfnCreateObject 成员等
如果是 RUNTIME_CLASS(CDog)->CreateObject() 则是动态创建机制,通过静态结构体变量名调用结构体中的 CreateObject() 函数创建对象
RUNTIME_CLASS(CDog)->CreateObject() 执行流程:
RUNTIME_CLASS(CDog) 拿到静态结构体变量的地址,
静态结构体变量里面保存的是:
AFX_COMDAT const CRuntimeClass CDog::classCDog = {
"CDog",
sizeof(class CDog),
0xFFFF,
CDog::CreateObject,
RUNTIME_CLASS(CAnimal),
NULL,
NULL
};
静态结构体变量的 m_pfnCreateObject 成员保存的就是动态创建机制声明宏展开得到的静态函数 CreateObject()
而在 CreateObject()函数中是在 new CDog类对象并返回对象地址
所以 RUNTIME_CLASS(CDog)->CreateObject() 就相当于 classCDog->CreateObject();
在 CRuntimeClass::CreateObject()函数内就是 (*m_pfnCreateObject)();
classCDog 是 CRuntimeClass 类 它的成员 m_pfnCreateObject 保存的就是 CDog类的静态函数CreateObject()
一定注意在 CRuntimeClass 结构体中也有一个 CreateObject()函数,和 CDog 类中的 CreateObject() 函数的函数名相同但功能不同
CObject* CRuntimeClass::CreateObject(){
return (*m_pfnCreateObject)();
}
CObject* CDog::CreateObject(){
return new CDog;
}
所以是先拿到 CRuntimeClass 的静态结构体变量地址,它的 m_pfnCreateObject 成员保存了 CDog::CreateObject() 在 CDog::CreateObject()函数中 new 对象
return (*m_pfnCreateObject)() 就相当于 return CDog::CreateObject(); 也就是返回 new 对象的地址
三十一. 切分窗口
注意:
从 Win32 改成 MFC 程序时 除了把
切分窗口有两种:
静态切分 ---- 在窗口创建出来的时候就已经完成了切分
动态切分 ---- 在程序执行过程中根据用户的需要实时完成切分,
但动态切分有局限性因为动态切分最多只能切出 2*2(行最多切2行,列最多切2列)个窗口,并且所有窗口显示的内容都一样
相关类
CSplitterWnd --- 父类是 CFrameWnd (框架窗口类), CSplitterWnd 类代表的是不规则框架窗口,该类里面封装了各种不规则框架窗口的操作
CFrameWnd 类代表的是规则框架窗口,规则框架窗口又叫"口"字框架窗口,只有一个窗口客户区的框架窗口就是规则框架窗口
主框架窗口一定是规则框架窗口
静态切分
在主框架窗口类中:
1> 定义 CSplitterWnd 类的对象
2> 重写 CFreameWnd::OnCreateClient() 虚函数
在虚函数中 利用 CSplitterWnd 类的对象 调用 CreateStatic()函数 创建不规则框架窗口
利用 CSplitterWnd 类对象 调用 CreateView()函数 给不规则框架窗口的各个客户区创建视图窗口(创建视图窗口就是在不规则窗口的窗口客户区显示数据)
CreateView() 函数内把视图窗口创建出来后,在处理该窗口的 WM_CREATE 消息的时候会把 视图类对象 和 文档类对象进行绑定,
因此在传参的时候会把文档类对象传进去
IdFromRowCol() 将窗口添加到指定的位置
如:
split2.CreateStatic(&split1,2,1,WS_CHILD|WS_VISIBLE,split1.IdFromRowCol(0,0)); //把split2建立的窗口添加到split1窗口的(0,0)的位置
SetRowInfo(); 设置框架的行高
如:
split1.SetRowInfo(0,200,100); //200是指定的行高,最后的100作用是如果没有200最低不能低于100高度
SetColumnInfo(); 设置框架的列宽
如:
split1.SetColumnInfo(0,200,100); //200是指定的列宽,最后的100作用是如果没有200最低不能低于100列宽
扩:
GetPane() 函数可以获取某一个客户区里面的视图窗口,返回获取到的视图窗口的对象地地址
如:
split1.CreateStatic(this,2,1);
CHtmlView* pView = (CHtmlView*)split1.GetPane(1,0);
pView->Navigate(L"http://www.baidu.com"); // Navigate()设置要显示的内容
动态切分
1> 定义 CSplitterWnd 类的对象
2> 重写 CFrameWnd::OnCreateClient 虚函数
在虚函数中 利用 CSplitterWnd 类对象 调用 Create()函数创建切分
注:
子窗口的创建一般都是在 父窗口的 WM_CREATE 消息里面创建,但在切分窗口里面 窗口的创建是在 OnCreateClient()虚函数中创建的,
这是因为如果子窗口没有处理 WM_CREATE 消息,其父窗口会帮着处理 WM_CREATE 消息,
在父类的 WM_CREATE 消息处理函数中会去调用子类的 OnCreateClient()虚函数,所以在 子类的 OnCreateClient()虚函数中创建子窗口
三十二. MFC的文档
注意:
从 Win32程序该成 MFC 程序时 除了 把
相关问题
文档主要提供了对数据的管理,以及和视图窗口的交互(文档负责管理数据,视图负责显示窗口)
相关类
CDocument --- 父类是 CCmdTarget 说明 CDocument 类支持消息映射机制,也支持动态创建机制
一个类只要支持消息映射机制就必定支持动态创建机制
创建过程
1> 利用 pFrame 调用 LoadFrame() 函数调用,创建主框架窗口
2> 在主框架窗口的 WM_CREATE 消息处理中创建视图窗口 //WM_CREATE 消息是窗口创建成功显示之前
3> 在视图窗口的 WM_CREATE 消息处理中将视图对象和文档类对象建立绑定关系
在视图类的祖宗类 CView 类的 OnCreate() 中 拿到文档类对象 并调用 文档类的 AddView()函数 在函数内部把 文档类对象地址 和 视图类对象地址进行绑定
this->m_viewList.AddTail(pView); //文档类对象用一个链表成员保存和它关联的试图类对象
pView->m_pDocument = this; //视图类对象用一个普通成员变量保存和它关联的文档类对象
通过上面 文档类对象 使用一个链表成员来保存和它关联的视图类对象 说明 一个文档类对象可以对应多个视图类对象,
由于视图类对象用一个普通成员保存和它关联的文档类对象,说明一个视图类对象 只能对应一个文档类对象
对象关系图
theApp // 全局变量 CMyWindApp theApp, AfxGetApp()/AfxGetThread() 两个MFC的全局函数 返回 theApp 的地址
|-> theApp.m_pMainWnd = pFrame // 应用程序类的成员保存了主框架窗口类的对象地址
|-> pFrame->m_pViewActive = pView //主框架窗口的成员变量 m_pViewActive 保存视图类对象地址,需要调用 GetActiveView()公有函数才能获取
|-> pView->m_pDocument = 文档类对象地址 //视图类的成员变量 m_pDocument 保存了 文档类对象地址,一个视图对象对应一个文档类对象
|-> 文档类对象地址->m_viewList = pView //文档类对象的 链表成员保存 上面的试图类对象地址,一个文档类对象对应多个视图对象
注:
m_pViewActive 主框架窗口的保护成员,里面保存了 视图类对象地址, 如果想在外部访问需要调用 GetActiveView() 公有函数才能获取
m_pDocument 视图类的保护成员,里面保存了 文档类对象地址 如果想在外部访问需要调用 GetDocument() 公有函数才能获取
文档类 和 视图类的联系
CDocment::UpdateAllViews() --- 文档类的成员函数, 功能: 可以刷新 和 文档类对象关联的所有视图窗口
参数: NULL 表示刷新和这个文档类关联的所有视图窗口,如果不为空则填写不刷新的视图类对象地址
CView::OnUpdate() --- 视图类的虚函数, 功能: 当视图窗口刷新时被调用
如果文档类的内容发生改变就可以调用文档类的 UpdateAllViews() 刷新和它关联的所有视图窗口,当刷新视图窗口时 视图窗口的 OnUpdate()就会被调用
SetWindowText() --- 可以设置窗口的文字信息
在 UpdateAllViews()函数内通过 GetFirstViewPosition() 可以拿到 文档类对象链表的迭代器
命令消息(WM_COMMAND)的处理顺序
先 CView(视图类) --> CDocument(文档类) --> CFrameWnd(框架类) --> CWinApp(应用程序类)
上面顺序是由 CFrameWnd::OnCmdMsg() 虚函数内的执行顺序决定的,所以上面的顺序只是默认顺序是可以被修改的
如:
新建一个类,让新类第一个去处理 WM_COMMAND 消息,如果新类不处理 WM_COMMAND 消息则再使用上面顺序执行 WM_COMMAND 消息
class CLmj : public CCmdTarget{ //定义了一个新类
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnNew();
};
BEGIN_MESSAGE_MAP(CLmj, CCmdTarget)
ON_COMMAND(ID_NEW, OnNew) //把 CLmj 类的 WM_COMMAND 消息的消息处理函数 放到本类静态结构体数组中
END_MESSAGE_MAP()
void CLmj::OnNew() {
AfxMessageBox(L"CLmj类处理了新建被点击");
}
class CMyFrameWnd :public CFrameWnd { //主框架窗口类
DECLARE_MESSAGE_MAP()
public:
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo);
};
BOOL CMyFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) {
CLmj lmj;
if (lmj.OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) {
// OnCmdMsg()是父类的虚函数,函数内确定了执行顺序,在CLmj类中没有重写则调用父类的OnCmdMsg()函数
// 起点函数是 CFrameWnd::OnCmdMsg(), 终点函数是 CCmdTarget::OnCmdMsg
return TRUE; //如果在 CCmdTarget::OnCmdMsg()最终函数的循环体中 如果找到了 WM_COMMAND 的处理函数 lmj.OnCmdMsg(...)则返回 非空
//如果没有找到那么 lmj.OnCmdMsg(...) 的返回值为 NULL 则不会执行 return TRUE 而是执行 下面的 return 语句 恢复默认的处理
}
return CFrameWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
}
三十三. 单文档视图构架程序
单文档视图构架程序由: 视图类,文档类,框架类,应用程序类 4个类组成
创建时除了把
单文档 视图构架程序: 只能管理一个文档(只能有一个文档类对象)
多文档 视图构架程序: 可以管理多个文档(可以有多个文档类对象)
相关类:
CView(视图类)/ CDocument(文档类)/ CFrameWnd(框架窗口类)/ CWinApp(应用程序类)
CSingleDocTemplate(单文档模版类) --- 其父类是 CDocTemplate
CDocManager(文档管理类)
关系图:
theApp
|-> theApp.m_pDocManager = new CDocManager //应用程序类的 m_pDocManager 成员保存文档管理类对象地址
|-> 文档管理类对象.m_templateList = pTemplate //该链表里保存单文档模版类对象地址,可以通过 AddTail()向链表里放数据 GetHead()获取链表头节点
|-> CSingleDocTemplate* pTemplate //单文档模版类对象地址
|-> pTemplate->m_pOnlyDoc //保存单文档类对象地址,是一个普通变量,是在 OnFileNew()函数里慢慢赋值的
|-> pTemplate->m_pDocClass //保存单文档类静态变量地址 RUNTIME_CLASS(CMyDoc)
|-> pTemplate->m_pFrameClass //保存框架类静态变量地址 RUNTIME_CLASS(CMyFrame)
|-> pTemplate->m_pViewClass //保存视图类静态变量地址 RUNTIME_CLASS(CMyView)
CSingleDocTemplate* pTemplate = new CSingleDocTemplate(IDR_MENU1, RUNTIME_CLASS(CMyDoc), RUNTIME_CLASS(CMyFrameWnd), RUNTIME_CLASS(CMyView));
AddDocTemplate(pTemplate);
上面2行代码就在建立上面的关系图
上面的关系图和 OnFileNew() 函数有关系
OnFileNew()函数的执行过程:
1> 通过 theApp 的一个成员 m_pDocManager 获取文档管理类对象地址
2> 通过文档管理类对象的一个成员 m_templateList 获取单文档模版类对象地址( pTemplate )
3> 利用 单文档模版类对象(pTemplate) 获取 m_pDocClass (文档类的静态变量) 并利用 这个静态变量调用 CreateObject() 函数动态创建文档类(CMyDoc)对象
4> 利用 单文档模版类对象(pTemplate) 获取 m_pFrameClass(框架类的静态变量) 并利用这个静态变量调用 CreateObject() 函数动态创建框架类(CMyFreameWnd)对象
5> 利用 框架类对象(pFrame) 调用 LoadFrame() 函数创建主框架窗口
6> 在创建主框架窗口成功时一定会产生 WM_CREATE 消息,在处理主框架窗口的 WM_CREATE 消息时 动态创建 试图类对象 并创建视图窗口
7> 当视图窗口创建成功的时候也一定产生 WM_CREATE 消息,在处理视图窗口的 WM_CREATE 消息时,将视图类对象 和 文档类对象 建立绑定关系
注:
以上建立的窗口视图等都是在 钩子函数中 将 对象 和 句柄 进行绑定的
PreCreateWindow() 函数功能: 是一个虚函数
1> 注册窗口类
2> 给注册窗口类时填写的12个成员中为 NULL 的成员重新赋值
三十四. 多文档视图构架程序
多文档可以同时管理多个文档(可以有多个文档类对象,一个文档类对象可以对应多个视图窗口)
创建时除了把
相关类
CView(视图类)/ CDocument(文档类)/ CMDIFrameWnd(主框架窗口类)/ CMDIChildWnd(子框架窗口类)/ CWinApp(应用程序类)
CMultiDocTemplate --- 多文档模版类,父类是 CDocTemplat(文档模版类)
CDocManager --- 文档管理类
关系图:
theApp
|-> theApp.m_pDocManager = new CDocManager //应用程序类的 m_pDocManager 成员保存了 文档管理类对象地址
|-> 文档管理类对象.m_templateList = pTemplate //该链表里保存多文档模版类对象地址,可以通过 AddTail()向链表里放数据
|-> CMultiDocTemplate pTemplate //多文档模版类
|-> pTemplate.m_docList //保存了文档类对象地址,通过 AddTail()向链表里放数据, 是一个链表,是在 OnFileNew()函数里慢慢赋值的
|-> pTemplate.m_pDocClass //文档类静态变量地址 RUNTIME_CLASS(CMyDoc)
|-> pTemplate.m_pFrameClass //子框架类静态变量地址 RUNTIME_CLASS(CMyChild)
|-> pTemplate.m_pViewClass //视图类静态变量地址 RUNTIME_CLASS(CMyView)
注:
m_docList 和 m_templateList 是文档管理类的保护成员需要使用 GetFirstXXXXX() 公有函数获取链表内的内容
以后凡是想获取类的链表成员的数据都可以通过 GetFirstXXXXX() 函数拿到链表的迭代器的位置,
然后通过 GetNextXXXX(迭代器位置) 函数就可以拿到链表的第一个节点的地址,再调 GetNextXXXX(迭代器位置) 就拿到第二个节点的地址
CMultiDocTemplate* pTemplate = new CMultiDocTemplate(IDR_MENU1, RUNTIME_CLASS(CMyDoc), RUNTIME_CLASS(CMyChild), RUNTIME_CLASS(CMyView));
AddDocTemplate(pTemplate);
//上面两行代码就是在建立上面的关系图
上面的关系图和 OnFileNew() 函数有关系
OnFileNew()函数的执行过程:
1> 通过 theApp 的一个成员 m_pDocManager 获取文档管理类对象地址
2> 通过文档管理类对象的一个成员 m_templateList 获取多文档模版类对象地址( pTemplate )
3> 利用 多文档模版类对象(pTemplate) 获取 m_pDocClass (文档类的静态变量) 并利用这个静态变量调用 CreateObject() 函数动态创建文档类(CMyDoc)对象
4> 利用多文档模版类对象(pTemplate)获取 m_pFrameClass(子框架类的静态变量)并利用这个静态变量调用CreateObject()函数动态创建子框架类(CMyFreameWnd)对象
5> 利用 子框架类对象(pFrame) 调用 LoadFrame() 函数创建子框架窗口
6> 在创建主框架窗口成功时一定会产生 WM_CREATE 消息,在处理子框架窗口的 WM_CREATE 消息时 动态创建 试图类对象 并创建视图窗口
7> 当视图窗口创建成功的时候也一定产生 WM_CREATE 消息,在处理视图窗口的 WM_CREATE 消息时,将视图类对象 和 文档类对象 建立绑定关系
在多文档中 调一次 OnFileNew() 函数就会执行一次上面的流程,也就是说调用一次 OnFileNew()函数就会创建一个新的文档类对象,并用多文档模版类的一个链表成员保存,
所以说多文档视图构架程序同时可以有多个文档类对象(有文档类对象就会有一个视图类对象,因此在多文档视图构架程序里面也有多个视图类对象)
单文档 和 多文档 的区别:
单文档 同一时间内只能管理一个文档类对象,在 OnFileNew()函数内一层一层进去发现 单文档是通过一个普通变量 m_pOnlyDoc 保存文档类对象地址
多文档 同一时间内可以管理多个文档类对象,在 OnFileNew()函数内一层一层进去发现 多文档是通过一个链表成员 m_docList 保存文档类对象地址的
练习:
点击 设置数据菜单项 给第三个文档类对象的成员 strDoc 赋值 刷新 和 第三个文档类对象相关联的所有视图窗口
回顾:
RUNTIME_CLASS(CMyDoc)->CreateObject() 流程
RUNTIME_CLASS(CMyDoc) 拿到静态结构体变量地址,利用这个地址调用静态结构体的 CreateObject() 成员函数,
在 CreateObject()函数内部 return (*m_pfnCreateObject)(); m_pfnCreateObject是静态结构体的一个成员变量是一个函数指针
m_pfnCreateObject 函数指针保存的是 动态创建机制声明宏展开的 静态函数 CreateObject() 是 CMyDoc 类的静态函数
CMyDoc 类的 CreateObject() 函数内是在 new CMyDoc 类对象并返回对象地址
VS的 类向导 快捷键: Shift + Ctrl + x (只有使用生成向导生成的代码才能使用类向导添加代码,自己写的代码不能使用类向导往里面添加代码)
三十五. MFC的绘图
在 Win32 中是 HDC(绘图句柄) 代表一个绘图设备
在 MFC 中是 CDC 类的对象 代表一个绘图设备( CDC 类的对象想代表一个绘图设备必须和 HCD绘图句柄 进行绑定 )
相关类
CDC 类是 绘图设备对象最基类, 父类是CObject 类,CDC 类中封装了关于绘图的各种 API 函数,还封装了一个非常重要的成员变量 m_hDC(保存的是 HDC 绘图设备句柄)
CDC类对象.m_hDC = HDC绘图句柄
CHandleMap映射类对象.m_permanentMap[(LPVOID)HDC绘图句柄] = CDC类对象
CDC 类的子类:
CClientDC 类,该类封装了在客户区中绘图的绘图设备 ( 在构造函数中调用 ::GetDc() Win32 API函数可以拿到 CClientDC 类对象 )
CWindowDC 类,该类封装了在整个窗口中绘图的绘图设备 (在构造函数中调用 ::GetWindowDC() Win32 API 函数可以拿到 CWindowDC 类对象)
CPaintDC 类,该类封装了在 WM_PAINT 消息中绘图的绘图设备 (在构造函数中调用 ::BeginPaint() Win32 API 函数可以拿到 CPaintDC 类对象)
CPaintDC 类是在构造函数中 调用 ::BeginPaint() 在析构函数中调用 ::EndPaint()
Win32 绘图相关的API函数:
::GetDC() 拿到绘图设备句柄只能在窗口客户区中绘图
::GetWindowDC() 拿到绘图设备句柄可以在整个窗口中绘图
::BeginPaint() 拿到的绘图设备句柄只能在 WM_PAINT 消息中绘图
CGdiObject 类是 GDI绘图对象最基类 父类是 CObject 类,
CGdiObject 类 封装了关于 GDI 绘图对象的各种 API 函数, 还封装了非常重要的成员 m_hObject(保存了相应的 GDI 绘图对象句柄)
CGdiObject 类的子类:
CPen 封装了画笔的使用, 必须和 Win32 的 HPEN 画笔句柄 进行绑定 CPen 类对象才能代表 画笔
CBrush 封装了画刷的使用, 必须和 Win32 的 HBRUSH 画刷句柄 进行绑定 CBrush 类对象才能代表 画刷
CFont 封装了字体的使用, 必须和 Win32 的 HFONT 字体句柄 进行绑定 CFont 类对象才能代表 字体
CBitmap 封装了位图的使用, 必须和 Win32 的 HBITMAP 位图句柄 进行绑定 CBitmap 类对象才能代表 位图
CRgn 封装了复合(不规则)图形的绘制
Win32 的 GDI 绘图相关的 API 函数:
画笔:
::CreatePen() 创建画笔,返回 HPEN 画笔句柄
::SelectObject() 绘图设备调用该函数,可以把新的画笔应用到绘图设备中,返回原来的画笔
::Rectangle() 绘图设备调用该函数来绘制矩形
::DeleteObject() 画笔调用该函数,销毁画笔
画刷:
::CreateSolidBrush() 创建实心画刷,返回实心画刷句柄
::CreateHatchBrush() 创建阴影画刷,返回阴影画刷句柄
字体:
::CreateFont() 创建字体,返回 HFONT 字体句柄
CRgn 封装了关于复合(不规则)图形的绘制
1> 将简单基本图形通过运算(通过 与/或/非 运算) 构建一个复杂几何图形
2> 使用
a. 利用一系列 CRgn::CreateXXXXXX()函数创建基本图形
b. 利用 CRgn::CombineRgn()函数将基本图形通过指明的运算进行组合填充复合图形
如:
CRgn rgn1;
rgn1.CreateEllipticRgn(100,100,300,300); //创建基本原型
CRgn rgn2;
rgn2.CreateEllipticRgn(200,200,400,400);
rgn1.CombineRgn(&rgn1,&rgn2,RGN_AND); //把rgn1和rgn2两个图形做运算(RGN_AND 与/ RGN_OR 或/ RGN_XOR 非)并把运算结果赋值给rgn1
c. 利用 CDC::FillRgn() 函数用指定画刷填充指定区域
练习:
实现简易的画图工具
::SetCapture() --- 捕获鼠标(当鼠标离开窗口后还能获取鼠标光标的位置这就是捕获鼠标)
::ReleaseCapture() --- 释放鼠标,没有参数
::SetROP2() --- 可以把当前背景色取反色
原型:
int SetROP2(
HDC hdc; //绘图设备句柄
int fnDrawMode // R2_NOT(取反色,是根据当前背景色取反色)
);
扩:
资源的 Loadxxxx()函数 一般都会做两件事:
1. 加载资源并拿到资源的 句柄
2. 把资源的句柄 和 资源对象 进行绑定
在框架窗口中是用对象的 m_hWnd 成员保存对应的窗口句柄
三十六. MFC文件的操作
相关类:
CFile ---- 文件操作类,该类对象代表一个文件(前提是必须和 HANDLE 文件句柄 进行绑定),该类内部封装了关于硬盘文件读写操作的各种API函数
CFile 的父类是 CObject
CFileind --- 文件查找类,封装了关于遍历操作的 API 函数
CFile 类的使用 (建议还是使用 Win32 的 API函数 因为 Win32 的 API 函数除了可以访问硬盘文件还可以访问外围设备,MFC 的 CFile 类只能访问硬盘文件不能访问设备)
1> 创建 或 打开文件 CFile::Open() 既可以打开也可以创建文件
2> 读/写 文件 CFile::Read()读/ CFile::Write()写
3> 设置文件读写位置 CFile::Seek()自定义文件读写位置/ CFile::SeekToBegin()把文件读写位置放在文件头/ CFile::SeekToEnd()把文件读写位置放在文件尾
4> 关闭文件 CFile::Close()
文件属性的获取 和 设置
CFile::GetStatus() --- 获取文件属性(有两个 GetStatus() 函数 静态 和 非静态)
CFile::SetStatus() --- 设置文件属性,只有静态函数
可以获取文件的属性:
· CTime m_ctime 文件创建的时间。
· CTime m_mtime 文件最后一次修改的时间。
· CTime m_atime 最后一次访问文件并读取的时间。
· LONG m_size 文件逻辑长度,以字节数表示,如同DIR命令报告的那样。
· BYTE m_attribute 文件属性字节。
· Char m_szFullName[_MAX_PATH] Windows字符集表示的全文件名
CFileFind 类的使用,是有先后顺序的
1> 利用CFileFind::FindFile()函数,开始查找指定的目录(找到返回true,反之返回false),当执行完FindFile()函数后不会得到文件的任何信息 它只是单纯的开始查找
2> 利用 CFileFind::FindNextFile()函数,找到当前文件,同时通过返回值获取下一个文件是否存在(存在返回 TRUE,不存在返回 FALSE)找到文件后就可以调用下面函数
3> 可以利用一系列 CFileFind::GetXXXX()获取文件信息,当CFileFind::FindNextFile()找到文件后就可以利用GetXXXX()函数获取文件信息了
4> 可以利用一系列 CFileFind::IsXXXX()判断文件属性,获取到文件信息就可以使用 IsXXXX() 函数判断文件属性了
5> 关闭查找 CFileFind::Close()
GetFileName() --- 获取文件名称包括文件的后缀
GetFileTitle() --- 只获取文件的名称,不包括文件的后缀
注:
虽然CFileFind::FindNextFile()函数是查找下一个文件,但只有先调用了 CFileFind::FindNextFile()函数才能得到第一个文件的信息,
得到第一个文件的信息后才能调用后面的 GetXXXX() 函数 ;
练习:
1. 查找指定目录下的文件和目录(只查找当前目录)
注:
CString 是 Unicode 编码 要使用 %S 输出,如果使用 %s 输出 则只能输出单个字符
2. 查找指定目录下的文件和目录(如果有子目录则继续查找子目录)
可以使用 递归 来遍历,在遍历时需要注意 避免 递归 "."目录
三十七. 序列化
概念:
将数据以二进制流的方式依次写入到文件中或者从文件中读取的过程就叫序列化
相关类:
CArchive 类 该类用来实现一般数据的序列化
在没有 CArchive 类时我们也可以通过 Read() 和 Write() 对文件的数据进行读/写,
1> 在使用 Read() 和 Write() 时 参数的类型是 void* 我们不知道写入的类型是什么所以我们要强制类型转换,但强制类型转换是存在潜在的危险的,
2> 在读写文件时一般都会维护一个缓冲区等缓冲区满时一次性把数据写入文件中这样会提高数据的读写效率,
但 Read() 和 Write() 函数没有参数能够指定缓冲区的大小需要新定义一个缓冲区,
而 CArchive 类在构造函数中就有设置缓冲区大小的参数默认是4k大小(解决了缓冲区问题),
然后通过重载 ">>" "<<" 运算符解决数据不同的问题(在运算符重载中 把所有基本数据类型都重载了 )
CArchive 类的构造函数原型:
CArchive( CFile* pFile, UINT nMode, int nBufSize = 4096, void* lpBuf = NULL );
在 CArchive 类中是通过 operator>>(读) 和 operator<<(写) 重载 ">>" "<<" 运算符来读写数据的,
CArchive类的使用:
1> 打开或新建文件
CFile::Open()
2> 文件读写
a. 定义 CArchive 对象,创建 CArchive 对象并和 CFile 对象进行关联 (第一个参数是要关联文件对象的地址,通过第一个参数和 CFile 对象相关连)
b. 通过 "<<"写数据 / ">>"读数据 ("<<"和">>"把绝大部分的类型都重载了所以可以直接使用 "<<"和">>"来读写数据)
c. 关闭 CArchive 对象( CArchive::Close() )
3> 关闭文件
CFile::Close()
三十八. 对象的序列化 (MFC 第六大机制)
概念:
序列化对象(写对象) ---- 将对象的类的信息 和 对象的成员变量以二进制流的方式依次写入到文件的过程
反序列化对象(读对象) ---- 首先读取类的信息,拿到类的信息后创建对象,然后读取文件中的成员变量的值赋值给新创建的对象的过程
对象的序列化机制:
在序列化对象时用了 运行时类信息机制(先获取到文件的时类信息然后在写入到文件中),
在反序列化对象时用到了 动态创建创建机制(先拿到类的信息,然后根据类的信息创建对象,最后再把文件中的数据赋值给新创建的对象)
对象的序列化声明宏展开后 是 动态创建宏 和 一个友元函数, 而动态创建宏展开后 是运行时类信息机制的声明宏
所以 对象的序列化 一定支持 动态创建机制 和 运行时类信息机制
对象序列化的使用:
1> 定义支持序列化的类
a. 该类必须派生自 CObject 类
b. 在类的内部添加 序列化的声明宏 DECLARE_SERIAL(当前类类名), 在类外添加序列化的实现宏 IMPLEMENT_SERIAL(子类类名,父类类名,版本号)
c. 重写 CObject 类的 Serialize()虚函数,在函数中完成类的成员变量的序列化(也就是在类中完成成员变量的二进制流的读写操作)
Serialize()原型:
virtual void Serialize( CArchive& ar );
在重写时可以在重写的 Serialize() 虚函数中 通过 CArchive 类的对象调用 IsStoring() 函数判断是读还是写,如果返回真 则表示要把数据写入到文件中
注:
通常情况下在调用当前类的 Serialize() 时,如果其父类也有成员需要序列化,通常是先调用父类的 Serialize() 函数
2> 使用
在读写时与读写基本数据类型的数据一样,只是参数变成了对象的地址
如:
//写对象
void ObjectStore(CStudent *pStu) {
CFile file;
file.Open(L"E:/whd.txt",CFile::modeCreate|CFile::modeWrite);
CArchive ar(&file,CArchive::store);
ar << pStu;
ar.Close();
file.Close();
}
//读对象
void ObjectLoad() {
CFile file;
file.Open(L"E:/whd.txt", CFile::modeRead);
CArchive ar(&file, CArchive::load);
CStudent *pStu = NULL;
ar >> pStu; //在计算机内部会先去动态创建一个对象
//并把数据存放到该内存中(先放类的信息,然后再执行重写的 Serialize()把数据写入成员变量),最后让 pStu 指向该对象
ar.Close();
file.Close();
if (pStu) { //如果想通过指针去调用对象的成员成员函数或是想引用成员变量 都需要对该指针进行判断
pStu->Show();
delete pStu;
pStu = NULL;
}
}
扩:
命名规则:
1> 驼峰命名法 --- 第一个单词字母小写,后面每个单词的首字母大写 如: setWindowText()
2> 帕斯卡命名法 --- 所有单词的首字母大写 如: SetWindowText()
3> 匈牙利命名法 --- 主要是对变量命名, 第一个字母表示变量的类型 如果是类的成员变量用 "m_" 开头 ,如果是私有以 "_" 开头
如: m_nAge (m_表示类的成员 n 表示 int )
内联:
在类的内部写实现的函数一般都会申请成为内联函数,但如果写在类内部的函数包含循环递归之类的比较耗时或某些复杂的函数编译就不会让其成为内联函数
在程序中可以使用 Ctrl + Tab 键在打开的两个文件中切换
在 MFC 中 可以把光标放在一行前面 然后 按 Ctrl + c 这样默认会拷贝一行
例子:
1. 自定义一个地址类型,支持序列化
地址类型:
class CAddress{
CString m_sProvince;
CString m_sCity;
};
2. 修改 CStudent 类,增加 CAddress 类型的成员变量,任然需要 CStudent 类支持序列化
三十九. MFC对话框
对话框分为:
模式对话框 (阻塞)
非模式对话框(无模式对话框)(非阻塞)
模式对话框和非模式对话框最本质的区别是,模式对话框是阻塞的(在没有关闭当前模式对话框前不能操作本进程其它的窗口),非模式对话框不阻塞
框架窗口和对话框窗口是本质区别:
虽然对话框窗口本质上也是一个窗口但对话框窗口必须和一个资源相关联,而框架窗口直接使用类就可以了
相关类:
CDialog 类 父类是 CWnd 类 所以对话框类本质上也是一个窗口,只是对话框窗口是拥有资源的(通常是要和资源相关联);
|-> CCommonDialog 类 父类是 CDialog 类, 该类是 通用对话框 类(在各个不同的程序中该对话框是一样如: 文件打开对话框,颜色对话框,字体对话框 等)
|-> CColorDialog 颜色对话框
|-> CFileDialog 文件对话框(如: 打开 和 另存为 对话框)
|-> CFindReplaceDialog 查找/替换 对话框
|-> CFontDialog 字体对话框
|-> COleDialog 对象的嵌入与链接,在一个软件中嵌入另一个软件的功能如word文档中可以直接编辑插入图片(该系列对话框现在已经被组件所代替了)
|-> COlxxxxxx
|-> COlxxxxxx
...
|-> CPageSetupDialog 属性页对话框,如: 在打印时
|-> CPrintDialog 打印对话框,如:连接远程打印机
以上对话框可以直接使用而不需要在从新开发,该7个对话框都是通用对话框
|-> CPropertyPage 类 父类是 CDialog 类, 该对话框是 属性页对话框,如在电脑桌面鼠标右键属性弹出来的哪个对话框就是属性页对话框
CDialog 类的使用:
1> 使用 CDialog 类创建基于模式对话框的应用程序 (阻塞的对话框)
步骤:
a. 添加对话框资源,并且与对话框类关联, 可以通过调用父类构造把当前对话框和资源相关联 如: CMyDlg() :CDialog(IDD_ABOUTBOX) {}
b. 在 App(应用程序类)的 InitInstance() 函数中,创建和显示对话框窗口,在模式对话框中 创建和显示对话框都是使用 CDialog::DoModal() 函数实现的
c. 关闭对话框(点击 0k/关闭 ...) 自动销毁和释放资源
当点击 ok 按钮时 会去调用 CDialog::OnOK()
当点击 关闭按钮事 会去调用 CDialog::OnCancel()
如:
CMyDlg dlg; //创建一个对话框对象
m_pMainWnd = &dlg; //把对话框作为整个应用程序的主窗口
dlg.DoModal(); //调用 DoModal()函数 创建并显示 模式对话框
2> 使用 CDialog 类创建基于非模式对话框的应用程序 (非阻塞的对话框)
步骤:
a. 添加对话框资源,并且与对话框类关联,可以通过调用父类构造把当前对话框和资源相关联 如: CMyDlg() :CDialog(IDD_ABOUTBOX) {}
b. 窗口的创建和显示, 创建和显示非模式对话框与一般的框架窗口类似,利用对话框对象调用Create()函数来创建
c. 非模式对话框的关闭需要程序员手动关闭的,如果没有手动释放 而是点 ok 或点 关闭 则只是隐藏 后台进程还有
因此 对话框需要 销毁
分为两步:
1> 在CDialog 派生类中 重写 CDialog::OnOK() 和 CDialog::OnCancel() 在函数中调用 DestroyWindow()销毁对话框窗口
2> 在CDialog 派生类中 重写 CWnd::PostNcDestroy() 函数 在函数中完成对象的自我销毁 (delete this) 销毁对象
几乎任何窗口的自杀都是在该函数中进行,CWnd类是 CDialog 类的爷爷类
PostNcDestroy() 函数是所有窗口执行的最后一个函数,所以在该函数中完成自我销毁
DoModal()函数的执行过程
1. 查找资源
调用 AfxGetResourceHandle() 获取资源句柄,
然后调用 ::FindResource()函数去资源句柄对应的那块内存中去找对应的对话框资源,
如果找到就调用 LoadResource()函数加载对话框资源;
2. 使用资源
在使用资源之前 调用 LockResource()函数加锁
然后在创建对话框窗口之前调用 ::EnableWindow(FALSE)函数 让父窗口不可用,这样就可以达到阻塞的效果(主窗口已经不可用了只能点击对话框窗口)
最后调用 CreateDlgIndirect() 创建对话框,并调用 RunModalLoop()函数进入对话框的消息循环这样就只能接收对话框的消息其它消息接收不到
当我们点击了对话框的 OK/Cancel/关闭按钮 会向对话框发对应的消息 就可以退出 RunModalLoop()函数结束对话框的消息循环
在结束对话框时还会调用 SetWindowPos()函数设置对话框的状况,状况包括: 对话框隐藏/对话框不能修改大小/对话框不能移动/对话框不是活动状态 ...
此时对话框只是隐藏并没有真正销毁,
然后在调用 ::EnableWindow(TRUE) 让父窗口可用 和 调用 ::SetActiveWindow()函数让父窗口活动(活动意味着当前的鼠标焦点停留在父窗口上)
当设置完父窗口状态后就调用 DestroyWindow() 销毁对话框窗口
然后调用 UnlockResource()函数 解锁
解锁后 调用 FreeResource() 函数释放对话框资源
3. 返回值
DoModal()函数是有返回值的,返回的是被点击按钮的值
注:
如果在 VS 中建立了一个类 但在类视图中没有 此时就可以在该类的头文件中随便修改(修改后还原也可以)然后保存,然后再在类视图中就可以找到该类了
在 VS2015中打开类向导的快捷键是: Ctrl + Shift + X
四十. 控件操作
在MFC中控件的创建是在创建窗口的时候就创建出来了,但如果想设置控件的状态是在控件创建成功显示之前的时候设置的这才是真正的时机
在MFC中如果想对控件做处理只需要 重写父类的 OnInitDialog() 虚函数即可,一般是用来做初始化的,在 Win32 中是处理 WM_INITDIALOG 消息
所有的控件的初始化都放在 OnInitDialog() 函数中处理
在子类调用 OnInitDialog() 函数做初始化的时候通常都需要先调用其父类的 OnInitDialog() 函数对父类做初始化
在Win32中可以使用 GetDlgItem()函数 通过控件的ID 获取控件的句柄 ,通过句柄可以找到控件所在的内存空间的地址,拿到地址就能找到那块内存空间;
在MFC中是通过拿到控件对象的地址进行操作控件的
在使用上面 Win32 的步骤去操作控件很麻烦尤其是要获取控件中的内容时,
比如获取文本框中输入的数字求和,当从文本框获取数字时获取到的是字符串,还需要程序员转成数字才能进行求和;
因此MFC 提供了对话框的数据交换技术,有了这个技术就可以让数据交换更简单;
对话框数据交换技术(DoDataExchange 简写成 DDX)
概念:
它是通过 将控件与对话框类的成员变量绑定,这样就可以通过操作成员变量的方式间接的操作控件;
使用步骤:
1. 添加类的成员变量
2. 重写父类的 CWnd::DoDataExchange()函数 在 CWnd::DoDataExchange()函数中,将变量与控件绑定;
使用 DDX_Control()函数 是把变量和控件类型进行绑定
使用 DDX_Text()函数 是把变量和值类型进行绑定,绑定后还需要调用 UpdateData()函数
UpdateData(FALSE) -- 表示将变量的值传递给控件
UpdateData(TRUE) -- 表示将用户在控件中输入的值传递给变量
注: UpdateData()函数只有在 值类型时才会调用
在绑定 资源和成员变量时 可以使用 Ctrl + Shift + X 类向导来绑定也
可以手动在重写的 DoDataExchange()中调用 DDX_Control() 或调用 DDX_Text() 来绑定
3. 把类的成员变量 和 控件绑定后就可以使用 DDX(对话框数据交换技术) 通过类的成员变量操作控件了
如果想操作控件(就是想调用控件的成员函数)则进行控件的绑定,如果想获取/修改 控件中的值则进行控件值的绑定
练习:
完成登录操作
对话框数据交换技术实现原理:
控件类型的绑定:
DDX_Control(){ //控件类型和成员变量的绑定,绑定后就可以通过成员变量来操作控件
//根据控件 ID 获取控件的句柄
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl){
*phWnd = ::GetDlgItem(m_hWnd, nID); //根据控件 ID 获取控件的句柄
}
... ...
//将控件句柄附加到对象中
SubclassWindow(hWndCtrl){
Attach(hWnd){ //将句柄附加到对象上
pMap->SetPermanent(m_hWnd = hWndNew, this){
m_permanentMap[(LPVOID)h] = permOb; //以控件句柄为键,以变量的地址为值 建立映射关系
}
}
}
}
值类型的绑定:
UpdateData(TRUE){
CDataExchange dx(this, bSaveAndValidate /*值就是 TRUE */); //dx 包含了对话框的地址 和 传递的形参的值
DoDataExchange(&dx){ //在该函数中将变量和控件进行绑定,由于我们重写了父类的DoDataExchange()函数所以调用子类重写的DoDataExchange()函数
DDX_Text(pDX,IDC_EDIT1,m_sText){ //值类型的绑定
HWND hWndCtrl = pDX->PrepareEditCtrl(nIDC); //根据控件的ID 获取控件的句柄 内部调用 GetDlgItem()函数获取句柄的
if (pDX->m_bSaveAndValidate){ //m_bSaveAndValidate 就是形参 此时是 TRUE
int nLen = ::GetWindowTextLength(hWndCtrl); //GetWindowTextLength()函数在获取控件中内容的长度
::GetWindowText(hWndCtrl, value.GetBufferSetLength(nLen), nLen+1); //GetWindowText()获取控件中的值 赋值给 变量
value.ReleaseBuffer();
}else{
AfxSetWindowText(hWndCtrl, value); //AfxSetWindowText()将变量的值 赋值给 控件
}
}
}
}
练习:
完成加法运算
两种方式:
1. 使用对话框数据交换技术
2. 使用 GetDlgItem()函数
扩:
在 VS 中 Ctrl + L 是删除一行
四十一. MFC控件介绍
控件可以根据功能进行分组:
1. 静态控件: 在代码中很少操作该类控件,并且控件的默认 ID 都是 IDC_STATIC ,但如果想操作静态控件就必须修改 控件的 ID 因为控件 ID 是唯一的才能被操作
Picture Control(图片)/Static Text(静态文本)/Group Box(分组框)
图片的添加方式是: 属性 -> Type选项(选择 Bitmap) 然后在 Type 选项上面 Image(位图的ID)
2. 按钮控件: 该控件的控件类都是 CButton 类
Buton(按钮)/Check Box(复选按钮)/Radio Button(单选按钮)
如果想在同一个对话框中添加多个单选按钮的分组,需要在 Tab 序号小的单选框右键属性设置该单选框的 Group 属性 否则所有的单选框都属于同一组单选框,
在设置单选框分组时分组的单选框的 Tab 顺序必须是连续的
扩:
在 VS2015 中可以使用 Ctrl + D 调出控件的 Tab 顺序
3. 组合框 和 列表框 控件 : 组合框 和 列表框 都可以包含多个数据项(字符串)
组合框控件: Combo Box
组合框控件类是 CComboBox 类
组合框控件可以在多个数据项(字符串)中选择一项,也可以接收用户的输入
鼠标右键组合框控件 -> 属性 -> Data 选项中可以添加组合框的数据项,在VC6.0中可以按 Ctrl + Enter 键换行添加多个,在VS2015中使用 ";" 分隔
鼠标点击 组合框 可以设置组合框的宽度,点击组合框的三角符号可以设置组合框的高度,将显示的虚框拉大,拉到最后编译器也会根据数据项的内容自动设置
鼠标右键 组合框 -> 属性 -> Type -> Dropdown(表示用户可以在组合框中输入)/下拉列表(表示不让用户在组合框中输入,只能使用默认的)
组合框默认是排序的 如果不想组合框排序 可以 鼠标右键 组合框 属性 -> Sort -> True(排序)/False(不排序)
列表框控件: List Box
列表框控件是 CListBox 类
列表框控件可以在多个数据项(字符串)中选择一项或者多项,但不能接收用户的输入
鼠标右键列表框控件属性 -> Selection -> Single(单选)/Multiple(多选)/Extended(扩展)/None(不设置)
列表框不能通过鼠标右键列表框属性来添加数据项,因为列表框控件属性里面没有 Data 选项, 只能通过列表框的成员函数添加数据项
方法:
1. 要操作控件首先绑定成员变量(可以通过类向导来绑定 VS2015 打开类向导是 Ctrl + Shift + X )
2. 绑定了成员变量后就可以做控件的初始化
在MFC中如果想对控件做处理只需要 重写父类的 OnInitDialog() 虚函数即可,一般是用来做初始化的,在 Win32 中是处理 WM_INITDIALOG 消息
所有的控件的初始化都放在 OnInitDialog() 函数中处理
在对话框(列表框/组合框等都是放在对话框内的)的 OnInitDialog()函数中 return 之前可以通过 成员变量 调用相关函数操作数据项:
组合框( CComboBox )也有类似的相关成员函数:
CListBox::AddString() --- 添加数据项,返回被添加数据项的索引值
CListBox::DeleteString() --- 删除数据项
CListBox::InsertString() --- 插入数据项
CListBox::FindString() --- 查找数据项,第一个参数如果填 -1 表示从头找到尾, 如果找到则返回找到数据项的索引否则返回 -1
CListBox::ResetContent() --- 清空数据项
CListBox::SetCurSel() --- 设置当前选择项
CListBox::GetCurSel() --- 获取当前选择项,获取到的是索引值(从0开始的编号)
CListBox::GetText() --- 根据索引值得到对应的文本字符串数据项
CListBox::GetCount() --- 获取数据项的数量
CListBox::SetItemData() --- 设置数据项的附加数据,可以用来保存数据项的路径,附加数据和数据项之间是一一对应的关系 4个字节
CListBox::GetItemData() --- 获取数据项的附加数据
扩:
Ctrl + Tab 可以在两个打开的文件中进行切换
4. 动画控件(Animation Control): 该控件类是 CAnimateCtrl 类,功能是能播放简单的帧动画( 部分 .avi 的文件)
CAnimateCtrl 类的成员函数:
CAnimateCtrl::Create() --- 创建控件,一般不用,因为控件直接拖拽到对话框中
CAnimateCtrl::Open() --- 打开动画文件
CAnimateCtrl::Play() --- 播放动画文件
CAnimateCtrl::Stop() --- 停止播放文件
CAnimateCtrl::Seek() --- 移动到第几帧就播放第几帧
CAnimateCtrl::Close() --- 关闭动画文件
CAnimateCtrl::Play() --- 播放动画文件
原型:
BOOL Play(
UINT nFrom, //如果两张图片间隔的时间小于0.02秒人眼看上去就是连续的视频,nFrom 表示从第几帧图片开始播放(帧数不能大于 65536)
UINT nTo, // nTo 表示播放到第几帧 (帧数不能大于 65536), nFrom == 0 表示从第一帧开始播放, nTo == -1 表示播放到最后
UINT nRep // 重复播放次数, nRep == -1 表示无限制重复播放, nRep == 1 表示播放一次重复播放一次 所以一共播放了2次 0表示不重复播放
);
注意:
只要是在需要在代码中操作的控件我们都需要手动为该控件设置一个有意义的 控件ID
鼠标右键 动画控件 属性 --> Animation Transparent(不显示背景色)/Transparent(不显示背景色)/Center(居中显示)
动画播放例子
1> CFileDialog 类 --- 文件对话框类(打开/另存为 ...)
CFileDialog 文件对话框的构造函数: 如果想精确的设计文件对话框就需要填写更多的参数
CFileDialog(
BOOL bOpenFileDialog, //TRUE 表示弹出打开文件对话框, FALSE 表示弹出另存为文件对话框
LPCTSTR lpszDefExt = NULL, //默认的文件后缀,打开的时候没有,但在另存为时一般就有默认的文件后缀(如: .jpg .txt 等)
LPCTSTR lpszFileName = NULL, //默认的文件名称,打开的时候没有,但在另存为时一般就有默认的文件名称(如: 未命名 )
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, //对话框的风格和样式
LPCTSTR lpszFilter = NULL, //文件类型的设置,可以过滤文件类型
CWnd* pParentWnd = NULL //父窗口,通常为 NULL 即可 很少设置
);
lpszFilter 文件类型设置,可以过滤文件的类型但需要设置过滤字符串的格式:
a. 每个数据项之间用 "|" 分割, 整个字符串以 "||" 结尾;
b. 每个数据项包括两部分, 显示 和 过滤 两部分,这两部分也以 "|" 隔开
如:
"视频文件(*.avi)|*.avi|所有文件(*.*)|*.*||";
"视频文件(*.avi;*.rmvb)|*.avi;*.rmvb|所有文件(*.*)|*.*||"; //多个格式使用 ";" 分隔
// "视频文件(*.avi)" 是显示部分 会显示到组合框中 "*.avi" 是过滤部分不会显示在组合框中 但编译器会以它为条件过滤文件的
获取文件的路径 和 名称 可以通过 CFileDialog 类的成员函数
CFileDialog::GetPathName(); --- 获取文件路径
CFileDialog::GetFileName(); --- 获取文件的名称
注:
在实际项目开发中 如果同一个项目中出现了两处或多处代码一样就应该把相同代码封装成函数然后在调用(代码复用)
5. 旋转按钮,进度条 和 滑块
水平/垂直 滚动条 参见Win32
旋转按钮: Spin Control
旋转按钮的类是 CSpinButtonCtrl 类
旋转按钮的使用一定是和一个编辑框一起使用的,很少单独使用,因此在使用旋转按钮时需要先添加一个编辑框(Edit Control)
使用旋转按钮时可以 鼠标右键 --> 属性 --> Alignment --> Right Align(右对齐)/Left(左对齐)/不对齐(Unattached)
鼠标右键 --> 属性 --> Auto Buddy(让编辑框和旋转按钮成为伙伴关系)
鼠标右键 --> 属性 --> Set Buddy Integer(点击旋转按钮时可以设置编辑框中的数值)
注意:
在使用旋转按钮时 一定是先添加 编辑框 再添加旋转按钮 否则两者不能进行关联
Alignment 功能是设置 旋转按钮 出现在编辑框的左边还是右边
Auto Buddy 功能是 让编辑框和旋转按钮成为伙伴关系,旋转按钮会嵌套在编辑框中
Set Buddy Integer 功能是 点击旋转按钮时编辑框中的数值会跟着改变
进度条(Progress Control)
进度条的控件类是 CProgressCtrl 类
滑块(Slider Control)
滑块的空间类是 CSliderCtrl 类
旋转按钮/进度条/滑块的使用步骤:
1. 设置/获取 控件的数值范围
SetRange()设置数值范围/GetRange()获取数值范围 //可以使用对话框数据交换技术(先添加成员变量,绑定控件,操作控件)
对话框的初始化都是放在 对话框的 OnInitDialog() 虚函数中 return TRUE 之前 初始化的
如果是做网页下载, 控件的数值范围 可以设置为下载文件的大小( B 字节为单位),
然后通过 GetPos()函数获取每次下载的大小 再通过 SetPos()函数设置 进度条/滑块 的位置
2. 设置控件的增量(进度条中叫步长) 默认为1
旋转按钮设置增量:
CSpinButtonCtrl::SetAccel() 设置增量
原型:
BOOL SetAccel(
int nAccel, //UDACCEL 结构体数组的元素个数
UDACCEL* pAccel //UDACCEL 结构体数组的指针,可以指定多少秒以后增量变成多少,可以设置多个时间段的增量(步长),一般只设置一个增量
);
UDACCEL 结构体原型:
typedef struct{
UINT nSec; //秒
UINT nInc //增量
}UDACCEL,FAR *LPUDACCEL;
UDACCEL 结构体的功能是当鼠标按着旋转按钮多少秒后增量变成多少
CSpinButtonCtrl::SetPos() 设置旋转按钮的数值
CSpinButtonCtrl::GetPos() 获取旋转按钮的当前数值
进度条设置步长:
CProgressCtrl::SetStep()
如:
设置增量(步长为5)
m_wndProgress.SetStep(5);
CProgressCtrl::StepIt() 让进度条前进一个步长
CProgressCtrl::GetPos() 获取进度条当前位置
滑块设置增量(步长):
CSliderCtrl::SetLineSize() 使用上下左右光标键时滑块移动的步长
CSliderCtrl::SetPageSize() 按 PgUp 和 PgDn 键时滑块移动的步长
CSliderCtrl::SetPos() 设置滑块的数值
CSliderCtrl::GetPos() 获取滑块的当前位置
6. 列表控件 ( List Control )
相关类:
CListCtrl 类 父类是 CWnd (控件类),常用于对话框程序中,提供了许多成员函数可以去操作控件 如:控件的 创建/设置/修改 等
CListView 类 父类是 CCtrlView (试图类), 常用于文档视图应用程序中,只提供了一个成员函数 GetListCtrl()获取视图所包含的控件
在列表视图中由控件默认提供的显示风格样式只有4种:
图标,小图标,列表,报表(指的就是 详细信息)
列表控件 和 列表视图的关系:
通过列表视图提供的 CListView::GetListCtrl() 函数 获取该视图客户区的控件,获取到的控件是属于列表控件( CListCtrl )的,
这样就可以通过列表控件所提供的成员函数来操作控件了;
设置列表控件的样式:
列表控件 -> 鼠标右键 -> 属性 -> Vlew 选项 -> Icon(图标)/Small Icon(小图标)/List(列表)/Report(报表)
ClistCtrl(列表控件) 类的使用:
1. 如果想让列表控件以多种方式显示需要定义两套图标列表(大图标/小图标 列表)
创建图标列表:
a.添加(导入)与图标列表关联的位图
b.创建列表控件的图标列表(至少需要两套图标列表, 小图标:18*18 大图标: 32*32 双击位图边缘可以修改位图大小 )
对应的类是: CImageList 调用该类的 Create() 函数创建图标列表
调用前要定义 CImageList 类的对象,在用该对象调用 成员函数 Create() 创建列表控件的图标列表 如: CImageList m_ilNormal; //图标列表
原型:
BOOL Create(
UINT nBitmapID, //位图ID
int cx, //位图的宽度
int nGrow, //图标列表是一个列表,列表是一个动态的集合没有固定的长度可以扩展, nGrow 就是扩展时一次扩展多少
COLORREF crMask //遮挡色(屏蔽色),遮挡色设置成什么颜色什么颜色就不显示出来
);
c.设置列表控件的图标列表
CListCtrl::SetImageList() //设置列表控件的图标列表
原型:
CImageList* SetImageList(
CImageList* pImageList, //图标列表的地址
int nImageList //图标列表的类型 LVSIL_NORMAL(大图标)/LVSIL_SMALL(小图标)/LVSIL_STATE(状态图标 不常用)
);
详细信息需要设置控件的列: 使用小图标
CListCtrl::InsertColumn()
原型:
int InsertColumn(
int nCol, //列的索引值(第一列为0,从0开始整数)
LPCTSTR lpszColumnHeading, //列的标题
int nFormat = LVCFMT_LEFT, //列的对齐方式
int nWidth = -1, //列的宽度
int nSubItem = -1 //与列相关联的子项的索引,缺省值为 -1 表示没有子项与列相关
);
d. 插入数据项
CListCtrl::InsertItem()
原型:
int InsertItem(
int nItem, //数据项的索引值(第一个数据项索引值为0,从0开始)
LPCTSTR lpszItem, //数据项的文本并且该文本只在第一列显示
int nImage, //图标的索引值(从0开始)
);
e. 给第一列以外的其它列插入数据
CListCtrl::SetItemText()
原型:
BOOL SetItemText(
int nItem, //行的索引第一个行是0,从0开始
int nSubItem, //列的索引 从0开始
LPTSTR lpszText //要设置的值
);
CListCtrl::DeleteAllItems() 情况所有的控件,在添加新控件时可以用来清空原有的所有控件
f. 修改列表控件的风格(显示方式)
可以借助于 CListCtrl 类从 CWnd 类继承过来的成员函数 ModifyStyle()
原型:
BOOL ModifyStyle(
DWORD dwRemove, //移除不要的风格
DWORD dwAdd, //需要添加的风格
UINT nFlags = 0 //一般给0即可
);
风格包括:
LVS_ICON 图标
LVS_LIST 列表
LVS_REPORT 详细信息( 报表 )
LVS_SMALLICON 小图标
利用 MFC 对话框数据交换技术 如果是要操作控件(调用控件的成员函数) 就要绑定成控件类型,如果是要获取控件的值就绑定成 值类型;
练习:
使用 CListCtrl 控件显示指定目录下的文件和目录 结合 CFileFind 类来编写
扩:
数值转换成 字符串类型(CString) 调用 CString::Format()
如:
int nLen = find.GetLength();//文件的大小
CString strLen;
strLen.Format(L"%d Byte",nLen); //数值转换成字符串
日期类型转换成字符串类型(CString) 调用 CString::Format()
如:
CTime t;
find.GetLastWriteTime(t); //获取文件的时间
CString sTime = t.Format("%Y年-%m月-%d日 %H:%M:%S"); //日期转换成字符串 参数是日期的格式
m_wndList.SetItemText(nItem,3,sTime);
如果想得到一个文件的路径可以通过添加文件时使用附加数据来保存其路径,虽然使用附加数据来保存路径这种方式可以,但在某些情况下会导致内存泄露
如文件非常多每点击一个文件就会分配内存保存其路径却没有释放内存所以会导致内存泄露,这时我们就可以使用另一种方式来保存文件的信息: CStringList 类
CStringList 字符串链表类:
使用:
1> 添加 CStringList 的成员变量
2> 清空链表 CStringList::RemoveAll();
3> 添加元素 CStringList::AddTail();尾部添加 CStringList::AddHead() 头部添加
4> 获取元素 CStringList::GetAt() 参数是一个迭代(就是数据的位置)
5> 获取数据的位置(迭代) CStringList::FindIndex() 根据元素的索引得到元素的位置
设置列表控件的背景图片
AfxOleInit() 初始化 OLE库
CListCtrl::SetBkImage(/* .bmp 图片的路径 */); // 注意在调用 SetBkImage()前必须确定 OLE库已经被初始化了否则 SetBkImage()函数无效
设置字体的背景色
CListCtrl::SetTextBkColor(/* 需要设置的颜色 */); // CLR_NONE 是透明色
7. 树控件 ( Tree Control ) 除根节点以外每个节点都有一个父节点
树控件的相关类:
CTreeCtrl 类, 其父类是 CWnd 类, 控件类, 主要在 对话框程序中使用
CTreeView 类, 其父类是 CCtrlView 类, 视图类, 主要在 文档视图程序中使用
树控件 和 树视图 的关系:
通过列表视图提供的 CTreeView::GetTreeCtrl() 函数 获取该视图客户区的控件,获取到的控件是属于树控件( CTreeCtrl )的,
这样就可以通过树控件所提供的成员函数来操作控件了;
树控件是除根节点以外每个节点都有一个父节点所以树控件不能像列表控件一样通过索引值去区分第几个数据项的,
所以树控件只能通过 Win32 句柄这种方式来标识节点的( 句柄 是句柄表的下标,句柄表的每一项里保存的是一块内存的地址 句柄->地址->内存 )
树控件的图标有两种状态 选中/非选中
树控件状态设置:
Has Buttons --- 设置节点前 扩展/不扩展 符号
Has Lines --- 设置节点之间虚线连接
Lines At Root --- 设置和根节点虚线连接
按照控件的对话框数据交换技术,要操作一个控件首先要让一个成员变量和控件进行绑定,绑定后就可以通过与控件绑定的成员变量操作该控件,
如果想要调用树控件所提供的函数也是先让树控件和成员变量进行绑定然后在通过成员变量调用树控件的成员函数;
树控件的位图一般是 16*16 (双击位图边缘可以修改位图大小)
树控件的初始化:
1> 创建图标列表
对应的类是: CImageList 类 调用该类的 Create() 函数创建图标列表
调用前要定义 CImageList 类的对象,在用该对象调用 成员函数 Create() 创建列表控件的图标列表 如: CImageList m_ilNormal; //图标列表
原型:
BOOL Create(
UINT nBitmapID, //位图ID
int cx, //位图的宽度
int nGrow, //图标列表是一个列表,列表是一个动态的集合没有固定的长度可以扩展, nGrow 就是扩展时一次扩展多少
COLORREF crMask //遮挡色(屏蔽色),遮挡色设置成什么颜色什么颜色就不显示出来
);
8. Tab 控件
End