Win32开发(一)事件与消息

ʕ •̀ o •́ ʔ

1 事件与消息

开发环境:Windows10 21H2+VS2019,参考文档MSDN离线文档。本文参考MSDNWin32 和 C++ 入门

动作就是一个事件,Windows 为了能够准确的描述这些信息,提供了一个结构体 tagMSG,该结构体里面记录的事件的详细信息,将事件进行封装。

1
2
3
4
5
6
7
8
9
typedef struct tagMSG {
HWND hwnd; // 事件发生的窗口句柄(该事件在哪个窗口产生和处理)
UINT message; // 消息类型,如左键使用WM_LBUTTONDOWN宏来表示(WM == Window Message)
WPARAM wParam; // 32位(long)消息的特定附加信息,对message参数的进一步说明
LPARAM lParam; // 32位(long)消息的特定附加信息,对message参数的进一步说明
DWORD time; // 动作触发的时间
POINT pt; // 消息产生的位置
DWORD lPrivate; // 新增成员,没有查到含义和用法
} MSG, *PMSG, *NPMSG, *LPMSG;

Win32 消息的种类有多少种可以参考:win32 MSG 值

消息的处理过程

  1. 用户输入:是事件产生的原因,然后操作系统将其封装成消息。
  2. 系统消息:将封装好的消息插入到系统的消息队列队列中。
  3. 线程消息队列:将同一个线程的所有窗口消息排队到消息队列中。
  4. 消息循环:
    • 调用 GetMessage() 函数从线程消息队列取消息,此函数从队列的头中删除第一条消息。 如果队列为空,则函数会阻止,直到另一条消息排队。该函数返回 0 时才会停止拉取消息。
    • 调用 TranslateMessage() 将消息进行第一次的加工翻译,如在键盘到底按了哪个键,将虚拟码转换为字符。如果没有它,只能自己去转换。
    • 调用 DispatchMessage() 让操作系统在其窗口表中查找对应窗口句柄,查找与窗口关联的函数指针,并调用该窗口处理函数函数。 (每次程序调用 DispatchMessage 函数时,它都会间接导致Windows为每个消息调用 WindowProc 函数一次。)
  5. 窗口处理函数返回时,它将返回到 DispatchMessage。 这会返回到下一条消息的消息循环。 只要程序正在运行,消息将继续到达队列。 因此,必须具有一个循环,该循环会不断从队列中拉取消息并调度它们。

1.png

1
2
3
4
5
6
7
// 错误的消息循环,循环不会结束。
while (1)
{
GetMessage(&msg, NULL, 0, 0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}

请注意

  1. GetMessage 除第一个参数外的其他三个参数允许筛选从队列获取的消息。 几乎所有情况下,都将这三个参数设置为零。

  2. 通常, GetMessage 返回非零值。如果要退出应用程序并中断消息循环,请在窗口处理函数中调用 PostQuitMessage 函数,因为窗口处理函数处理好后会返回到 DispatchMessage() 函数,然后又继续循环。退出窗口的原理:PostQuitMessage 函数在消息队列上放置WM_QUIT消息。 WM_QUIT 是一条特殊消息:它会导致 GetMessage 返回零,从而向消息循环的末尾发出信号。(因为是队列在 WM_QUIT 之前的消息依然能够能到处理)

  3. 所以改进后的消息循环为:

    1
    2
    3
    4
    5
    6
    7
    // 正确的消息循环处理
    MSG msg = {0};
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
    }

参考:MSDN文档-创建窗口CSDN-消息类型CSDN-创建一个Win32窗口

2 窗口的创建

Win32 创建窗口有固定的流程:

  1. 定义WinMain函数
  2. 定义窗口处理函数(自定义窗口处理回调函数,进行消息处理)
  3. 设计并注册窗口类WNDCLASSEX(向操作系统写入一些数据)
  4. 创建窗口(内存中创建窗口)
  5. 显示窗口(绘制窗口的图像)
  6. 刷新窗口
  7. 消息循环(获取/翻译/派发消息)
  8. 窗口回调函数消息处理
1
2
3
WNDCLASS wc = { };
// 上面这种声明是错误的,海哥的课上带大家看了MSDN离线文档说声明时必须赋初始值为0
WNDCLASS wc = {0};

2.1 GUI 入口函数

GUI 应用程序的入口函数是 wWinMain , 这是一个自定义的入口函数。**wWinMain **函数采用的是 Windows 标准调用方式。

函数原型如下:

