【Win32】初识Win32编程

简介: 【Win32】初识Win32编程

补发:发表于2021-11-08

链接

Windows编程

应用程序分类

  • 控制台程序Console

    • DOS程序,本身没有窗口,通过Windows DOS窗口执行。(DOS是操作系统预留的)
  • 窗口程序

    • 拥有自己的窗口,可以与用户交互。
  • 库程序

    • 存放代码、数据的程序、执行文件可以从中取出代码执行和获取数据
    • 静态库程序:扩展名LIB,在编译链接程序时,将代码放入到执行文件中。
    • 动态库程序:扩展名DLL,在执行文件时从中获取代码 。

    静态库中的代码是直接嵌入到你的项目中,而动态库中的内容是通过地址来找到。

    • 静态库程序无法执行,也就是说它最终生成的文件无法进入内存。
    • 动态库程序有入口函数,可以执行。但是它不能独立运行。谁调动态库里面的东西,它就依附于谁。

应用程序对比

  • 入口函数

    • 控制台程序-main
    • 窗口程序-WinMain
    • 动态库程序-DllMain
    • 静态库程序-无入口函数
  • 文件存在方式

    • 控制台程序、窗口程序-EXE文件
    • 动态库程序-DLL文件
    • 静态库程序-LIB文件

编译工具

  • 编译器CL.EXE,将源代码编译成目标代码.obj。
  • 链接器LINK.EXE,将目标代码、库链接生成最终文件。
  • 资源编译器RC.EXE,(.rc)将资源编译,最终通过链接器存入最终文件

库文件和头文件

库文件

  • kernel32.dll-提供核心的API,消息进程,线程,内存管理等。
  • user32.dll-提供了窗口、消息等API。
  • gdi32.dll-绘图相关的API

头文件

  • windows.h——所有windows头文件的集合
  • windef.h——windows数据类型
  • winbase.h——kernel32的API
  • wingdi.h——user32的API
  • winnt.h——UNICODE字符集支持

相关函数

int WINAPI wWinMain(
    HINSTANCE hInstance,//当前程序的实例句柄,找到你当前进程所占据的那块内存
    HINSTANCE hPrevInstance,//当前程序前一个示例句柄,废弃了
    PWSTR pCmdLine, //命令行参数字符串
    int nCmdShow//窗口的显示方式
);

暂时可以将句柄理解成,句柄是用来找到内存的东西,但绝对不是指针。


int MessageBox(
  [in, optional] HWND    hWnd,//父窗口句柄
  [in, optional] LPCTSTR lpText,//显示在消息框中的文字
  [in, optional] LPCTSTR lpCaption,//显示在标题栏中的文字
  [in]           UINT    uType//消息框中的按钮、图标显示了类型
);//返回点击的按钮ID

能够将程序暂停在这里,说明它是个阻塞函数。它执行,可能不会立即返回。


如何理解分析一个阻塞函数
1.这个函数什么情况下阻塞。

2.这个函数什么情况下解除阻塞返回。


看到数据类型以H开头,多半就是个句柄。


程序编译过程

  • 可以直接用vs集成好的环境。image-20211029171632365
  • 编译程序-CL,CL.EXE -C xxx.c,生成obj文件
  • 链接程序-LINK,LINK.EXE xxx.obj xxx.lib
  • 执行生成的exe文件

  • 编写资源的文件,.rc资源脚本文件
  • 编译rc文件,RC.EXE
  • 将资源链接到程序中,LINK.EXE

.res文件和.obj文件统称为目标文件(中间文件,因为不是最终文件)


编译链接过程:将代码转换为机器语言,将生成的res文件和obj文件加上使用的库链接到一起,整合出一个exe文件,这是用编译器所感受不到的。

image-20211029172712321


窗口创建过程

  • 定义WinMain函数
  • 定义窗口的处理函数(自定义,消息处理)
  • 注册窗口类(向操作系统中写入一些数据)
  • 创建窗口(内存中创建窗口)
  • 显示窗口(绘制窗口的图像)
  • 消息循环(获取/翻译/派发消息)
  • 消息处理

第一个windows窗口

#include<windows.h>

//窗口处理函数(自定义、处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
    return DefWindowProc(hWnd, msgID, wParam, lParam);
}

//入口函数
int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPerIns, LPSTR lpCmdLine, 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 = "myWindow";
    wc.lpszMenuName = NULL;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    //将上面赋的这些值全部写入操作系统
    RegisterClass(&wc);

    //在内存中创建窗口
    HWND hWnd = CreateWindow("myWindow", "menu", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
    //显示窗口
    ShowWindow(hWnd, SW_SHOW);
    //再画一遍(刷新窗口)
    UpdateWindow(hWnd);
    //消息循环
    MSG nMsg = { 0 };
    while (GetMessage(&nMsg,NULL,0,0))
    {
        TranslateMessage(&nMsg);
        DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理

    }

    return 0;
}

窗口有无与进程退不退没有关系。

Unit04字符编码

编码历史背景

  • ASC——7位代表一个字符
  • ASCII——8位代表一个字符
  • DBCS——单双字节混合编码,没有同一标准,存在解析问题。
  • UNICODE——字符集,有多种编码方式,一般windows只的是utf-16(所有的字符无论中文汉子还是英文字母,都按两个字符编码。),linux utf-8,有统一标准,不存在解析问题。

宽字节数据类型

这个数据类型下的所有字符, 都占2个字节。

wchar_t每个字符占2个字节,wchar_t实际上是unsigned short类型,定义时,需要增加‘L’,通知编译器按照双字节编译字符串,采用UNICODE编码。

需要使用支持wchar_t函数操作宽字节字符串,例如:
wchar_t* s1= L"123456";

wprintf(L"%s\n",s1);


有操作char类型字符串的函数,肯定就有对应操作宽字节字符串的函数。


#include<stdio.h>
#include<windows.h>
int main(void)
{
    const wchar_t* s1 = L"hello";
    printf("%d\n", wcslen(s1));//5,有效字符个数
    return 0;
}

TCHAR

如果定义了unicode宏,该字符串为wchar类型,反之为char 类型。

打印UNICODE字符

UNICODE字符打印,wprintf对UNICODE字符打印支持不完善。

在windows下使用WriteConsole打印UNICODE字符。

GetStdHandle获取标准句柄(标准输入句柄、标准输出句柄、标准错误句柄)。

WriteConsole(标准输出句柄,pszText,wcslen(pszText),NULL,NULL);

下面的使用案例

    short width = LOWORD(lParam);
    short height = HIWORD(lParam);
    char szText[256] = { 0 };
    sprintf_s(szText, "WM_SIZE:宽度%d,高度:%d\n", width, height);
    WriteConsole(g_HOUTPUT, szText, strlen(szText), NULL, NULL);

创建项目时候为什么要改成多字节字符集?

如果项目的属性选择是UNICODE字符集,编译器会自动给你增加一个UNICODE的宏定义。反之,则不会。


系统调用函数的参数类型

(对已有的类型进行重命名)

LPSTR => char*
LPSTR => const char*
LPWSTR => wchar_t*
LPCWSTR => const wchar_t*
   
用的多的是这两个
LPTSTR => TCHAR 
LPCTSTR => const TCHAR*

注册窗口类

窗口类的概念

  • 窗口类是包含了窗口的各种参数信息的数据结构。
  • 每个窗口都具有窗口类,基于窗口类创建窗口。
  • 每个窗口类都具有一个名称,使用前必须注册到系统。

在操作系统内核里存着就叫窗口类,在程序里存着就叫窗口类。


窗口类的分类

  • 系统窗口类

    • 系统已经定义好的窗口类,所有应用程序都可以直接使用。
    • 不需要注册,直接使用窗口类即可。系统已经注册好了。
    • 例如:按钮-BUTTON,编辑框-EDIT
  • 应用程序全局窗口类

    • 由用户自己定义,当前应用程序所有模块都可以使用。
  • 应用程序局部窗口类

    • 由用户自己定义,当前应用程序中本模块可以直接使用。

全局及局部窗口类

注册窗口类的函数

(ATOM——unsigned short)

