ʕ •̀ 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; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; DWORD lPrivate; } MSG, *PMSG, *NPMSG, *LPMSG;
Win32 消息的种类有多少种可以参考:win32 MSG 值 。
消息的处理过程 :
用户输入:是事件产生的原因,然后操作系统将其封装成消息。
系统消息:将封装好的消息插入到系统的消息队列队列中。
线程消息队列:将同一个线程的所有窗口消息排队到消息队列中。
消息循环:
调用 GetMessage()
函数从线程消息队列取消息,此函数从队列的头中删除第一条消息。 如果队列为空,则函数会阻止,直到另一条消息排队。该函数返回 0
时才会停止拉取消息。
调用 TranslateMessage()
将消息进行第一次的加工翻译,如在键盘到底按了哪个键,将虚拟码转换为字符。如果没有它,只能自己去转换。
调用 DispatchMessage()
让操作系统在其窗口表中查找对应窗口句柄,查找与窗口关联的函数指针,并调用该窗口处理函数函数。 (每次程序调用 DispatchMessage 函数时,它都会间接导致Windows为每个消息调用 WindowProc 函数一次。)
窗口处理函数返回时,它将返回到 DispatchMessage 。 这会返回到下一条消息的消息循环。 只要程序正在运行,消息将继续到达队列。 因此,必须具有一个循环,该循环会不断从队列中拉取消息并调度它们。
1 2 3 4 5 6 7 while (1 ) { GetMessage(&msg, NULL , 0 , 0 ); TranslateMessage(&msg); DispatchMessage(&msg); }
请注意 :
GetMessage
除第一个参数外的其他三个参数允许筛选从队列获取的消息。 几乎所有情况下,都将这三个参数设置为零。
通常, GetMessage 返回非零值 。如果要退出应用程序并中断消息循环,请在窗口处理函数中调用 PostQuitMessage 函数,因为窗口处理函数处理好后会返回到 DispatchMessage()
函数,然后又继续循环。退出窗口的原理:PostQuitMessage 函数在消息队列上放置WM_QUIT 消息。 WM_QUIT 是一条特殊消息:它会导致 GetMessage 返回零 ,从而向消息循环的末尾发出信号。(因为是队列在 WM_QUIT
之前的消息依然能够能到处理)
所以改进后的消息循环为:
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 创建窗口有固定的流程:
定义WinMain函数
定义窗口处理函数(自定义窗口处理回调函数,进行消息处理)
设计并注册窗口类WNDCLASSEX(向操作系统写入一些数据)
创建窗口(内存中创建窗口)
显示窗口(绘制窗口的图像)
刷新窗口
消息循环(获取/翻译/派发消息)
窗口回调函数消息处理
1 2 3 WNDCLASS wc = { }; 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, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _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 ) ;
当我们调用 PostQuitMessage
传入的 nExitCode
(程序退出代码,0为正常退出,非0为异常)。
调用了 PostQuitMessage
后会向消息队列中添加一个 WM_QUIT
(窗口退出消息),然后 GetMessage
接收到 WM_QUIT
的 MSG
消息。此时MSG
的 wParam
为 PostQuitMessage
中的 nExitCode
,所以最后 return
的值也就是 PostQuitMessage
的 nExitCode
。
参考《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, IN WPARAM wParam, IN LPARAM lParam ) ; 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
语句。
针对不同的 uMsg
其 wParam
、lParam
参数意义不同。
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); int height = HIWORD(lParam); OnSize(hwnd, (UINT)wParam, width, height); } break ; case WM_DESTROY: PostQuitMessage(0 ); break ; default : return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0 ; }
关于 WindowProc
返回值:
返回 0
:可以在每个 case
后 break
,表示当前消息已经被成功处理了,窗口函数直接返回 0
。窗口回调函数处理过的消息,必须传回 0
。
返回 DefWindowProc
:如果传来的消息不被当前的窗口处理函数命中,则可以调用 DefWindowProc
函数,该函数叫做默认消息处理函数。意思是我当前的处理窗口不处理该消息,返回给操作系统去处理。
注意:case
后的代码在遇到下一个 case/default
前都有效,跟 {}
无关。如按钮也是一个窗口,也需要窗口处理回调函数 。
2.3 创建窗口 创建窗口的步骤:
设计并注册窗口类WNDCLASSEX。
创建窗口(内存中创建窗口)。
显示窗口。
刷新窗口。
2.3.1 WNDCLASSEX WNDCLASSEX
是一个结构,不是一个 C++ 的类。它的成员描述了一个窗口的所有属性 。它与RegisterClassEx 和GetClassInfoEx 函数一起使用。
在向操作系统注册(RegisterClassEx
) 之前,必须对该结构的成员进行初始化 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct tagWNDCLASSEXW { UINT cbSize; UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCWSTR lpszMenuName; LPCWSTR lpszClassName; HICON hIconSm; } WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;
以下四个成员必须赋值,不能为 0/NULL
,其他成员可以设置为 0
。
cbSize :该结构体的大小,sizeof(WNDCLASSEX)
。
lpfnWndProc :必须赋值为上一节定义的窗口处理回调函数地址。
hInstance :从 wWinMain 的 hInstance
参数获取此值。
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, [in] int X, [in] int Y, [in] int nWidth, [in] int nHeight, [in, optional] HWND hWndParent, [in, optional] HMENU hMenu, [in, optional] HINSTANCE hInstance, [in, optional] LPVOID lpParam ) ;
必须的参数 :
lpClassName
:类名。
lpWindowName
:窗口名。
hInstance
:模块基地址。
nWidth
、nHeight
:必须指定大小或 CW_USEDEFAULT
。
dwStyle
: WS_OVERLAPPEDWINDOW
,表示有最小化、最大化和关闭 。
返回值:
2.3.3 ShowWindow 上一节虽然创建了一个窗口,但是窗口是隐藏的,若要使窗口可见,则需要调用 ShowWindow
。
1 2 3 4 BOOL ShowWindow ( [in] HWND hWnd, [in] int 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 创建第一个窗口
打开 VS2019,创建新项目-Windows桌面向导-项目名称和存放位置-创建-桌面应用程序-空项目-创建。
源文件-右键添加新建项-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, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { HWND hFirstWindow = NULL ; WCHAR CLASS_NAME[] = L"First Windows" ; WCHAR WINDOW_NAME[] = L"My First Window" ; WNDCLASSEX WndClass = {0 }; WndClass.cbSize = sizeof (WndClass); WndClass.lpfnWndProc = WindowProc; WndClass.lpszClassName = CLASS_NAME; WndClass.hInstance = hInstance; ATOM atom = RegisterClassExW(&WndClass); if (!atom) { MessageBoxW(NULL , L"注册失败" , L"A1v1n" , 0 ); } hFirstWindow = CreateWindowExW( 0 , CLASS_NAME, WINDOW_NAME, WS_OVERLAPPEDWINDOW, 0 , 0 , CW_USEDEFAULT, CW_USEDEFAULT, NULL , NULL , hInstance, NULL ); if (hFirstWindow) { ShowWindow(hFirstWindow, nCmdShow); UpdateWindow(hFirstWindow); } 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 #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, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); 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; } 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); } 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; } 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); 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/控制台 项目
打开 VS2019,创建新项目-Windows桌面向导-项目名称和存放位置-创建-桌面应用程序/控制台-空项目-创建。
源文件-右键添加新建项-C++文件。
一、解决方案的显示有两种
引用、外部依赖项、头文件、源文件、资源文件。
显示所有文件。
切换显示方式:菜单-项目-显示所有文件。
二、如果资源管理器不见了
菜单-窗口-浮动。找到资源窗口后右键-停靠。
三、在项目里建立多文件夹多文件
在项目里建立一个文件夹(不是在VS2019里面)。
项目->属性->VC++目录->包含目录->编辑-点击最左边文件夹图标-点击三个“…”-选择需要添加的文件夹-应用-确定。
在项目解决源文件上右键-添加-新建筛选器-输入步骤1文件夹的名字。
在新建的筛选器里新建一个.cpp文件(注意新建的文件路径要放在刚才的文件夹),并新建一个函数 UINT DebugerMain(LPVOID pPara)
。
在其他文件里使用 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无法包含头文件 。