创建一个窗口

创建窗口需要那些步骤?

  • 1.注册窗口类
  • 2.创建窗口实例
  • 3.显示窗口
  • 4.更新窗口
  • 5.建立消息循环
  • 6.实现窗口过程函数

使用RegisterClass注册窗口类

RegisterClass的作用是告知系统我们所需要的窗口风格

1
2
3
ATOM RegisterClass(
const WNDCLASS *lpWndClass
);

参数 lpWndClass为指向WNDCLASS结构体的指针

WNDCLASS结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _WNDCLASS { 
UINT style; //风格
//一般填下面两项(当窗口被修改时,系统会通知窗口重新绘制界面)。
//CS_VREDRAW || CS_HREDRAW
WNDPROC lpfnWndProc; //回调函数
int cbClsExtra; //额外大小,窗口类使用
int cbWndExtra; //额外大小,窗口实例使用
//不使用额外大小,填0即可
HINSTANCE hInstance; //窗口句柄,可以从main函数的参数中获取。
HICON hIcon; //图标
HCURSOR hCursor; //光标
HBRUSH hbrBackground; //背景颜色
//以上三项,没有可以填NULL
LPCTSTR lpszMenuName; //菜单名称,没有可以填NULL
LPCTSTR lpszClassName; //窗口类的名字
} WNDCLASS, *PWNDCLASS;

此结构体详细参数可以查看官方文档☛[WNDCLASSA structure]

提供给此结构体lpfnWndProc参数的回调函数

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM 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
#include <windows.h>

//定义回调函数
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
return 1;
}

/*
APIENTRY: __stdcall调用约定
_In_ : 说明性宏
HINSTANCE hInstance : 实例句柄,代表应用程序本身。
HINSTANCE hPrevInstance : 已弃用,为了兼容老的入口函数。
LPWSTR lpCmdLine : 命令行参数。
int nCmdShow : 控制显示。
*/
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
//设计注册窗口类的结构体
WNDCLASS wc;
wc.style = CS_VREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = NULL;
wc.cbWndExtra = NULL;
wc.hInstance = hPrevInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = TEXT("NEWCLASS");
//使用结构体注册窗口类
ATOM ret = RegisterClass(&wc);

//判断注册窗口类是否成功
if (ret != 0)
{
MessageBox(NULL, TEXT("注册窗口类成功"), TEXT("提示"), MB_OK);
return 0;
}
return 0;
}

使用CreateWindow创建窗口实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HWND CreateWindow
(
LPCTSTR lpClassName, // 类名
LPCTSTR lpWindowName, // 标题
DWORD dwStyle, // 风格
/*三种基本风格:其他风格都是依次为基础添加的
WS_CHILD:子窗口
WS_OVERLAPPED:重叠 WS_OVERLAPPEDWINDOW:重叠PLUS版 要用肯定用PLUS版
WS_POPUP:对话框
这三种风格互斥,且必须有一个,
*/
//xy坐标,窗口宽高,如果不想填写可以使用 CW_USEDEFAULT
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent, // 父窗口
HMENU hMenu, // 菜单
HINSTANCE hInstance, // 实例句柄
LPVOID lpParam // 参数,没有参数填NULL
);

使用CreateWindow创建窗口实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建窗口实例
HWND hWnd = CreateWindow(
TEXT("NEWCLASS"),
TEXT("Text Window"),
WS_OVERLAPPEDWINDOW,
600, 600, 600, 600,
NULL,
NULL,
hInstance,
NULL
);

if (hWnd != NULL)
{
MessageBox(NULL, TEXT("创建窗口实例成功"), TEXT("提示"), MB_OK);
}

使用ShowWindow显示窗口

1
2
3
4
BOOL ShowWindow(
HWND hWnd, // 窗口句柄
int nCmdShow // 显示状态
);

直接将上面创建窗口实例返回的句柄传给ShowWindow

1
ShowWindow(hWnd, SW_SHOW);

ShowWindow显示状态的详细参数可以查看官方文档☛[ShowWindow]

运行后就可以看到任务栏出现了一个窗口
img

使用UpdateWindow更新窗口

1
2
3
4
CPP
BOOL UpdateWindow(
HWND hWnd
);

就传一个句柄,没啥好说的

1
UpdateWindow(hWnd);

建立消息循环

如果要理解什么是“消息循环”首先要知道这样一个概念:

1
Windows是一个“基于事件的,消息驱动”的操作系统
  • Windows消息机制

在正常情况下,程序是无法直接操作输入输出设备(如鼠标、键盘、显示器等)的,因为所有的输入输出设备都被操作系统接管,关系图如下:
img

