Win32 GUI 编程 1.1 intro

前置知识

  • 了解什么是回调函数
  • 了解句柄概念

窗口创建的过程

  • 定义 WinMain 函数
  • 定义窗口处理函数(自定义,消息处理回调)
  • 向操作系统注册窗口类(向操作系统写入数据)
  • 创建窗口(内存中创建窗口)
  • 显示窗口(绘制窗口)
  • 消息循环(获取、翻译、分发消息)
  • 消息处理
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <windows.h>

/*
代码中出现的宏定义:
HINSTANCE:整个程序的句柄(不恰当的比方:指向整个结构体的指针)
HWND:指向整个程序的某一个窗口的句柄(不恰当的比方:指向结构体的某个成员的指针)
LPSTR:Long Pointer String,本质是char *
CALLBACK:人如其名,回调。
LRESULT:Long Result,本质是long
UINT:Unsigned Integer
*/

//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(
HWND hWnd, // 传入某个窗口的句柄,操作系统告诉你是哪个窗口遇到了需要处理的事件
UINT msgID, // 传入窗口发生的事件,操作系统告诉你当前窗口发生了什么事情
WPARAM wParam, //附加参数 1
LPARAM lParam //附加参数 2
) {
return DefWindowProc(hWnd,msgID,wParam,lParam);
}

#define MYWINDOWBUFFER 100
#define MYCLASSBUFFER 100

// 入口函数
int CALLBACK WinMain(
HINSTANCE hIns, //当前程序句柄,操作系统传入
HINSTANCE hPerIns, //前一个程序句柄,操作系统传入
LPSTR lpCmdLine, //由此可获取命令行输入的命令
int nCmdShow // 当前的显示状态,操作系统传入
) {

//新建窗口类,或者说是新建一个窗口,开始设定窗口的属性
WNDCLASS mainWindow = { 0 };
mainWindow.cbClsExtra = MYCLASSBUFFER;//给窗口设定 class 缓冲区
mainWindow.cbWndExtra = MYWINDOWBUFFER;//给窗口设定 window 缓冲区
mainWindow.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //给窗口设定颜色,这里是白色
mainWindow.hCursor = NULL; //光标(Cursor)进入窗体时,使用默认光标,你也可以传进来一个图片
mainWindow.hIcon = NULL; //窗体的Icon使用默认Icon,你也可以传进来一个图片
mainWindow.hInstance = hIns;//窗口的程序句柄
mainWindow.lpfnWndProc = WndProc;//窗口处理函数,以便于回调
mainWindow.lpszClassName = TEXT("Main");//窗口的类的名字
mainWindow.lpszMenuName = NULL; //窗口的菜单的名字,NULL表示没有
mainWindow.style = CS_HREDRAW | CS_VREDRAW; //风格,为什么是常量通过与运算来确定,你点进常量里面看一看就懂了(Hint:二进制)。具体有哪些属性可以查微软API

//窗口属性填入完毕,注册窗口类,将以上所有赋值全部写入操作系统
RegisterClass( &mainWindow );

//在内存中创建窗口,返回一个句柄
HWND mainWindowHandle = CreateWindow(
TEXT("Main"), // 创建哪个窗口,这里显然我们想要创建刚刚注册的那个窗口,这里传入的类名要和前面的一样
TEXT("Hello World!"), // 窗口的标题
WS_OVERLAPPEDWINDOW, // 窗口的风格
100, //窗口的位置数据,x,y,width,height
100,
500,
500,
NULL, //这个窗口有没有父窗口(eg.QQ密码登陆错误,跳出来一个报错消息框,报错消息框是QQ登录窗口的子窗口) 没有就传NULL
NULL, //这个窗口有没有菜单,没有传NULL
hIns, //这个窗口从属于哪个程序,传句柄
NULL //这个窗口有没有别的参数
);
//注意,创建了窗口!=窗口被操作系统显示,我们要调用函数让操作系统显示窗口
ShowWindow(
mainWindowHandle,//传入想要显示的窗口的句柄
SW_SHOW //显示的方法,具体可查API
);
UpdateWindow( mainWindowHandle ); //刷新窗口,不写也行,就是说明有这个玩意


/*
消息循环
窗口程序之所以你不去点他他就一直存在,是因为存在消息循环一直在执行。
通过消息循环,整个程序不断地去检测你对整个程序干了些什么事情,然后对你的行动做出相应反应。
GetMessage返回值:
如果函数取得WM_QUIT之外的其他消息,返回非零值。
如果函数取得WM_QUIT消息,返回值是零。
如果出现了错误,返回值是-1。
若想获得更多的错误信息,请调用GetLastError函数。
*/
MSG msgLoop = { 0 };
while (GetMessage(
&msgLoop , // GetMessage向msgLoop丢消息
NULL , // 取得其消息的窗口的句柄。当其值取NULL时,GetMessage会去取我们写的程序的所有窗口发生的事件
0 , // 指定被检索的最小消息值,现在不用管这个
0 // 指定被检索的最大消息值,现在不用管这个
)) {
TranslateMessage(&msgLoop);
DispatchMessage(&msgLoop);
// 分发消息,也就是交给WndProc来处理msg
}

return 0;
}