1
2
3
4
5
6
7
8
9
int APIENTRY wWinMain(
_In_ HINSTANCE hInstance, //当前程序ImageBase
_In_opt_ HINSTANCE hPrevInstance, //16位程序使用,现在已经废弃(始终为零)
_In_ LPWSTR lpCmdLine, //命令行参数, Unicode 字符串
_In_ int nCmdShow) //窗口的显示方式,指示主应用程序窗口是最小化、最大化还是正常显示
{
...
return (int) msg.wParam;
}

关于 return (int) msg.wParam

GetMessage 获取到的消息为WM_DESTROY(销毁窗口)的时候调用我们的处理:

1
2
3
4
5
6
7
case WM_DESTROY:
PostQuitMessage(0);
break;

void PostQuitMessage(
[in] int nExitCode //应用程序退出代码。此值用作WM_QUIT消息的wParam参数。
);

当我们调用 PostQuitMessage 传入的 nExitCode (程序退出代码,0为正常退出,非0为异常)。

调用了 PostQuitMessage 后会向消息队列中添加一个 WM_QUIT(窗口退出消息),然后 GetMessage 接收到 WM_QUITMSG 消息。此时MSGwParamPostQuitMessage 中的 nExitCode,所以最后 return 的值也就是 PostQuitMessagenExitCode

参考《Win32 程序开发:创建一个应用程序窗口》。

2.2 窗口处理函数

窗口处理回调函数由DispatchMessage 函数调用。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
LRESULT CALLBACK WindowProc(  	
IN HWND hwnd, // 处理函数所属的窗口句柄
IN UINT uMsg, // 消息ID(类型), 如WM_SIZE消息指示窗口已调整大小
IN WPARAM wParam, // 对参数2消息进一步描述, 取决于参数2
IN LPARAM lParam // 对参数2消息进一步描述, 取决于参数2
);

typedef __int64 LONG_PTR, *PLONG_PTR;
typedef unsigned __int64 UINT_PTR, *PUINT_PTR;
typedef UINT_PTR WPARAM;
typedef LONG_PTR LPARAM;
typedef LONG_PTR LRESULT;

典型的窗口处理函数,只是一个在消息代码上切换的大型 switch 语句。

针对不同的 uMsgwParamlParam 参数意义不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
{
int width = LOWORD(lParam); // Macro to get the low-order word.
int height = HIWORD(lParam); // Macro to get the high-order word.

// Respond to the message:
OnSize(hwnd, (UINT)wParam, width, height);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}

关于 WindowProc 返回值:

  1. 返回 0:可以在每个 casebreak,表示当前消息已经被成功处理了,窗口函数直接返回 0。窗口回调函数处理过的消息,必须传回 0
  2. 返回 DefWindowProc:如果传来的消息不被当前的窗口处理函数命中,则可以调用 DefWindowProc 函数,该函数叫做默认消息处理函数。意思是我当前的处理窗口不处理该消息,返回给操作系统去处理。

注意:case 后的代码在遇到下一个 case/default 前都有效,跟 {} 无关。如按钮也是一个窗口,也需要窗口处理回调函数

2.3 创建窗口

创建窗口的步骤:

  1. 设计并注册窗口类WNDCLASSEX。
  2. 创建窗口(内存中创建窗口)。
  3. 显示窗口。
  4. 刷新窗口。

2.3.1 WNDCLASSEX

WNDCLASSEX 是一个结构,不是一个 C++ 的类。它的成员描述了一个窗口的所有属性。它与RegisterClassExGetClassInfoEx函数一起使用。