但是这就会出现另一个问题,操作系统知道消息什么时候会来,但是不知道怎么处理这个消息,应用程序知道这个消息怎么处理,但是不知道消息什么时候会来。这个问题的解决方法就是使用回调函数

  • 用户产生的消息千千万,系统是如何知道把哪个信息传递给哪个程序处理呢?

首先软件开发者会在程序定义一个回调函数,程序运行后,操作系统会获得回调函数的地址,当接收到消息时,操作系统会通过回调函数将消息传递回给应用程序处理。

在Windows上执行一个程序,只要用户进行了影响窗口的动作(如键盘输入、鼠标点击、移动窗口等)该动作就会触发一个相应的“事件”,系统每次检测到一个事件时,就会通过回调函数给程序发送一个信息,从而使程序可以处理该事件。

当然,操作系统可不会来一条信息发一条给程序,等程序处理完了再发下一条,操作系统会将消息存入“消息队列”中,应用程序从队列中取出消息进行处理,处理完该条后,去从队列中取出下一条进行处理,依次循环,这个处理过程就叫做“消息循环”。

img

建立消息循环需要两个步骤

  • 1.使用GetMessage从消息队列获取消息
1
2
3
4
5
6
BOOL GetMessage(
LPMSG lpMsg, // 指向MSG结构的指针,该结构从线程的消息队列接收消息信息。
HWND hWnd, // 窗口句柄
UINT wMsgFilterMin,
UINT wMsgFilterMax //限制获取消息的最大最小值,一般为0(不限制)
);

MSG结构:

1
2
3
4
5
6
7
8
9
10
typedef struct tagMSG {
HWND hwnd; //窗口句柄,用于区分多个窗口
UINT message; //消息标识符。应用程序只能使用低位字;高位字由系统保留。
WPARAM wParam; //有关消息的其他信息
LPARAM lParam; //有关消息的其他信息

DWORD time; //消息产生时间
POINT pt; //消息发布时的光标位置,以屏幕坐标表示。
DWORD lPrivate;
} MSG, *PMSG, *NPMSG, *LPMSG;

使用方法:

1
2
MSG msg;
GetMessage(&msg,mull,0,0);
  • 2.使用DispatchMessage将信息调度到窗口过程函数
1
2
3
LRESULT DispatchMessage(
const MSG *lpMsg //指向包含消息的结构的指针。
);

直接将声明好的msg结构体传给它即可:

1
DispatchMessage(&msg);
  • 3.建立消息循环
1
2
3
4
while(GetMessage(&msg,NULL,0,0))
{
DispatchMessage(&msg);
}

实现窗口过程函数

因为现在还用不到接收到的信息,所以就直接使用DefWindowProc函数让系统默认帮我们处理信息。

函数参数:

1
2
3
4
5
6
LRESULT LRESULT DefWindowProcA(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);

直接在回调函数中调用:

1
2
3
4
5
6
7
8
9
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

窗口销毁

如果窗口关闭后不进行窗口销毁,那么程序将会一直运行着消息循环,那么如何进行窗口销毁呢?
首先消息循环是建立在GetMessage的返回值上的,如果返回值为0,while循环自然会停止。
那么又如何让GetMessage返回0呢?微软文档是这样描述的:

如果GetMessage函数检索到WM_QUIT以外的消息,则返回值为非零。
如果GetMessage函数检索WM_QUIT消息,则返回值为零。

在我们点击关闭窗口时,会发送一个WM_CLOSE消息。

那么很简单,只需要在回调函数处理一下WM_CLOSE消息让,他给消息队列发送一个WM_QUIT消息即可。

这里需要用到PostQuitMessage函数发送WM_QUIT消息。

1
2
3
void PostQuitMessage(
int nExitCode //应用程序退出代码 随便填
);

在回调函数中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LRESULT CALLBACK FirstWindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch (uMsg)
{
case WM_CLOSE:
//向消息队列中投递一个WM_QUIT消息用来结束消息循环。
PostQuitMessage(0);
return 0;

default:
break;
};
return DefWindowProc(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
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
#include <windows.h>

//定义回调函数
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch (uMsg)
{
case WM_CLOSE:

//向消息队列中投递一个WM_QUIT消息用来结束消息循环。
PostQuitMessage(0);
return 0;

default:
break;
};
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

/*
APIENTRY: __stdcall调用约定
_In_ : 说明性宏
HINSTANCE hInstance : 实例句柄,代表应用程序本身。
HINSTANCE hPrevInstance : 已弃用,为了兼容老的入口函数。
LPWSTR lpCmdLine : 命令行参数。
int nCmdShow : 控制显示。
*/

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
//设计注册窗口类的结构体
WNDCLASS wc;
wc.style = CS_VREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = NULL;
wc.cbWndExtra = NULL;
wc.hInstance = hPrevInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = TEXT("NEWCLASS");
//使用结构体注册窗口类
ATOM ret = RegisterClass(&wc);

//判断注册窗口类是否失败
if (ret == 0)
{
MessageBox(NULL, TEXT("注册窗口类失败"), TEXT("提示"), MB_OK);
return 0;
}

//创建窗口实例
HWND hWnd = CreateWindow(
TEXT("NEWCLASS"),
TEXT("Text Window"),
WS_OVERLAPPEDWINDOW,
600, 600, 600, 600,
NULL,
NULL,
hInstance,
NULL
);

if (hWnd != NULL)
{
MessageBox(NULL, TEXT("创建窗口实例成功"), TEXT("提示"), MB_OK);
}

ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);