Win32 GUI 编程 1.2 窗口

窗口类是什么

  • 窗口类包含了窗口的各种参数信息的一种结构体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //新建窗口类,或者说是新建一个窗口,开始设定窗口的属性
    WNDCLASS mainWindow = { 0 };
    mainWindow.cbClsExtra = MYCLASSBUFFER;//给窗口设定 class 缓冲区
    mainWindow.cbWndExtra = MYWINDOWBUFFER;//给窗口设定 window 缓冲区
    mainWindow.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //给窗口设定颜色,这里是白色
    mainWindow.hCursor = NULL; //光标(Cursor)进入窗体时,使用默认光标,你也可以传进来一个图片
    mainWindow.hIcon = NULL; //窗体的Icon使用默认Icon,你也可以传进来一个图片
    mainWindow.hInstance = hIns;//窗口的程序句柄
    mainWindow.lpfnWndProc = WndProc;//窗口处理函数,以便于回调
    mainWindow.lpszClassName = TEXT("Main");//窗口的类的名字
    mainWindow.lpszMenuName = NULL; //窗口的菜单的名字,NULL表示没有
    mainWindow.style = CS_HREDRAW | CS_VREDRAW; //风格,为什么是常量通过与运算来确定,你点进常量里面看一看就懂了(Hint:二进制)。具体有哪些属性可以查微软API
  • 每个窗口都有窗口类,每个窗口基于自己的窗口类创建窗口。

  • 每一个窗口类都有一个名称,使用前必须注册到系统。

    1
    RegisterClass( &mainWindow );

窗口类分类

  • 系统窗口类

    ​ 系统已经定义好的窗口类,应用程序拿来就用

  • 应用程序全局窗口类

    ​ 由用户自己定义,当前应用程序可用

  • 应用程序局部窗口类

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

系统窗口类

​ example:

​ 按钮 - BUTTON

​ 编辑框 - EDIT

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
#include <windows.h>
#define MYWINDOWBUFFER 100
#define MYCLASSBUFFER 100

int CALLBACK WinMain(
HINSTANCE hIns,
HINSTANCE hPerIns,
LPSTR lpCmdLine,
int nCmdShow
) {
HWND mainWindowHandle = CreateWindow(
TEXT("Button"), //系统内置的类,不用自己写
TEXT("Hello World!"),
WS_OVERLAPPEDWINDOW,
100,
100,
500,
500,
NULL,
NULL,
hIns,
NULL
);
ShowWindow(
mainWindowHandle,
SW_SHOW
);
UpdateWindow(mainWindowHandle);
MSG msgLoop = { 0 };
while (GetMessage(
&msgLoop,
NULL,
0,
0
)) {
TranslateMessage(&msgLoop);
DispatchMessage(&msgLoop);
}

return 0;
}

全局窗口类、局部窗口类

注册窗口类的函数原型:

1
ATOM RegisterClass(CONST WNDCLASS *lpWndClass); //传入窗口类

补充:ATOM是原子操作的意思,意味着当程序执行到一个ATOM类型的函数时不可以被操作系统打断,必须一次性执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct WNDCLASS {
UINT style; //窗口类的风格
WNDPROC lpfnWndProc; //窗口处理函数
int cbClsExtra;//窗口类的附加数据缓存大小
int cbWndExtra;//窗口的附加数据缓存大小
HINSTANCE hInstance;//当前模块的程序句柄(区别程序(实例)句柄和窗口句柄)
HICON hIcon;//icon
HCURSOR hCursor;//cursor
HBRUSH hbrBackground;//background
LPCTSTR lpszMenuName;//窗口菜单的资源ID字符串
LPCTSTR lpszClassName;//窗口类的名称
}WNDCLASS,*PWNDCLASS;

窗口类创建

  • CreateWindow / CreateWindowEx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HWND CreateWindowEx(
DWRD dwExStyle,//Ex独有属性,扩展风格
LPCTSTR lpClassName,//已经注册的窗口类名称
LPCTSTR lpWindowName//窗口标题栏的名字
DWORD dwStyle,//窗口的基本风格
int x,//窗口左上角水平坐标
int y,//窗口左上角垂直坐标
int nWidth,//窗口宽度
int nHeight,//窗口高度
HWND hWndParent,//窗口的父窗口句柄
HMENU hMenu,//窗口菜单句柄
HINSTANCE hInstance,//应用程序的实例句柄
LPVOID lpParam//窗口创建时的附加参数
);//创建成功后返回窗口句柄

这个函数干了什么:你可以认为这个函数疯狂的 malloc 了一大堆信息

