发布于 

Windows 编程学习笔记 01

0x01 准备工作

VS2022 & C++

新建空项目并设置 项目属性->链接器->系统->子系统窗口

遇到不确定用法的函数,可以选中并按 F1

0x02 Windows 常见的数据类型

UINT 无符号32位整型
DTORD 32位整数
PDWORD 32位整数类型指针。
BOOL 布尔类型
SHORT 带符号16位整数。
LRESULT 32位函数返回值
TPARAM 32位的消息参数。
LPARAM 32位的消息参数。
HANDLE 是 Windows 编程中一个特别重要的概念,在计算机中翻译为句柄。用于标示操作系统中的某个对象。
HANDLE 通用句柄
HWND 窗口句柄
HINSTANCE 实例句柄
LPSTR 字符串指针

0x03 一个简单的 HelloWorld

1
2
3
4
5
6
7
8
9
10
11
#include<Windows.h>
int WINAPI WinMain( // 这是程序入口,类似原本的 main
HINSTANCE hInstance, // 程序实例句柄
HINSTANCE hPreHinstance, // 上一个程序实例句柄(已弃用)
LPSTR lpCmdeLine, // 传入参数,类似arg[]
int nCmdeShow // 显示方式
){
MessageBoxA(NULL,"HelloWorld!","标题",MB_OKCANCEL);// A(ANSI),W(Unicode)
// 关于窗口返回值可以按F1去看手册
return 0;
}

0x04 Windows 字符串

C++ 支持两种字符串,ANSI 编码(使用””包裹)和 Unicode 编码(使用L””包裹)

  1. 普通字符串类型 CHAR,即 char,多字节字符集类型
  2. 宽字符串类型 WCHAR,即 wchar_t,Unicode 类型,输出使用 %ls
  3. 通用字符串类型 TCHAR,类型未知,由环境决定,需要引入 tchar.h 头文件。环境设置可见 项目属性->高级->字符集

微软将其通过条件编译进行统一(通过 _UNICODE 和 UNICODE 宏 ),于是有了_T("") 这样的字符串

常用字符串处理函数

分三种系列:str, wcs, _tcs

  1. 长度计算:strlen, wcslen, _tcslen
  2. 字符串转数字:
    1. A 版本:atoi, strtol
    2. W 版本:_wtoi, wcstol
    3. T 版本:_ttoi, _tcstol
  3. 数字到字符串:
    1. A 版本:_itoa_s, itoa(不安全)
    2. W 版本:_itow_s
    3. T 版本:_itot_s

两者相互转换(建议定义一个宏):

  1. W 到 A 的转换:WideCharToMultiByte()
  2. A 到 W 的转换:MultiByteToWideChar()

0x05 第一个窗口程序

写一个窗口程序需要四步:创建窗口类,注册窗口类,创建窗口实例,显示窗口

  1. 首先使用 WNDCLASSW 创建一个窗口类,关于它的参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct tagWNDCLASSW {
    UINT style; // 样式
    WNDPROC lpfnWndProc; // 回调函数 *这项必须有
    int cbClsExtra; // 0
    int cbWndExtra; // 0
    HINSTANCE hInstance; // 程序的实例句柄
    HICON hIcon; // 程序图标
    HCURSOR hCursor; // 光标
    HBRUSH hbrBackground; // 背景画刷
    LPCWSTR lpszMenuName; // 菜单名称
    LPCWSTR lpszClassName; // 程序类名 *这项必须有
    } WNDCLASSW, *PWNDCLASSW, *NPWNDCLASSW, *LPWNDCLASSW;

    于是写作:

    1
    2
    3
    4
    // 1.创建一个窗口类
    WNDCLASSW myclass = { 0 };
    myclass.lpszClassName = L"windowClassTry"; // 定义类名
    myclass.lpfnWndProc = nullptr; // 回调函数,先设置为无,之后修改

    当然,这里还有其他版本的创建函数,类似操作。

  2. 注册窗口类:一个类名只能注册一次

    1
    RegisterClassW(&myclass);
  3. 使用 CreateWindowW 创建窗口实例,关于它的参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    HWND CreateWindowW(
    [in, optional] lpClassName, // 程序类名
    [in, optional] lpWindowName, // 窗口名称
    [in] dwStyle, // 窗口样式,例如WS_OVERLAPPEDWINDOW,可按F12快速查看
    [in] x, // 坐标,可以使用 CW_USEDEFAULT 用户默认
    [in] y, // x使用默认后可设置为0
    [in] nWidth, // 宽,可设置默认 CW_USEDEFAULT
    [in] nHeight, // 使用默认后可设置为0
    [in, optional] hWndParent, // 父窗口句柄,没有则为NULL
    [in, optional] hMenu,
    [in, optional] hInstance,
    [in, optional] lpParam // 回调参数,没有则可设置为0
    );

    于是写作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 3.创建窗口实例
    HWND hwindow = CreateWindowW(
    myclass.lpszClassName,
    L"窗口名称",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    0,
    CW_USEDEFAULT,
    0,
    NULL,
    NULL,
    hInstance,
    0
    );
  4. 使用 ShowWindow 显示窗口

    它只有两个参数,窗口句柄和显示样式(SW_开头,F12可查),这里写作

    1
    2
    // 4.显示窗口
    ShowWindow(hwindow, SW_SHOWNORMAL);