MSG msg;

while (GetMessage(&msg, NULL, 0, 0))
{
DispatchMessage(&msg);
}

return 0;
}

绘图消息

什么是绘图消息?

当窗口出现“无效区“时,系统会检测并发送WM_PAINT消息。对窗口的无效区部分进行重新绘制。

什么是无效区?

以下的几种情况会产生无效区:

  • 1.窗口创建
  • 2.窗口尺寸修改
  • 3.窗口最小化最大化
  • 4.窗口被遮盖的部分重现

看到上面四个产生无效区的情况,想必你已经能大概知道窗口会在何时产生无效区,当窗口(一部分或是全部)从屏幕上消失后,需要重新显示在屏幕上,消失的部分就被称为”无效区“。

WM_PAINT消息定义

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);

使用DrawText函数绘制文本

DrawText函数定义:

1
2
3
4
5
6
7
int DrawText(
HDC hdc, //设备上下文的句柄
LPCTSTR lpchText, //指向指定要绘制文本的字符串的指针
int cchText, //字符串的长度(以字符为单位)
LPRECT lprc, //指向RECT结构体的指针,该结构包含要在其中格式化文本的矩形
UINT format //文本格式
);

DrawText函数的第一个参数hdc在官方文档的描述是设备上下文的句柄.

DC的全称是(Device Context)Ennnn…. 这又是个啥玩意?

在以前的DOS系统上,系统把屏幕抽象成了一个内存,直接往相应的内存空间中写入内容即可在屏幕上进行输出。而在Windows系统中则将屏幕抽象成了DC,操作DC即可在屏幕上进行输出。

RECT结构体:

1
2
3
4
5
6
typedef struct tagRECT {
LONG left; //指定矩形左上角的x坐标
LONG top; //指定矩形左上角的y坐标。
LONG right; //指定矩形右下角的x坐标。
LONG bottom;//指定矩形右下角的y坐标。
} RECT, *PRECT, *NPRECT, *LPRECT;

使用BeginPaint获取DC

BeginPaint定义:

1
2
3
4
HDC BeginPaint(
HWND hWnd, //处理要重绘的窗口
LPPAINTSTRUCT lpPaint //指向将接收绘画信息的PAINTSTRUCT结构的指针。
);

使用EndPaint释放DC

在DC资源使用完之后,必须要对其进行释放,否则GDI对象的占用数将会越来越高。
EndPaint定义:

1
2
3
4
BOOL EndPaint(
HWND hWnd, //处理已重新绘制的窗口。
const PAINTSTRUCT *lpPaint //指向一个PAINTSTRUCT结构的指针,该结构包含BeginPaint检索的绘画信息。
);

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case WM_PAINT://绘图消息
{
//获取DC句柄
PAINTSTRUCT ps;
HDC hDC= BeginPaint(hwnd, &ps);
//绘制文本
char szBuff[] = { "Hello World" };

RECT rc = {
600,600 //左上角
0,0 //右下角
};

DrawText(
hDC,
szBuff,
strlen(szBuff),
&rc,
DT_CENTER
);

//释放资源
EndPaint(hwnd, &ps);
}

在系统不发送WM_PAINT消息时绘图

使用InvalidateRect函数手动发送WM_PAINT消息

使用InvalidateRect手动制造一块需要绘制的区域(手动产生无效区)。

1
2
3
4
5
BOOL InvalidateRect(
HWND hWnd, //窗口的句柄
const RECT *lpRect,//需要重新绘制的矩形大小,填NULL整个客户区重新绘制
BOOL bErase //是否擦除背景
);

使用方法:

1
InvalidateRect(hWnd, NULL, TRUE);

使用GetDC函数

GetDC函数检索指定窗口的客户区域或整个屏幕的设备上下文(DC)的句柄。可以在后续的GDI函数中使用返回的句柄来绘制DC。

函数原型:

1
2
3
HDC GetDC(
HWND hWnd
);

使用方法:

1
HDC hDc = GetDC(hwnd);