在向操作系统注册(RegisterClassEx) 之前,必须对该结构的成员进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct tagWNDCLASSEXW {
UINT cbSize; // 该结构的大小(以字节为单位)。设置为sizeof(WNDCLASSEX)在调用GetClassInfoEx函数之前,请务必设置此成员。
UINT style; // 窗口的风格
WNDPROC lpfnWndProc; // 消息处理函数指针
int cbClsExtra; // 指定紧跟在窗口类结构后的附加字节数,系统将字节初始化为零。
int cbWndExtra; // 指定紧跟在窗口示例后的附加字节数,系统将字节初始化为零。
HINSTANCE hInstance; // 本模块的基地址,wWinMain参数一的值。
HICON hIcon; // 类图标的句柄,窗口左上角图标的句柄。
HCURSOR hCursor; // 类光标的句柄。此成员必须是光标资源的句柄。NULL表示鼠标进入应用程序窗口时,显式为光标形状。
HBRUSH hbrBackground; // 背景画刷的句柄,也可以是颜色值。
LPCWSTR lpszMenuName; // 菜单名,如果您使用整数来识别菜单,请使用MAKEINTRESOURCE宏。
LPCWSTR lpszClassName; // 该窗口类的名称
HICON hIconSm; // 小图标句柄
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;

以下四个成员必须赋值,不能为 0/NULL,其他成员可以设置为 0

  • cbSize:该结构体的大小,sizeof(WNDCLASSEX)
  • lpfnWndProc:必须赋值为上一节定义的窗口处理回调函数地址。
  • hInstance:从 wWinMainhInstance 参数获取此值。
  • lpszClassName:是标识窗口类的字符串。类名是当前进程的本地名称,因此该名称仅在进程内唯一。标准Windows控件已经定义好了如 Button,所以自己定义的窗口类名不能与预定义好的重名。

窗口类初始化好之后需要调用 RegisterClassExW 来向操作系统注册这个窗口。

1
2
3
4
5
6
ATOM RegisterClassExW(
[in] const WNDCLASSEXW *unnamedParam1
);

typedef WORD ATOM;
typedef unsigned short WORD;

返回值:

如果我们使用Button等窗口的话,直接调用CreateWindowExW创建窗口即可,因为操作系统为我们把这些窗口类注册好了

具体见下一篇子窗口处理

2.3.2 CreateWindowExW

当窗口类初始化并注册好之后,需要调用 CreateWindowExW 来进行创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HWND CreateWindowExW(
[in] DWORD dwExStyle, // 扩展的窗口样式。
[in, optional] LPCWSTR lpClassName, // 类名,上一节初始化的类名
[in, optional] LPCWSTR lpWindowName,// 标题
[in] DWORD dwStyle, // 窗口风格, WS_OVERLAPPEDWINDOW表示有最小化、最大化和关闭
[in] int X, // 窗口X坐标,相对于屏幕左上角,可为CW_USEDEFAULT
[in] int Y, // 窗口y坐标,相对于屏幕左上角,可为CW_USEDEFAULT
[in] int nWidth, // 窗口宽度,可为CW_USEDEFAULT
[in] int nHeight, // 窗口高度,可为CW_USEDEFAULT
[in, optional] HWND hWndParent, // 父窗口句柄,可以为NULL
[in, optional] HMENU hMenu, // 窗口的菜单句柄
[in, optional] HINSTANCE hInstance, // 窗口所在模块基地址,wWinMain参数一的值。
[in, optional] LPVOID lpParam // 用户数据
);

必须的参数

  • lpClassName:类名。
  • lpWindowName:窗口名。
  • hInstance :模块基地址。
  • nWidthnHeight:必须指定大小或 CW_USEDEFAULT
  • dwStyleWS_OVERLAPPEDWINDOW表示有最小化、最大化和关闭

返回值:

  • 创建成功:返回窗口句柄。
  • 创建失败:返回 NULL。请调用DWORD GetLastError()

2.3.3 ShowWindow

上一节虽然创建了一个窗口,但是窗口是隐藏的,若要使窗口可见,则需要调用 ShowWindow

1
2
3
4
BOOL ShowWindow(
[in] HWND hWnd, // 要显示的窗口句柄
[in] int nCmdShow // 窗口显示方式(最小化或最大化窗口),该值为wWinmain参数4(nCmdShow)
);

2.3.4 UpdateWindow

该函数直接向窗口处理回调函数发送一个 WM_PAINT 消息,该消息不会插入消息队列(如果更新区域为空,则不会发送任何消息。)。该函数是非必须的。如果失败则返回0。

1
2
3
BOOL UpdateWindow(
[in] HWND hWnd // 窗口句柄
);

2.3.5 消息循环

消息循环除了接收、初步翻译之外,最重要的是要调用窗口处理回调函数。

1
2
3
4
5
6
7
// 正确的消息循环处理
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

2.4 GetLastError()

在单步调试中,将该函数用在可能发生错误的函数后面,紧跟在后面。用法:

1
UINT uError = GetLastError();

返回的错误码:Debug system error codes

3 创建第一个窗口

  1. 打开 VS2019,创建新项目-Windows桌面向导-项目名称和存放位置-创建-桌面应用程序-空项目-创建。
  2. 源文件-右键添加新建项-C++文件。
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
#include <windows.h>

// 窗口处理函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int APIENTRY wWinMain(
_In_ HINSTANCE hInstance, //当前程序ImageBase
_In_opt_ HINSTANCE hPrevInstance, //16位程序使用,现在已经废弃(始终为零)
_In_ LPWSTR lpCmdLine, //命令行参数, Unicode 字符串
_In_ int nCmdShow) //窗口的显示方式,指示主应用程序窗口是最小化、最大化还是正常显示
{
HWND hFirstWindow = NULL;
// 类名
WCHAR CLASS_NAME[] = L"First Windows";
// 窗口名
WCHAR WINDOW_NAME[] = L"My First Window";

// 1. 创建窗口类
WNDCLASSEX WndClass = {0};

WndClass.cbSize = sizeof(WndClass);
WndClass.lpfnWndProc = WindowProc;
WndClass.lpszClassName = CLASS_NAME;
WndClass.hInstance = hInstance;


// 2. 注册窗口类
ATOM atom = RegisterClassExW(&WndClass);
if (!atom)
{
MessageBoxW(NULL, L"注册失败", L"A1v1n", 0);
}

// 3. 创建窗口
hFirstWindow = CreateWindowExW(
0,
CLASS_NAME,
WINDOW_NAME,
WS_OVERLAPPEDWINDOW, 0, 0, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL,
hInstance,
NULL
);

// 4. 显示窗口
if (hFirstWindow)
{
ShowWindow(hFirstWindow, nCmdShow);
UpdateWindow(hFirstWindow);
}

// 5. 消息循环
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return (int)msg.wParam;
}