编译运行,程序发生异常,原因是原本创建窗口类时的回调函数使用了空指针,需要进行修改。对 WNDCLASSW 按 F1 可查找到 lpfnWndProc 必须使用 CallWindowProc 函数调用窗口过程。 有关详细信息,请参阅 WindowProc。于是找到

1
2
3
4
5
6
7
LRESULT Wndproc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{...}

直接将函数内容设置为 return 0,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
LRESULT Wndproc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
return 0;
}

int WINAPI WinMain(
HINSTANCE hInstance, // 程序实例句柄
HINSTANCE hPreHinstance, // 上一个程序实例句柄(已弃用)
LPSTR lpCmdeLine, // 传入参数,类似arg[]
int nCmdeShow // 显示方式
)
{
// 1.创建一个窗口类
WNDCLASSW myclass = { 0 };
myclass.lpszClassName = L"windowClassTry"; // 类名
myclass.lpfnWndProc = Wndproc; // 回调函数
// 2.注册窗口类:一个类名只能注册一次
RegisterClassW(&myclass);
// 3.创建窗口实例
HWND hwindow = CreateWindowW(
myclass.lpszClassName,
L"窗口名称",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
NULL,
NULL,
hInstance,
0
);
// 4.显示窗口
ShowWindow(hwindow, SW_SHOWNORMAL);
return 0;
}

此时程序没有发生异常,但刚运行便结束。关于如何解决这个问题,需要了解 Windows 的消息机制。

0x06 消息机制初步

Windows 系统是基于消息的操作系统。每一个窗口都在不停的处理消息,所有的操作都是接收到消息之后,进行处理的结果。围绕着消息的处理,产生了获取消息的消息泵机制也叫消息循环,以及处理消息的窗口回调函数机制。

  1. Windows 下产生消息的时机共有以下四种:

    1. 用户主动产生的消息
    2. Windows 系统本身产生的消息:
    3. 应用程序本身产生的消息
    4. 其他应用程序产生的消息。
  2. 消息的发送与接收

    Windows 系统有一个系统消息队列,每个 GUI 程序也有自己的消息队列,系统消息队列负责将消息发送给不同程序的消息队列。

    1. GetMessage:从消息队列中取出消息,关于参数:

      1
      2
      3
      4
      5
      6
      GetMessageW(
      _Out_ LPMSG lpMsg, // MSG 类型的消息变量
      _In_opt_ HWND hWnd,// 窗口句柄
      _In_ UINT wMsgFilterMin, // 获取消息的范围
      _In_ UINT wMsgFilterMax);// 获取消息的范围
      // 后面两个可以都设置为0来获取所有消息
    2. DispatchMessage:将消息发送给处理消息的回调函数

    了解到以上知识,可以增加如下消息机制:

    1
    2
    3
    4
    5
    6
    // 5.获取消息
    MSG msg = { 0 };
    while (GetMessageW(&msg,0,0,0)){
    DispatchMessageW(&msg);// 分发消息
    }
    // 第二个参数使用 0 表示处理全部程序的消息,以免无法发送最后的程序退出信号

    对应地,将原来的回调函数更改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    LRESULT Wndproc(
    HWND hwnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
    )
    {
    switch (uMsg)
    {
    case WM_CREATE:
    MessageBoxW(hwnd,L"窗口已创建",L"窗口标题",MB_OK);
    break;
    case WM_CLOSE:
    MessageBoxW(hwnd, L"窗口已关闭", L"窗口标题", MB_OK);
    DestroyWindow(hwnd); // 销毁窗口
    PostQuitMessage(0); // 发送程序退出消息,这里和java的swt类似
    default:
    break;
    }
    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
    }

最后我们得到一个可以完整的窗口程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include<Windows.h>
LRESULT Wndproc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch (uMsg)
{
case WM_CREATE:
MessageBoxW(hwnd,L"窗口已创建",L"窗口标题",MB_OK);
break;
case WM_CLOSE:
MessageBoxW(hwnd, L"窗口已关闭", L"窗口标题", MB_OK);
DestroyWindow(hwnd); // 销毁窗口
PostQuitMessage(0); // 发送程序退出消息,这里和java的swt类似
default:
break;
}
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(
HINSTANCE hInstance, // 程序实例句柄
HINSTANCE hPreHinstance, // 上一个程序实例句柄(已弃用)
LPSTR lpCmdeLine, // 传入参数,类似arg[]
int nCmdeShow // 显示方式
)
{
// 1.创建一个窗口类
WNDCLASSW myclass = { 0 };
myclass.lpszClassName = L"windowClassTry"; // 类名
myclass.lpfnWndProc = Wndproc; // 回调函数
// 2.注册窗口类:一个类名只能注册一次
RegisterClassW(&myclass);
// 3.创建窗口实例
HWND hwindow = CreateWindowW(
myclass.lpszClassName,
L"窗口名称",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
NULL,
NULL,
hInstance,
0
);
// 4.显示窗口
ShowWindow(hwindow, SW_SHOWNORMAL);
// 5.获取消息
MSG msg = { 0 };
while (GetMessageW(&msg,0,0,0)){
DispatchMessageW(&msg);// 分发消息
}
return 0;
}

注:写最后部分代码时遇到了一点点小坑,代码注释中可见一斑。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。