使用GetDC后对应使用ReleaseDC来释放空间。

函数原型:

1
2
3
4
int ReleaseDC(
HWND hWnd, // handle to window
HDC hDC // handle to DC
);

BeginPaint和GetDC的区别

  1. BeginPaint会清除无效区,GetDC不会。
  2. BeginPaint绘制区域不超过无效区。
  3. GetDC无视无效区。
  4. BeginPaint只能在PAINT消息中使用。

简要区分:BeginPaint在WM_PAINT消息中使用,其他地方用GetDC。

键盘消息

WM_KEYDOWN信息

在键盘按下时会发送WM_KEYDOWN信息

WM_KEYDOWN的定义:

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_KEYDOWN
WPARAM wParam, // virtual-key code 虚拟码
LPARAM lParam // key data 扫描码
);

注意!WM_KEYDOWN是无法区分大小写的,如要区分大小写,需要使用WM_CHAR来处理消息。

使用方法:

1
2
3
4
5
6
7
case WM_KEYDOWN:
{
char szBuff[MAXBYTE] = {0};
wsprintf(szBuff,"[TEXT] %c",wParam);
OutputDebugString(szBuff);
return 0;
}

使用”OutputDebugString“后,键盘消息就可以被DebugView检测到

img

WM_CHAR消息

如果要使用WM_CHAR消息区分大小写,需要在消息循环使用TranslateMessage函数将WM_KEYDOWN消息转换为WM_CHAR消息。

TranslateMessage函数原形:

1
2
3
BOOL TranslateMessage(  
CONST MSG *lpMsg // message information
);

使用时只需要将msg的结构体指针传给他就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))//拿到所有消息
{
//将WM_KEYDOWN消息转换为WM_CHAR消息
TranslateMessage(&msg);
//调用回调函数
DispatchMessage(&msg);
}
//回调函数内部
case WM_CHAR:
{
char szBuff[MAXBYTE] = { 0 };
wsprintf(szBuff, "CHAR: %c", wParam);
OutputDebugString(szBuff);
return 0;
}

使用WM_CHAR处理中文

1
2
3
4
5
6
7
8
9
10
case WM_CHAR:
{
static char szBuff[MAXBYTE] = { 0 };
static int nIdx = 0;
szBuff[nIdx++] = wParam;
char szBuffFmt[MAXBYTE] = { 0 };
wsprintf(szBuffFmt, "[TEXT] char: %s", szBuff);
OutputDebugString(szBuffFmt);
return 0;
}

鼠标消息

鼠标信息有非常的多,这里就不一 一举例了,需要处理哪一种消息,只需要在回调函数中定义就行。

这里使用WM_LBUTTONDOWN(鼠标左键单击)作为例子

1
2
3
4
5
6
7
8
9
10
case WM_LBUTTONDOWN:
{
WORD xPos = GET_X_LPARAM(lParam);//需要调用windowsx.h头文件
WORD yPos = GET_Y_LPARAM(lParam);

CHAR szBuff[MAXBYTE] = { '\0' };
wsprintf(szBuff, TEXT("x:%d y:%d"), xPos, yPos);
MessageBox(hwnd, szBuff, TEXT("坐标"), MB_OK);
break;
}

更多鼠标消息可以查看官方文档☛[mouse-input]

定时器消息

使用SetTimer函数设置定时器

在设置定时器之后,定时器会每隔一段时间给窗口发一次WM_TIMER消息。

函数定义:

1
2
3
4
5
6
UINT_PTR SetTimer(
HWND hWnd, //窗口句柄
UINT_PTR nIDEvent, //定时器ID,必须为非0
UINT uElapse, //时间间隔(毫秒)
TIMERPROC lpTimerFunc //当时间到了以后执行回调函数,可以设置为NULL
);

使用方法:

1
SetTimer(hwnd,1,1000,NULL);

WM_TIMER消息

定义:

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_TIMER
WPARAM wParam, // timer identifier
LPARAM lParam // timer callback (TIMERPROC)
);

在回调函数中使用:

1
2
3
4
5
6
7
8
9
case WM_TIMER:
{
char szBuff[MAXBYTE] = { 0 };
static int nCount = 0;
nCount++;
wsprintf(szBuff, "Timer: %d", nCount);
OutputDebugString(szBuff);
return 0;
}

效果:

img

使用KillTimer函数关闭定时器

1
2
3
4
BOOL KillTimer(
HWND hWnd, // 与指定计时器关联的窗口的句柄。
UINT_PTR uIDEvent // 定时器id
);

发送消息

微软提供了两个API让程序员可以手动向窗口发送消息

使用PostMessage将消息投递到消息队列

函数定义:

1
2
3
4
5
6
7
CPP
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);

使用方法:

1
PostMessage(hWnd, WM_KEYDOWN, 'a', 0);

使用SendMessage直接调用窗口过程函数

函数定义:

1
2
3
4
5
6
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);

使用方法:

1
SendMessage(hWnd, WM_KEYDOWN, 0, 0);

自定义消息

为了区分系统的消息和自定义的消息,windows提供了WM_USER宏。

img

系统的消息都是在0x400以下,所以自定义消息,要在400以上,才不会和系统消息冲突。

demo:

1
2
3
4
5
6
7
8
9
10
//设置自定义消息
#define WM_MYMSG WM_USER+1
//发送消息
SendMessage(hWnd, WM_MYMSG, 0, 0);
//在窗口过程函数内处理消息
case WM_MYMSG:
{
MessageBox(hwnd, "WM_MYMSG", NULL, NULL);
return 0;
}

资源

右击项目->添加->资源 中可以新建所需要的资源文件

img

Icon图标资源

img

使用LoadIcon函数加载图标资源

函数定义:

1
2
3
4
HICON LoadIconA(
HINSTANCE hInstance, //实例句柄
LPCSTR lpIconName //图标名称
);

第二个参数lpIconName是指图标属性中的ID:
img

使用RegisterClass中的结构体加载Icon图标资源:

1
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1);

Cursor光标资源

img

使用LoadCursor函数加载光标资源

函数定义和icon一样:

1
2
3
4
HCURSOR LoadCursor(
HINSTANCE hInstance,
LPCSTR lpCursorName
);

使用RegisterClass中的结构体加载Cursor图标资源:

1
wc.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(CUR_CUR1));

hbrBackground背景资源

微软提供了一些预定义好的宏,需要注意的是官方文档中说明,在使用官方宏的时候需要在所选颜色上+1.