// 窗口处理函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_LBUTTONDOWN:
MessageBoxW(hwnd, L"这是第一个窗口", L"A1v1n", 0);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}

4 VS2019 提供的样例

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// FirstWindow.cpp : 定义应用程序的入口点。
//

#include "framework.h"
#include "FirstWindow.h"

#define MAX_LOADSTRING 100

// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名

// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, //当前程序ImageBase
_In_opt_ HINSTANCE hPrevInstance, //16位程序使用,现在已经废弃
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

// TODO: 在此处放置代码。

// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_FIRSTWINDOW, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);

// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}

HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FIRSTWINDOW));

MSG msg;

// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

return (int) msg.wParam;
}



//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_FIRSTWINDOW));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_FIRSTWINDOW);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

return RegisterClassExW(&wcex);
}

//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中

HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

if (!hWnd)
{
return FALSE;
}

ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

return TRUE;
}

//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;

case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}

5 VS2019 设置

0、新建 Win32/控制台 项目

  1. 打开 VS2019,创建新项目-Windows桌面向导-项目名称和存放位置-创建-桌面应用程序/控制台-空项目-创建。
  2. 源文件-右键添加新建项-C++文件。

一、解决方案的显示有两种

  1. 引用、外部依赖项、头文件、源文件、资源文件。
  2. 显示所有文件。

切换显示方式:菜单-项目-显示所有文件。

二、如果资源管理器不见了

菜单-窗口-浮动。找到资源窗口后右键-停靠。

三、在项目里建立多文件夹多文件

  1. 在项目里建立一个文件夹(不是在VS2019里面)。
  2. 项目->属性->VC++目录->包含目录->编辑-点击最左边文件夹图标-点击三个“…”-选择需要添加的文件夹-应用-确定。
  3. 在项目解决源文件上右键-添加-新建筛选器-输入步骤1文件夹的名字。
  4. 在新建的筛选器里新建一个.cpp文件(注意新建的文件路径要放在刚才的文件夹),并新建一个函数 UINT DebugerMain(LPVOID pPara)
  5. 在其他文件里使用 extern UINT DebugerMain(LPVOID pPara); 后就可以直接使用这个函数了。

筛选器只不过是文件夹的逻辑映射,不一定需要和文件夹同名,但是为了方便,一般设置同名(也就是将本地文件映射到项目里,不映射的话在项目里无法直接 #include 并使用)。

参考设置包含目录vs2017解决方案列表添加文件夹与实际目录中的文件夹对应

四、头文件重复包含

如果多个文件里都包含 windows.h 头文件,可以使用如下方式解决,在每个文件中都要使用:

1
2
3
4
5
6
#ifndef _FILE_WINDOWS_
#define _FILE_WINDOWS_

#include <windows.h>

#endif

五、无法包含头文件

在项目->属性->VC++目录->包含目录->编辑-点击最左边文件夹图标-点击三个“…”-选择包含整个项目的主文件夹-应用-确定。

解决VS中#include无法包含头文件