ATOM RegisterClass(

​ CONST WNDCLASS *lpWndClass//窗口类的数据);

//注册成功后 ,返回一个数字标识。(0失败,非0成功。)

image-20211030091746959


style窗口类风格

应用程序全局窗口类的注册,需要在窗口类的风格中添加CS_GLOBALCLASS。

应用程序局部类窗口类注册,无需添加如上风格。

不建议使用全局窗口类——因为局部窗口类能完成全局窗口类的功能,并且全局窗口类可能会产生冗余。

CS_HREDRAW ——当窗口水平变化时,窗口重新绘制
CS_VREDRAW ——当窗口垂直变化时,窗口重新绘制
CS_DBLCLKS ——允许窗口接收鼠标双击
CS_NOCLOSE ——窗口没有关闭按钮

窗口创建

窗口创建

CreateWindow / CreateWindowEx

image-20211030092735421

CreateWindow内部是如何实现的

  • 系统(CreateWindows函数内部)根据传入的窗口类名称,在应用程序局部窗口类中查找,如果找到执行2 ,没找到执行3。
  • 比较局部窗口与创建窗口时传入的HINSTANCE变量。如果有发现相等。创建和注册类在同一模块,创建窗口返回。如果不相等,继续执行3。
  • 在应用程序全局窗口类,如果找到,执行4, 没找到执行5。
  • 使用找到的窗口类信息,创建窗口返回。
  • 在系统窗口类中查找,如果找到创建窗口返回,否则创建窗口失败。
#include<windows.h>

//窗口处理函数(自定义、处理消息)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
    switch (msgID)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        break;
    }
    return DefWindowProc(hWnd, msgID, wParam, lParam);
}

//入口函数
int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPerIns, LPSTR lpCmdLine, 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 = "myWindow";
    wc.lpszMenuName = NULL;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    //将上面赋的这些值全部写入操作系统
    RegisterClass(&wc);

    //在内存中创建窗口
    HWND hWnd = CreateWindow("myWindow", "menu", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL);
    //显示窗口
    ShowWindow(hWnd, SW_SHOW);
    //再画一遍(刷新窗口)
    UpdateWindow(hWnd);
    //消息循环
    MSG nMsg = { 0 };
    while (GetMessage(&nMsg, NULL, 0, 0))
    {
        TranslateMessage(&nMsg);
        DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理

    }

    return 0;
}

子窗口创建过程

  • 创建时要设置父窗口句柄
  • 创建风格要增加WS_CHILD | WS_VISBLE

(根据注册的窗口类,来创建多个窗口。)

(个人理解发现:注册的窗口可以复用(覆盖创建一个新的))。

HWND hChild1 = CreateWindowEx(0, "Child", "C1", WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, 0, 0, 200, 200, hWnd, NULL, hIns, NULL);

HWND hChild2 = CreateWindowEx(0, "Child", "C2", WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, 200, 0, 200, 200, hWnd, NULL, hIns, NULL);

消息基础

消息的概念和作用

  • 消息组成(windows平台下)

    • 窗口句柄
    • 消息ID
    • 消息的两个参数(两个附带信息)
    • 消息产生的时间
    • 消息产生时的鼠标位置
  • 消息的作用

    • 当系统通知窗口工作时,就采用消息的方式(DispatchMessage)派发给(调用)窗口的窗口处理函数(将MSG的前四个信息传递给消息处理函数)。
    • 每一个窗口都有窗口处理函数

MSG结构体接收消息

结构体定义如下:

//对应解释同上消息组成
typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
#ifdef _MAC
    DWORD       lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

DispatchMessage如何找到窗口处理函数

nMsg.hwnd->保存窗口数据的内存->找到对应的窗口处理函数->WndProc
    
回到你自己定义的消息处理函数->传递参数->处理消息
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
    
    传递这四个参数,不用关系后两个

窗口处理函数

每个窗口都必需有窗口处理函数,只要基于窗口类创建窗口,就肯定要有个窗口处理函数。

窗口处理依照如下结构定义

LRESULT CALLBACK WindowProc(
    HWND hWnd;//窗口句柄
    UINT uMsg;//消息ID
    WPARAM wParam;//消息参数
    LPARAM lParam;//消息参数
);

当系统通知窗口时,(DispatchMessage)会调用窗口处理函数,同时将消息ID和消息参数传递给窗口处理函数。

在窗口处理函数中,不处理的消息使用缺省窗口处理函数

例如:DefWindowProc(可以给各种消息做默认处理)


消息循环中的相关函数(浅谈)

GetMessage-到系统的某个地方抓本进程的消息

函数原型如下:

BOOL GetMessage(
    LPMSG lpMsg,//存放获取到消息的BUFF,
    HWND hWndp;//窗口句柄,要是定为NULL,将会抓取本进程中所有窗口中的消息
    UNIT wMsgFilterMin,//获取的最小ID
    UNIT wMsgFilterMax//获取消息的最大ID
    //最后两个参数都为0,就是不管ID有多大,只要是本进程的消息都抓过来
);

其中后三个参数可以限制抓取消息的范围,如果设置为NULL,0,0那其实就是没有进行限制,只要是本进程的消息我都把它抓过来。

GetMessage的返回值

消息WM_QUIT会使GetMessage返回0,从而中终止消息接收。

PostQuitMessage(0);会在进程中扔出WM_QUIT这个消息,get后从而使得消息循环终止。


TranslateMessage-翻译消息——它可不是什么消息都翻译。

将按键(可见字符按键,a~z)消息翻译成字符消息。

所以进入到它的内部, 它所做的第一件事就是检查这个消息是否合法,是否是它要翻译的消息类型。

如果不是按键类型消息,不做任何处理,继续执行。

函数原型如下:

BOOL TranslateMessage
{
    CONST  MSG* lpMsg;//要翻译的消息地址    
}

DispatchMessage-派发消息(调用对应窗口的消息处理函数)

函数原型如下

LRESULT DispatchMessage(
    CONST MSG* lpmsg//要派发的消息
);

常见消息

如何学习一个消息

1.掌握这个消息的产生时间

  1. 分析每个消息附带的两个信息
  2. 这个消息可以用来干什么(一般用法)。

WM_DESTORY

  • 产生时间:窗口被销毁时产生
  • 附带信息:wParam:为0,lParam:为0
  • 一般用法:常用于在窗口被销毁前,做相应的善后处理,例如资源、内存等(该回收回收,该释放释放。)。

WM_SYSCOMMAND

  • 产生时间:当点击窗口最大化,最小化,关闭等。
  • 附带信息:

    • wParam:具体点击的位置,例如关闭SC_CLOSE等,
    • lParam:鼠标光标的位置(这个不重要,我们只需要知道点没点就行,具体在哪个位置其实无所谓(具体情况具体使用)),LOWORD(lParam);水平位置,HIWORD(lParam);垂直位置。(高两字节传纵坐标,低两字节传横坐标。)
  • 一般用法:常用在窗口关闭时,提示用户处理。

WM_CREATE

  • 产生时间:在窗口创建成功但还没显示时。
  • 附带信息:

    • wParam:为0
    • lParam:为CREATESTRUCT类型的指针(强转成这个类型再用),通过这个指针可以获取CreatWindowEx中全部12个参数的信息。
  • 一般用法:常用于初始化窗口函数、资源等等,包括创建子窗口等。

WM_SIZE

  • 产生时间:在窗口的大小发生变化后。
  • 附带信息:

    • wParam:窗口大小变化的原因。
    • lParam:窗口变化后的大小

      • LOWORD(lParam)变化后的宽度
      • HIWORD(lParam)变化后的高度
  • 一般用法:常用于窗口大小发生变化后,调整窗口内各个部分的布局。

WM_QUIT

  • 产生时间:程序员发送。
  • 附带信息:

    • wPram:PostQuitmessage函数传递的参数。
    • lParam:0。
  • 一般用法:用于结束消息循环,当GetMessage收到这个消息后,会返回FALSE,结束while处理,退出消息循环。

这个消息不用我们去处理,进不去我们定义的窗口处理函数,GetMessage()返回了0,无法进入循环获取消息。


WM_PAINT

  • 产生时间:当窗口需要绘制的时候 or( GetMessage没消息可抓的时候,详情请看下面。)
  • 附带信息:(没用)

    • wParam: 0
    • lParam:0
  • 专职用法:就是用于绘图。

第一个WM_PAINT是系统发送的,当第一次创建窗口。

也就是说产生WM_SIZE消息的同时肯定回产生WM_PAINT消息,重新绘制。

