Win32笔记
- 前言
- P1 1.01 应用程序分类
- P2 1.02 开发工具和库
- P3 1.03 第一个windows窗口
- P4 1.04 字符编码
- P5 2.01 注册窗口类
- P6 2.02 窗口创建
- P7 2.03 消息基础
- P8 2.04 创建消息
- P9 2.05 消息循环原理
- P10 3.01 消息队列
- P11 3.02 键盘消息
- P12 3.03 鼠标消息
- P13 3.04 定时器消息
- P14 3.05 菜单资源
- P15 4.01 图标资源、光标资源
- P16 4.02 字符串、加速键资源
- P17 4.03 绘图编程
- P18 4.04 GDI绘图对象
- P19 4.05 位图
- P20 4.06 文本绘制
- P21 5.01 对话框
- P22 5.02 静态库
- P23 5.03 动态库1
- P24 5.04 动态库2
- P25 6.01 线程
- P26 6.02 线程同步(1 原子锁)
- P27 6.03 线程同步(2 互斥)
- P28 6.04 线程同步(3 事件)
- P29 6.05 线程同步(4 信号量)
前言
目前在维护一个用MFC开发的软件,对其中很多东西都不得要领。MFC和SDK关系十分紧密,所以花了一个多星期的时间系统学习了一下win32的东西。由于个人能力不足,尽管MFC在使用过程中确实没Qt使用起来方便,但对C++的学习还是有不少帮助的。
以下记录学习过程中的部分笔记和代码,以备遗忘时查阅。
视频来源:https://www.bilibili.com/video/BV1Qb4y1o7u9?p=1
P1 1.01 应用程序分类
Windows应用程序分类
-
控制台程序Console
DOS程序,本身没有窗口,通过WIndows DOS窗口执行
-
窗口程序
拥有自己的窗口,可以与用户交互
-
库程序
存放代码、数据的程序,执行文件可以从中取出代码执行和获取数据
静态库程序:扩展名LIB,在编译链接程序时,将代码放入到执行文件中
动态库程序:扩展名DLL,在执行文件执行时从中获取代码
应用程序对比
-
入口函数
控制台程序 - main
窗口程序 - WinMain
动态库程序 - DllMain (有入口函数,不能独立运行,需依赖调用它的程序运行)
静态库程序 - 无入口函数
-
文件存在方式
控制台程序、窗口程序 - exe文件
动态库程序 - dll文件
静态库程序 - lib文件
P2 1.02 开发工具和库
编译工具
-
编译器CL.EXE 将源代码翻译成目标代码.obj
-
链接器LINK.EXE 将目标代码、库链接生成最终文件
-
资源编译器RC.EXE (.rc)将资源编译,最终通过链接器存入最终文件
路径:C:\Program Files(x86)\Microsoft Visual Studio 10.0\VC\bin
库和头文件
-
Windows库
kernel32.dll - 提供了核心的API,例如进程、线程、内存管理
user32.dll - 提供了窗口、消息等API
gdi32.dll - 绘图相关的API
路径
C:\WIndows\System32
-
头文件
不用管dll内部如何实现,但需要dll内部API的声明,所以有以下重要的头文件
windows.h - 所有windows头文件的集合
windef.h - windows数据类型
winbase.h - kernel32的API
wingdi.h - user32的API
winuser.h - user32的API
winnt.h - UNICODE的字符集支持
路径
C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include
相关函数
int WINAPI WinMain(
HINSTANCE hInstance, //当前程序的实例句柄,能找到当前进程所占的内存
HINSTANCE hPrevInstance, //当前程序前一个实例句柄 此参数已被废弃
LPSTR lpCmdLine, //命令行参数字符串
int nCmdShow //窗口的显示方式 只有3种 1.最大化 2.最小化 3.原样显示
);
//返回点击的按钮ID
int MessageBox(
HWND hWnd, //父窗口句柄
LPCTSTR lpText, //显示在消息框中的文字
LPCTSTR lpCaption, //显示在标题栏中的文字
UINT uType //消息框中的按钮、图标显示类型
);
句柄:一个可以找到内存的东西,但切记不是指针
HINSTANCE:看到不认识的数据类型,藐视它,因为它一般是基本数据类型的别名
LPSTR:就是char *
HAND:窗口句柄 一般以H开头的为句柄
阻塞函数
- 什么情况下阻塞
- 什么情况下解除阻塞
编译链接过程
-
编译环境准备
VCVARS32.bat
-
编译程序 - CL
CL.EXE -c xxx.c
-
链接程序 - LINK
LINK.EXE xxx.obj xxx.lib
-
执行
P3 1.03 第一个windows窗口
Contents目录
- 窗口创建过程
- 代码示例
窗口创建过程
- 定义WinMain函数
- 定义窗口处理函数(自定义,处理消息)
- 注册窗口类(向操作系统写入一些数据)
- 创建窗口(内存中创建窗口)
- 显示窗口(绘制窗口的图像)
- 消息循环(获取/翻译/派发消息)
- 消息处理
#include <windows.h>
//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch (msgID) {
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
//入口函数
int CALLBACK WinMain(_In_ HINSTANCE hIns, _In_opt_ HINSTANCE hPreIns, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
//注册窗口类
WNDCLASS wc = { 0 };
wc.cbClsExtra = 0; //申请缓冲区
wc.cbWndExtra = 0; //申请缓冲区
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //背景色
wc.hCursor = NULL; //默认光标
wc.hIcon = NULL; //默认图标 标题栏左上角
wc.hInstance = hIns;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = "Main"; //任意起
wc.lpszMenuName = NULL; //不创建菜单
wc.style = CS_HREDRAW | CS_VREDRAW; //垂直或水平方向有变化就重绘
//写入操作系统 即 注册窗口类
RegisterClass(&wc);
//在内存中创建窗口
HWND hWnd = CreateWindow("Main", "windowshww", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
//显示窗口
ShowWindow(hWnd, SW_SHOW); //SW_SHOW 原样显示
UpdateWindow(hWnd); //刷新窗口 可调可不调
//消息循环
MSG nMsg = { 0 };
while (GetMessage(&nMsg, NULL, 0, 0)) {
TranslateMessage(&nMsg); //翻译
DispatchMessage(&nMsg); //派发
}
return 0;
}
P4 1.04 字符编码
Contents目录
- 编码历史背景
- DBCS和UNICODE码
- 宽字节字符
- TCHAR
- 打印UNICODE字符
LPSTR == char* ; LPCSTR == const char* ;
LPWSTR == wchar_t* ; LPCWSTR == const wchar_t* ;
LPTSTR == TCHAR* ; LPCTSTR == const TCHAR*;
P5 2.01 注册窗口类
Contents
- 窗口类的概念
- 窗口类的分类
- 系统窗口类
- 全局及局部窗口类
窗口类的概念
-
窗口类是包含了窗口的各种参数信息的数据结构
在程序中是一个结构体,注册到系统中就叫窗口类
-
每个窗口都具有窗口类,基于窗口类创建窗口
-
每个窗口类都具有一个名称,使用前必须注册到系统
窗口类就是写到操作系统的一堆数据
窗口类的分类
-
系统窗口类
系统已经定义好的窗口类,所有应用程序都可以直接使用
-
应用程序全局窗口类
由用户自己定义,当前应用程序所有模块都可以使用
-
应用程序局部窗口类
由用户自己定义,当前应用程序中本模块(进程)可以使用
系统窗口类
不需要注册,直接使用窗口类即可,系统已经注册好了
例如:
按钮 - BUTTON
编辑框 - EDIT
HWND hWnd = CreateWindow(L"Button", L"windowshww", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
全局及局部窗口类
-
注册窗口类的函数
ATOM RegisterClass( CONST WNDCLASS *lpWndClass //窗口类的数据 ) //注册成功后,返回一个数字标识
-
注册窗口类的结构体
typedef struct _WNDCLASS{ UINT style; //窗口类的风格 WNDPROC lpfnWndProc; //窗口处理函数 int cbClsExtra; //窗口类的附加数据buff的大小 int cbWndExtra; //窗口的附加数据buff的大小 HINSTANCE hInstance; //当前模块的实例句柄 HICON hIcon; //窗口图标句柄 HCURSOR hCursor; //鼠标的句柄 HBRUSH hbrBackground; //绘制窗口背景的画刷句柄 LPCTSTR lpszMenuName; //窗口菜单的资源ID字符串 LPCTSTR lpszClassName; //窗口类的名称 }WNDCLASS, *PWNDCLASS;
-
窗口类的风格
应用程序全局窗口类的注册,需要在窗口类的风格中增加CS_GLOBALCLASS
例如:
WNDCLASS wc = {0};
wc.style = ...|CS_GLOABLCLASS;
应用程序局部窗口类:在注册窗口类时,不添加CS_GLOBALCLASS;
CS_HREDRAW - 当窗口水平变化时,窗口重新绘制
CS_VREDRAW - 当窗口垂直变化时,窗口重新绘制
CS_DBLCLKS - 允许窗口接收鼠标双击
CS_NOCLOSE - 窗口没有关闭按钮
P6 2.02 窗口创建
Contents
- 窗口创建
- 窗口创建执行过程
- 子窗口创建
窗口创建
CreateWindow / CreateWindowEx //非加强版与加强版 加强版多了第1个参数
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格 一般没什么用,给0
LPCTSTR lpClassName, //已经注册的窗口类名称
LPCTSTR lpWindowName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置
int y, //窗口左上角垂直坐标位置
int nWidth, //窗口的宽度
int nHeight,//窗口的高度
HWND hWndParent, //父窗口句柄
HMENU hMenu, //窗口菜单句柄
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时附加参数
)
窗口创建过程 CreateWindowEx
- 系统根据传入的窗口类名称,在应用程序局部窗口类中查找,如果找到执行2,如果未找到执行3.
- 比较局部窗口类与创建窗口时传入的HINSTANCE变量。如果发现相等,创建和注册的窗口类在同一模块(指进程),创建窗口返回。如果不相等,继续执行3.
- 在应用程序全局窗口类,如果找到,执行4,如果未找到,执行5
- 使用找到的窗口类的信息,创建窗口返回
- 在系统窗口类中查找,如果找到创建窗口返回,否则创建窗口失败
CreateWindowEx("Main"....hIns)
{
匹配查找窗口类
if(找到窗口类){
申请一大块内存,将窗口的数据信息存入这块内存
return 这块内存的句柄
}else{
return NULL;
}
}
子窗口的创建过程
- 创建时要设置父窗口的句柄
- 创建风格要增加WS_CHILD|WS_VISIBLE
P7 2.03 消息基础
Contents
- 消息的概念和作用
- 窗口处理函数
- 浅谈消息相关函数
消息的概念和作用
-
消息组成(windows平台下)
窗口句柄
消息ID
消息的两个参数(两个附带信息)
消息产生的时间
消息产生时的鼠标位置
-
消息的作用
当系统通知窗口工作时,就采用消息的方式派发给窗口(的窗口处理函数)
DispatchMessage(&nMsg)
{
nMsg.hwnd-->保存窗口数据的内存-->Wndproc;
//派发消息,即调用
WndProc(nMsg.hwnd, nMsg.message, nMsg.wParam, nMsg.lParam){
//回到自己的代码(可以处理函数)
}
}
窗口处理函数
-
每个窗口都必须具有窗口处理函数
LRESULT CALLBACK WindowProc( HWND hwnd, //窗口句柄 UINT uMsg, //消息ID WPARAM wParam, //消息参数 LPARAM lParam //消息参数 );
-
当系统通知窗口时,会调用窗口处理函数,同时将消息ID和消息参数传递给窗口处理函数。在窗口处理函数中,不处理消息时,使用缺省窗口处理函数。例如DefWindowProc。
浅谈消息相关函数
-
GetMessage - 获取本进程消息
BOOL GetMessage( LPMSG lpMsg, //存放获取到的消息buff HWND hWnd, //窗口句柄 UINT wMsgFilterMin, //获取消息的最小ID UINT wMsgFilterMax //获取消息的最大ID ); //lpMsg - 当获取到消息后,将消息的参数存放到MSG结构中 //hWnd - 获取到hWnd所指定窗口的消息 //wMsgFilterMin和wMsgFilterMax - 只能获取到由他们指定的消息范围内的消息,如果都为0,表示没有范围GetMessage - 获取本进程消息
-
TranslateMessage - 翻译消息。将按键消息,翻译成字符消息
BOOL TranslateMessage( CONST MSG *lpMsg //要翻译的消息的地址 );
检查消息是否是按键的消息,如果不是按键消息,不做任何处理,继续执行
P8 2.04 创建消息
Contents目录
- WM_DESTROY
- WM_SYSCOMMAND
- WM_CREATE
- WM_SIZE
- WM_QUIT
消息关注点:产生时间、附带的两个信息、一般用法
PostQuitMessage(0)给GetMessage埋雷,插入WM_QUIT消息
WM_DESTROY
- 产生时间:窗口被销毁时的消息
- 附带信息:wParam为0,lParam为0
- 一般用法:常用于在窗口被销毁之前,做相应的善后处理,例如资源、内存等
WM_SYSCOMMAND
-
产生时间:当点击窗口的最大化、最小化、关闭等
-
附带信息:wParam:具体点击的位置,例如关闭SC_CLOSE等
lParam:鼠标光标的位置 LOWORD(lParam)水平位置HIWORD(lParam)垂直位置
-
一般用法:常用在窗口关闭时,提示用户处理
//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch(msgID){
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_SYSCOMMAND:
if(wParam == SC_CLOSE){
int nRet = MessageBox(hWnd,"是否退出","Info",MB_YESNO);
if(nRet == IDNO) return 0;
}
//MessageBox(hWnd,"WM_SYSCOMMAND","Info",MB_OK);
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
/*
点击窗口的最大化、最小化、关闭按钮,都会触发WM_SYSCOMMAND消息。点击关闭时,触发WM_SYSCOMMAND消息,进入case对应的分支,该分支执行完,退出switch语句,执行DefWindowProc,该默认处理函数会销毁我们的窗口。关闭窗口后,会产生WM_DESTROY消息,再执行该消息对应的case语句,即埋雷关闭进程
*/
WM_CREATE
-
产生时间:在窗口创建成功但还未显示时(CreateWindow执行完,ShowWindow还没执行时)
-
附带信息:wParam为0,lParam为CREATESTRUCT类型的指针
通过这个指针可以获取CreateWindowEx中全部12个参数的信息
因为lParam是长整型,使用时,要强转为CREATESTRUCT类型指针
-
一般用法:常用于初始化窗口的参数、资源等等,包括创建子窗口等
void OnCreate(HWND hWnd, LPARAM lParam){
CREATESTRUCT* pcs = (CREATESTRUCT*)lParam;
MessageBox(NULL, "WM_CREATE", "Info", MB_OK);
CreateWindowEx(0,"Edit","hello",WS_CHILD|WS_VISIBLE|WS_BORDER,0,0,200,200,hWnd, NULL,0,NULL);
}
//CreateWindowEx的参数列表就是CREATESTRUCT
//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch(msgID){
case WM_CREATE:
OnCreate(hWnd, lParam);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_SYSCOMMAND:
if(wParam == SC_CLOSE){
int nRet = MessageBox(hWnd,"是否退出","Info",MB_YESNO);
if(nRet == IDNO) return 0;
}
//MessageBox(hWnd,"WM_SYSCOMMAND","Info",MB_OK);
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
WM_SIZE
-
产生时间:在窗口的大小发生变化后(最大化、最小化、双击标题栏、拖动边界改变大小,窗口创建成功到第一次显示)
-
附带信息:wParam为窗口大小变化的原因,lParam为窗口变化后的大小
LOWORD(lParam)变化后宽度 HIWORD(lParam)变化后高度
-
一般用法:常用于窗口大小变化后,调整窗口内各个部分的布局
WM_QUIT
- 产生时间:程序员发送
- 附带信息:wParam为PostQuitMessage函数传递的参数,lParam为0
- 一般用法:用于结束消息循环,当GetMessage收到这个消息后,会返回False,结束while处理,退出消息循环
P9 2.05 消息循环原理
Contents目录
- 消息循环的阻塞
- 发送消息
- 消息分类
消息循环的阻塞
-
GetMessage - 从系统获取本进程消息,将消息从系统中移除,阻塞函数。当系统无消息时,会等候下一条消息(抓到消息就返回,抓不到,就停在那)
-
PeekMessage - 以查看方式从系统获取消息,可以不将消息从系统移除,非阻塞函数。当系统无消息时,返回false,继续执行后续代码
BOOL PeekMessage( LPMSG lpMsg, //message information HWND hWnd, //handle to window UINT wMsgFilterMin, //first message UINT wMsgFilterMax, //last message UINT wRemoveMsg //移除标识 PM_REMOVE / PM_NOREMOVE ); while(1) { if(PeekMessgae(&nMsg, NULL, 0, 0, PM_NOREMOVE)){ //有消息 if(GetMessage(&nMsg, NULL, 0, 0)){ TranslateMessage(&nMsg); DispatchMessage(&nMsg); }else{ return 0; } }else{ //空闲处理 WriteConsole(g_Output,"OnIdle", strlen("OnIdle"), NULL, NULL); } }
发送消息
-
SendMessage - 发送消息,会等候消息处理的结果(打电话)
-
PostMessage - 投递消息,消息发送后立刻返回,不等侯消息执行的结果(发短信)
BOOL SendMessage/PostMessage( HWND hWnd, //消息发送的目的窗口 UINT Msg, //消息ID WPARAM wParam, //消息参数 LPARAM lParam //消息参数 ); WndProc(){ case WM_DESTROY: //PostQuitMessage(0); PostMessage(hWnd, WM_QUIT, 0, 0); }
消息分类
-
系统消息 - ID范围 0 - 0x03FF (程序员要么只负责发送,不负责处理,要么只负责处理,不负责发送)
由系统定义好的消息,可以直接在程序中使用
-
用户自定义消息 - ID范围 0x0400 - 0x7FFF(31743)(程序员自己发送,自己处理)
由用户自己定义,满足用户自己的需求。由用户自己发出消息,并响应处理
自定义消息宏:WM_USER
#define WM_MYMESSAGE 0x400 + 1001 //WM_USER + 1001 PostMessage(hWnd, WM_MYMESSAGE,1,2); //1,2为自定义附带消息,随便填 //自定义消息,用SendMessage也行 WndProc(...) { case WM_MYMESSAGE: OnMyMessage(hWnd, wParam, lParam); break; }
P10 3.01 消息队列
消息队列就是前文讲的抓消息的某个地方
Contents目录
- 消息队列的概念
- 消息队列分类
- 消息和队列关系
- 深谈GetMessage原理
- WM_PAINT消息
消息队列的概念
- 消息队列是用于存放消息的队列
- 消息在队列中先入先出
- 所有窗口程序都有自己的消息队列
- 程序可以从队列中获取消息
消息队列的分类
-
系统消息队列 - 由系统维护的消息队列。存放系统产生的消息,例如鼠标、键盘等
-
程序消息队列 - 属于每一个应用程序(主线程(每个进程的主线程))的消息队列。由应用程序(线程)维护(每个进程都有GetMessage)
所有消息先进系统队列,之后再由系统转发到各进程队列.
消息和消息队列的关系 -
消息和消息队列的关系
- 当鼠标、键盘产生消息时,会将消息存放到系统的消息队列
- 系统会根据存放的消息,找到对应程序的消息队列
- 将消息投递到程序的队列中
-
根据消息和消息队列之间使用关系,将消息分成两类:
队列消息 - 消息的发送和获取,都是通过消息队列完成(消息进队列了)
消息发送后,首先放入队列,然后通过消息循环,从队列当中获取
GetMessage - 从消息队列中获取消息
PostMessage - 将消息投递到消息队列
常见队列消息:WM_PAINT 键盘 鼠标 定时器
非队列消息 - 消息的发送和获取,是直接调用消息的窗口处理完成(消息没有进队列)
消息发送时,首先查找消息接收窗口的窗口处理函数,直接调用处理函数,完成消息
SendMessage - 直接将消息发送给窗口的处理函数,并等候处理结果(直接调用处理函数)
常见消息 - WM_CREATE WM_SIZE等
WM_CREATE不能进队列:WM_CREATE在窗口创建之后显示之前,GetMessage在ShowWindow后面,如果就进队列,没法抓到此消息(ShowWindow没执行,GetMessage更没执行)(也就是说,放进队列的消息,必须具备被立刻捕获的能力)
WM_QUIT必须进队列:WM_QUIT不仅队列,GetMessage就抓不到,抓不到就无法退出消息循环
深谈GetMessage
- 在程序(线程)消息队列查找消息,如果队列有消息,检查消息是否满足指定条件(HWND,ID范围),不满足条件就不会取出消息,否则从队列取出消息返回
- 如果程序(线程)消息队列没有消息,向系统质询是否有消息。如果系统队列的当前消息属于本程序,系统会将消息转发到程序消息队列中
- 如果系统消息队列也没有消息,检查当前进程的所有窗口需要重新绘制的区域,如果发现有需要重绘的区域,产生WM_PAINT消息,取得消息返回处理
- 如果没有重新绘制的区域,检查定时器,如果有到时的定时器,产生WM_TIME,返回处理执行
- 如果没有到时的定时器,整理程序的资源、内存等等
- GetMessage会继续等候下一条消息。PeekMessage会返回false,交出程序的控制权
- 注意:GetMessage如果获取到的是WM_QUIT,函数会返回false
WM_PAINT
-
产生时间:当窗口需要绘制的时候(由GetMessage发送)
-
附带信息:wParam为0,lParam为0
-
专职用法:用于绘图
窗口无效区域:需要重新绘制的区域
BOOL InvalidateRect( HWND hWNd, //窗口句柄 CONST RECT* lpRect, //区域的矩形坐标 BOOL bErase //重绘前是否先擦除 );
消息处理步骤
- 开始绘图
HDC BeginPaint(
HWND hWnd, //绘图窗口
LPPAINTSTRUCT lpPaint //绘图参数的buff
); //返回绘图设备句柄HDC
-
正式绘图
-
结束绘图
BOOL EndPaint(
HWND hWnd, //绘图窗口
CONST PAINTSTRUCT *lpPaint //绘图参数的指针BeginPain返回
)
WndProc(...)
{
case WM_PAINT:
OnPaint(hWnd);
}
void OnPaint(HWND hWNd)
{
PAINTSTRUCT ps = {0};
HDC hdc = BeginPain(hWnd, &ps);
TextOut(hdc, 100, 100, "hello", 5);
EndPaint(hWnd, &ps);
//以上绘制图的代码,必须放在处理WM_PAINT消息时调用
}
P11 3.02 键盘消息
Contents目录
- 键盘消息分类
- 字符消息
键盘消息分类
-
WM_KEYDOWN - 按键被按下时产生
-
WM_KEYUP - 按键被放开时产生
-
WM_SYSKEYDOWN - 系统键按下时产生 比如ALT F10
-
WM_SYSKEYUP - 系统键放开时产生
附带消息:
WPARAM - 按键的Virtual Key(重要,键码值)
LPARAM - 按键的参数,例如按下次数
字符消息(WM_CHAR)
-
TranslateMessage在转换WM_KEYDOWN消息时,对于可见字符可以产生WM_CHAR,不可见字符无此消息
-
附带消息
WPARAM - 输入的字符的ASCII字符编码值
LPARAM - 按键的相关参数
TranslateMessage(&nMsg) { if(nMsg.message != WM_KEYDOWN) return ...; /*根据nMsg.wParam(键码值)可以获知哪个按键被按下*/ if(不可见字符的按键) return...; /*查看CapsLock(大写锁定键)是否处于打开状态*/ if(打开){ PostMessage(nMsg.hWnd, WM_CHAR, 65,...); }else{ PostMessage(nMsg.hWnd, WM_CHAR, 97,...); } } WndProc(...) { case WM_CHAR: OnChar(hWnd, wParam); break; } void OnChar(HWND hWnd, WPARAM wParam) { char szText[256] = {0}; sprintf(szText, "WM_CHAR:wParam=%d\n", wParam); WriteConsole(g_Output, szText, strlen(szText), NULL, NULL); }
P12 3.03 鼠标消息
Contents目录
- 鼠标消息分类
- 鼠标基本消息
- 鼠标双击消息
- 鼠标滚轮消息
鼠标消息分类
-
基本鼠标消息
WM_LBUTTONDOWN - 鼠标左键按下
WM_LBUTTONUP - 鼠标左键抬起
WM_RBUTTONDOWN - 鼠标右键按下
WM_RBUTTONUP - 鼠标右键抬起
WM_MOUSEMOVE - 鼠标移动消息
附带消息:
wParam:其他按键的状态,例如Ctrl/Shift
lParam:鼠标的位置,窗口客户区坐标系
LOWORD x坐标位置
HIWORD y坐标位置
一般情况鼠标按下、抬起成对出现。在鼠标移动过程中,会根据移动速度产生一系列的WM_MOUSEMOVE消息。
-
双击消息
WM_LBUTTONDBLCLK - 鼠标左键双击
WM_RBUTTONDBLCLK - 鼠标右键双击
-
滚轮消息
WM_MOUSEWHEEL - 鼠标滚轮消息
WndProc(...)
{
case WM_LBUTTONDOWN:
OnLButtonDown(hWnd, wParam, lParam);
break;
case WM_LBUTTONUP:
OnLButtonUp(hWnd, wParam, lParam);
break;
case WM_MOUSEMOVE:
OnMouseMove(hWnd, wParam, lParam);
break;
}
void OnLButtonDown(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
char szText[256] = {0};
sprintf(szText, "WM_LBUTTONDOWN:其他按键状态:%d, X=%d, Y=%d\n",
wParam, LOWORD(lParam), HIWORD(lParam));//窗口客户区左上角为(0,0)
WriteConsole(g_Output, szText, strlen(szText), NULL, NULL);
}
void OnRButtonDown(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
char szText[256] = {0};
sprintf(szText, "WM_RBUTTONDOWN:其他按键状态:%d, X=%d, Y=%d\n",
wParam, LOWORD(lParam), HIWORD(lParam));
WriteConsole(g_Output, szText, strlen(szText), NULL, NULL);
}
void OnMouseMove(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
char szText[256] = {0};
sprintf(szText, "WM_MOUSEMOVE:其他按键状态:%d, X=%d, Y=%d\n",
wParam, LOWORD(lParam), HIWORD(lParam));
WriteConsole(g_Output, szText, strlen(szText), NULL, NULL);
}
鼠标双击消息
-
附带信息:
wParam - 其他按键的状态,如Ctrl/Shift
lParam - 鼠标的位置,窗口客户区坐标系
LOWORD(lParam) x坐标
HIWORD(lParam) y坐标
-
消息产生顺序
以左击双击为例:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
使用时需要在注册窗口类的时候添加CS_DBLCLKS风格
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
WndProc(...)
{
case WM_LBUTTONDBLCLK:
OnLButtonDblClk(hWnd, wParam, lParam);
break;
}
void OnLButtonDblClk(HWND hWnd, WPARAM wParam,LPARAM lParam)
{
char szText = "WM_LBUTTONDBLCLK";
sprintf();
}
鼠标滚轮消息
-
附带信息
wParam:
LOWORD -其他按键的状态
HIWORD - 滚轮的偏移量,通过正负值表示滚轮方向
正:向前滚动 负:向后滚动
lParam: 鼠标当前的位置,屏幕坐标系
LOWORD - x坐标
HIWORD - y坐标
-
使用
通过偏移量,获取滚轮的方向和距离
WndProc(...)
{
case WM_MOUSEWHEEL:
OnMouseWheel(hWnd, wParam, lParam);
break;
}
void OnMouseWheel(HWND hWnd, WPARAM wParam,LPARAM lParam)
{
short nDelta = HIWORD(wParam); //偏移量
char szText[256] = {0};
sprintf(szTxt, "WM_MOUSEWHEEL: nDetal=%d\n",nDelta);
WriteConsole(g_Output, szText, strlen(szText), NULL, NULL);
}
P13 3.04 定时器消息
Contents目录
- 定时器消息介绍
- 创建销毁定时器
定时器消息介绍
- 产生时间 : 在程序中创建定时器,当到达时间间隔时,定时器(其实是GetMessage)会向程序发送一个WM_TIMER消息。定时器的精度是毫秒,但是准确度很低。例如,设置时间间隔为1000ms,但是会在非1000ms到达消息
- 附带消息 :wParam 定时器ID lParam 定时器处理函数的指针
创建和销毁定时器
创建定时器
UINT_PTR SetTimer(
HWND hWnd, //定时器窗口句柄
UINT_PTR nIDEvent, //定时器ID
UINT uElapse, //时间间隔 ms
TIMERPROC lpTimerFunc //定时器处理函数指针(一般不使用,为NULL)
);//创建成功,返回非0
关闭定时器
BOOL KillTimer(
HWND hWnd, //定时器窗口句柄
UINT_PTR uIDEvent //定时器ID
);
P14 3.05 菜单资源
菜单是资源的一种
Contents目录
- 菜单分类
- 资源相关
- 菜单资源使用
- 命令消息处理
- 菜单项状态
- 上下文菜单
菜单分类
- 窗口的顶层菜单(菜单栏顶层菜单)
- 弹出式菜单(右键菜单、菜单栏点击顶层菜单后的下拉菜单)
- 系统菜单(点击左上角图标出现的菜单,系统默认提供)
HMENU类型表示菜单,ID表示菜单项
菜单是容器,里面包含一个一个的菜单项
资源相关
- 资源脚本文件:*.rc文件
- 编译器:RC.EXE
菜单资源的使用
- 添加菜单资源(可视化图像界面)
- 加载菜单资源
- 注册窗口类时设置菜单
- 创建窗口传参设置菜单
- 在主窗口WM_CREATE消息中利用SetMenu函数设置菜单
HMENU LoadMenu(
HINSTANCE hInstance, //handle to module
LPCTSTR lpMenuName //menu name or resource identifier
);//LoadMenu在本进程的内存中找到菜单的数据,返回保存菜单数据内存的句柄
//方式1
wc.lpszMenuName = (char*)IDR_MENU1; //创建菜单
//方式2
HMENU hMenu = LoadMenu(hIns, (char*)IDR_MENU1);
HWND hWnd = CreateWindowEx(0, "Main", "window", WS_OVERLAPPEDWINDOW, 100,
100, 500, 500, NULL, hMenu, hIns, NULL);
//方式3
WinProc(...)
{
case WM_CREATE:
OnCreate(hWnd);
break;
}
void OnCreate(HWND hWnd)
{
HMENU hMenu = LoadMenu(hIns, (char*)IDR_MENU1);
SetMenu(hWnd, hMenu);
}
命令消息(WM_COMMAND)处理
-
附带信息
wParam:
HIWORD - 对于菜单项为0
LOWORD - 菜单项的ID
lParam - 对于菜单为0
WndProc(...)
{
switch(msgID){
case WM_COMMAND:
OnCommand(hWnd, wParam);
break;
}
}
void OnCommand(HWND hWNd, WPARAM wParam)
{
switch(LOWORD(wParam)){
case ID_NEW:
MessageBox(hWnd,"新建被点击", "Info", MB_OK);
break;
case ID_EXIT:
MessageBox(hWnd,"退出被点击", "Info", MB_OK);
break;
case ID_ABOUT:
MessageBox(hWnd,"关于被点击", "Info", MB_OK);
break;
}
}
P15 4.01 图标资源、光标资源
Contents目录
- 图标资源
- 光标资源
- 字符串资源
图标资源(ico文件)
-
添加资源
注意图标的大小,一个图标文件中,可以有多个不同大小的图标
-
加载
HICON LoadIcon( HINSTANCE hInstance, //handle to application instance LPCTSTR lpIconName //name string or resource identifier ); //成功返回HICON句柄
-
设置
注册窗口类
wc.hIcon = LoadIcon(hIns, (char*)IDI_ICON1); //默认图标 标题栏左上角
光标资源
-
添加光标资源
光标的大小默认时32x32像素,每个光标有HotSpot,是当前鼠标的热点(箭头的尖的那个点)
-
加载资源
HCURSOR LoadCursor( HINSTANCE hInstance, LPCTSTR lpCursorName ); //hInstance可以为NULL,获取系统默认的Cursor
-
设置资源
在注册窗口时,设置光标
使用SetCursor设置光标(必须在WM_SETCURSOR消息中处理)
wc.hCursor = LoadCursor(hIns, (char*)IDC_CURSOR1); //设置客户区光标
HCURSOR SetCursor( HCURSOR hCursor //handle to cursor );
WM_SETCURSOR 消息参数
产生时间:只要光标移动,就会连续不断产生这个消息
wParam - 当前使用的光标句柄
lParam - LOWORD 当前区域的代码(Hit-Test code)
HTCLIENT 表示当前光标在客户区活动
HTCAPTION 表示当前光标在标题栏区活动
HIWORD - 当前鼠标消息ID
HINSTANCE g_hIns = 0; //窗口处理函数(自定义,处理消息) //实现程序运行过程中更改光标 LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) { switch (msgID) { case WM_SETCURSOR: HCURSOR hCursor1 = LoadCursor(g_hIns, (char*)IDC_CURSOR1); HCURSOR hCursor2 = LoadCursor(g_hIns, (char*)IDC_CURSOR2); if(LOWORD(lParam) == HTCAPTION) SetCursor(hCursor1); else SetCursor(hCursor2); return 0; //return 0很重要,否则默认处理会根据注册类更改调设置 } return DefWindowProc(hWnd, msgID, wParam, lParam); }
P16 4.02 字符串、加速键资源
步骤
-
添加字符串资源
添加字符串表,在表中增加字符串
-
字符串资源的使用
int LoadString( HINSTANCE hInstance, UINT uID, //字符串ID LPTSTR lpBuffer, //存放字符串的buff int nBufferMax //字符串长度 ); //成功返回字符串长度,失败0
char szText[256] = {0};
LoadString(hIns, IDS_WND, szText, 256);
HWND hWnd = CreateWindow("Main", szText, WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
使用字符串表,可以避免修改程序,只用修改字符串表就可以。尽量使用字符串资源
加速键资源
-
添加 资源添加加速键表,增加命令ID对应的加速键
-
使用
//加载加速键表 HACCEL LoadAccelerators( HINSTANCE hInstance, LPCTSTR lpTableName ); //返回加速键表句柄 //翻译加速键 int TranslateAccelerator( HWND hWnd, //处理消息的窗口句柄 HACCEL hAccTable, //加速键表句柄 LPMSG lpMsg //消息 ); //如果是加速键,返回非0
//加速键资源
/*
命令ID 修饰符 键 类型
ID_SLE_ACCEL Ctrl P VIRTKEY
*/
HACCEL hAccel = LoadAccelerators(hIns, (char*)IDR_ACCELERATOR1);
//消息循环
while(GetMessage(&nMsg, NULL, 0, 0))
{
//如果消息与加速键表找不到匹配项
if(!TranslateAccelerator(hWnd, hAccel, &nMsg)){
TranslateMessage(&nMsg);
DispatchMessage(&nMsg); //将消息交给窗口函数处理
}
}
WndProc(...)
{
case WM_COMMAND:
if(LOWORD(wParam)==ID_SLE_ACCEL){
}
}
在WM_COMMAND中相应消息,消息参数
wParam : HIWORD为1表示来自加速键,为0表示来自菜单项
LOWORD为命令ID(加速键ID,菜单项ID,统称命令ID)
lParam:为0
加速键总结
消息循环时,首先执行TranslateAccelerator将消息与加速键表匹配,若匹配成功,则是加速键消息,会发送WM_COMMAND消息。WndProc接收到WM_COMMAND消息,在识别命令ID,根据命令ID,执行相应操作。注意,消息ID与命令ID的区别
P17 4.03 绘图编程
Contents目录
- 绘图基础
- 基本图形绘制
- GDI绘图对象
绘图基础
-
绘图设备DC(Device Context), 绘图上下文/绘图描述表
-
HDC - DC句柄,表示绘图设备
-
GDI - Windows graphics device interface(Win32提供的绘图API)
-
颜色
计算机使用红、绿、蓝(RGB)
每一个点颜色是3个字节24位保存
16位:5,5,6
32位:8,8,8,8绘图或透明度
HDC hdc = BeginPaint(hWnd,..);//抓绘图设备这个存在于操作系统的画家,并告诉他在哪里绘图
/*开始画画*/
TextOut(hdc, 100, 100, "hello", ...);//命令hdc在具体位置绘制
//
EndPaint(hWnd,...);
-
颜色使用
COLORREF - 实际DWORD
例如:COLORREF nColor = 0;
-
赋值使用RGB宏
例如:nColor = RGB(0,0,255);
-
获取RGB值
GetRValue/GetGValue/GetBValue
例如:BYTE nRed = GetRValue(nColor);
基本图形绘制
-
SetPixel 设置指定点的颜色
-
线的使用(直线、弧线)
MoveToEx - 指明窗口当前点
LineTo - 从窗口当前点到指定点绘制一条直线
当前点:上一次绘图时的最后一点,初始为(0,0)点
-
封闭图形:能够用画刷填充的图形
Rectangle / Ellipse
COLORREF SetPixel( HDC hdc, //DC句柄 int X, //x坐标 int Y, //y坐标 COLORREF crColor //设置的颜色 ); //返回点原来的颜色
WndProc() { case WM_PAINT: OnPaint(hWnd); break } void OnPaint(HWND hWNd) { PIANTSTRUCT ps = {0}; HDC hdc = BeginPaint(hWnd, &ps); //DrawPit(hdc); //DrawLine(hdc); //绘制直线 //DrawRect(hdc); //绘制矩形 DrawEll(hdc); //绘制圆 } void DrawPit(HDC hdc) { for(int i = 0; i < 256; i++){ for(int j = 0; j < 256; j++){ SetPixel(hdc, i, j, RGB(255,0,0)); } } } void DrawLine(HDC hdc){ MoveToEx(hdc, 100, 100, NULL); LineTo(hdc, 300, 300); } void DrawRect(HDC hdc) { Rectangle(hdc, 100, 100, 300, 300); //左 、上、右、下 } void DrawEll(HDC hdc) { Ellipse(hdc, 100, 100, 300, 300); //圆的外接矩形 }
P18 4.04 GDI绘图对象
Contents目录
- 01 画笔
- 02 画刷
01 GDI绘图对象 - 画笔
-
画笔的作用
线的颜色、线型、线粗
HPEN - 画笔句柄
-
画笔的使用
-
创建画笔
-
将画笔应用到DC中
-
绘图
-
取出DC中的画笔
将原来的画笔,使用SelectObject函数,放入到设备DC中,就会将我们创建的画笔取出
-
释放画笔
HPEN CreatePen( int fnPenStyle, //画笔的样式 int nWidth, //画笔的粗细 COLORREF crColor //画笔的颜色 ); //创建成功返回句柄 //PS_SOLID-实心线,可以支持多个像素宽,其他线型只能是一个像素宽 HGDIOBJ SelectObject( HDC hdc, //绘图设备句柄 HGDIOBJ hgdiobj //GDI绘图对象句柄,画笔句柄 );//返回原来的GDI绘图对象句柄 //注意保存原来DC当中的画笔 BOOL DeleteObject( HGDIOBJ hObject //GDI绘图对象句柄,画笔句柄 ); //只能删除不被DC使用的画笔,所以释放前,必须将画笔从DC中取出
-
OnPaint(HWND hWnd)
{
PAINTSTRUCT ps = {0};
HDC hdc = BeginPaint(hWnd, &ps);
HPEN hPen = CreatePen(PS_SOLID,1,RGB(255,0,0));
HGDIOBJ nOldPen = SelectObject(hdc, hPen);
//绘图
DrawEll(hdc);
//绘制完毕
SelectObject(hdc, nOldPen);
DelectObject(hdc, nOldPen);
EndPaint(hWnd, &ps);
}
02 画刷
-
画刷相关
画刷 - 封闭图形的填充的颜色、图案
HBRUSH - 画刷句柄
-
画刷的使用
-
创建画刷
CreateSolidBrush - 创建实心画刷
CreateHatchBrush - 创建纹理画刷
-
将画刷应用到DC中
SelectObject
-
绘图
-
将画刷从DC中取出
SelectObject
-
删除画刷
DelectObject
-
OnPaint(HWND hWnd)
{
PAINTSTRUCT ps = {0};
HDC hdc = BeginPaint(hWnd, &ps);
HPEN hPen = CreatePen(PS_SOLID,1,RGB(255,0,0));
HGDIOBJ nOldPen = SelectObject(hdc, hPen);
//HBRUSH hBrush = CreateSolidBrush(RGB(0,255,0));
//HBRUSH hBrush = CreateHatchBrush(HS_CROSS, RGB(0,255,0));
HBRUSH hBrush = GetStockObject(NULL_BRUSH);
HGDIOBJ nOldBrush = SelectObject(hdc, hBrush);
//绘图
DrawEll(hdc);
//绘图注意,只要是绘制封闭图形,都会使用画刷进行填充,如果想不填充,必须使用透明画刷
//绘制完毕
SelectObject(hdc, nOldPen);
SelectObject(hdc, nOldBrush);
DelectObject(hdc, nOldPen);
//DelectObject(hdc, hBrush); 透明画刷不要delete
EndPaint(hWnd, &ps);
}
-
其他
可以使用GetStockObject函数获取系统维护的画刷、画笔等等。
如果不使用画刷填充,需要使用NULL_BRUSH参数,获取不填充的画刷。
GetStockObject返回的画刷不需要DeletObject。
P19 4.05 位图
位图(bmp)是GDI绘图对象之一
Contens目录
- 01 位图绘制
01 位图绘制
-
位图相关
光栅图形 - 记录图形中没一点的颜色等信息(如bmp)
矢量图形 - 记录图形算法、绘图指令等
HBITMAP - 位图句柄
-
位图的使用
-
在资源中添加位图资源
-
从资源中加载位图LoadBitmap
-
创建一个与当前DC相匹配的DC(内存DC)
HDC CreateCompatibleDC( HDC hdc //当前DC句柄使用(使用屏幕DC) ); //返回创建好的DC句柄
当前DC是BeginPaint抓取到的dc,在当前窗口上绘制;内存DC,在内存虚拟的区域画图
-
将位图放入匹配的DC中 SelectObject
-
成像(1:1)
BOOL BitBlt( HDC hdcDest, //目的DC(当前DC) int nXDest, //目标左上X坐标 int nYDest, //目标左上Y坐标 int nWidth, int nHeight, HDC hdcSrc, //源DC(内存DC) int nXSrc, //源左上X坐标 int nYSrc, //源左上Y坐标 DWORD dwRop //成像方法 SRCCOPY原样成像 );
缩放成像
BOOL StretchBlt( HDC hdcDest int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, HDC hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, DWORD dwRop );
-
取出位图 SelectObject
-
释放位图 DeleteObject
-
释放匹配的DC DeleteDC
-
void DrawBmp(HDC hdc)
{
//添加位图资源(不需要代码)
//
HBITMAP hBmp = LoadBitmap(g_hIns, (char*)IDB_BITMAP1);
//创建内存DC(内部构建一个虚拟区域,并且内存DC在虚拟区域绘图)
HDC hMemdc = CreateCompatibleDC(hdc);
//将位图数据送给内存DC,内存DC在虚拟区域中将位图绘制出来
HGDIOBJ nOldBmp = SelectObject(hMemdc, hBmp);
//将虚拟区域中绘制好的图形成像到窗口中
BitBlt(hdc, 100, 100, 48, 48, hMemdc, 0, 0, SRCCOPY);
StretchBlt(hdc, 200,200,96,96,hMemdc,0,0,48,48,SRCCOPY);
//
SelectObject(hMemdc, nOldBmp);
DeleteObject(hBmp);
DeleteDC(hMemdc);
}
void OnPaint(HWND hWnd)
{
PAINTSTRUCT ps = { 0 };
HDC hdc = BeginPaint(hWnd, &ps);
DrawBmp(hdc);
}
P20 4.06 文本绘制
文字的绘制
TextOut //将文字绘制在指定位置,单行绘制
int DrawText(
HDC hDC, //DC句柄
LPCTSTR lpString, //字符串
int nCount, //字符数量
LPRECT lpRect, //绘制文字的矩形框
UINT uFormat //绘制的方式
);
文字颜色和背景
- 文字颜色 : SetTextColor
- 文字背景色 : SetBkColor
- 文字背景模式 : SetBkMode(OPAQUE不透明 / TRANSPARENT透明)
字体
字体是GDI绘图对象
Windows常用的字体为TrueType格式的字体文件
字体名 - 标识字体类型
HFONT - 字体句柄
程序员创建字体依赖于C:\Windows\\Fonts中的字体文件
字体的使用
- 创建字体
- 应用字体到DC - SelectObject
- 绘制文字 - DrawText / TextOut
- 取出字体 - SelectObject
- 删除字体 - DeleteObject
//创建字体
HFONT CreateFont(
int nHeight, //字体高度
int nWeight, //字体宽度 一般只给定高度,宽度补0, 系统会自动匹配一个合适的宽度
int nEscapement, //字符串倾斜角度 以0.1°为单位
int nOrientation, //字符旋转角度 平面图时补0即可
int fnWeight, //字体的粗细
DWORD fdwItalic, //斜体
DWORD fdwUnderline, //字符下划线
DWORD fdwStrikeOut, //删除线
DWORD fdwCharSet, //字符集 DB2312
DWORD fdwOutputPrecision, //输出精度 用处不大,补0
DWORD fdwClipPrecision, //剪切精度 用处不大,补0
DWORD fdwQuality, //输出质量 用处不大,补0
DWORD fdwPitchAndFamily, //匹配字体 用处不大,补0
LPCTSTR lpszFace //字体名称
);
//在绘制坐标轴Y轴的标题时,倾斜角度nEscapement=900
//旋转角度nOrientation在二维平面图中不起作用,补0
void OnPaint(HWND hWnd)
{
PAINTSTRUCT ps = {0};
HDC hdc = BeginPaint(hWnd, &ps);
SetTextColor(hdc, RGB(255,0,0)); //设置字符串颜色
SetBkColor(hdc, RGB(0,255,0)); //设置字符串背景色,只适用在不透明背景模式
SetBkMode(hdc, TRANSPARENT); //设置透明背景时,SetBkColor就失效了
//HFONT hFont = CreateFont(30, 0, 450, 0, 900, 0, 1, 1, GB2312_CHARSET, 0, 0, 0, 0, "黑体");
HFONT hFont = CreateFont(30, 0, 0, 0, 900, 0, 1, 1, GB2312_CHARSET, 0, 0, 0, 0, "黑体");
HGDIOBJ nOldFont = SelectObject(hdc, hFont);
//char szText[] = "hello txt long long long long long long long LONG";
char szText[] = "hello txt";
TextOut(hdc, 100, 100, szText, strlen(szText));
RECT rc;
rc.left = 100;
rc.top = 150;
rc.right = 200;
rc.bottom = 200;
Rectangle(hdc,100,150,200,200);
//DrawText(hdc, szText, strlen(szText), &rc, DT_LEFT|DT_TOP|DT_WORDBREAK|DT_NOCLIP);
//DrawText(hdc, szText, strlen(szText), &rc, DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOCLIP);
DrawText(hdc, szText, strlen(szText), &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP);
//DT_WORDBREAK-多行绘制 DT_NOCLIP画不下可以超出区域
//DT_VCENTER与DT_WORDBREAK冲突,有DT_WORDBREAK,写了DT_VCENTER也不会垂直居中
//DT_VCENTER和DT_BOTTOM只适用于DT_SINGLELINE(单行绘制)
SelectObject(hdc, nOldFont);
DeleteObject(hFont);
EndPaint(hWnd, &ps);
}
P21 5.01 对话框
普通窗口:自定义函数调用缺省函数
//普通窗口处理消息
WndProc(...)
{
...
DefWindowProc(...);
}
对话框窗口:缺省函数 调用 自定义函数
缺省函数(…){
…
自定义函数
…
}
对话框原理
-
对话框分类
模式对话框 - 当对话框显示时,会禁止本进程其他窗口与用户交互操作。
无模式对话框 - 在对话框显示后,其他窗口仍可以和用户交互操作
-
对话框基本使用
- 对话框窗口处理函数
- 注册窗口类(不使用)
- 创建对话框
- 对话框关闭
对话框的窗口类是操作系统注册的,名为“Dialog”
对话框窗口处理函数(并非真正的对话框窗口处理函数)
INT CALLBACK DialogProc(
HWND hwndDlg, //窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //消息参数
LPARAM lParam //消息参数
);
/*
返回TRUE - 缺省处理函数不需要处理
返回FALSE - 交给缺省函数处理
不需要调用缺省对话框处理函数
*/
模式对话框
-
创建模式对话框
INT DialogBox( HINSTANCE hInstance, LPCTSTR lpTemplate, //对话框资源ID HWND hWndParent, //对话框父窗口 DLGPROC lpDialogFunc //自定义函数 ); //DialogBox是一个阻塞函数,只有当对话框关闭后,才会返回,继续执行后续代码 //返回值是通过EndDialog设置。
-
对话框的关闭
BOOL EndDialog( HWND hDlg, //关闭的对话框窗口句柄 INT_PTR nResult //关闭的返回值 ); //销毁对话框并解除阻塞 /* 关闭模式对话框,只能使用EndDialog,不能使用DestroyWindow等函数。nResult是DialogBox函数退出时的返回值 */
-
对话框的消息
WM_INITDIALOG - 对话框创建之后,显示之前,通知对话框窗口处理函数,可以完成自己的初始化相关的操作。
无模式对话框
-
创建对话框
HWND CreateDialog( HINSTANCE hInstance, LPCTSTR lpTemplate, //模板资源ID HWND hWndParent, //父窗口 DLGPROC lpDialogFunc //自定义函数 ); /* 非阻塞函数,创建成功返回窗口句柄,需要使用ShowWindow函数显示对话框 */
-
对话框的关闭
关闭时使用DestroyWindow销毁窗口,不能使用EndDialog关闭对话框
#include <windows.h>
#include "resource.h"
HINSTANCE g_hIns = 0;
INT CALLBACK DlgProc(HWND hwnddlg, UINT msgID, WPARAM wParam, LPARAM lParam) {
switch (msgID)
{
case WM_SYSCOMMAND:
if (wParam == SC_CLOSE) {
EndDialog(hwnddlg, 100);
}
break;
case WM_INITDIALOG:
MessageBox(hwnddlg, "WM_INITDIALOG", "Info", MB_OK);
break;
case WM_CREATE:
MessageBox(hwnddlg, "WM_CREATE", "Info", MB_OK);
break;
default:
break;
}
return FALSE; //return false将消息交给真正的对话框窗口处理函数的后续代码帮我们处理
}
INT CALLBACK NoDlgProc(HWND hWnd, UINT MsgID, WPARAM wParam, LPARAM Param) {
switch (MsgID)
{
case WM_SYSCOMMAND:
if (wParam == SC_CLOSE) {
DestroyWindow(hWnd);
}
default:
break;
}
return false;
}
void OnCommand(HWND hWnd, WPARAM wParam)
{
switch (LOWORD(wParam))
{
case ID_MODEL:
{
int nRet = DialogBox(g_hIns, (char*)IDD_DIALOG1, hWnd, DlgProc);
if (nRet == 100) {
MessageBox(hWnd, "successful", "Info", MB_OK);
}
}
break;
case ID_NOMODEL:
{
HWND hnodialog = CreateDialog(g_hIns, (char*)IDD_DIALOG1, hWnd, NoDlgProc);
ShowWindow(hnodialog, SW_SHOW);
}
break;
default:
break;
}
}
//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch (msgID) {
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COMMAND:
OnCommand(hWnd, wParam);
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}
//入口函数
int CALLBACK WinMain(_In_ HINSTANCE hIns, _In_opt_ HINSTANCE hPreIns, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
g_hIns = hIns;
//注册窗口类
WNDCLASS wc = { 0 };
wc.cbClsExtra = 0; //申请缓冲区
wc.cbWndExtra = 0; //申请缓冲区
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //背景色
wc.hCursor = NULL; //默认光标
wc.hIcon = NULL; //默认图标 标题栏左上角
wc.hInstance = hIns;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = "Main"; //任意起
wc.lpszMenuName = (char*)IDR_MENU1; //不创建菜单
wc.style = CS_HREDRAW | CS_VREDRAW; //垂直或水平方向有变化就重绘
//写入操作系统 即 注册窗口类
RegisterClass(&wc);
//在内存中创建窗口
HWND hWnd = CreateWindow("Main", "windowshww", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
//显示窗口
ShowWindow(hWnd, SW_SHOW); //SW_SHOW 原样显示
UpdateWindow(hWnd); //刷新窗口 可调可不调
//消息循环
MSG nMsg = { 0 };
while (GetMessage(&nMsg, NULL, 0, 0)) {
TranslateMessage(&nMsg); //翻译
DispatchMessage(&nMsg); //派发
}
return 0;
}
P22 5.02 静态库
Contents目录
- 01 静态库特点
- 02 C语言静态库
- 03 C++语言静态库
01 静态库特点
- 运行不存在
- 静态库源码被链接到调用程序中
- 目标程序的归档
02 C语言静态库
-
C静态库的创建
-
创建一个静态库项目
VS2019为例:直接添加静态库工程,或建立空项目,修改属性-常规-配置类型为静态库(lib)
-
添加库程序,源文件使用C文件
添加完成后,右击项目,点击build(生成,只编译链接)
//CLib.c int CLib_add(int add1, int add2) { return (add1 + add2); } int CLib_sub(int sub1, int sub2) { return (sub1 - sub2); }
build之后,静态库文件lib就做好了。
-
-
C静态库的使用
库路径设置:可以使用pragma关键字设置
#pragma comment(lib, “…/lib/clib.lib”)
注意 C静态库可以不使用头文件
在一个新的工程中,以下程序只编译是可以通过的,但是无法链接。无法链接的原因是链接器找不到函数的源码
int main() { int sum, sub; sum = CLib_add(5, 3); sub = CLib_sub(5, 3); printf("sum=%d, sub=%d\n", sum,sub); return 0; }
既然编译通过,链接器因为找不到源码链接无法通过,则可以通过pragma commet告诉链接器源码(即静态库)的具体位置。如下:
#include <stdio.h> #pragma comment(lib, "../Debug/CLib.lib") //告诉链接器到哪里抓源码 /* 静态库CLib.lib可以放置在任意位置,#pragma comment中,lib的位置可以使用相对路径(推荐),也可以使用绝对路径,前提是必须可以找到lib。 */ int main() { int sum, sub; sum = CLib_add(5, 3); sub = CLib_sub(5, 3); printf("sum=%d, sub=%d\n", sum,sub); return 0; }
03 C++语言静态库
-
C++静态库的创建(同C,区别只是源文件的不同)
- 创建一个静态库项目
- 添加库程序,源文件使用cpp文件
int CPPLib_add(int add1, int add2) { return (add1 + add2); } int CPPLib_sub(int sub1, int sub2) { return (sub1 - sub2); }
-
C++静态库的使用
库路径设置:可以使用pragma关键字设置
#pragma comment(lib, “…/lib/cpplib.lib”)
新建工程,不同于C静态库,如下代码,编译会报错,报错原因:C++静态库调用必须包含函数声明
#include <iostream> using namespace std; int main() { int sum, sub; sum = CPPLib_add(5, 3); sub = CPPLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; return 0; }
既然必须包含声明,则添加声明,如下,可编译通过,但依旧无法链接
#include <iostream> using namespace std; int CPPLib_add(int a, int b); int CPPLib_sub(int a, int b); int main() { int sum, sub; sum = CPPLib_add(5, 3); sub = CPPLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; return 0; }
同理,使用pragma comment告诉链接器源代码的位置,就可链接了。
#include <iostream> using namespace std; int CPPLib_add(int a, int b); int CPPLib_sub(int a, int b); //以上代码给编译器看的 #pragma comment(lib, "../Debug/CPPLib.lib") //给链接器看的 int main() { int sum, sub; sum = CPPLib_add(5, 3); sub = CPPLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; return 0; }
-
C++调用C静态库
按照以上C++调用C++静态库的步骤,取调用C静态库,如下代码,会报错(原因是C++编译器会给函数换名,实际运行时的函数名不是我们写的函数名)
#include <iostream> using namespace std; int CPPLib_add(int a, int b); int CPPLib_sub(int a, int b); int CLib_add(int a, int b); int CLib_sub(int a, int b); //以上代码给编译器看的 #pragma comment(lib, "../Debug/CPPLib.lib") #pragma comment(lib, "../Debug/CLib0.lib") //给链接器看的 int main() { int sum, sub; sum = CPPLib_add(5, 3); sub = CPPLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; sum = CLib_add(5, 3); sub = CLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; return 0; }
解决办法:函数声明前,添加extern “C”,目的是告诉C++编译器,该函数按照C风格来编译
#include <iostream> using namespace std; int CPPLib_add(int a, int b); int CPPLib_sub(int a, int b); extern "C" int CLib_add(int a, int b); extern "C" int CLib_sub(int a, int b); //以上代码给编译器看的 #pragma comment(lib, "../Debug/CPPLib.lib") #pragma comment(lib, "../Debug/CLib0.lib") //给链接器看的 int main() { int sum, sub; sum = CPPLib_add(5, 3); sub = CPPLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; sum = CLib_add(5, 3); sub = CLib_sub(5, 3); cout << "sum=" << sum << " ,sub=" << sub << endl; return 0; }
P23 5.03 动态库1
Contents目录
- 01 动态库特点
- 02 动态库创建
- 03 动态库的使用
- 04 动态库中封装类
01 动态库的特点
- 特点:
- 运行时独立存在(运行时,是一个独立的进程)
- 源码不会链接到执行程序
- 使用时加载(使用动态库,必须使动态库执行)
- 与静态库比较
- 由于静态库是将代码嵌入到使用程序中,多个程序使用时,会有多份代码,所以代码体积增大。动态库的代码只需要存一份,其他程序通过函数地址使用,所以代码体积小
- 静态库发生变化后,新的代码需要重新链接嵌入到执行程序中。动态库发生变化后,如果库中函数的定义(或地址)未变化,其他使用dll的程序不需要重新链接。
02 动态库的创建
-
创建动态库项目
-
添加库程序
//添加库程序 int CPPdll_add(int add1, int add2) { return add1 + add2; } int CPPdll_sub(int sub1, int sub2) { return sub1 - sub2; } int CPPdll_mul(int mul1, int mul2) { return mul1 * mul2; }
以上代码没有执行下面的导出步骤,build时只生成一个dll文件,没有生成配套的lib文件。这个dll是无法使用的。
-
库程序导出 - 提供给使用者库中的函数等信息。以下两种方法:
-
声明导出:使用_declspec(dllexport)导出函数(相对地址)
注意:动态库编译链接后,也会有lib文件,是作为动态库函数映射使用,与静态库不完全相同
_declspec(dllexport) int CPPdll_add(int add1, int add2) { return add1 + add2; } _declspec(dllexport) int CPPdll_sub(int sub1, int sub2) { return sub1 - sub2; } _declspec(dllexport) int CPPdll_mul(int mul1, int mul2) { return mul1 * mul2; }
以上生成之后,动态库就制作完成了。
-
模块定义文件.def
例如:LIBRARY DLLFunc //库 EXPORTS //库导出表 DLL_Mul @1 //导出的函数
-
03 动态库的调用
-
隐式链接(操作系统负责使动态库执行)
-
头文件在函数原型的声明之前,增加_declspec(dllimport)
-
导入动态库的lib文件
-
在程序中使用函数
-
隐式链接的情况,dll文件可以存放的路径(后4个不建议用):
(1)与执行文件同一个目录下(建议)
(2)当前工作目录(程序员调试用的路径,发布后就没有了,不建议用)
(3)Windows目录
(4)Windows/System32目录
(5)Windows/System
(6)环境变量PATH指定目录
#include <iostream> using namespace std; _declspec(dllimport)int CPPdll_add(int a, int b); _declspec(dllimport)int CPPdll_sub(int a, int b); _declspec(dllimport)int CPPdll_mul(int a, int b); #pragma comment(lib, "../Debug/CPPdll.lib") //通知链接器,到哪抓 编号和dll文件名("CPPdll.dll") int main() { int sum = CPPdll_add(5, 4); int sub = CPPdll_sub(5, 4); int mul = CPPdll_mul(5, 4); cout << "sum=" << sum << ", sub=" << sub << ", mul=" << mul << endl; return 0; } //以上就可完成dll的调用
-
-
显示链接(程序员自己负责使动态库执行)
-
定义函数指针类型 typedef
-
加载动态库
HMODULE LoadLibrary( LPCTSTR lpFileName //动态库文件名或全路径 ); //返回dll的实例句柄(就是HINSTANCE)
-
获取函数绝对地址
FARPROC GetProcAddress( HMODULE hModule, //dll句柄,能够拿到动态库首地址 LPCSTR lpProcName //函数名称 ); //成功返回函数真实地址
-
使用函数
-
卸载动态库
BOOL FreeLibrary( HMODULE hModule //dll实例句柄 );//释放内存,结束动态库的执行
-
P24 5.04 动态库2
01 显示链接
用动态库第一种制作方法(_declspec(dllexport)声明导出),dll内记录函数的名字警告了转换(如Func被转换成?Func@@YAHHH@Z)。所以下面的过程中,使用的是这个恶心的函数名。
#include <windows.h>
#include <iostream>
using namespace std;
//第一步:定义函数指针
typedef int(*ADD)(int m, int n);
typedef int(*SUB)(int m, int n);
typedef int(*MUL)(int m, int n);
int main()
{
//第二步:动态库进内存,获取句柄(同HMODULE)
HINSTANCE hDll = LoadLibrary("CPPdll.dll");
cout << "hDll:" << hDll << endl;
ADD myAdd = (ADD)GetProcAddress(hDll, "?CPPdll_add@@YAHHH@Z");
//?CPPdll_add@@YAHHH@Z 才是dll中函数的真实名字
cout << "myAdd:" << myAdd << endl;
SUB mySub = (SUB)GetProcAddress(hDll, "?CPPdll_sub@@YAHHH@Z");
cout << "myAdd:" << mySub << endl;
MUL myMul = (MUL)GetProcAddress(hDll, "?CPPdll_mul@@YAHHH@Z");
cout << "myAdd:" << myMul << endl;
int sum = myAdd(5, 4);
int sub = mySub(5, 4);
int mul = myMul(5, 4);
cout << "sum=" << sum << ", sub=" << sub << ", mul=" << mul << endl;
FreeLibrary(hDll);
return 0;
}
要想避免使用这个恶心的函数名,使用真实的函数名,在制作dll的时候,可以用前述第2种制作方法(模块定义文件.def)。
修改dll生成源文件如下:
//CPPdll.cpp
int CPPdll_add(int add1, int add2)
{
return add1 + add2;
}
int CPPdll_sub(int sub1, int sub2)
{
return sub1 - sub2;
}
_declspec(dllexport) int CPPdll_mul(int mul1, int mul2)
{
return mul1 * mul2;
}
在上面CPPdll.cpp同目录下添加模块定义文件,格式如下:
LIBRARY CPPdll
EXPORTS
CPPdll_add @1
CPPdll_sub @2
//这里只实验性的以.def文件方式导出CPPdll_add和CPPdll_sub的地址,CPPdll_mul还按照声明导出_declspec(dllexport)的方式。在调用的时候可以看到,CPPdll_add和CPPdll_sub可以使用这个真实函数名,但是CPPdll_mul不行,还得使用?CPPdll_mul@@YAHHH@Z才行
调用示例代码如下:
#include <windows.h>
#include <iostream>
using namespace std;
//第一步:定义函数指针
typedef int(*ADD)(int m, int n);
typedef int(*SUB)(int m, int n);
typedef int(*MUL)(int m, int n);
int main()
{
//第二步:动态库进内存,获取句柄(同HMODULE)
HINSTANCE hDll = LoadLibrary("CPPdll.dll");
cout << "hDll:" << hDll << endl;
ADD myAdd = (ADD)GetProcAddress(hDll, "CPPdll_add");
cout << "myAdd:" << myAdd << endl;
SUB mySub = (SUB)GetProcAddress(hDll, "CPPdll_sub");
cout << "myAdd:" << mySub << endl;
MUL myMul = (MUL)GetProcAddress(hDll, "?CPPdll_mul@@YAHHH@Z");
cout << "myAdd:" << myMul << endl;
int sum = myAdd(5, 4);
int sub = mySub(5, 4);
int mul = myMul(5, 4);
cout << "sum=" << sum << ", sub=" << sub << ", mul=" << mul << endl;
FreeLibrary(hDll);
return 0;
}
02 动态库封装类
-
在类名称前增加_declspec(dllexport)定义,例如:
//.h文件 类声明 class _declspec(dllexport) CMath{ int Add(int a, int b); int SUb(int a, int b); }
-
通常使用预编译开关切换类的导入导出定义,例如:
//.h文件 类声明 #ifdef DLLCLASS_EXPORTS #define EXT_CLASS _declspec(dllexport) //dll #else #define EXT_CLASS _declspec(dllimport) //使用者 #endif class EXT_CLASS CMath{ int Add(int a, int b); int SUb(int a, int b); }
//.cpp文件 函数实现 #define DLLCLASS_EXPORT #include "ClassDll.h" int CMath::Add(int add1, int add2) { return add1 + add2; } int CMath::Sub(int sub1, int sub2) { return sub1 - sub2; }
上面的.h和.cpp文件就是生成dll的全部源码。注意.cpp文件开头的**#define DLLCLASS_EXPORT**。build之后就可以生成dll和lib。
下面是调用dll的示例代码:
#include <iostream> using namespace std; #include "../ClassDll/ClassDll.h" #pragma comment(lib, "../Debug/ClassDll.lib") int main() { CMath math; int sum = math.Add(5, 6); int sub = math.Sub(5, 6); cout << "sum=" << sum << " sub=" << sub << endl; }
P25 6.01 线程
Contents目录
- 01 线程基础
- 02 创建线程
- 03 线程挂起/销毁
- 04 线程相关操作
01 线程基础
-
Windows线程是可以执行的代码的实例。系统是以线程为单位的调度程序。一个程序当中可以有多个线程,实现多任务的处理(主线程只有1个)
-
Windows线程特点
- 线程都具有1个ID
- 每个线程都具有自己的内存栈
- 同一进程中的线程使用同一个地址空间
-
线程的调度
操作系统将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程。
线程轮询:线程A —> 线程B ---->线程A …
02 创建线程
- 创建
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性,置空,已被废弃
SIZE_T dwStackSize, //线程栈的大小,以1M对齐,如1.02M,实际2M
LPTHREAD_START_ROUTINE lpStartAddress, //线程处理函数的函数地址
LPVOID lpParameter, //传递给线程处理函数的参数
DWORD dwCreationFlags, //线程的创建方式 1.立即执行(补0) 2.挂起
LPDWORD lpThreadId //创建成功,该变量接收线程的ID
); //创建成功,返回线程句柄
- 定义线程处理函数
DWORD WINAPI ThreadProc(
LPVOID lpParameter //创建线程时,传递给线程的参数
);
代码示例:
#include <windows.h>
#include <stdio.h>
DWORD CALLBACK TestProc1(LPVOID pParam)
{
const char* pseText = (const char*)pParam;
for (int i = 0; i < 100; i++) {
printf("%s\n", pseText);
Sleep(1000);
}
return 0;
}
//回调函数,自己写的函数自己不调用,系统调用
int main()
{
DWORD nID1 = 0, nID2 = 0;
const char* pszText1 = "********";
const char* pszText2 = "--------";
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText1, 0, &nID1);
HANDLE hThread2 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText2, 0, &nID2);
//子线程执行的时候,若主线程结束,内存等会被回收,子线程也会消失
getchar();
return 0;
}
03 线程挂起和销毁
挂起(休眠)
//挂起
DWORD SuspendThread(
HANDLE hThread //handle to thread
);
唤醒
//唤醒挂起的线程
DWORD ResumeThread(
HANDLE hThread //handle to thread
);
对创建线程时的代码进行一点改动,挂起/唤醒代码示例如下:
#include <windows.h>
#include <stdio.h>
DWORD CALLBACK TestProc1(LPVOID pParam)
{
const char* pseText = (const char*)pParam;
for (int i = 0; i < 100; i++)
{
printf("%s\n", pseText);
Sleep(1000);
}
return 0;
}
//回调函数,自己写的函数自己不调用,系统调用
int main()
{
DWORD nID1 = 0, nID2 = 0;
const char* pszText1 = "********";
const char* pszText2 = "--------";
//0 立即执行 CREATE_SUSPENDED挂起
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText1, 0, &nID1);
HANDLE hThread2 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText2, CREATE_SUSPENDED, &nID2);
//初始状态,线程1唤醒,线程2挂起
//子线程执行的时候,若主线程结束,内存等会被回收,子线程也会消失
getchar();
//按Enter之后,将两个线程挂起/唤醒状态交换
SuspendThread(hThread1);
ResumeThread(hThread2);
getchar();
return 0;
}
销毁线程
-
结束指定线程
BOOL TerminateThread( HANDLE hThread, //handel to thread DWORD dwExitCode //exit code 退出码,无实际意义,随便填 ); //主线程内杀死指定的子线程
-
结束函数所在的线程
VOID ExitThread( DWORD dwExitCode //exit code for this thread.退出码,无实际意义,随便填 ); //本函数子线程内使用,结束子线程(子线程自杀)
04 线程相关操作
-
获取当前线程的ID - GetCurrentThreadId()
-
获取当前线程的句柄 - GetCurrentThread
-
等候单个句柄有信号(重要)
VOID WaitForSingleObject( HANDLE handle, //可等候句柄buff的地址 DWORD dwMilliseconds //最长等候时间(ms) INFINITE ); /* 当句柄有信号,该函数立刻返回,无信号时,该函数会阻塞,一直等到句柄有信号,但最多等dwMilliseconds的时间,就会返回。INFINITE,等候时间无限长 */
线程句柄是可等候的句柄。
可等候句柄:必须具有有信号和无信号两种状态
-
同时等候多个句柄有信号
DWORD WaitForMultipleObjects( DWORD nCount, //句柄数量 CONST HANDLE* lpHandles, //句柄buff的地址,是一个存放所有句柄的数组名 BOOL bWaitAll, //等候方式 DWORD dwMilliseconds //等候时间 ); /* bWaitAll - 等候方式 TRUE - 表示所有句柄都有信号,才结束等候 FALSE - 表示句柄中只要有1个有信号,就结束等候 */
线程执行过程中,处于无信号状态;线程执行结束后,处于有信号状态。
示例代码:
DWORD CALLBACK TestProc1(LPVOID pParam)
{
const char* pseText = (const char*)pParam;
for (int i = 0; i < 100; i++)
{
printf("%s\n", pseText);
Sleep(1000);
}
return 0;
}
//回调函数,自己写的函数自己不调用,系统调用
int main()
{
DWORD nID1 = 0, nID2 = 0;
const char* pszText1 = "********";
const char* pszText2 = "--------";
//0 立即执行 CREATE_SUSPENDED挂起
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText1, 0, &nID1);
//TestProc1持续100s
/*
WaitForSingleObject(hThread1, 5000);
阻塞5s后,返回,继续往下执行
*/
/*
WaitForSingleObject(hThread1, INFINITE);
永久阻塞在此,下面代码无法执行
*/
HANDLE hThread2 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText2, 0, &nID2);
getchar();
return 0;
}
P26 6.02 线程同步(1 原子锁)
临界资源:多个线程操作同一个资源的时候,称资源为临界资源。
01 原子锁
-
相关问题
多个线程对同一个数据进行原子操作(指运算符操作),会产生结果丢失。比如执行++运算时。
-
错误代码分析:
当线程A执行g_value++时,如果线程切换时间正好是线程A将值保存到g_value之前,线程B继续执行g_value++,那么当线程A再次被切换回来之后,会将原来线程A保存的值保存到g_value上,线程B进行的加法操作被覆盖。
-
使用原子锁函数
InterlockedIncrement InterlockedDecrement InterlockedCompareExchange InterlockedExchange ...
原子锁的实现:直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问。
-
局限性:只能对运算符操作进行原子锁,并且需要记忆大量函数。加原子锁极大降低了运行效率,但保证了准确性,并且在所有的加锁机制中,原子锁的效率是最高的。
#include <windows.h>
#include <stdio.h>
long g_value = 0;
DWORD CALLBACK TestProc1(LPVOID pParam)
{
for (int i = 0; i < 1000000; i++) {
//g_value++;
InterlockedIncrement(&g_value);
}
return 0;
}
DWORD CALLBACK TestProc2(LPVOID pParam)
{
for (int i = 0; i < 1000000; i++) {
//g_value++;
InterlockedIncrement(&g_value);
}
return 0;
}
int main()
{
DWORD nID = 0;
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, TestProc1, NULL, 0, &nID);
hThread[1] = CreateThread(NULL, 0, TestProc2, NULL, 0, &nID);
WaitForMultipleObjects(2, hThread, true, INFINITE);
printf("%d\n", g_value);
return 0;
}
过程分析:
上图是g_value++对应的3条汇编指令。假设此时g_value的值是0。在执行A线程的时候,可能执行完add eax, 1,操作系统分配给A线程的片时间段到了,操作系统会保存现场后,去执行B线程,此时g_value的值依旧是0,eax寄存器中值是1。开始执行B线程的g_value++时,g_value的值还是0,如果B线程执行完完整的3条汇编指令后,g_value的值是1。再去执行A线程,操作系统恢复现场,eax寄存器值为1,再执行第3条汇编指令,将eax中的1赋值给g_value。g_value的值还是1。也就是说,g_value++在A、B两个线程各执行1次,但g_value从初始值0变成了1,而不是2。
为了避免这个问题,对g_valuej进行加1操作,不使用g_value++(即不使用++)操作符,而是使用原子锁函数InterlockedIncrement(&g_value)。原子锁函数的原理简述如下:在A线程中,执行函数InterlockedIncrement(&g_value)函数时,首先会对g_value所在的内存进行上锁,执行完加一操作后,会对内存进行解锁。假设A线程使用原子锁函数对g_value进行加一操作,但加一操作未执行完,CPU就转而执行线程B,线程B使用同样的原子锁函数尝试对g_value进行加一操作,但由于从A线程转而执行B线程之前,A线程对内存进行了上锁操作,B线程无法操作g_value所在的内存,所以B线程会阻塞在原子锁函数的地方,一直等到内存解锁为止。这样就避免了操作符存在的问题。
P27 6.03 线程同步(2 互斥)
02 互斥
-
相关的问题
多线程下代码或资源的共享使用
-
互斥的使用
(1)创建互斥
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性,无用参数,置空 BOOL bInitialOwner, //初始的拥有者 true/false LPCTSTR lpName //命名 可任意起一个名 ); //创建成功则返回互斥句柄 互斥句柄是可等候的句柄 /* 互斥特性1: 1.任何时间点上,只能有一个线程拥有互斥 2.互斥具有独占性和排他性 3.没有互斥的线程只能等拥有互斥的线程扔掉互斥 */ /* 互斥特性2: 当任何线程不拥有互斥,互斥句柄有信号;当有线程拥有互斥,互斥句柄无信号 */
(2)等候互斥
WaitFor… 互斥的等候遵循谁先等候谁先获取
(3)释放互斥
BOOL ReleaseMutex( HANDLE hMutex //handle to mutex );
(4)关闭互斥句柄
CloseHandle
问题示例代码:
#include <windows.h> #include <stdio.h> DWORD CALLBACK TestProc1(LPVOID pParam) { const char* pseText = (const char*)pParam; while (1) { //printf("%s\n", pseText); //Sleep(1000); for (int i = 0; i < strlen(pseText); i++) { printf("%c", pseText[i]); Sleep(125); } printf("\n"); } return 0; } DWORD CALLBACK TestProc2(LPVOID pParam) { const char* pseText = (const char*)pParam; while (1) { //printf("%s\n", pseText); //Sleep(1000); for (int i = 0; i < strlen(pseText); i++) { printf("%c", pseText[i]); Sleep(125); } printf("\n"); } return 0; } int main() { DWORD nID1 = 0, nID2 = 0; const char* pszText1 = "********"; const char* pszText2 = "--------"; //0 立即执行 CREATE_SUSPENDED挂起 HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText1, 0, &nID1); HANDLE hThread2 = CreateThread(NULL, 0, TestProc2, (LPVOID)pszText2, 0, &nID2); getchar(); return 0; }
问题现象:
原因简要分析:
执行A线程的for循环时,for循环还未执行结束,转而执行B线程的for循环,B线程同样未结束,再转而执行A线程的for循环,导致在窗口中打印的字符交叉显示(注意A、B线程的打印的目标字符串是“*************************”和“---------”)。
解决办法:使用互斥句柄给线程中的for循环和打印\n加锁
#include <windows.h>
#include <stdio.h>
HANDLE g_hMutex = 0;
DWORD CALLBACK TestProc1(LPVOID pParam)
{
const char* pseText = (const char*)pParam;
while (1) {
//printf("%s\n", pseText);
//Sleep(1000);
/*
等待g_hMutex有信号,一旦有信号,解除阻塞,并拥有互斥,互斥变为无信号状态
*/
WaitForSingleObject(g_hMutex, INFINITE);
for (int i = 0; i < strlen(pseText); i++) {
printf("%c", pseText[i]);
Sleep(125);
}
printf("\n");
/*释放互斥的时刻,任何线程不拥有互斥,互斥有信号,其他线程可拥有它*/
ReleaseMutex(g_hMutex);
}
return 0;
}
DWORD CALLBACK TestProc2(LPVOID pParam)
{
const char* pseText = (const char*)pParam;
while (1) {
//printf("%s\n", pseText);
//Sleep(1000);
/*
等待g_hMutex有信号,一旦有信号,解除阻塞,并拥有互斥,互斥变为无信号状态
*/
WaitForSingleObject(g_hMutex, INFINITE);
for (int i = 0; i < strlen(pseText); i++) {
printf("%c", pseText[i]);
Sleep(125);
}
printf("\n");
/*释放互斥的时刻,任何线程不拥有互斥,互斥有信号,其他线程可拥有它*/
ReleaseMutex(g_hMutex);
}
return 0;
}
int main()
{
/*
第2个参数false:创建互斥的主线程不拥有互斥。一般情况下,是子线程之间互斥
此时任何线程不拥有此互斥,互斥处于有信号状态
*/
g_hMutex = CreateMutex(NULL, false, NULL);
DWORD nID1 = 0, nID2 = 0;
const char* pszText1 = "********";
const char* pszText2 = "--------";
//0 立即执行 CREATE_SUSPENDED挂起
HANDLE hThread1 = CreateThread(NULL, 0, TestProc1, (LPVOID)pszText1, 0, &nID1);
HANDLE hThread2 = CreateThread(NULL, 0, TestProc2, (LPVOID)pszText2, 0, &nID2);
getchar();
return 0;
}
正常现象:
执行过程简要描述:假设线程A拥有互斥,在执行线程A的for循环时,即使for循环没有执行完,CPU就轮询到B线程,B线程也只能阻塞在WaitForSingleObject(g_hMutex, INFINITE)
-
互斥与原子锁的比较
原子锁只能对运算符进行加锁,互斥可以对一段代码进行加锁。原子锁能解决的问题,互斥都能解决,互斥能解决的问题,原子锁不一定能解决。但原子锁效率比互斥高。
P28 6.04 线程同步(3 事件)
原子锁和互斥使用加锁机制,线程之间互相排斥
事件和信号量实现的线程之间协调工作的关系
03 事件
-
相关问题
线程之间的通知问题
-
事件的使用
(1)创建事件
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性 BOOL bManualReset, //事件重置(复位)方式,TRUE手动,FALSE自动 BOOL bInitialState, //事件初始状态, TRUE有信号 LPCTSTR lpName //事件命名 );//若创建成功,则返回事件句柄 事件句柄是可等候句柄(事件句柄有无信号,由程序员控制) /* 事件重置(复位):事件从有信号变为无信号,叫做复位(与之相对的是,事件从无信号变为有信号,叫做触发) */
(2)等候事件
WaitForSingleObject / WaitForMultipleObjects
(3)触发事件(将事件设置为有信号状态)
BOOL SetEvent( HANDLE hEvent //handle to event );
(4)复位事件(将事件设置为无信号状态)
BOOL ResetEvent( HANDLE hEvent //handle to event );
(5)关闭事件 CloseHandle
-
小心事件的死锁
//线程1 { WaitForSingleObject(Event1); //A SetEvent(Event2); //B } //线程2 { WaitForSingleObject(Event2); //C SetEvent(Event1); //D } /* 要A代码通过阻塞,需要D代码执行,D代码执行需要C代码通过阻塞,C代码通过阻塞需要B代码执行,B代码执行需要A代码通过阻塞。循环一圈,就是说,要A代码通过阻塞,前提是需要A代码通过阻塞,矛盾,死锁 */
示例代码:
#include <windows.h>
#include <stdio.h>
HANDLE g_hEvent = 0; //接收事件句柄
DWORD CALLBACK PrintProc(LPVOID pParam)
{
while (1) {
WaitForSingleObject(g_hEvent, INFINITE);
//如果创建Event的时候,使用的是自动复位,自动复位发生在通过阻塞后
ResetEvent(g_hEvent);
printf("*************\n");
/*
如果main中创建事件为g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
则此处代码应为:
WaitForSingleObject(g_hEvent, INFINITE);
//自动复位下,隐含ResetEvent(g_hEvent);
printf("*************\n");
*/
}
return 0;
}
DWORD CALLBACK CtrlProc(LPVOID pParam)
{
while (1) {
Sleep(1000);
SetEvent(g_hEvent);
}
return 0;
}
int main()
{
//手动重置,初始无信号
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD nID = 0;
HANDLE hThread[2] = { 0 };
//线程1负责打印东西
hThread[0] = CreateThread(NULL, 0, PrintProc, NULL, 0, &nID);
//线程2负责控制线程1每一秒打印一次
hThread[1] = CreateThread(NULL, 0, CtrlProc, NULL,0, &nID);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(g_hEvent);
return 0;
}
P29 6.05 线程同步(4 信号量)
04 信号量
-
相关问题
类似于事件,解决通知的相关问题。但提供一个计数器,可以设置次数。
-
信号量的使用
(1)创建信号量
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoneAttributes, //安全属性 LONG lInitialCount, //初始化信号量数量 LONG lMaximumCount, //信号量最大值 设置计数器数量不能超过这个值 LPCTSTR lpName //命名 );//若创建成功,返回信号量句柄 信号量句柄是可等候句柄 /* 当WaitFor信号量时,若信号量数量(次数)不为0,WaitFor函数会解除阻塞,但信号量的次数为减1.若为0,WaitFor会阻塞。 也就是说,信号量的句柄在计数器不为0的时候,有信号,在计数器为0的时候,无信号 */
(2)等候信号量
WaitFor… 每等候通过一次,信号量的信号减1,直到为0阻塞
(3)给信号量指定计数值
BOOL ReleaseSemaphore( HANDLE hSemaphore, //信号量句柄 LONG lReleaseCount, //释放数量 也就是新的计数值 LPLONG lpPreviousCount //接收释放前原来信号量的数量,可以为NULL ); /* 假如当前信号量计数值为5,WaitFor消耗3次后,计数值为2,此时调用ReleaseSemaphore函数重新设置计数值为5,最后一个参数将接收重新设置计数值前的计数值,即2 */
(4)关闭句柄 CloseHandle
示例代码:
HANDLE g_hSema = 0;
DWORD CALLBACK TestProc(LPVOID lParam)
{
while (1) {
WaitForSingleObject(g_hSema, INFINITE);
printf("***********\n");
}
return 0;
}
int main()
{
g_hSema = CreateSemaphore(NULL, 3, 10, NULL);
DWORD nID = 0;
HANDLE hThread = CreateThread(NULL, 0, TestProc, NULL, 0, &nID);
getchar();
ReleaseSemaphore(g_hSema, 5, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(g_hSema);
return 0;
}
/*
代码执行流程:
主线程创建子线程后,主线程阻塞在getchar()函数,子线程立即执行。由于信号量初始计数值为3,子线程的while循环执行3次后,信号量计数值变为0,第4次进入while循环时,WaitForSIngleObject阻塞。这时按下键盘Enter键,主线程的getchar()解除阻塞并返回,执行ReleaseSemaphore函数,重新设置计数值为5,由于计数值不为0,信号量变为有信号状态,所以子线程的WaitFor解除阻塞,继续执行5次循环,再次阻塞在WaitFor上
*/