关于 dwStyle:https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles

  • CreateWindow 执行过程:

    1、函数根据传入的名称,在应用程序局部窗口类中查找,如果找到执行2,未找到执行3.

    2、比较局部窗口类与创建窗口时传入的hInstance变量。如果相等,创建和注册的窗口类在同一模块,创建窗口返回。如果不相同,执行3。

    3、在应用程序全局窗口类查找,如果找到,执行4,未找到执行5。

    4、使用找到的窗口类信息,创建窗口并返回。

    5、在系统窗口类中查找,如果找到,创建窗口并返回;否则执行失败。

  • 创建子窗口:

    • 创建时,设置父窗口句柄
    • 创建时,风格增加:WS_CHILD|WS_VISIBLE
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <windows.h>

/*
代码中出现的宏定义:
HINSTANCE:整个程序的句柄(不恰当的比方:指向整个结构体的指针)
HWND:指向整个程序的某一个窗口的句柄(不恰当的比方:指向结构体的某个成员的指针)
LPSTR:Long Pointer String,本质是char *
CALLBACK:人如其名,回调。
LRESULT:Long Result,本质是long
UINT:Unsigned Integer
*/

//窗口处理函数(自定义,处理消息)
LRESULT CALLBACK WndProc(
HWND hWnd, // 传入某个窗口的句柄,操作系统告诉你是哪个窗口遇到了需要处理的事件
UINT msgID, // 传入窗口发生的事件,操作系统告诉你当前窗口发生了什么事情
WPARAM wParam, //附加参数 1
LPARAM lParam //附加参数 2
) {
switch (msgID) {
case WM_DESTROY: {
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}

#define MYWINDOWBUFFER 100
#define MYCLASSBUFFER 100

// 入口函数
int CALLBACK WinMain(
HINSTANCE hIns, //当前程序句柄,操作系统传入
HINSTANCE hPerIns, //前一个程序句柄,操作系统传入
LPSTR lpCmdLine, //由此可获取命令行输入的命令
int nCmdShow // 当前的显示状态,操作系统传入
) {

//新建窗口类,或者说是新建一个窗口,开始设定窗口的属性
WNDCLASS mainWindow = { 0 };
mainWindow.cbClsExtra = MYCLASSBUFFER;//给窗口设定 class 缓冲区
mainWindow.cbWndExtra = MYWINDOWBUFFER;//给窗口设定 window 缓冲区
mainWindow.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //给窗口设定颜色,这里是白色
mainWindow.hCursor = NULL; //光标(Cursor)进入窗体时,使用默认光标,你也可以传进来一个图片
mainWindow.hIcon = NULL; //窗体的Icon使用默认Icon,你也可以传进来一个图片
mainWindow.hInstance = hIns;//窗口的程序句柄
mainWindow.lpfnWndProc = WndProc;//窗口处理函数,以便于回调
mainWindow.lpszClassName = TEXT("Main");//窗口的类的名字
mainWindow.lpszMenuName = NULL; //窗口的菜单的名字,NULL表示没有
mainWindow.style = CS_HREDRAW | CS_VREDRAW; //风格,为什么是常量通过与运算来确定,你点进常量里面看一看就懂了(Hint:二进制)。具体有哪些属性可以查微软API

//窗口属性填入完毕,注册窗口类,将以上所有赋值全部写入操作系统
RegisterClass(&mainWindow);

//在内存中创建窗口,返回一个句柄
HWND mainWindowHandle = CreateWindow(
TEXT("Main"), // 创建哪个窗口,这里显然我们想要创建刚刚注册的那个窗口,这里传入的类名要和前面的一样
TEXT("Hello World!"), // 窗口的标题
WS_OVERLAPPEDWINDOW, // 窗口的风格
100, //窗口的位置数据,x,y,width,height
100,
500,
500,
NULL, //这个窗口有没有父窗口(eg.QQ密码登陆错误,跳出来一个报错消息框,报错消息框是QQ登录窗口的子窗口) 没有就传NULL
NULL, //这个窗口有没有菜单,没有传NULL
hIns, //这个窗口从属于哪个程序,传句柄
NULL //这个窗口有没有别的参数
);

//创建子窗口
WNDCLASS childWindow = { 0 };
childWindow.cbClsExtra = MYCLASSBUFFER;//给窗口设定 class 缓冲区
childWindow.cbWndExtra = MYWINDOWBUFFER;//给窗口设定 window 缓冲区
childWindow.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //给窗口设定颜色,这里是白色
childWindow.hCursor = NULL; //光标(Cursor)进入窗体时,使用默认光标,你也可以传进来一个图片
childWindow.hIcon = NULL; //窗体的Icon使用默认Icon,你也可以传进来一个图片
childWindow.hInstance = hIns;//窗口的程序句柄
childWindow.lpfnWndProc = DefWindowProc;//窗口处理函数,以便于回调
childWindow.lpszClassName = TEXT("Child");//窗口的类的名字
childWindow.lpszMenuName = NULL; //窗口的菜单的名字,NULL表示没有
childWindow.style = CS_HREDRAW | CS_VREDRAW; //风格,为什么是常量通过与运算来确定,你点进常量里面看一看就懂了(Hint:二进制)。具体有哪些属性可以查微软API
RegisterClass(&childWindow);
HWND childWindowHandle = CreateWindow(
TEXT("Child"),
TEXT("Child Window"),
WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW,
0,
0,
200,
200,
mainWindowHandle,
NULL,
hIns,
NULL
);



//注意,创建了窗口!=窗口被操作系统显示,我们要调用函数让操作系统显示窗口
ShowWindow(
mainWindowHandle,//传入想要显示的窗口的句柄
SW_SHOW //显示的方法,具体可查API
);
UpdateWindow(mainWindowHandle); //刷新窗口,不写也行,就是说明有这个玩意


/*
消息循环
窗口程序之所以你不去点他他就一直存在,是因为存在消息循环一直在执行。
通过消息循环,整个程序不断地去检测你对整个程序干了些什么事情,然后对你的行动做出相应反应。
GetMessage返回值:
如果函数取得WM_QUIT之外的其他消息,返回非零值。
如果函数取得WM_QUIT消息,返回值是零。
如果出现了错误,返回值是-1。
若想获得更多的错误信息,请调用GetLastError函数。
*/
MSG msgLoop = { 0 };
while (GetMessage(
&msgLoop, // GetMessage向msgLoop丢消息
NULL, // 取得其消息的窗口的句柄。当其值取NULL时,GetMessage会去取我们写的程序的所有窗口发生的事件
0, // 指定被检索的最小消息值,现在不用管这个
0 // 指定被检索的最大消息值,现在不用管这个
)) {
TranslateMessage(&msgLoop);
DispatchMessage(&msgLoop);
// 分发消息,也就是交给WndProc来处理msg
}

return 0;
}

Win32 GUI 编程 1.3 消息

消息的概念与作用

  • 消息的组成:

    • 窗口句柄
    • 消息id
    • 消息的两个参数
    • 消息产生时间
    • 消息产生时鼠标位置
  • 消息的作用

    • 当系统通知窗口工作时,就采用消息的方式派发给窗口
1
2
3
4
5
6
7
8
typedef struct tagMSG {
HWND hwnd;//window handle
UINT message;//msg
WPARAM wParam;//w args
LPARAM lParam;//l args
DWORD time;//produce time
POINT pt;//produce pointer direction
}

Dispatch 分发机制

1
2
3
4
5
6
DispatchMessage(&nMsg) {
nMsg.hwnd -> 保存窗口数据的内存 -> WndProc;//通过窗口句柄找到窗口的内存地址
WndProc(...nMsg) {
//调用窗口的WndProc
}
}
  • 窗口处理函数(window Procedure Function)的原型(Prototype)
    • 只有满足这几个参数,才能称之为窗口处理函数
1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // 窗口句柄
UINT uMsg, //消息ID
WPARAM wParam, //args w
LPARAM lParam //args l
);

窗口处理函数实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
LRESULT CALLBACK mainWindowProc(
HWND hwnd,
UINT msgID,
WPARAM wParam,
LPARAM lParam
) {
switch(msgID) {
case WM_DESTORY: // 处理关闭事件
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd,msgID,wParam,lParam);
}

常见消息

WM_DESTORY

  • 产生时间:窗口被销毁时

  • 附带信息:

    • wParam:0
    • lParam:0
  • 一般用法:常用于在窗口被销毁之前,做相应的处理(free…)

WM_SYSCOMMAND

  • 产生时间:窗口系统区域发生事件时
  • 附带信息:
    • wParam:点击发生事件,例如:SC_CLOSE
    • lParam:鼠标光标的位置。
      • LOWORD(lParam) 水平位置
      • HIWORD(lParam) 垂直位置

WM_CREATE

  • 产生时间:在窗口创建成功但是还未显示时。

  • 附带信息:

    • wParam:为0
    • lParam:为 CREATESTRUCT 类型的指针。通过这个指针可以反射CreateWindowEx的12个参数。
  • 一般用法:常用于初始化

WM_SIZE

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

  • 附带信息:

    • wParam:窗口大小发生变化的原因
    • lParam:窗口变化后的大小
      • LOWORD(lParam) 变化后的宽度
      • HIWORD(lParam)变化后的高度
  • 利用调试技巧:新增Console

1
2
3
4
5
6
7
8
9
HANDLE g_hOutput = 0; //作为static var

//在WinMain函数中:
int CALLBACK WinMain(...) {
AllocConsole();
}
//在要输出内容的位置
//可以用sprintf格式化字符串
WriteConsole(g_hOutput,(text that you want to emit),strlen(...),NULL,NULL);

WM_QUIT

  • 产生时间:PostQuitMessage( integer );
  • 附带信息:
    • wParam:PostQuitMessage函数传递的参数
    • lParam:0
  • 一般用法:用于结束消息循环,当GetMessage收到这个消息后,退出消息循环。

WM_PAINT

  • 消息处理步骤

    • 开始绘图:

      1
      2
      3
      4
      HDC BeginPaint(
      HWND hwnd,//绘图所在窗口
      LPPAINTSTRUCT lpPaint //绘图参数缓存
      );//返回绘图设备句柄HDC
    • 正式绘图

    • 结束绘图

      1
      2
      3
      4
      BOOL EndPaint(
      HWND hWnd,
      CONST PAINTSTRUCT *lpPaint //free pointer
      );

消息循环

消息循环的阻塞

  • PeekMessage - 以查看的方式从系统获取消息,可以不将消息从系统移除,非阻塞函数。当系统无消息时,返回false,继续执行后续代码。
1
2
3
4
5
6
7
8
BOOL PeekMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg//移除标识
//PM_REMOVE/PM_NOREMOVE
);
  • GetMessage - 阻塞函数