相关函数

  • 窗口无效区域:需要重新绘制的区域。
  • 调用这个函数,让窗口需要重新绘制,GetMessAge会发送WM_PAINT消息,注意,是谁发送消息。 (就可以理解说这个函数告诉GetMessage说,这个窗口需要绘制了,GetMessage发送消息到消息队列,然后转发到对应进程的消息队列中,开始执行。)
BOOL InvalidateRect(
  [in] HWND       hWnd,//窗口句柄
  [in] const RECT *lpRect,//区域的矩形坐标,对窗口的哪一部分进行重新绘制
  [in] BOOL       bErase//重绘前是否先擦除
);

调试技巧

设置一个全局变量
    HANDLE g_HOUTPUT = 0;//接收标准输出句柄
增加一个dos窗口
    AllocConsole();
    g_HOUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);

dos窗口只能接收字符串类型

sprintf转,writeconsole打印


消息循环的原理

消息循环的阻塞

  • GetMessage-从系统获取消息,将消息从系统中移除,阻塞函数。当系统无消息时,会等候下一条消息。
对人来说消息是一直存在的,但是对于CPU来说(速度接近光速),消息不是经常有的,所以会经常发生阻塞。这样程序的效率就不高,从而引出下面这个函数。
  • PeekMessage-以查看的方式从系统中获取消息,可以不将消息从系统出移除,非阻塞函数。当系统无消息时,返回FALSE,继续执行后续代码。

函数原型如下:

(前四个参数同GetMessage)

最后一个参数是,是否赋予它抓取消息的能力,一般是不给它的,也就是填写

BOOL PeekMessageA(
  [out]          LPMSG lpMsg,
  [in, optional] HWND  hWnd,
  [in]           UINT  wMsgFilterMin,
  [in]           UINT  wMsgFilterMax,
  [in]           UINT  wRemoveMsg
);

也就是说,更好的流程是,先派PeekMessage去侦查是否有消息,有就告诉GetMessage让它来处 理。没有就不要派Get去了,因为它会一直在那里等着消息的出现。

例如

while (1)
    {
        if (PeekMessage(&nMsg, NULL, 0, 0, PM_NOREMOVE))
        {
            //有消息-判断是否是WM_QUIT
            if (GetMessage(&nMsg, NULL, 0, 0))
            {
                TranslateMessage(&nMsg);
                DispatchMessage(&nMsg);
            }
            else
            {
                return 0;
            }
        }
        else
        {
            //没有消息——空闲处理
            WriteConsole(g_HOUTPUT, "空闲ing\n", strlen("空闲ing"), NULL, NULL);
        }
    }

发送消息

Windows平台上的消息,都是它们两个造出来的。

  • SendMessage-发送消息,会等候消息处理的结果。
  • PostMessage-投递消息,消息发出后立刻返回,不等候消息执行结果。

函数原型如下

LRESULT SendMessage(
  [in] HWND   hWnd,//消息发送的目的创建
  [in] UINT   Msg,//消息ID
  [in] WPARAM wParam,//消息参数
  [in] LPARAM lParam//消息参数
);

这四个参数就是一个消息的前四个参数,剩下的两个参数函数内部以某种手段自加来获取。

其中,sendmessage的消息扔到哪去,我们不知道,在之后的课程中会补充。postmessage的消息会扔到getmessage能接收到的区域)

消息分类

  • 系统消息-ID范围0~0x03FF

    • 由系统定义好的消息,可以在程序中直接使用。
    • 程序员只负责一头,要么发送不用处理,要么处理不用发送。
  • 用户自定义的消息-ID范围0x0400(WM_USER) - 0x7FFF(31743)

    • 由用户自己定义,满足用户自己的需求。由用户自己发出消息,并响应处理。
    • 由程序员,自己定制,自己发送,自己处理。
    • 自定义消息宏:WM_USER(叫什么都行)

例如:

定义消息名称

#define WM_MYMESSAGE WM_USER+1001

发送,在哪发都可以,附加消息,你自己的,附加什么都行。

PostMessage/SendMessage(hWnd, WM_MYMESSAGE, 1, 2);

消息队列

消息队列的概念

  • 消息队列是用于存放消息的队列。
  • 消息在队列中先进先出。
  • 所有窗口都具有消息队列。
  • 程序(GetMessage())可以从队列中获取消息。

消息队列的分类

  • 系统消息队列——由系统维护的消息队列。存放系统产生的消息,例如鼠标、键盘等。
  • 程序消息队列——属于每个应用程序的(线程)的消息队列。由应用程序(线程)维护。
每个进程都有一个消息队列,都有GetMessage(),在本进程的消息队列中抓取消息。

细节解释:
所有进程产生的消息都先进系统消息队列,操作系统会每个一段时间,将消息转发到各个进程中去。所以才能在本进程中抓到本进程的消息。

如何做到正确转发:
消息的第一个参数是窗口句柄,保存窗口数据的内存,通过它可以找到当前程序实例句柄,找到本进程对应内存,从而正确转发。

SendMessage既没有扔到系统消息队列里,也没有扔到进程消息队列中。


消息和消息队列的关系

  • 消息和消息队列的关系

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

    • 队列消息-消息的发送和获取,都是通过消息队列完成。

      • 消息发送后,首先放入队列,然后通过消息循环,从队列中获取。
      • GetMessage-从消息队列中获取消息
      • PostMessage-将消息投递到消息队列
      • 常见的消息队列:WM_PAINT、键盘、鼠标、定时器
    • 非队列消息-消息的发送和获取,是直接调用消息的窗口处理函数完成。

      • 消息发送时,首先找到消息接收窗口的窗口处理函数,直接调用处理函数,完成消息。

        • SendMessage-直接将消息发送窗口的处理函数,并等待处理结果
        • 常见消息:WM_CREATE(它是必须不能进队列的,否则就没办法把它抓出来了)、WM_SIZE等。

GetMessage

  • 在程序(线程)消息队列查找消息,如果队列有消息,检查消息是否满足指定条件(HWND,ID范围),不满足条件就不会取出消息,否则从队列取出消息返回。
  • 如果程序(线程)消息队列没有消息,会向系统消息队列获取(找系统要,发起请求)属于班本程序的消息(之前说系统会定时分发属于对应进程的消息,如果这个时间没到,但是有的进程没消息了,会打破这个时间,提前分发。)。如果系统队列的当前消息属于本程序,系统会将消息转发到程序消息队列中。
  • 如果系统消息也没有消息(那也不闲着),检查当前进程的所有窗口需要重新绘制的区域,如果发现有需要绘制的区域,产生WM_PAINT消息扔到系统的消息队列,取得消息返回处理。
  • 如果没有重新绘制区域,检查定时器,如果有到时的定时器,产生WM_TIMER,返回处理执行。
  • 如果没有到时的定时器,整理程序的资源、内存等等。
  • GetMessage会继续等候下一条消息。PeekMessage会返回FASLE,交出程序的控制权。
  • 注意:GetMessage如果获取到的是WM_QUIT,函数会返回FALSE。

总结:总结起来就是,GetMessage非常的死心眼,没消息就找系统要,要不来就自己造,造不来就整理下资源(干点打扫卫生的活儿),还没有消息来,那就阻塞,等待消息的传来,但是依然不返回

阻塞了,就说明GetMessage已经尽力了。

PeekMessage也会干上面的事儿,但是它最后不会傻等

键盘消息

键盘消息分类

  • WM_KEYDOWN-按键被按下时产生
  • WM_KEYUP-按键被放开时产生
  • WM_SYSKEYDOWN-系统键按下时产生,比如ALT、F10
  • WM_SYSKEYUP-系统键放开时产生

附带信息:
.WPARAM-按键的Virtual Key(每个这个对应一个按键-这个虚拟键码值无法区分大小写,所以我们才需要去翻译消息)

LPARAM-按键的参数,例如按下的次数

字符消息(WM_CHAR)

  • TranslateMessage在转换WM_KEYDOWN消息时,对于可见字符可以产生WM_CHAR,不可见字符无此消息。
  • 附带信息:

    • WMPARAM-输入的字符的ASCII字符编码值
    • LPARAM-按键的相关参数

翻译消息的内部大致流程,如下图所示:

image-20211031160052913

文字解释:

先检查是否有按键被按下, 没有直接return,有则判断是否是可见字符消息(刚才的按键消息再一次被GET过来时,已经变成了字符消息,不是按键消息,在这部就直接return了),不是直接return,都通过了,那就判断大写键是否打开,根据大小写发出相应的消息。

