让我们与星光探索者一起,探索Windows吧!

使用计时器

计时器是Windows的一种资源。定时器每隔一段时间,会产生WM_TIMER消息 。WM_TIMER附带的两个信息wParam, lParam分别是计时器的id, 计时器到点的处理函数。定时器的精度是毫秒,但是准确度不好。也就是说如果设置间隔1000ms,不一定会在1000ms后刚好产生消息。但是误差不会这么大,最多也就几毫秒。

要使用计时器,需要用SetTimer函数创建定时器。SetTimer的定义如下:

1
2
3
4
5
6
UINT_PTR SetTimer(
HWND hWnd, // 计时器关联的窗口句柄
UINT_PTR nIDEvent, // 计时器的ID,自己随意定,但不能为0
UINT uElapse, // 计时器的间隔,单位毫秒
TIMERPROC lpTimerFunc // 计时器到点的处理函数
);

这个函数返回创建的计时器ID,如果创建失败了返回0。如果lpTimerFunc传的是NULL,计时器的时间到了,就将发送WM_TIMER消息通知关联的窗口过程函数处理,否则就不发WM_TIMER消息,直接调用lpTimerFunc处理。计时器依赖于消息循环,也就是要使用计时器,就需要消息循环。

TIMERPROC 是计时器回调函数的类型,因此计时器回调函数lpTimeProc因定义为:

1
2
3
4
5
6
void CALLBACK lpTimerProc(
HWND hWnd, // 与计时器关联
UINT nMsg, // 消息类型(WM_TIMER)
UINT_PTR uTImerid, // 计时器ID
DWORD dwTime // 时间延迟
);

计时器是系统资源,销毁计时器使用KillTimer 函数

1
2
3
4
BOOL KillTimer(
HWND hWnd, // 销毁计时器的关联窗口
UINT_PTR uIDEvent
);

示例代码:创建两个计时器,时间间隔分别为1000毫秒和2000毫秒,每调一次将打印一些字符串。

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
// 各位如果用Visual Studio开发工具,可能会出现某些函数VS觉得不安全不给用
// 如果还是想用,加上这句语句
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <iostream>


#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "kernel32.lib")

LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);


INT APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
static const TCHAR TITLE[] = TEXT("window");
static const TCHAR CLASSNAME[] = TEXT("MyStruct");

WNDCLASS wc{0};
wc.hbrBackground = (HBRUSH)GetStockObject(DKGRAY_BRUSH);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hInstance = hInstance;
wc.lpfnWndProc = WinProc;
wc.lpszClassName = CLASSNAME;
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW;

RegisterClass(&wc);

HWND hwnd = CreateWindowEx(
0,CLASSNAME, TITLE, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL
);

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

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

return 0;
}

LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static UINT_PTR timer1, timer2;
switch (uMsg)
{
case WM_CLOSE:
FreeConsole();
// 销毁定时器
KillTimer(hwnd, timer1);
KillTimer(hwnd, timer2);
PostQuitMessage(0);
return 0;
case WM_CREATE:
AllocConsole();
freopen("CONOUT$", "w+t", stdout);
// 创建定时器
timer1 = SetTimer(hwnd, 1,
1000, NULL);
timer2 = SetTimer(hwnd, 2, 2000, NULL);
case WM_TIMER:
// 处理计时器消息
if (wParam == timer1)
{
printf("timer1\n");
return 0;
}
if (wParam == timer2)
{
printf("timer2\n");
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

使用菜单资源

我们创建窗口时很想让这个窗口显示自己的东西,那么我们应该怎么做呢?

星光探索者前面的说过有个资源编译器rc.exe,可以编译资源文件。首先我们先探索一下如何添加资源吧!

对准想添加资源的项目右键,弹出下面的菜单。依次点击添加(Add),资源(Resource)即可。

然后弹出这样的对话框,选择菜单(Menu),然后点击新建(New)


然后将会在Visual Studio弹出可视化的编辑器。相信不用星光探索者多说,各位小伙伴都会摸索的出来如何添加菜单项吧。

之后在你的项目管理器会添加一个resource.h(可以改名)头文件,和拓展名为.rc的资源文件。资源文件是一种用脚本语言书写的代码文件,当然如果小伙伴乐意,可手写资源文件。在编译过程中,资源文件会被资源编译器(在VC++是rc.exe)编译成.rsc的目标文件,进而通过链接器(在VC++是link.exe)编译成最终文件。

资源创建了,如何使用呢?刚才添加的resource.h文件定义了那些ID标识符。在打开的资源管理器(Resource View)中,选中添加的菜单,然后右键,点击(Resource Symbol)即可查看添加的资源的id。星光探索者这里创建的菜单ID为IDC_MENU1。当然也可以直接查看resource.h头文件

还记得创建窗口类时的WNDCLASS结构体吗?那里有一个lpszMenuName属性,给他赋值就可以使用我们添加的菜单资源了。lpszMenuName需要LPCTSTR的类型,资源文件定义的ID都是数值,我们只需进行强制转换即可。或者CreateWindowEx的第10个参数也是叫传菜单句柄的,也可以选择在那里使用菜单资源。

既然有了菜单,我们就应该处理菜单的相关消息。处理菜单消息应该处理WM_COMMAND消息。当窗口内的控件(如按钮,文本等)被点击时,或菜单项被点击时,或窗口标题栏那些按钮(如最小化)被点击时产生此消息。当菜单项被点击时,wParam的高字节为0,低字节为点击的菜单项ID,菜单项ID之前已探索过如何找到。

于是我们写出像下面这样的代码来使用菜单资源:

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
#define _CRT_SECURE_NO_WARNINGS
#include "resource.h"
#include <windows.h>

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "kernel32.lib")

LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);