1
2
3
4
5
6
7
8
9
10
11
12
while(1) {
if(PeekMessage(&nMsg,NULL,0,0,PM_NOREMOVE)) {
if(GetMessage(&nMsg,NULL,0,0)) {
TranslateMessage(&nMsg);
DispatchMessage(&nMsg);
} else {
return 0;
}
} else {
//do something...
}
}

发送消息(emit)

  • SendMessage - 发送消息,并阻塞等候消息处理的结果。
  • PostMessage - 投递消息,消息发出后立即返回,不等候结果。
1
2
3
4
5
6
BOOL SendMessage(
HWND hWnd,
UNIT msg,
WPARAM wParam,
LPARAM lParam
);
  • 系统消息 - ID范围 0-0x03ff
  • 用户自定义消息 - ID范围 0x0400 - 0x7FFF
1
#define WM_MYMESSAGE WM_USER+...

SendMessage :直接内部调用窗口处理函数,不走消息循环

PostMessage:将消息加入消息队列。

键盘消息

键盘按下

  • WM_KEYDOWN - 按键被按下时产生
  • WM_KETUP - 按键被放开时产生
  • WM_SYSKEYDOWN - 系统键被按下时产生(如ALT、F10)
  • WM_SYSKEYUP - 系统键放开时产生