鼠标消息

鼠标消息的分类

  • 基本鼠标消息

    • WM_LBUTTONDOWN-鼠标左键按下
    • WM_LBUTTONUP-鼠标左键抬起
    • WM_RBUTTONDOWN-鼠标右键按下
    • WM_RBUTTONUO-鼠标右键抬起
    • WM_MOUSEMOVE-鼠标移动消息
  • 双击消息

    • WM_ LBUTTONDBLCLK-鼠标左键双击
    • WM_RBUTTONDBLCLK-鼠标右键双击
  • 滚轮消息

    • WM_MOUSEWHEEL-鼠标滚轮消息

鼠标基本消息

  • 附带信息

    • WPARAM:其他按键的状态,例如Ctrl/Shift等。

      • LPARAM:鼠标的位置,窗口客户区坐标系。

        • LOWWORD X坐标位置
        • HIWORD Y坐标位置
  • 一般情况鼠标按下/抬起成对出现。在鼠标移动过程中,会根据移动速度产生一系列的WM_ MOUSEMOVE消息。

鼠标双击消息

  • 附带消息:

    • WPARAM-其它按键的状态,例如CTRL/SHIFT。
    • LPARAM-鼠标的位置,窗口客户区坐标系。

      • LOWORD(lParam)-X坐标位置
      • HIWORD(lParam)-Y坐标位置
  • 消息产生顺序

    • 一左键双击为例:

      • WM_LBUTTONDOWN
      • WM_LBUTTONUP
      • WM_LBUTTONDBLCLK
      • WM_LBUTTONU
      • 使用时需要在注册窗口类的时候添加CS_DBLCLKS风格。

鼠标滚轮消息

  • 附带消息

    • WPARAM

      • LOWORD-其他按键的状态
      • HIWORD-滚轮的偏移量(120的倍数 ),通过正负值表示滚轮的方向
      • 正-向前滚动,负-向后滚动
    • LPARAM:鼠标当前的位置,屏幕坐标系

      • LOWORD-X坐标
      • HIWORD-Y坐标
  • 使用:通过偏移量,获取滚轮的方向和距离

定时器消息

定时器消息介绍

  • 产生时间:
    在程序中创建定时器,当到达时间间隔时,定时器会向程序发送一个WM_TIMER消息。定时器的精度是毫秒,但是准确度很低。例如时间间隔为1000ms,但是会在非1000毫秒到达消息。
  • 附带信息:

    • WPARAM:定时器ID(到时间的)
    • LPARAM:定时器处理函数的指针

GetMessage肯定有没事干的时候,所以WM_TIMER消息肯定会产生。

按照一定周期去执行,时间要求不严格,都可以用这个定时器。

创建销毁定时器

  • 创建定时器

函数原型如下

UINT_PTR SetTimer(
  [in, optional] HWND      hWnd,//定时器窗口句柄
  [in]           UINT_PTR  nIDEvent,//定时器ID    
  [in]           UINT      uElapse,//时间间隔(毫秒为单位)
  [in, optional] TIMERPROC lpTimerFunc;//定时器处理函数指针(一般不使用,为NULL)创建成功,返回非0。
);
  • 关闭定时器

函数原型如下

BOOL KillTimer(
  [in, optional] HWND     hWnd,//窗口句柄
  [in]           UINT_PTR uIDEvent//定时器ID
);

菜单资源

菜单分类

  • 窗口的顶层菜单(不需要也不能设置ID,因为它的唯一作用的就是弹出下拉次菜单)
  • 弹出式菜单
  • 系统菜单

HMENU(菜单句柄)类型表示菜单,ID表示菜单项。

资源相关

  • 资源脚本文件:.rc文件
  • 编译器:RC.EXE