![img](https://s1.ax1x.com/2020/09/20/wTSqhR.png

官方预定义好的背景宏:

  • COLOR_ACTIVEBORDER
  • COLOR_ACTIVECAPTION
  • COLOR_APPWORKSPACE
  • COLOR_BACKGROUND
  • COLOR_BTNFACE
  • COLOR_BTNSHADOW
  • COLOR_BTNTEXT
  • COLOR_CAPTIONTEXT
  • COLOR_GRAYTEXT
  • COLOR_HIGHLIGHT
  • COLOR_HIGHLIGHTTEXT
  • COLOR_INACTIVEBORDER
  • COLOR_INACTIVECAPTION
  • COLOR_MENU
  • COLOR_MENUTEXT
  • COLOR_SCROLLBAR
  • COLOR_WINDOW
  • COLOR_WINDOWFRAME
  • COLOR_WINDOWTEXT

使用方法:

1
wc.hbrBackground = (HBRUSH)(COLOR_ACTIVEBORDER + 1);

img

在类中绑定菜单

1
wc.lpszMenuName = MAKEINTRESOURCE(MN_MN1);

创建窗口时使用LoadMenu绑定菜单

函数定义:

1
2
3
4
HMENU LoadMenuA(
HINSTANCE hInstance,
LPCSTR lpMenuName
);

使用方法:

1
LoadMenu(hInstance, MAKEINTRESOURCE(MN_MN1));

菜单消息

使用WM_COMMAND来处理菜单消息。
需要注意的是WM_COMMAND并不单单处理菜单的消息,还会处理控件和快捷键的消息。

消息来源 wParam(高字) wParam(低字) lParam
菜单 0 菜单标识符(IDM_ *) 0
快捷键 1 快捷键标识符(IDM_ *) 0
控件 控件定义的通知代码 控件标识符 Handle to the control window

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case WM_COMMAND:
{
WORD wFrom = HIWORD(wParam);//取高位
WORD wID = LOWORD(wParam);//取低位
if(wFrom == 0)//判断是否来自于菜单
{
switch(wID)
{
case MN_MENU1:
MessageBox(NULL, "MENU1", "提示", MB_OK);
break;
case MN_MENU2:
MessageBox(NULL, "MENU2", "提示", MB_OK);
break;
case MN_MENU3:
MessageBox(NULL, "MENU3", "提示", MB_OK);
break;
}
}
break;
}

Accelerator快捷键资源

img

设置快捷键

img

在“修饰符”处,选择快捷键。

在”键“处,设置按键,如果是字母按键直接输入即可。

使用LoadAccelerators加载快捷键

函数定义:

1
2
3
4
HACCEL LoadAcceleratorsA(
HINSTANCE hInstance,
LPCSTR lpTableName
);

使用方法:

1
HACCEL hAccel = LoadAccelerators(hInstance,MAKEINIRESOURCE(ACCEL_AC1));

在消息循环中,使用TranslateAccelerator函数将按键消息转换成WM_COMMAND消息。

1
2
3
4
5
6
//将按键消息转换成快捷键消息
if(!TranslateAccelerator(hWnd, hAccel, &msg))
{
//根据窗口调用对应的回调函数
DispatchMessage(&msg);
}

处理快捷键消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case WM_COMMAND:
{
WORD wFrom = HIWORD(wParam);//取高位
WORD wID = LOWORD(wParam);//取低位
if(wFrom == 1)//判断是否来自于菜单
{
switch(wID)
{
case CTRL_A:
MessageBox(NULL, "CTRL_A", "提示", MB_OK);
break;
case SHIFT_B:
MessageBox(NULL, "SHIFT_B", "提示", MB_OK);
break;
}
}
break;
}

对话框

img

模态对话框(阻塞父窗口)

创建模态对话框需要那些步骤?

  • 1.设计对话框资源模板
  • 2.实现对话框过程函数
  • 3.创建对话框

设计对话框资源模板

1
资源视图`->`右键菜单`->`添加资源`->`Dialog

实现对话框过程函数

1
2
3
4
5
6
7
8
9
//模态对话框过程函数
INT_PTR CALLBACK DialogProc(
HWND hwndDlg, // handle to dialog box
UINT uMsg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
}

使用DialogBox创建对话框

1
2
3
4
5
6
7
//创建对话框
DialogBox(
GetModuleHandle(NULL), //实例句柄
MAKEINTRESOURCE(IDD_DIALOG1),//资源ID
hwnd,//句柄
(DLGPROC)DialogProc//模态对话框过程函数
)

使用EndDialog销毁模态对话框

函数定义:

1
2
3
4
BOOL EndDialog(
HWND hDlg,
INT_PTR nResult
);

使用方法:

1
EndDialog(hwndDlg, NULL);

完整代码

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
//模态对话框过程函数
INT_PTR CALLBACK DialogProc(
HWND hwndDlg, // handle to dialog box
UINT uMsg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
switch (uMsg)
{
case WM_CLOSE:
{
//向消息队列中投递一个WM_QUIT消息用来结束消息循环。
//此处不能使用DestroyWindow销毁模态对话框
EndDialog(hwndDlg, NULL);//Dialog对话框需要使用EndDialog来销毁
return TRUE;
}
default:
break;
}
return FALSE;
}
//创建对话框
DialogBox(
GetModuleHandle(NULL), //实例句柄
MAKEINTRESOURCE(IDD_DIALOG1),//资源ID
hwnd,//句柄
(DLGPROC)DialogProc//模态对话框过程函数
)

详细信息可以查看官方文档☛[Dialog Box Functions]

模态对话框阻塞夫窗口的原因

    1. 内部自建消息循环
    1. 内部调用EnableWindow(禁用鼠标和键盘输入到指定的窗口或控件)

取消对父窗口的禁用

使用WM_INITDIALOG消息让对话框在初始化的时候取消对父窗口的禁用。

WM_INITDIALOG的定义:

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_INITDIALOG
WPARAM wParam, // handle to control (HWND) 窗口句柄
LPARAM lParam // 调用对话框时,设置为NULL
);

EnableWindow的定义:

1
2
3
4
BOOL EnableWindow(
HWND hWnd,
BOOL bEnable
);

完整代码:

1
2
3
4
5
6
case WM_INITDIALOG:
{
HWND hParent = GetParent(hwndDlg);//给予子窗口句柄返回父窗口
EnableWindow(hParent, TRUE);//允许父窗口接收键盘和鼠标输入
return TRUE;
}

EnableWindow设置True后,在模态对话框运行时,父窗口仍然可以接收鼠标和键盘的消息。

非模态对话框(不阻塞父窗口)

使用CreateDialog创建非模态对话框

函数定义:

1
2
3
4
5
6
void CreateDialog(
hInstance, //实例句柄
lpName, //资源ID
hWndParent, //hwnd
lpDialogFunc //对话框过程函数
);

使用方法:

1
HWND hWnd = CreateDialog(GetModuleHandle(NULL),MAKEINTRESOURCE(DLG_TEXT),hwnd,(DLGPROC)DialogProc);

使用ShowWindow 显示非模态对话框

1
ShowWindow(hWnd,SW_SHOW);

使用DestroyWindow销毁非模态对话框

1
DestroyWindow(hwndDlg);

控件

控件是微软封装好的一个个窗口

详细信息可以查看官方文档☛[Control Library]

按钮

前缀以N结尾的消息,当按钮状态发生改变时进行告知。

前缀以M结尾的消息,对按钮做操作。

处理按钮按下消息BN_CLICKED