附带信息:

  • WPARAM - 按键的 Virtual Key
  • LPARAM - 按键的参数,例如按下了几次

虚拟键码(Virtual Key)不等于输入内容

字符消息

  • TranslateMessage 在转换 WM_KEYDOWN 消息时,对于可见字符产生一个WM_CHAR的事件,如果按下不可见字符则无此消息
  • 附带信息:
    • WPARAM - 输入的字符的 ASCII 字符编码
    • 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
void onKeyDown(HWND hWnd, WPARAM wParam) {
char szText[256] = { 0 };
sprintf_s(szText, "WM_KEYDOWN:virtual key: %d\n", wParam);
WriteConsole(g_hOutput, szText, strlen(szText), NULL, NULL);
}
void onKeyUp(HWND hWnd, WPARAM wParam) {
onKeyDown(hWnd, wParam);
}
void onProdChar(HWND hWnd,WPARAM wParam) {
char szText[256] = { 0 };
sprintf_s(szText, "WM_CHAR:%c\n", (char)wParam);
WriteConsole(g_hOutput, szText, strlen(szText), NULL, NULL);
}

LRESULT CALLBACK WndProc(
HWND hWnd, // 传入某个窗口的句柄,操作系统告诉你是哪个窗口遇到了需要处理的事件
UINT msgID, // 传入窗口发生的事件,操作系统告诉你当前窗口发生了什么事情
WPARAM wParam, //附加参数 1
LPARAM lParam //附加参数 2
) {
switch (msgID) {
case WM_DESTROY:
//PostQuitMessage(0);
PostMessage(hWnd, WM_QUIT, 0, 0);
break;
case WM_KEYDOWN:
onKeyDown(hWnd, wParam);
break;
case WM_KEYUP:
onKeyUp(hWnd, wParam);
break;
case WM_CHAR:
onProdChar(hWnd, wParam);
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}

鼠标消息

  • 基本鼠标消息:

    WM_LBUTTONDOWN - 鼠标左键按下

    WM_LBUTTONUP - 鼠标左键抬起

    WM_RBUTTONDOWN - 鼠标右键按下

    WM_RBUTTONUP - 鼠标右键抬起

    WM_MOUSEMOVE - 鼠标移动消息

  • 双击消息

    WM_LBUTTONDBLCLK - 鼠标左键双击

    WM_RBUTTONDBLCLK - 鼠标右键双击

  • 滚轮消息

    WM_MOUSEWHEEL - 鼠标滚轮消息

  • 附带消息:

    wParam :其他button的状态,例如ctrl/shift

    lParam:鼠标的位置

定时器

定义

  • 产生时间:

    在程序中创建定时器,当到达时间间隔时,定时器会向程序发送信息。定时器的精度是ms,但是准确度很低。

  • 附带信息:

    wParam:定时器ID

    lParam:定时器处理函数的指针

1
2
3
4
5
6
UINT_PTR SetTime(
HWND hWnd, //定时器窗口句柄
UINT_PTR nIDEvent //定时器ID
UINT uElapse, //时间间隔
TIMERPROC lpTimerFunc //定时器处理函数,可以为NULL
);
1
2
3
4
BOOL KillTimer(
HWND hWnd, //定时器窗口句柄
UINT_PTR uIDEvent //定时器id
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_CREATE:
SetTimer(hWnd, 1, 1000, NULL);
SetTimer(hWnd, 2, 2000, NULL);
break;
case WM_TIMER:
OnTimer(hWnd, wParam);
break;

void OnTimer(HWND hWnd, WPARAM wParam) {
switch (wParam) {
case 1:
WriteConsole(g_hOutput, "Timer1\n", strlen("Timer1\n"), NULL, NULL);
break;
case 2:
WriteConsole(g_hOutput, "Timer2\n", strlen("Timer2\n"), NULL, NULL);
}
}

Win32 GUI 编程 2.1 资源

资源

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

菜单

菜单类型

  • 窗口的顶层菜单
  • 弹出式菜单
  • 系统菜单

HMEMU:菜单句柄

ID:菜单项

菜单的装载

  • 添加菜单资源

  • visual studio有图形化界面

  • 加载菜单资源

    • 注册窗口类时设置菜单

      1
      mainWindow.lpszMenuName = (char*)IDR_MENU;
    • 创建窗口传参设置菜单

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      HMENU mainWindowMenu = LoadMenu(
      hIns, //current thread handle
      (LPCTSTR)IDR_MENU // resource identifier
      );

      HWND mainWindowHandle = CreateWindow(
      TEXT("Main"), // 创建哪个窗口,这里显然我们想要创建刚刚注册的那个窗口,这里传入的类名要和前面的一样
      TEXT("Hello World!"), // 窗口的标题
      WS_OVERLAPPEDWINDOW, // 窗口的风格
      100, //窗口的位置数据,x,y,width,height
      100,
      500,
      500,
      NULL, //这个窗口有没有父窗口(eg.QQ密码登陆错误,跳出来一个报错消息框,报错消息框是QQ登录窗口的子窗口) 没有就传NULL
      mainWindowMenu, //这个窗口有没有菜单,没有传NULL,有传入菜单句柄
      hIns, //这个窗口从属于哪个程序,传句柄
      NULL //这个窗口有没有别的参数
      );
    • 在主窗口 WM_CREATE 消息中利用 SetMenu 设置菜单

      1
      SetMenu(HWND hWnd,HMENU hMenu);

菜单事件监听

事件名称:WM_COMMAND

LOWORD(wParam):点击的菜单项的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void onCommand(HWND hWnd, WPARAM wParam) {
//LOWORD(wParam) indicate id of tab that has been clicked
switch (LOWORD(wParam)) {
case ID_FILE_EXIT:
PostQuitMessage(0);
break;
case ID_FILE_CREATE:
MessageBox(
hWnd,
TEXT("We have not yet fulfill this"),
TEXT("sumimase!"),
MB_OK
);
}
}

图标资源

  • 添加图标资源:visual studio有成型的方案

  • 加载:

    1
    2
    3
    4
    HICON LoadIcon(
    HINSTANCE hInstance,
    LPCTSTR lpIconName
    );
  • 设置:注册窗口类

字符串资源

  • 添加字符串资源:添加字符串表

  • 字符串资源引用:

    1
    2
    3
    4
    5
    6
    int LoadString(
    HINSTANCE hInstance,
    UINT uID, //字符串id
    LPTSTR lpBuffer, //存放字符串的buffer char*
    int nBufferMax //字符串buffer长度
    );

Win32 GUI 编程 2.2 绘图

概念

  • 绘图设备 - DC(Device Context) ,绘图上下文、绘图描述表

  • HDC - DC句柄,表示绘图设备

  • GDI - Windows graphics device interface (Win32 提供的绘图API)

  • 颜色:

    • R - red
    • G - green
    • B - blue
  • 颜色的使用

    COLORREF - 实际DWORD

    例如:COLORREF nColor = 0;

  • 赋值使用RGB宏

    例如:nColor = RGB(0,0,225);

  • 获取RGB值:

    GetRValue….

    BYTE nRed = GetRValue( nColor );

图形绘制

  • SetPixel 设置指定点的颜色

    1
    2
    3
    4
    5
    6
    COLORREF SetPixel(
    HDC hdc,
    int X,
    int Y,
    COLORREF crColor
    );
    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
    void drawPit(HDC hdc) {
    SetPixel(hdc, 100, 100, RGB(255, 0, 0));
    }

    void onFirstPaint(HWND hWnd) {
    PAINTSTRUCT nowWindowPaint = { 0 };
    HDC handlePaint = BeginPaint(hWnd, &nowWindowPaint);
    drawPit(handlePaint);
    }


    //窗口处理函数(自定义,处理消息)
    LRESULT CALLBACK WndProc(
    HWND hWnd,
    UINT msgID,
    WPARAM wParam,
    LPARAM lParam
    ) {
    switch (msgID) {
    case WM_PAINT:
    onFirstPaint(hWnd);
    break;
    case WM_DESTROY:
    PostQuitMessage(0);
    break;
    }
    return DefWindowProc(hWnd, msgID, wParam, lParam);
    }
  • 线的使用(Line,Curve)

    MoveToEx - 指向窗口当前点

    LineTo - 从窗口当前点到指定点绘制一条直线

    当前点:上次绘图时的最后一点,初始为(0,0)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void drawLine(HDC hdc) {
    MoveToEx(
    hdc, // handle DC
    100, // X
    100, // Y
    NULL // getLastPit
    );
    LineTo(hdc, 300, 300);
    }
    void onFirstPaint(HWND hWnd) {
    PAINTSTRUCT nowWindowPaint = { 0 };
    HDC handlePaint = BeginPaint(hWnd, &nowWindowPaint);
    //drawPit(handlePaint);
    drawLine(handlePaint);
    }
  • 封闭图形:能够用画刷填充的图形

    Rectangle,Eclipse

GDI绘图对象 - 画笔

  • 画笔的作用

    线的颜色、线的形状、线的粗细

    HPEN - 画笔句柄

  • 画笔的使用

    1、创建画笔

    1
    2
    3
    4
    5
    HPEN CreatePen(
    int fnPenStyle, //画笔的样式
    int nWidth, //画笔的粗细
    COLORREF crColor // 颜色
    ); //创建成功返回句柄

    2、HDC 绑定 HPEN

    1
    2
    3
    4
    HGDIOBJ SelectObject(
    HDC hdc, // 绘图设备句柄
    HGDIOBJ hgdiobj //GDI绘图对象句柄 - 画笔句柄
    ); //返回原来的GDI绘图对象句柄 - 原来的画笔句柄

    HGDIOBJ - Handle of GDI object

    GDI - Graphic Device Interface

    3、绘图

    4、取出画笔(用SelectObject,将原来的画笔还回去)

    5、释放画笔

    1
    2
    3
    BOOL DeleteObject(
    HGDIOBJ hObject //GDI绘图对象句柄 - 画笔句柄
    );

GDI绘图对象 - 画刷

  • 画刷相关

    画刷 - 封闭突匈的填充的颜色、pattern

    HBRUSH - 画刷句柄

  • 画刷的使用

    1 创建画刷

    • CreateSolidBrush - 创建实心画刷
    • CreateHatchBrush - 创建纹理画刷

    2 将画刷应用到DC中

    • SelectObject

    3 绘图

    4 将画刷取出

    • SelectObject

    5 删除画刷

    • DeleteObject
  • 注意:Device Context既有刷子又有画笔

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
void onFirstPaint(HWND hWnd) {
PAINTSTRUCT nowWindowPaint = { 0 };
HDC handlePaint = BeginPaint(hWnd, &nowWindowPaint);

HPEN hPen = CreatePen(
PS_SOLID, //Pen style
1, // Pen width
RGB(255, 0, 0) //Pen color
);

HBRUSH hBrush = CreateSolidBrush(RGB(0, 255, 0));
HGDIOBJ nOldBrush = SelectObject(
handlePaint,
hBrush
);

HGDIOBJ nOldPen = SelectObject(
handlePaint, //Handle of Device Context
hPen // New Pen
);

//drawPit(handlePaint);
//drawLine(handlePaint);
drawRect(handlePaint);
drawEll(handlePaint);

SelectObject(handlePaint, nOldPen);
SelectObject(handlePaint, nOldBrush);
DeleteObject(hPen);
DeleteObject(hBrush);
}
  • 可以用GetStockObject函数获取系统维护的画刷、画笔等。

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

    GetStockObject返回的画刷不需要DeleteObject。

位图

位图绘制

  • 位图概念

    光栅图形 - 保存图像中每一个颜色的信息(位图)

    矢量图形 - 记录图像算法

    HBITMAP - 位图句柄

  • 位图的使用

    1 在资源中添加位图

    2 从资源中加载位图LoadBitmap

    3 创建一个与当前DC相匹配的DC(Memory_DC)

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

    注意:当前DC是直接在屏幕上画图,CreateCompatibleDC创建出来的DC是在内存里面画图

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

    5 成像(1:1)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    BOOL bitBlt(
    HDC hdcDest, //Destination
    int nXDest,
    int nYDest,
    int nWidth,
    int nHeight,
    HDC hdcSrc, //Source
    int nXSrc,
    int nYSrc,
    DWORD dwRop // 成像方法 如SRCCOPY
    );

    6 取出位图

    7 释放位图、Memory_DC

  • 缩放位图:StretchBlt

Win32 GUI 编程 3.1 常用系统窗口类(标准控件)

完整文档,请访问:

https://docs.microsoft.com/en-us/windows/win32/controls/window-controls

提示:找到左边的 Control Library,点进去。

按钮

基本属性

窗口类标签:Button

创建一个按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
HWND hwndButton = CreateWindow(
L"Button",
L"Submit",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON | WS_VISIBLE,
200,
200,
100,
50,
mainWindowHandle,
(HMENU)1001, //对于子组件,hmenu的值作为id让父窗口区分
hIns,
NULL
);

对于子窗口,HMENU 的值可以被父窗口调用。

事件的监听

按钮向消息队列中发送 Notification ,收信的是父窗口。

父窗口监听 WM_COMMAND 事件,以上面代码生成的按钮为例子,此按钮被点击后,生成 WM_COMMAND事件发送给父窗口;

LOWORD(wParam) = 1001 (对于子组件,hmenu的值作为id让父窗口区分)

HIWORD(wParam) = BN_CLICKED

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (msgID) {
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case 1001:
//do something ...
break;
}
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}

复选框

基本属性

窗口类标签:ComboBox

创建一个复选框:

1
2
3
4
5
6
7
8
9
10
11
12
13
HWND hWndComboBox = CreateWindow(
TEXT("ComboBox"),
TEXT(""),
CBS_DROPDOWNLIST | CBS_HASSTRINGS | WS_CHILD | WS_OVERLAPPED | WS_VISIBLE,
100,
100,
200,
200,
mainWindowHandle,
NULL,
hIns,
NULL
);

添加选择项

ComboBox可以通过:(二选一)

  • SendMessage 发送CB_ADDSTRING 消息
  • 调用 ComboBox_AddString 函数(需要 #include<windowsx.h> )

这两种方式添加可选择项

获得目前被选择项

1
int selectionComboBox = SendMessage(g_hWndComboBox, CB_GETCURSEL, 0, 0);

SendMessage:CB_GETCURSEL。

返回的是目前被选中的项目的数字编号(第一个被选中返回0,第二个被选中返回1,以此类推)

实例

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <windows.h>
#include <windowsx.h>
#include <stdio.h>

LPCTSTR labelComboBox[6] = {
L"巨佬A",
L"巨佬B",
L"巨佬C",
L"巨佬D",
L"巨佬E",
L"巨佬F",
};

HWND g_hWndButtonSubmit = 0;
HWND g_hWndComboBox = 0;
HANDLE g_console = 0;

LRESULT CALLBACK WndProc(
HWND hWnd,
UINT msgID,
WPARAM wParam,
LPARAM lParam
) {
switch (msgID) {
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case 1001:
int selectionComboBox = SendMessage(g_hWndComboBox, CB_GETCURSEL, 0, 0);
LPCTSTR showWord = L"you didn't choose";
if (selectionComboBox != CB_ERR)
showWord = labelComboBox[selectionComboBox];
if (HIWORD(wParam) == BN_CLICKED) {
MessageBox(
hWnd,
showWord,
L"the people you choose is ...",
MB_OK
);
}
break;
}
break;
}
return DefWindowProc(hWnd, msgID, wParam, lParam);
}

#define MYWINDOWBUFFER 100
#define MYCLASSBUFFER 100

int CALLBACK WinMain(
HINSTANCE hIns,
HINSTANCE hPerIns,
LPSTR lpCmdLine,
int nCmdShow
) {
AllocConsole();
g_console = GetStdHandle(STD_OUTPUT_HANDLE);
WNDCLASS mainWindow = { 0 };
mainWindow.cbClsExtra = MYCLASSBUFFER;
mainWindow.cbWndExtra = MYWINDOWBUFFER;
mainWindow.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
mainWindow.hCursor = NULL;
mainWindow.hIcon = NULL;
mainWindow.hInstance = hIns;
mainWindow.lpfnWndProc = WndProc;
mainWindow.lpszClassName = TEXT("Main");
mainWindow.lpszMenuName = NULL;
mainWindow.style = CS_HREDRAW | CS_VREDRAW;

RegisterClass(&mainWindow);

HWND mainWindowHandle = CreateWindow(
TEXT("Main"),
TEXT("Hello World!"),
WS_OVERLAPPEDWINDOW,
100,
100,
500,
500,
NULL,
NULL,
hIns,
NULL
);

HWND hWndComboBox = CreateWindow(
TEXT("ComboBox"),
TEXT(""),
CBS_DROPDOWNLIST | CBS_HASSTRINGS | WS_CHILD | WS_OVERLAPPED | WS_VISIBLE,
100,
100,
200,
200,
mainWindowHandle,
NULL,
hIns,
NULL
);
g_hWndComboBox = hWndComboBox;

HWND hwndButton = CreateWindow(
L"Button",
L"Submit",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON | WS_VISIBLE,
200,
200,
100,
50,
mainWindowHandle,
(HMENU)1001, //对于子组件,hmenu的值作为id让父窗口区分
hIns,
NULL
);
g_hWndButtonSubmit = hwndButton;



for (int i = 0; i < 6; i++) {
ComboBox_AddString(hWndComboBox, labelComboBox[i]);
}

ShowWindow(
mainWindowHandle,
SW_SHOW
);
UpdateWindow(mainWindowHandle);

MSG msgLoop = { 0 };
while (GetMessage(
&msgLoop,
NULL,
0,
0
)) {
TranslateMessage(&msgLoop);
DispatchMessage(&msgLoop);
}

return 0;
}