菜单资源的使用

  • 添加菜单资源-添加文件.rc文件
  • 加载菜单资源

    1. 注册窗口类时设置菜单

      1. (wc.lpszMenuName = (char*)IDR_MENU1;//菜单设置)
    2. 创建窗口传参设置菜单

      1. CreateWindow的导数第三个参数
    3. 在主窗口WM_CREATE消息中利用SetMenu函数设置菜单

LoadMenuW函数原型:

HMENU LoadMenuW(
  [in, optional] HINSTANCE hInstance,
  [in]           LPCWSTR   lpMenuName
);

SetMenu函数原型:

BOOL SetMenu(
  [in]           HWND  hWnd,
  [in, optional] HMENU hMenu
);

命令消息(WM_COMMAND)处理

  • 附带消息:

    • WPARAM:

      • HIWORD-对于菜单为0(没用)
      • LOWRD-被鼠标点击的菜单项的ID
    • LPARAM

      • 对于菜单项为0(没用)

Windows资源

图标资源

指的就是.ico为后缀的图片

  • 添加资源

    • 注意图标的大小, 一个图标文件中,可以有多个不同大小的图标。
  • 加载

函数原型如下:

到本进程的内存中找图标的数据同loadMenu
HICON LoadIconA(
  [in, optional] HINSTANCE hInstance,
  [in]           LPCSTR    lpIconName
);//成功返回HICON句柄

例:使用
wc.hIcon = LoadIcon(hIns,(char*)IDI_ICON1);
  • 设置

    • 注册窗口类

光标资源

  • 添加光标的资源

    • 光标的大小默认是32X32像素,每个光标有HotSpot,是当前鼠标的热点(点击图标生效的那个点)。
  • 加载资源

函数原型如下:

HCURSOR LoadCursorW(
  [in, optional] HINSTANCE hInstance,//可以为NULL,获取系统默认的Cursor
  [in]           LPCWSTR   lpCursorName
);
  • 设置资源

    • 在注册窗口时,设置光标。
    wc.hCursor = LoadCursor(hIns,(char*)IDC_CURSOR1);
    • 使用SetCursor设置光标。
    HCURSOR SetCursor(
        HCURSOR hCursor
    );
    
    //必须放在处理下面这个消息的时候调用
    • WM_SETCURSOR消息参数

      • WPARAM-当前使用的光标句柄
      • LPARAM-LOWORD-当前区域代码(Hit-Test code)HICLIENT/HTCAPTION...
      • HIWORD-当前鼠标消息ID

字符串资源

  • 添加字符串资源

    • 添加字符串资源,在表中增加字符串。
  • 字符串资源的使用

    • 函数原型如下
int LoadStringW(
  [in, optional] HINSTANCE hInstance,
  [in]           UINT      uID,//字符串ID
  [out]          LPWSTR    lpBuffer,//存放字符串 
  [in]           int       cchBufferMax
);//成功返回字符串长度,失败0

使用:例如实现中英文两版的软件。使用这个就非常方便。


字符串能写就写在这张字符串表中,容易修改。

用loadstring去读,然后在字符串表中改。

char szTitle[256] = { };
LoadString(hIns, IDS_WIND, szTitle, 256);

image-20211102190831358


加速键资源

什么ctrl+c,v之类的,就是快捷键呗。

能使用加速键(组合键)的功能,在菜单栏中也对应有一个,一般是绑定使用。

  • 添加,资源添加加速键表,增加命令ID对应的加速键。
  • 使用:

函数原型如下:

加载加速键表

HACCEL LoadAcceleratorsA(
  [in, optional] HINSTANCE hInstance,
  [in]           LPCSTR    lpTableName//加速键表资源ID
);//返回加速键表句柄

翻译加速键

int TranslateAcceleratorA(
  [in] HWND   hWnd,//处理消息的句柄
  [in] HACCEL hAccTable,//加速键句柄
  [in] LPMSG  lpMsg//消息
);//如果是加速键,返回非0

位置:一定是放在GetMessage的后面,因为它不抓哪来的消息。
并且放在TranslateMessage的后面,假如我按了ctrl+m,我是想让他区分按的是大M还是小m吗?不是,我为的是实现对应的功能(产生对应的消息WM_COMMAND。)
    
如果按的是加速键,在内部会发送出WM_COMMAND消息。
  

代码示例

    HACCEL hAcc = LoadAccelerators(hIns, (char*)IDR_ACCELERATOR1);//加载加速键表
    while (GetMessage(&nMsg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(hWnd, hAcc, &nMsg))//不是加速键
        {
            TranslateMessage(&nMsg);
            DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理
        }
    }

注意
由TranslateAccelerator发出的WM_COMMMAND消息,HIWORD(WPARAM)为1。

而鼠标点击菜单资源的HIWORD(WPARAM)为0。

LOWORD(WPARAM)都代表命令ID。

LPARAM为0。


绘图编程

绘图基础

  • 绘图设备(上下文/描述表)DC(Device Context)绘图上下文/绘图描述表
  • HDC - DC句柄,表示绘图设备,绘图设备句柄
  • GDI-Windows graphics device interface(Win32提供的绘图API)
  • 颜色

    • 计算机使用红、绿、蓝(RGB)
    • 每个点颜色是3个字节24位保存0~2^24 - 1种颜色
    • 16位:5,5,6
    • 32位:8,8,8,8绘图或透明度
  • 颜色的使用

    • COLORREF-实际DWORD

      • 例如:COLORREF nColor = 0;
    • 赋值使用RGB宏

      • 例如:nColor = RGB(0,0,255);
    • 获取RGB值

      • GetRBValue/GetGValue/GetBValue
      • 例如:BYTE nRed =GetRValue(nColor);

基本图形绘制

在绘图消息中绘图

绘制点

  • SetPixel设置指定点的颜色

函数原型如下:

COLORREF SetPixel(
  [in] HDC      hdc,//绘图设备句柄
  [in] int      x,//X坐标 
  [in] int      y,//Y坐标
  [in] COLORREF color//设置的颜色
);//返回点原来的颜色

例如

    PAINTSTRUCT PS = { };//抓到画家
    HDC hdc = BeginPaint(hwnd, &PS);//开始绘图    
    SetPixel(hdc,100,100,RGB(255,0,0));
    EndPaint(hwnd,&PS);//放掉画家

  • 线的使用(直线、弧线)

    • MoveToEx-指名窗口当前点(更改窗口当前点)

      BOOL MoveToEx(
        [in]  HDC     hdc,
        [in]  int     x,
        [in]  int     y,
        [out] LPPOINT lppt//为空则不返回上一个点
      );
      • LineTo-从窗口当前点到指定点绘制一条直线(并且致命窗口当前点我)
      BOOL LineTo(
        [in] HDC hdc,
        [in] int x,
        [in] int y
      );
      • 当前点:上一次绘图时的最后一点,初始为(0,0)点
  • 封闭图形:能够使用画刷填充的图形(反之则不是)。

    • Rectangle/Ellipse——(圆形/矩形)

image-20211103164837584

image-20211103164851493

内切圆,参数相同。

GDI绘图对象

画笔

  • 画笔的作用

    • 线的颜色、线型、线粗。
    • HPAN-画笔句柄
  • 画笔的使用

1.创建画笔,函数原型如下

HPEN CreatePen(
  [in] int      iStyle,//画笔的样式
  [in] int      cWidth,//画笔的粗细
  [in] COLORREF color//画笔的颜色
);//创建成功画笔句柄

PS_SOILD-实心线,可以支持多个像素宽,其它线型只能是一个像素宽。

2.将画笔应用到DC中

HGDIOBJ SelectObject(
  [in] HDC     hdc,//绘图设备句柄
  [in] HGDIOBJ h//GDI绘图对象句柄,(包括)兼容画笔句柄
);//返回原来的GDI绘图对象句柄

3.绘图

4.取出DC中的画笔

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

5.释放画笔,函数原型如下

BOOL DeleteObject(
  [in] HGDIOBJ ho//GDI绘图对象句柄,画笔句柄。
);

只能删除不被DC使用的画笔,所以在释放前,必须从将画笔从DC中取出。

大致过程就是,创建-交换-绘画-取出-释放

示例

    PAINTSTRUCT PS = { };//抓到画家
    HDC hdc = BeginPaint(hwnd, &PS);
    HPEN hPen = CreatePen(PS_SOLID,20,RGB(255,0,0));//创建
    HGDIOBJ nOldPen = SelectObject(hdc, hPen);//交换
    Ellipse(hdc, 100, 100, 300, 300);
    SelectObject(hdc, nOldPen);//交换,这里可以不用接收,上面创建时的句柄可以找到这跟笔,让画家松手就行了,我们就能销毁了。
    DeleteObject(hPen);
    EndPaint(hwnd,&PS);

画刷

  • 画刷相关

    • 画刷-封闭图形的填充颜色、图案
    • HBRUSH-画刷句柄
    • 画刷的使用套路就跟画笔一样
    • 默认是画刷颜色是白色的
  • 画刷的使用

1.创建画刷

创建实心画刷
HBRUSH CreateSolidBrush(
  [in] COLORREF color
);    
创建纹理画刷
HBRUSH CreateHatchBrush(
  [in] int      iHatch,
  [in] COLORREF color
);

2.将画刷应用到DC中

SelectObject

3.绘图

4.将画刷从DC中取出

SelectObject

5.删除画刷

DeleteObject


  • 其他

可以使用GetStockObject函数获取系统维护的画刷,画笔等。

如果不使用画刷填充,需要使用NULL_BRUSH参数,获取不填充的画刷。

GetStockObject返回的画刷不需要DeleteObject。

示例

//向操作系统借一把透明刷子
HGDIOBJ hBrush = GetStockObject(NULL_BRUSH);

位图

位图绘制

  • 位图相关

    • 光栅图形-记录图像中每一点的颜色等信息
    • 矢量图形-记录图像算法、绘图指令等。
    • HBITMAP-位图句柄
  • 位图的使用

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

    2.从资源中加载位图——loadBitMap

    3.创建一个与当前DC相匹配的DC(内存DC)(在内存的一个虚拟的区域画)


    HDC CreateCompatibleDC(
      [in] HDC hdc//当前DC句柄,可以为NULL(使用屏幕DC)
    );//返回创建好的DC句柄

    兼具资源的步骤和GDI绘图对象的步骤。

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

    5.成像(1:1)

    BOOL BitBlt(
      [in] HDC   hdc,//目的DC
      [in] int   x,//目的左上X坐标
      [in] int   y,//目的左上Y坐标
      [in] int   cx,//目的宽度
      [in] int   cy,//目的高度
      [in] HDC   hdcSrc,//源DC
      [in] int   x1,//源左上X坐标
      [in] int   y1,//源左上Y坐标
      [in] DWORD rop//成像方法SRCCOPY
    );

    缩放成像

    BOOL StretchBlt(
      [in] HDC   hdcDest,
      [in] int   xDest,
      [in] int   yDest,
      [in] int   wDest,
      [in] int   hDest,
      [in] HDC   hdcSrc,
      [in] int   xSrc,
      [in] int   ySrc,
      [in] int   wSrc,
      [in] int   hSrc,
      [in] DWORD rop
    );

6.取出位图

SelectObject

7.释放位图

DeleteObject

8.释放匹配的DC

DeleteDC

代码示例:

PAINTSTRUCT ps = { };
HDC hdc = BeginPaint(hwnd, &ps);
HBITMAP hBmp = LoadBitmap(g_HINSTANCE, (char*)IDB_BITMAP1);
HDC hMemdc = CreateCompatibleDC(hdc);//创建一个内存DC,并构建一个虚拟区域,并且内存DC在虚拟区域中绘图
HGDIOBJ nOldBmp = SelectObject(hMemdc,hBmp);//将位图数据送给内存DC,内存DC在虚拟区域中将位图绘制出来,返回旧位图(原来并没有,这是个假的,只是为了我们将来能够换回来)
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);//释放内存DC

文本绘制

文字的绘制

  • TextOut-将文字绘制在指定坐标位置
BOOL TextOutW(
  [in] HDC     hdc,
  [in] int     x,
  [in] int     y,
  [in] LPCWSTR lpString,
  [in] int     c
);
  • DrawText
int DrawText(
  [in]      HDC     hdc,//DC句柄
  [in, out] LPCTSTR lpchText,//字符串
  [in]      int     cchText,//字符数量
  [in, out] LPRECT  lprc,//绘制文字的矩形框
  [in]      UINT    format//绘制的方式
);
  • 文字颜色和背景

    • 文字颜色:SetTextColor
    COLORREF SetTextColor(
      [in] HDC      hdc,
      [in] COLORREF color
    );
    • 文字背景色:SetBkColor——只适用在不透明
    • 文字背景模式:SetBkMode(OPAQUE/TRANSPARENT)(透明(默认)/非透明)

字体

  • 字体相关:
  • window常用的字体为TrueType格式的字体文件

    • 字体名-标识字体类型
    • HFONT-字体句柄(保存字体的数据信息——外观形状)
  • 字体的使用