BN_CLICKED的定义:

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_COMMAND
WPARAM wParam, // identifier of button, BN_CLICKED//按钮的Id
LPARAM lParam // handle to button (HWND)//为按钮的句柄
);

wParam高位是消息的ID,底位是控件的ID。

在WM_COMMAND中处理BN_CLICKED:

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
case WM_COMMAND:
{
WORD wMessage = HIWORD(wParam);//控件消息
WORD wControlId = LOWORD(wParam);//控件的Id
switch (wMessage)
{
case BN_CLICKED:
{
switch (wControlId)
{
case IDC_BUTTON_TEST:
{
MessageBox(hwndDlg, "按钮点击", NULL, MB_OK);
break;
}
case IDC_BUTTON_TEST2:
{
MessageBox(hwndDlg, "按钮点击2", NULL, MB_OK);
break;
}
}
}
}
return 0;
}

BM_CLICK模拟鼠标点击

使用GetDlgItem拿到对话框中控件的窗口句柄。

1
2
3
4
5
6
7
8
case IDC_BUTTON_SEND:
{
//获取按钮的窗口句柄
HWND hBtn2 = GetDlgItem(hwndDlg,IDC_BUTTON_TEST2);
//向按钮发送消息,模拟鼠标点击
SendMessage(hBtn2, BM_CLICK, 0, 0);
break;
}

文本编辑框

获取编辑框内容

使用GetLine按行获取文本内容。

1
2
3
4
5
6
7
8
9
10
case BTN_GETTEXT:
{
HWND hEdit = GetDlgItem(hwndDlg, IDC_EDIT_TEST);
char szBuff[MAXBYTE] = { 0 };
//缓冲区第一个字的内容,为缓冲区的大小
*(WORD *)szBuff = sizeof(szBuff);
SendMessage(hEdit, EM_GETLINE, 0 , (LPARAM)szBuff);
MessageBox(hwndDlg, szBuff, "编辑框内容", MB_OK);
break;
}

使用WM_GETTEXT获取本文内容(可以用来获取控件标题)

1
2
3
4
5
6
7
8
case BTN_GETTEXT:
{
HWND hEdit = GetDlgItem(hwndDlg, IDC_EDIT_TEST);
char szBuff[MAXBYTE] = { 0 };
SendMessage(hEdit, WM_GETTEXT, sizeof(szBuff), (LPARAM)szBuff);
MessageBox(hwndDlg, szBuff, "编辑框内容", MB_OK);
break;
}

使用GetWindowText获取文本编辑框内容

1
2
3
4
5
6
7
8
case BTN_GETTEXT:
{
HWND hEdit = GetDlgItem(hwndDlg, IDC_EDIT_TEST);
char szBuff[MAXBYTE] = { 0 };
GetWindowText(hEdit, szBuff, sizeof(szBuff));
MessageBox(hwndDlg, szBuff, "编辑框内容", MB_OK);
break;
}

使用GetDlgItemText获取文本编辑框内容

1
2
3
4
5
6
7
case BTN_GETTEXT:
{
char szBuff[MAXBYTE] = { 0 };
GetDlgItemText(hwndDlg, IDC_EDIT_TEST, szBuff, sizeof(szBuff));
MessageBox(hwndDlg, szBuff, "编辑框内容", MB_OK);
break;
}

如要设置设置编辑框文本内容,将上面GET改为SET即可

列表框

关闭列表框自动排序功能。

img

使用LB_ADDSTRING添加值

1
2
3
4
5
6
7
8
case IDC_BUTTON_ADD:
{
char szBuff[MAXBYTE] = { 0 };
GetDlgItemText(hwndDlg, IDC_EDIT1, szBuff, sizeof(szBuff));
HWND hLstb = GetDlgItem(hwndDlg, IDC_LIST_TEXT);
SendMessage(hLstb, LB_ADDSTRING, 0, (LPARAM)szBuff);
break;
}

使用LB_GETCURSEL获取当前选中项的索引值

1
2
3
HWND hLstb = GetDlgItem(hwndDlg, IDC_LIST_TEXT);
//获取被选中的索引值
int nIndex = SendMessage(hLstb, LB_GETCURSEL, 0, 0);

使用LB_DELETESTRING删除值

1
2
3
4
5
6
7
8
9
10
case IDC_BUTTON_DEL:
{
HWND hLstb = GetDlgItem(hwndDlg, IDC_LIST_TEXT);
//获取被选中的索引值
int nIndex = SendMessage(hLstb, LB_GETCURSEL, 0, 0);

//删除被选中的项
SendMessage(hLstb, LB_DELETESTRING, nIndex, 0);
break;
}

使用LB_GETTEXT获取内容