INT APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
static const TCHAR TITLE[] = TEXT("window");
static const TCHAR CLASSNAME[] = TEXT("MyStruct");

WNDCLASS wc{0};
wc.hbrBackground = (HBRUSH)GetStockObject(DKGRAY_BRUSH);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hInstance = hInstance;
wc.lpfnWndProc = WinProc;
wc.lpszClassName = CLASSNAME;
// 强制转换即可使用,星光探索者这里的菜单资源为 IDR_MENU1
wc.lpszMenuName = (LPCWSTR)IDR_MENU1;
wc.style = CS_HREDRAW | CS_VREDRAW;

RegisterClass(&wc);

HWND hwnd = CreateWindowEx(
0,CLASSNAME, TITLE, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
// CreateWindowEx的第10个参数也是叫传菜单句柄的
// 也可以使用LoadMenu来加载菜单资源
//LoadMenu(NULL, LPCTSTR(IDR_MENU1)),
NULL,
hInstance, NULL
);

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

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

return 0;
}

LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
PostQuitMessage(0);
return 0;
case WM_COMMAND:
// 处理菜单项被点击,需要处理WM_COMMAND消息
// wParam的低字节为被点击的菜单项ID,用LOWORD进行获取
switch (LOWORD(wParam))
{
// 星光探索者已经改过这些菜单项的名字了
case ID_NEW:
MessageBox(hwnd, TEXT("点击新建选项"), TEXT("星光探索者"), MB_OK);
break;
case ID_OPEN:
MessageBox(hwnd, TEXT("点击打开选项"), TEXT("星光探索者"), MB_OK);
break;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

运行程序,点击菜单的新建选项,将出现这样的效果:

右键菜单实现

用过办公软件和浏览器的小伙伴都会体验过,鼠标右键会弹出一个菜单。那么我们应该如何实现呢?

直接说结论:我们使用TrackPopupMenu 函数可以弹出一个快捷菜单,这就是我们期望的。TrackPopupMenu是阻塞函数,直到菜单消失才会返回。

1
2
3
4
5
6
7
8
9
BOOL TrackPopupMenu(
HMENU hMenu, // 菜单句柄
UINT uFlags, // 菜单弹出方式
int x, // 菜单的x坐标
int y, // 菜单的y坐标
int nReserved, // 预留参数,传NULL即可
HWND hWnd, // 拥有快捷菜单的窗口句柄
const RECT *prcRect // 此参数被忽略,传NULL
);

然后什么时候弹出呢?我们可以通过处理WM_RBUTTONDOWN(鼠标右键按下)消息来实现,可以是可以。但是事实告诉我们微软提供了WM_CONTEXTMENU消息,这样更便捷,这是上下文菜单的消息。正好可以使用右键菜单。

此消息附带的两个参数如下:

  • wParam 用户右键点击鼠标的窗口句柄

  • lParam 低字节表示鼠标光标的x坐标,高字节表示y坐标,这是相对屏幕而言的

添加图标和光标资源

和前面添加菜单资源一样,但是如果我们有现成的,在弹出这个对话框时,就可以点击导入(Import)使用现有资源。这个步骤相信各位摸索都可以摸索出来。

然后再调用LoadIcon,LoadCursor即可。

本期到此结束,下期看星光探索者探索Windows绘图编程和对话框