1.创建字体CreateFont

HFONT CreateFontA(
  [in] int    cHeight,//字体高度
  [in] int    cWidth,//字体宽度
  [in] int    cEscapement,//字符串倾斜角度
  [in] int    cOrientation,//字体的旋转角度
  [in] int    cWeight,//字体的粗细
  [in] DWORD  bItalic,//斜体
  [in] DWORD  bUnderline,//字符下划线
  [in] DWORD  bStrikeOut,//删除线
  [in] DWORD  iCharSet,//字符集-GB2312_CHARSET
    
    //下面的4参数全写0即可
    
  [in] DWORD  iOutPrecision,//输出精度
  [in] DWORD  iClipPrecision,//剪切精度
  [in] DWORD  iQuality,//输出质量
  [in] DWORD  iPitchAndFamily,//匹配字体
    
    //这个得写
  [in] LPCSTR pszFaceName//字体名称
);

代码示例

PAINTSTRUCT ps = { };
HDC hdc = BeginPaint(hwnd,&ps);

SetTextColor(hdc, RGB(255, 0, 0));
SetBkColor(hdc, RGB(0, 255, 0));


HFONT hFront = CreateFont(30,0,45,0,900,1,1,1, GB2312_CHARSET,0,0,0,0,"黑体");


HGDIOBJ nOldFron = SelectObject(hdc, hFront);//将创建的字体给它

const char* szText = "我是TEXTOUT";
TextOut(hdc,100,100,szText,strlen(szText));



//DrawText在矩形的范围内画,所以首先要确定一个范围
RECT rc;
rc.left = 100;
rc.top = 150;
rc.right = 200;
rc.bottom = 200;
//DrawText有矩形作为限制边界(可以打破)
DrawText(hdc, szText, strlen(szText),&rc,DT_LEFT | DT_TOP);
//DT_VCENTER和DT_BOTTOM只适用于DT_SINGLELINE和DT_WORDBREAK冲突

SelectObject(hdc, nOldFron);//让它放开
DeleteObject(hFront);//字体更要记得释放,因为字体占的内存较大
EndPaint(hwnd,&ps);

对话框

处理消息的方式与普通窗口相反,缺省的函数调用自定义函数。

对话框原理

  • 对话框的分类

    • 模式对话框-当前对话框显示时,会禁止本进程其它窗口和用户交互操作。
    • 无模式对话框-在对话框显示后,本进程其它窗口仍然可以和用户进行操作。
  • 对话框基本使用

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

谁注册窗口类,窗口处理函数就由谁来实现。


  • 对话框处理函数(并非真正的对话框处理函数,真正的系统内部。)
DLGPROC Dlgproc;

INT_PTR Dlgproc(
  HWND unnamedParam1,//窗口句柄
  UINT unnamedParam2,//消息ID
  WPARAM unnamedParam3,//消息参数
  LPARAM unnamedParam4//消息参数
);

返回TRUE——缺省处理函数不需要处理。

返回False——交给缺省处理函数处理。不需要调用缺省对话框处理函数。

模式对话框

void DialogBoxA(
  [in, optional]  hInstance,//当前程序实例句柄
  [in]            lpTemplate,//对话框资源ID
  [in, optional]  hWndParent,//对话框父窗口
  [in, optional]  lpDialogFunc//自定义函数
);

DialogBox是一个阻塞函数,只有当对话框关闭后,才会返回,继续执行后续代码。

返回值是通过EndDialog设置。

  • 对话框的关闭
BOOL EndDialog(
  [in] HWND    hDlg,//关闭的对话框窗口
  [in] INT_PTR nResult//关闭的返回值,能指定返回值就说明能接触阻塞
);

关闭模式对话框,只能使用EndDialog,不能使用DestoryWindow等函数。

nRsult是DiglogBox函数退出时的返回值。

  • 对话框的消息

WM_INITDIALOG-对话框创建之后在显示之前,通知对话框窗口处理函数,可以完成自己的初始化相关的操作。


EndDialog销毁对话框,并且解除阻塞。

而DestroyWindow只能销毁对话框,并不能解除阻塞。


无模式对话框

  • 创建对话框
HWND CreateDialog(
HINSTANCE hInstance,//应用程序实例句柄
        LPCTSTR lpName,//模板资源ID
        HWND hWndParent hWndParent,//父窗口
        DLGPROC lpDialogFunc//自定义函数
);

非阻塞函数,创建成功后返回窗口句柄,需要使用ShowWindow函数显示对话框。

  • 对话框关闭

关闭时使用DestroyWindow销毁窗口,不能使用EndDiaglog关闭对话框。

代码示例

HWND noModel = CreateDialog(g_Hinstance, (char*)IDD_DIALOG1,hwnd, DlgProc);
ShowWindow(noModel, SW_SHOW);


xxx
DestroyWindow(hwnd);

静态库

Windows上静态库和Linux上的静态库在原理上没有任何区别,都是封装一堆东西等着别人去掉。

静态库的特点

  • 运行不存在。

    • 没有如何,不能执行,生成的文件无法形成静态影像,无法进内存。
  • 静态库源码被链接到调用程序中。
  • 目标程序的归档。

C语言静态库

  • C静态库的创建

    • 创建一个静态库程序。
    • 添加库程序,源文件使用C文件。
  • C静态库的使用

    • 库路径设置:可以使用#pragma关键字设置

      • pragma comment(lib,"../lib/clib.lib")

C++静态库

  • C++静态库的创建

    • 创建一个静态库项目
    • 添加库程序,源文件使用CPP文件。
  • C++静态库的使用

    • 库路径设置:可以使用pragma关键字设置
    • pragma comment(lib,"../xx/xxx.lib")

示例

#include<iostream>
using namespace std;
//给编译器看
int CLIB_add(int add1, int add2);
int CLIB_sub(int add1, int add2);
//给链接器看
#pragma comment(lib,"../Debug/CPPLIB.lib")


int main(void)
{
    cout << CLIB_add(5, 2) << endl;
    cout << CLIB_sub(5, 2) << endl;
    return 0;
}

C++调库中函数需要在前面声明。告诉编译器到底换不换名。

C++编译器编译时会更改函数名。

解决

extern"C" 函数声明;告诉编译器以C语言方式编译(不要给我改名)。

动态库

动态库的特点

  1. 运行时独立存在(依附着其它程序运行起来的之后)
  2. 源码不会链接到执行程序
  3. 使用时加载(使用动态库必须使动态库执行)

与静态库进行比较

  1. 由于静态库是将代码嵌入到使用程序中,多个程序使用时,会有多分代码,所以代码体积会增大。动态库的代码只需要存一份,其他程序通过函数地址使用,所以代码体积较小。
  2. 静态库发生变化后,新的代码需要重新链接嵌入到执行程序中。动态库发生变化后,如果库中函数的定义(或地址)未变化(仅仅是函数的实现发生了变化),其他使用DLL的程序不需要重新链接。

动态库的创建

  • 创建动态库项目
  • 添加库程序
  • 库程序导出-提供给使用库者库中的函数等信息。

    1. 声明导出:使用_declspec(dllexport)导出函数。将函数的地址存放在动态库的文件头中。(换名之后的函数)

      注意:动态库编译链接后,也会有LIB文件(与该DLL配套生成的),是作为动态库函数映射使用,与静态库不完全相同。LIB中存放每个函数的名字和它对应的标号,还有与它配套的DLL文件名——xxx.dll

    2. 模块定义文件.def

      例如:
      LIBRARY DLLFunc //库
      EXPORTS    //库导出表
      DLL_Mul  //导出的函数

动态库的使用

  • 隐式链接(操作系统负责使动态库执行,系统偷偷的把动态库扔到内存中。)

    1. 头文件和函数原型,在函数原型的声明前,增加_declspec(dllimport)
    2. 导入动态库的LIB文件
    3. 在程序中使用函数

      1. 隐式的链接的情况,dll文件可以存放的路径(编译器可以找到的位置):
      2. 与执行文件中同一个目录下
      3. 当前工作目录
      4. Windows目录,(三个window..不建议使用,因为是系统路径, 公共场所,容易出现问题。)
      5. Windows/System32目录
      6. Windows/System
      7. 环境变量PATH指定目录

  • 显式链接(程序员自己负责使动态库执行)