1
2
3
4
5
6
7
8
9
10
11
case IDC_BUTTON_GET:
{
HWND hLstb = GetDlgItem(hwndDlg, IDC_LIST_TEXT);
//获取被选中的索引值
int nIndex = SendMessage(hLstb, LB_GETCURSEL, 0, 0);
//获取字符串
char szBuff[MAXBYTE] = { 0 };
SendMessage(hLstb, LB_GETTEXT, nIndex, (LPARAM)szBuff);
MessageBox(NULL, szBuff, NULL, MB_OK);
break;
}

详细信息可以查看官方文档☛[List Box Messages]

子类化

什么是子类化?

在很多情况下,官方所封装好的控件功能并不能满足软件功能的需求,这时候就需要用到子类化技术,给控件添加我们自己所需要的功能。子类化允许你接管被子类化的窗口,使你对它有绝对的控制权。

实现自己需要的窗口过程函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//g_pfnOldEditProc需要定义成全局的 否则MyEditProc无法获取
WNDPROC g_pfnOldEditProc = NULL;

LRESULT CALLBACK MyEditProc(HWND hEdit,UINT message,WPARAM wParam,LPARAM lParam)
{
if(message == WM_CHAR)
{
if(
(('0' <= wParam) && (wParam <= '9')) ||
(('a' <= wParam) && (wParam <= 'z')) ||
(('A' <= wParam) && (wParam <= 'Z')))
{
//交给原来的窗口过程函数显示
return g_pfnOldEditProc(hEdit,message,wParam,lParam);
}
else
{
//不显示
return 0;
}
}
//其他消息依旧交给原来的窗口过程函数显示
return g_pfnOldEditProc(hEdit,message,wParam,lParam);
}

使用SetWindowLong实现子类化

函数定义:

1
2
3
4
5
LONG SetWindowLong(
HWND hWnd, //窗口的句柄
int nIndex, //从零开始的要设置值的偏移量
LONG dwNewLong //替换值。
);

使用方法:

1
2
3
4
5
6
//获取需要子类化的控件的句柄
HWND hEdit = GetDlgItem(hDlg,EDT_SUBCALSS);
//保存原来的窗口过程函数
g_pfnOldEditProc = (WNDPROC)GetWindowLong(hEdit,GWL_WNDPROC);
//设置新的窗口过程函数
SetWindowLong(hEdit,GWL_WNDPROC,(LONG)MyEditProc);

一个bug

如果在进行子类化之后再次进行子类化,那么第二次子类化GetWindowLong获取到的就是我们自己设计的窗口过程函数的地址,造成递归死循环,所以在GetWindowLong处需要做一下判断,判断是否为我们自己的窗口过程函数。

1
2
3
4
5
6
7
8
9
10
//获取需要子类化的控件的句柄
HWND hEdit = GetDlgItem(hDlg,EDT_SUBCALSS);
//判断GetWindowLong
if (g_pfnOldEditProc == NULL)
{
//保存原来的窗口过程函数
g_pfnOldEditProc = (WNDPROC)GetWindowLong(hEdit,GWL_WNDPROC);
//设置新的窗口过程函数
SetWindowLong(hEdit,GWL_WNDPROC,(LONG)MyEditProc);
}

使用SetClassLong子类化

SetClassLongSetWindowLong的区别在于,SetWindowLong会马上将窗口进行子类化,而SetClassLolng则会将窗口类的属性进行修改,这样只会影响到新创建的窗口,并不会影响当前已经创建好的窗口。

函数定义:

1
2
3
4
5
DWORD SetClassLong(
HWND hWnd,
int nIndex,
LONG dwNewLong
);

使用方法与SetWindowLong一致:

1
2
3
4
5
6
HWND hEdit = GetDlgItem(hDlg,EDT_SUBCALSS);
if (g_pfnOldEditProc == NULL)
{
g_pfnOldEditProc = (WNDPROC)GetClassLong(hEdit,GWL_WNDPROC);
SetClassLong(hEdit,GWL_WNDPROC,(LONG)MyEditProc);
}

其他

查看SDK错误信息

使用GetLastError获取API失败的信息

函数定义:

1
_Post_equals_last_error_ DWORD GetLastError();

使用方法:

1
DWORD dwErr = GetLastError();

返回的错误码可以在VS中的错误查找中查询:

img

使用FormatMessage将错误码转换为错误信息

封装函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//显示错误信息
void ShowErrorMsg()
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR)&lpMsgBuf,
0,
NULL
);
//弹窗显示错误信息
MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error"), MB_OK | MB_ICONINFORMATION);
//释放
LocalFree(lpMsgBuf);
}

直接在监视窗口中查看错误信息

  • 1.VS:@err,hr
  • 2.VC6.0:*(unsigned long*)(tib+0x34),hr
  • 3.直接错误码,hr

img