Win32Notes


前言

目前在维护一个用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)

    所有消息先进系统队列,之后再由系统转发到各进程队列.
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    消息和消息队列的关系

  • 消息和消息队列的关系

    1. 当鼠标、键盘产生消息时,会将消息存放到系统的消息队列
    2. 系统会根据存放的消息,找到对应程序的消息队列
    3. 将消息投递到程序的队列中
  • 根据消息和消息队列之间使用关系,将消息分成两类:

    队列消息 - 消息的发送和获取,都是通过消息队列完成(消息进队列了)

    ​ 消息发送后,首先放入队列,然后通过消息循环,从队列当中获取

    ​ 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  //重绘前是否先擦除
    );
    

    消息处理步骤

    1. 开始绘图
  HDC BeginPaint(
  ​		HWND hWnd, //绘图窗口
  ​		LPPAINTSTRUCT lpPaint  //绘图参数的buff
  ); //返回绘图设备句柄HDC
  1. 正式绘图

  2. 结束绘图

  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

菜单资源的使用

  • 添加菜单资源(可视化图像界面)
  • 加载菜单资源
    1. 注册窗口类时设置菜单
    2. 创建窗口传参设置菜单
    3. 在主窗口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 - 画笔句柄

  • 画笔的使用

    1. 创建画笔

    2. 将画笔应用到DC中

    3. 绘图

    4. 取出DC中的画笔

      将原来的画笔,使用SelectObject函数,放入到设备DC中,就会将我们创建的画笔取出

    5. 释放画笔

      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 - 画刷句柄

  • 画刷的使用

    1. 创建画刷

      CreateSolidBrush - 创建实心画刷

      CreateHatchBrush - 创建纹理画刷

    2. 将画刷应用到DC中

      SelectObject

    3. 绘图

    4. 将画刷从DC中取出

      SelectObject

    5. 删除画刷

      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 - 位图句柄

  • 位图的使用

    1. 在资源中添加位图资源

    2. 从资源中加载位图LoadBitmap

    3. 创建一个与当前DC相匹配的DC(内存DC)

      HDC CreateCompatibleDC(
      	HDC hdc  //当前DC句柄使用(使用屏幕DC)
      ); //返回创建好的DC句柄
      

      当前DC是BeginPaint抓取到的dc,在当前窗口上绘制;内存DC,在内存虚拟的区域画图

    4. 将位图放入匹配的DC中 SelectObject

    5. 成像(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
      );
      
    6. 取出位图 SelectObject

    7. 释放位图 DeleteObject

    8. 释放匹配的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中的字体文件

字体的使用

  1. 创建字体
  2. 应用字体到DC - SelectObject
  3. 绘制文字 - DrawText / TextOut
  4. 取出字体 - SelectObject
  5. 删除字体 - 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(...);
}

对话框窗口:缺省函数 调用 自定义函数

缺省函数(…){

自定义函数

}

对话框原理

  • 对话框分类

    模式对话框 - 当对话框显示时,会禁止本进程其他窗口与用户交互操作。

    无模式对话框 - 在对话框显示后,其他窗口仍可以和用户交互操作

  • 对话框基本使用

    1. 对话框窗口处理函数
    2. 注册窗口类(不使用)
    3. 创建对话框
    4. 对话框关闭

对话框的窗口类是操作系统注册的,名为“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静态库的创建

    1. 创建一个静态库项目

      VS2019为例:直接添加静态库工程,或建立空项目,修改属性-常规-配置类型为静态库(lib)

    2. 添加库程序,源文件使用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,区别只是源文件的不同)

    1. 创建一个静态库项目
    2. 添加库程序,源文件使用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 动态库的特点

  • 特点:
  1. 运行时独立存在(运行时,是一个独立的进程)
  2. 源码不会链接到执行程序
  3. 使用时加载(使用动态库,必须使动态库执行)
  • 与静态库比较
  1. 由于静态库是将代码嵌入到使用程序中,多个程序使用时,会有多份代码,所以代码体积增大。动态库的代码只需要存一份,其他程序通过函数地址使用,所以代码体积小
  2. 静态库发生变化后,新的代码需要重新链接嵌入到执行程序中。动态库发生变化后,如果库中函数的定义(或地址)未变化,其他使用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是无法使用的。

  • 库程序导出 - 提供给使用者库中的函数等信息。以下两种方法:

    1. 声明导出:使用_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;
      }
      

      以上生成之后,动态库就制作完成了。

    2. 模块定义文件.def
      例如:

      LIBRARY DLLFunc //库
      EXPORTS  //库导出表
      DLL_Mul  @1  //导出的函数
      

在这里插入图片描述
在这里插入图片描述
03 动态库的调用

  • 隐式链接(操作系统负责使动态库执行)

    1. 头文件在函数原型的声明之前,增加_declspec(dllimport)

    2. 导入动态库的lib文件

    3. 在程序中使用函数

    4. 隐式链接的情况,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的调用
    
  • 显示链接(程序员自己负责使动态库执行)

    1. 定义函数指针类型 typedef

    2. 加载动态库

      HMODULE LoadLibrary(
      	LPCTSTR lpFileName  //动态库文件名或全路径
      );  //返回dll的实例句柄(就是HINSTANCE)
      
    3. 获取函数绝对地址

      FARPROC GetProcAddress(
      	HMODULE hModule,  //dll句柄,能够拿到动态库首地址
          LPCSTR lpProcName //函数名称
      ); //成功返回函数真实地址
      
    4. 使用函数

    5. 卸载动态库

      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. 线程都具有1个ID
    2. 每个线程都具有自己的内存栈
    3. 同一进程中的线程使用同一个地址空间
  • 线程的调度

    操作系统将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;
    }
    

    问题现象:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcgRuP6e-1638561740851)(C:\Users\hww\AppData\Roaming\Typora\typora-user-images\image-20211204021023324.png)]

    原因简要分析:

    执行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上
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值