1.定义函数指针类型 typedef

2.加载动态库

HMODULE LoadLibrary(
      LPCTSTR lpFileName//动态库文件名或全路径  
);//返回DLL的实例局句柄(HINSTANCE)

3.获取函数(绝对/真实)地址

FARPROC  GetProcAddress(
    HMODULE hModule,//DLL句柄
         LPCSTR lpProcName//函数名称
);//成功返回函数地址

4.使用函数

5.卸载动态库(释放那块内存,结束动态库执行。)

BOOL FreeLibrary(
    HMODULE hModule//DLL的实例句柄
);

代码示例

LIBRARY Dll3
EXPORTS
    Print @1
#include<windows.h>
#include<iostream>
using namespace std;
typedef void(*PRINT)();

int main(void)
{
    HINSTANCE hDll = LoadLibrary("Dll3.dll");
    PRINT MYPRINT = (PRINT)GetProcAddress(hDll, "Print");
    MYPRINT();
    FreeLibrary(hDll);
    return 0;
}

动态库中封装类

类导出的其实也是函数地址。

  • 在类名称前增加_declspec(dllexport)定义,例如:
class _declspec(dllexport)CMath
{
    ...
}
  • 通常使用预编译开关切换类的导入导出定义,例如:
    image-20211106214536869

代码示例

(如果生成错误就改一下代码顺序)

动态库中封装类.h

#ifndef _DLLCLASS_H
#define _DLLCLASS_H

#ifdef DLLCLASS_EXPORTS
#define EXT_CLASS _declspec(dllexport)//导出
#else
#define EXT_CLASS _declspec(dllimport)//导入
#endif
 

class EXT_CLASS Print
{
public:
    void PRINT();
};

#endif

动态库中实现.cpp

#include "pch.h"
#include<iostream>
#include"CLASSDLL.h"
using    namespace std;
#define DLLCLASS_EXPORTS
void Print::PRINT()
{
    cout << "123" << endl;
}

使用该动态库

#include<iostream>
using namespace std;
#include"../CLASSDLL/CLASSDLL.h"
#pragma comment(lib,"../Debug/CLASSDLL.lib")
int main(void)
{
    Print p1;
    p1.PRINT();
    return 0;
}

线程

线程基础

  • Windows的线程是可以执行代码的实例。系统是以线程为单位调度程序。一个程序当中可以有多个线程,实现多任务的处理。
  • 进程开启意味着分内存,线程开辟意味着程序的执行。
  • Windows线程的特点:

    1. 线程都具有1个ID
    2. 每个线程都具有自己的内存栈
    3. 同一进程中的线程使用同一个地址空间
  • 线程的调度:

操作系统将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程

线程轮询:线程A->线程B->线程A......

在一个时间点上,CPU只能执行一个线程。

创建线程

  • 创建线程

函数原型如下:

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,//安全属性
  [in]            SIZE_T                  dwStackSize,//线程栈的大小
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,//线程处理函数的函数地址
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,//传递给线程处理函数的参数
  [in]            DWORD                   dwCreationFlags,//线程的创建方式
  [out, optional] LPDWORD                 lpThreadId//创建成功,返回线程的ID
);//创建成功,返回线程句柄。

只要看到安全属性这个参数就通通置空,这个参数已经被废弃了。

线程栈大小,按照1MB对齐。

看到处理函数,就说明这个函数由我们程序员去定义,由系统调用。

线程的创建方式有两种,立即执行方式,挂起方式。

多线程,宏观上同时开始(并行),真实是,一个时间点只能执行一个线程(串行)。

  • 定义线程处理函数
DWORD WINAPI ThreadProc(
    LPVOID lpParameter//创建线程时,传递给线程的参数。
);

代码示例

#include<windows.h>
#include<iostream>
using namespace std;

DWORD CALLBACK TestProc(LPVOID pParam)
{
    while (1)
    {
        cout << (char*)pParam << endl;
        Sleep(1000);
    }
    return 0; 
}
DWORD CALLBACK TestProc1(LPVOID pParam)
{
    while (1)
    {
        cout << (char*)pParam << endl;
        Sleep(1000);
    }
    return 0;
}
int main(void)
{
    DWORD nID = 0;
    const char* text = "123";
    HANDLE hTread = CreateThread(NULL, 0, TestProc, (void*)text, 0, &nID);

    const char* text1 = "abc";
    HANDLE hTread1 = CreateThread(NULL, 0, TestProc1, (void*)text1, 0, &nID);

    getchar();
    return 0;
}

线程挂起/销毁

挂起:

 DWORD SuspendTread(
     HANDLE hThread //线程句柄    
 );

唤醒

DWORD ResumeTread(
    HANDLE hThread//线程句柄
);

结束指定线程

BOOL Terminate Thread(
    HANDLE hThread,//线程句柄
        DWORD dwExitCode//退出码,没有实际意义
);

结束函数所在的线程

VOID ExitThread(
    DWORD dwExitCode//退出码
);

线程相关操作

获取当前线程ID

GetCurrentThreadId();

获取当前线程的句柄

GetCurrentThread();

等候(可等候)单个句柄有信号——线程句柄

VOID WaitForSingleObject(
    HANDLE handle,//句柄BUFF的地址
    DOWRD dwMilliseconds//等候时间INFINITE(无限大)
);

一个可等候的句柄,一定有有信号和无信号的两种状态。

等候的时间已经到了,该句柄还是无信号那就返回了。

同时等候多个句柄有信号

DWORD WaitForMultipleObjects(
    DOWRD nCount,//要等候的句柄数量
    CONST HANDLE*lpHandle,//句柄BUFF的地址
    
    BOOL bWaitAll,//等候方式
    //TRUE-所有事件都有信号才返回
    //FALSE-有一个事件有信号就返回
    
    
    DWORD dwMilliseconds//等候时间INFINITE
);

bWaitAll-等候方式

  • TRUE-表示所有句柄都有信号,才结束等候。
  • FALSE-表示句柄中只要1个有信号,就结束等候。

线程句柄执行的时候无信号,结束的时候有信号。


线程同步

原子锁

  • 相关问题

多个线程对同一个数据进行原子操作,会产生结果丢失。比如执行++运算时。

  • 错误代码分析

当线程A执行g_value++时,如果线程切换时间正好是在线程A再次被切换回来之后,会将原来线程A保存的值保存到g_values上,线程B进行的加法操作被覆盖。(汇编角度)

当CPU在执行某个线程的时候,一旦时间到了它要离开,它会将这个线程已经执行到的位置保护起来,一般是压栈保护。将位置信息压到本线程的栈中。再来,先弹栈读取,恢复战场。
  • 图示

image-20211107172354028

解释/错误分析:

第一个线程刚要将+1后的值赋给g_value,cpu给它的时间就到了,这时切换到线程2,线程2执行完毕,g_value == 1,切换回线程1,先弹栈恢复,执行完毕得到g_value == 1,这就丢失了一次数据。

解决:使用原子锁函数,来++

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;
}
  • 使用原子锁函数
InterlockedIncrement
......
每一个操作符都有对应的一个原子锁函数

原子锁的实现:直接对数据所在的内存进行操作,并且在任何一个瞬间只能有一个线程访问这块内存。(++并不是直接对内存进行操作,而是对寄存器进行操作,然后再赋值给变量。)

锁的是数据所在的这块内存,并不是CPU。

解释

先执行线程1,进来直接给g_value(内存)上锁,如果这时候CPU给的时间到了,那就压栈保护,然后转去执行线程2,线程2一看已经g_value已经被锁了,不会重复锁,发生阻塞,等到时间的耗尽,又转去执行线程1,弹栈恢复,完成执行完后,给g_value解锁,再去执行线程2,线程2就能给g_value上锁了......重复上述过程。

简单来说就是,上了锁之后可以保证一个线程对该变量,完成一次完整的操作后,另一个线程再进行一次完整的操作。

使用原子锁之后执行效率变慢,因为这两个线程老是对着睡觉。

重要的是保证结果的正确性,如果保证不了正确性,宁可牺牲效率也要保证结果正确。

  • 局限性&优点

只能对运算符进行原子锁。而且有大量函数。但是在所有的枷锁机制中,它的效率最高。


互斥

  • 相关的问题

多线程下代码或资源的共享使用

  • 互斥的使用

一.创建互斥

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,//安全属性
    BOOL bInitialOwner,//初识的拥有者TRUE/FALSE
    LPCTSTR lpName//命名
);//创建成功返回互斥句柄

互斥不光能对操作符进行枷锁,也能对其它东西进行枷锁,比如说锁定一段或一行代码。

原子锁能解决的问题,互斥都能解决,但是互斥能解决的问题,原子锁不一定能解决。

互斥句柄也是可等候句柄。

特点

1.任何一个时间点上只能有一个线程拥有互斥。其它线程只能等待这个线程把互斥扔掉才能拥有。独占性和排他性、

  1. 当任何一个线程都不拥有互斥,互斥句柄有信号。一旦某个线程有互斥,互斥句柄无信号。
  2. 谁先等待互斥,谁先得到互斥。

二.等到互斥

WaitForSingleObject/WaitForMultipleObjects...互斥的等候遵循谁先等候谁先获取

三.释放互斥

BOOL ReleaseMutex(
    HANDLE hMutex//互斥句柄
);

四.关闭互斥句柄

CloseHandle

代码示例

#include<windows.h>
#include<iostream>
using namespace std;
HANDLE g_hMutex = 0;//接收互斥句柄

DWORD CALLBACK TestProc(LPVOID pParam)
{
    char* text = (char*)pParam;
    while (1)
    {
        WaitForSingleObject(g_hMutex, INFINITE);
        for (int i = 0; i < strlen(text); i++)
        {
            cout << text[i];
            Sleep(333);
        }
        cout << endl;
        ReleaseMutex(g_hMutex);
    }
    return 0;
}
DWORD CALLBACK TestProc1(LPVOID pParam)
{
    char* text = (char*)pParam;
    while (1)
    {
        WaitForSingleObject(g_hMutex, INFINITE);
        for (int i = 0; i < strlen(text); i++)
        {
            cout << text[i];
            Sleep(333);
        }
        cout << endl;
        ReleaseMutex(g_hMutex);
        
    }
    return 0;
}
int main(void)
{
    //创建互斥的这个主线程也不拥有它,
    g_hMutex = CreateMutex(NULL,FALSE,NULL);
    DWORD nID = 0;
    const char* text = "123";
    HANDLE hTread = CreateThread(NULL, 0, TestProc, (void*)text, 0, &nID);

    const char* text1 = "abc";
    HANDLE hTread1 = CreateThread(NULL, 0, TestProc1, (void*)text1, 0, &nID);


    getchar();
    CloseHandle(g_hMutex);
    return 0;
}

解释:互斥句柄在主函数中创建出来,设备FALSE,谁都不拥有,互斥句柄有信号,等待抢占,假设CPU先执行线程1, 拿到互斥句柄,互斥句柄变为无信号,执行打印,到时间后,压栈保存,转去执行线程2,此时WaitForSingObject就无法通过,阻塞,等待时间结束,又执行线程1,恢复现场,假设执行完毕,线程1丢掉互斥,互斥句再次变为有信号,线程2WaitForSingObject通过,拿到互斥......。


原子锁和互斥都是枷锁机制,实现的都是多个线程之间有排斥关系。 多个线程之间去竞争临界资源。


下面这两个同步技术,事件和信号量,实现的是线程之间的协调工作关系


事件

  • 相关问题

程序(线程)之间的通知的问题。

两个线程要协调工作,它们两个就得通信。

  • 事件的使用

1.创建事件

HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,//安全属性
    BOOL bManualReset,//事件重置(复位(将事件变为无信号))方式,TRUE手动,FALSE自动
    BOOL bInitialState,//事件初识状态,TRUE有信号
    LPCTSTR lpName//事件命名
);//创建成功返回事件句柄

2.等候事件

WaitForSingleObject/WaitForMultipleObjects

3.触发事件(将事件设置成有信号状态)

BOOL SetEvent(
    HANDLE hEvent //事件句柄 
)

4.复位事件(将事件设置成无信号状态)

BOOL ResetEvent(
    HANDLE hEvent//事件句柄
);

5.关闭事件

CloseHandle

代码示例

#include<iostream>
#include<Windows.h>
using namespace std;

HANDLE g_hEvent = 0;//接收事件句柄

DWORD CALLBACK PrintProc(LPVOID pParam)
{
    while (1)
    {
        WaitForSingleObject(g_hEvent,INFINITE);
        //ResetEvent(g_hEvent);//将事件设置成无信号
        cout << "*************" << endl;
    }
    return 0;
}

//通过事件进行通信

DWORD CALLBACK CtrlProc(LPVOID pParam)
{
    while (1)
    {
        Sleep(1000);
        SetEvent(g_hEvent);//解救线程1
    }

}
int main(void)
{
    //FALSE为自动复位,当经过WaitForSingleObject函数之后
    //TRUE为手动复位,手动在WaitForSingleObject后ResetEvent
    g_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
    DWORD nID = 0;
    HANDLE hThread[2] = { 0 };
    hThread[0] = CreateThread(NULL,0,PrintProc,NULL,0,&nID);
    hThread[1] = CreateThread(NULL,0,CtrlProc,NULL,0,&nID);
    WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
     

    return 0;
}

注意小心事件死锁。

image-20211107213811934


信号量

  • 相关的问题

类似于事件(不是原理类似时间,是作用类似事件),解决通知的相关问题。但提供一个计数器可以设置次数

  • 信号量的使用

1.创建 信号量

HANDLE CreateSemaphoreW(
  [in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//安全属性
  [in]   LONG    lInitialCount,//初始化信号量数量(初识计数值)
  [in]   LONG    lMaximumCount,//信号量(计数器)的最大值
  [in, optional] LPCWSTR   lpName//命名
);//创建成功返回信号量句柄

信号量的计数器不为0时,信号量句柄有信号。
2.等候信号量

WaitForSingleObject/WaitForMultipleObjects

每等候一次,信号量减1,直到为0阻塞。

3.给信号量指定计数值

BOOL ReleaseSemaphore(
  [in]            HANDLE hSemaphore,//信号量句柄
  [in]            LONG   lReleaseCount,//释放数量
  [out, optional] LPLONG lpPreviousCount//释放前原来信号量的数量,可以为NULL
);

4.关闭句柄

CloseHandle  

代码示例

#include<windows.h>
#include<iostream>
using namespace std;

HANDLE h_handle = 0;//信号量句柄

DWORD CALLBACK TestProc(LPVOID pParam)
{
    while (1)
    {
        WaitForSingleObject(h_handle, INFINITE);
        cout << "******" << endl;
    }
    return 0;
}

int main(void)
{
    h_handle = CreateSemaphore(NULL, 3, 10, NULL);
    DWORD nID = 0;
    HANDLE hThread = CreateThread(NULL, 0, TestProc, NULL, 0, &nID);

    getchar();//回车之后信号量数量改为5
    ReleaseSemaphore(h_handle, 5, NULL);

    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(h_handle);
    return 0;
}        
相关文章
|
6月前
在 Win 11 中添加小鹤双拼
在 Win 11 中添加小鹤双拼
271 0
推荐5款简单实用的win10小工具
今天要给大家推荐5款压箱底的宝贝软件了,百度搜索一下就能找到下载链接了。
91 0
|
SQL 编解码 网络协议
Win教程3/3: Win10 使用技巧
妙用快捷键 常用快捷键 文件重命名 F2 文件 / 内容 全选 Ctrl + A CV 复制粘贴 剪切操作 Ctrl + X ,配合粘贴可完成移动操作 显示桌面 Win + D 切换程序的 Ctrl + Tab 浏览器的快捷键
310 0
Win教程3/3: Win10 使用技巧
|
编解码 安全 网络安全
Win教程加餐2-小知识收藏
Host 文件所在路径 %WINDIR%\System32\drivers\etc Hosts 文件存放的是一些网站主机的域名和对应的 IP 地址。当我们使用浏览器浏览一个网站时,系统会先检查本地 hosts 文件,看其中是否有对应域名和 ip 地址的对应关系,如果有就会直接采用,因此会节约大量时间。
135 0
Win教程加餐2-小知识收藏
|
数据可视化 iOS开发 UED
错怪了Win10!
哪个版本的Windows最好用? 每个人都有自己的答案,而我的选择是Windows10。 提起Windows,最吸引我的一点就是引入了UWP应用。
错怪了Win10!
win引导修复
win引导修复 转载自博客园 sikewang https://www.cnblogs.com/sikewang/p/4609527.html
999 0