C++使用代码创建一个Windows桌面应用程序
WinMain函数
Windows应用程序的唯一程序入口。
函数原型
1 int WINAPI WinMain 2 { 3 HINSTANCE hInstancem 4 HINSTANCE hPreInstance, 5 LPSTR lpCmdLine, 6 int nCmdShow 7 }
WINAPI定义如下
#define WINAPI _stdcall
_stdcall是一个函数调用约定,除此之外,还有__cdecl,fastcall,thiscall,naked call等函数调用约定。
_stdcall调用约定又称Pascal调用约定,也是Pascal语言的调用约定。它使用的方式为:
1 int __stdcall sum(int a,int b);
__stdcall:函数的多个参数由调用者按从右到左的顺序压入堆栈,被调用函数获得参数的序列是从左到右的的;清理堆栈的工作由被调用函数负责。
在Visual C++中,常用宏WINAPI或CALLBACK来表示__stdcall调用约定。
更详细的说明可以查看
https://docs.microsoft.com/en-us/cpp/cpp/stdcall?view=vs-2019
__cdecl(也可写成_cdecl)调用约定又称C调用约定,是C函数默认的调用约定,也是C++全局函数的默认调用约定,通常省略。
如
1 int sum(int a,int b); 2 int __cdecl sum(int a,int b);
__cdecl:函数的多个参数由调用者按从右向左的顺序压入堆栈,被调函数获得参数的序列是从左到右的;清理堆栈的工作由调用者负责
。
更详细的说明可以查看
https://docs.microsoft.com/en-us/cpp/cpp/cdecl?view=vs-2019
WinMain函数的各参数说明
hInstance
应用程序当前运行的实例的句柄,该句柄由Windows系统生成。
hPrevInstance
当前实例的前一个实例的句柄,在Win32环境下,该参数总是NULL,不再起作用
lpCmdLine
一个以空终止的字符串,代表传递给程序的命令行参数。
nCmdShow
指定窗口的显示状态
常用值如下
nCmdShow = 0;不显示
nCmdShow = 1;正常显示(默认)
nCmdShow = 2;最小化显示
nCmdShow = 3;最大化显示
使用代码创建Windows程序的步骤
1、设计一个Windows类
2、在Windows系统中注册Windows类
3、用该Windows类创建一个窗口
4、显示窗口
5、创建一个消息循环
6、创建一个窗口过程函数WndProc
一、设计Windows类
在创建一个窗口前,必须对窗口进行设计,指定窗口的属性。系统已经定义了WNDCLASS结构用于描述待创建窗口的参数。
WNDCLASS声明如下
1 typedef struct tagWNDCLASSA { 2 UINT style; 3 WNDPROC lpfnWndProc; 4 int cbClsExtra; 5 int cbWndExtra; 6 HINSTANCE hInstance; 7 HICON hIcon; 8 HCURSOR hCursor; 9 HBRUSH hbrBackground; 10 LPCSTR lpszMenuName; 11 LPCSTR lpszClassName; 12 } WNDCLASSA, *PWNDCLASSA, *NPWNDCLASSA, *LPWNDCLASSA;
下面介绍各参数
style
窗口样式,可用值如下
窗口样式 | 说明 |
CS_VREDRAW | 垂直重绘,当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,在垂直方向上调整窗口高度时,将不会重绘窗口。 |
CS_HREDRAW | 水平重绘,当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,在水平方向上调整窗口高度时,将不会重绘窗口。 |
CS_OWNDC | 独占设备描述表,为该类中的每个窗口分配一个单值的设备描述表。 |
CS_SAVEBITS | 在一个窗口中保存用户图像,以便于在该窗口被遮住、移动时不必每次刷新屏幕。但是,这样会占用更多的内存,并且比人工进行同样操作时要慢得多。 |
CS_DBLCLKS | 使窗口可以检测到鼠标双击事件,当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息 |
CS_BYTEALLGNCLIENT | 鼠标用户区域按字节对齐显示。 |
CS_BYTEALLGNWINDOW | 鼠标用户窗口按字节对齐显示。 |
CS_PARENTDC | 在父窗口中设定一个子窗口的剪切区,以便于子窗口能够画在父窗口中。 |
CS_NOCLOSE | 系统菜单中没有CLOSE菜单项,窗口没有关闭按钮。 |
lpfnWndProc
指向窗口过程函数的函数指针。窗口过程函数是一个回调函数,针对Windows的消息处理机制,窗口过程函数被调用的过程如下:
1、在设计窗口类的时候,将窗口过程函数的地址赋给lpfnWndProc成员变量
2、调用RegisterClass(&wndclass)注册窗口类,系统就有了用户编写的窗口过程函数的地址
3、当应用程序接收到某一窗口的信息时,调用DispatchMessage(&msg)将消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理
cbClsExtra
Windows系统为窗口类结构分配追加的额外字节数。一般为0
cbWndExtra
Windows系统为窗口实例分配或追加的额外字节数,一般为0。如果应用程序使用资源文件里的CLASS指令创建对话框,并用WNDCLASS结构注册对话框框时,cbWndExtra必须设置成DLGWINDOWEXTRA
hInstance
包含窗口过程程序的实例句柄。一般直接赋WinMain()的hInstance即可
hIcon
窗口类的图标资源。这个成员变量必须是一个图标资源的句柄。可以使用LoadIcon()函数加载图标,如果hIcon为NULL,窗口将使用系统提供的默认图标
hCursor
窗口类的光标句柄。这个成员变量必须是一个光标资源的句柄。可以使用LoadCursor()函数加载光标。如果hCursor为NULL,应用程序必须在鼠标进入应用程序窗口时,明确设置光标的形状
hbrBackground
窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来填充窗口的背景。该成员可以指定为用于绘制背景的物理画刷的句柄,也可以指定为标准的系统颜色值。如下:
BLACK_BRUSH 黑色
DKGRAY_BRUSH 深灰
GRAY_BRUSH 灰色
HOLLOW_BRUSH 空
LTGRAY_BRUSH 浅灰
NULL_BRUSH 等同于HOLLOW_BRUSH
WHITE_BRUSH 白色
BLACK_BRUSH 黑色
lpszMenuName
指向一个以空终止的字符串,该字符串描述菜单的资源名。若使用整数来标识菜单,需要用MAKEINTRESOURCE宏来进行转换。如果lpszMenuName设置为NULL,那么基于窗口类创建的窗口将没有默认菜单
lpszClassName
指向一个以空终止的字符串,该字符串描述窗口类的名字。这个类名可以是由RegisterClass或者RegisterClassEx注册的名字,或者是任何预定义的控件类名
WNDCLASS使用实例如下
1 WNDCLASS wc; 2 3 wc.style = CS_HREDRAW | CS_VREDRAW; 4 wc.lpfnWndProc = WndProc; 5 wc.cbClsExtra = 0; 6 wc.cbWndExtra = 0; 7 wc.hInstance = hInstance; 8 wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1)); 9 wc.hCursor = LoadCursor(nullptr, IDC_ARROW); 10 wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 11 wc.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1); 12 wc.lpszClassName = szWindowClass;
二、注册Windows类
Windows类设计完成时,需要调用RegisterClass()函数去注册这个类,才可以创建该类型的窗口
1 ATOM RegisterClass( 2 const WNDCLASSA *lpWndClass 3 );
注册代码如下
if(!RegisterClass(&wc)) { return 0; }
三、创建窗口
使用CreateWindow函数创建窗口,如果函数调用成功,返回值为新窗口的句柄;如果调用失败,返回值为NULL。可以使用GetLastError()函数获取错误信息
1 HWND CreateWindow( 2 LPCTSTR lpClassName, 3 LPCTSTR lpWindowName, 4 DWORD dwStyle, 5 int x, 6 int y, 7 int nWidth, 8 int nHeight, 9 HWND hWndParent, 10 HMENU hMenu, 11 HANDLE hInstance, 12 PVOID lpParam 13 );
lpClassName
指定窗口类的名称,这个名称就是WNDCLASSA中的lpszClassName。如果在调用CreateWindow函数之前,没有调用RegisterClass函数注册这个类,系统无法得知窗口的相关信息,窗口创建就会失败。
lpWindowName
指定窗口名称,如果指定了标题栏,那么这里指向的字符串就会显示在标题栏上。
dwStyle
指定创建窗口的样式,可以组合不同的窗口样式
常量 | 说明 |
WS_CAPTION(0x00C00000L) | 创建一个有标题栏的窗口 |
WS_SYSMENU(0x00080000L) | 创建一个在标题栏上带有系统菜单的窗口(需要和WS_CAPTION一起使用) |
WS_MINIMIZEBOX(0x00020000L) | 创建一个具有最小化按钮的窗口(需要和WS_SYSMENU一起使用) |
WS_MAXIMIZEBOX(0x00010000L) | 创建一个具有最大化按钮的窗口(需要和WS_SYSMENU一起使用) |
WS_TILED(0x00000000L) | 创建一个层叠的窗口,层叠的窗口有一个标题栏和一个边框 |
WS_TILEDWINDOW | 创建一个使用(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)样式的层叠的窗口 |
WS_CHILD(0x40000000L) | 创建窗口为子窗口,不能应用于弹出式窗口样式 |
WS_OVERLAPPED | 与WS_TILED样式相同 |
WS_OVERLAPPEDWINDOW | 创建一个使用(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)样式的层叠的窗口 |
WS_EX_TOPMOST | 创建一个始终置顶的窗口(不管窗口是否已经激活) |
WS_POPUP(0x80000000L) | 创建一个弹出式窗口(不能与WS_CHILD一起使用) |
WS_VISIBLE(0x10000000L) | 创建一个初始状态为可见的窗口(可以使用ShowWindow函数来控制显示或隐藏窗口) |
完整窗口样式可以访问:https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles |
x
指定窗口左上角的x坐标
y
指定窗口左上角的y坐标
nWidth
以设备单元指定窗口的宽度
nHeight
以设备单元指定窗口的高度
hWndParent
指定被创建窗口的父窗口的句柄。如果要创建一个子窗口,这里就需要提供父窗口的句柄。
hMenu
菜单句柄,指向附属于该窗口的菜单
hInstance
WinMain函数中传入的应用程序实例的句柄
lpParam
作为WM_CREATE消息的附加参数lParam传入的数据指针。在创建多文档界面的客户窗口时,lpParam必须指向CLIENTCREATESTRUCT结构体。多数窗口将这个参数设置为NULL
CreateWindow示例代码如下
HWND hwnd; hwnd = CreateWindow( "MainWClass", "Test Window", WS_OVERLAPPEDWINDOW, 0, 0, CW_USEDEFAULT, // 默认宽度 CW_USEDEFAULT, // 默认高度 NULL, // 没有父窗体 NULL, // 没有菜单 hinstance, NULL); //没有附加数据
四、显示窗口
执行CreateWindow函数,窗体创建成功之后,需要调用ShowWindow函数把窗口显示在桌面上
BOOL ShowWindow(HWND hWnd,int nCmdShow);
hWnd
CreateWindow创建窗口成功后返回的窗口句柄
nCmdShow
指示窗口显示的状态
常用的窗口显示状态如下
窗口状态 | 说明 |
SW_HIDE | 隐藏窗口并激活其它窗口 |
SW_SHOW | 在窗口原来的位置以原来的尺寸激活并显示窗口 |
SW_SHOWMAXIMIZED | 激活并以最大化显示窗口 |
SW_SHOWMINIMIZED | 激活并最小化显示窗口 |
SW_SHOWNORMAL | 激活并显示窗口。如果窗口是最大化或最小化的状态,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口时,应该使用这种状态 |
调用ShowWindow()函数之后,需要调用UpdateWindow()函数来更新窗口。
1 BOOL UpdateWindow(HWND hwnd);
hwnd:调用CreateWindow()成功创建窗口后返回的窗口句柄
UpdateWindow()函数通过发送一个WM_PAINT消息来刷新窗口。如果窗口更新的区域不为空,UpdateWindow()函数绕过应用程序的消息队列,直接发送WM_PAINT消息给指定窗口的窗口过程函数进行处理。如果更新区域为空,则不发送消息。
至此,我们就完成了Window类的设计和注册,窗口的创建、显示及更新,接下来开始处理窗体的消息。
五、创建消息循环
在窗口创建成功之后,需要编写一个消息循环来不断地从消息队列中取出消息,并进行响应。
调用GetMessage()函数从消息队列中取出消息
1 BOOL GetMessage( 2 LPMSG lpMsg, 3 HWND hWnd, 4 UINT wMsgFilterMin, 5 UINT wMsgFilterMax 6 );
lpMsg:指向一个消息结构体(MSG),GetMessage从线程的消息队列中取出的消息将保存在该结构体对象中。
hWnd:指向被接收消息的窗口句柄(指定接收属于哪一个窗口的消息),设置为NULL时,函数接收属于调用线程的所有窗口的窗口消息。
wMsgFilterMin:指定要获取的消息的最小值,通常设置为0
wMsgFilterMax:指定要获取的消息的最大值,如果wMsgFilterMin和wMsgFilterMax都设置为0,则接收所有消息。
关于消息的介绍,可以参考:
https://www.cnblogs.com/zhaotianff/p/11285312.html
取出消息后,需要对消息进行转换。这个时候就需要调用TranslateMessage()函数,该函数将虚拟消息转换为字符消息。字符消息被送到调用线程的消息队列里,当下一次线程调用函数GetMessage()或PeekMessage()时被读出。
1 BOOL TranslateMessage( 2 const MSG *lpMsg 3 );
lpMsg:指向MSG结构的指针,该结构用于存放调用函数GetMessage()或PeekMessage()从消息队列里取出的消息
返回值:如果消息可以得到,返回非零值;如果没有消息,返回值是0
当消息转换后,需要将消息分发到窗口过程,由窗口过程函数对消息进行处理。
此时就需要调用DispatchMessage()函数
3 DispatchMessage( 4 _In_ CONST MSG *lpMsg);
lpMsg:指向含有消息的MSG结构的指针。
返回值:返回值是窗口过程的返回值。
六、窗口过程函数
在前面注册窗口类的时候,有这样一行代码
1 wcex.lpfnWndProc = WndProc;
这里就是注册窗口过程函数。
窗口过程函数的作用是处理发送给窗口的消息,根据不同的消息,作出不同的响应。
一个Windows应用程序的主要代码部分都集中在窗口过程函数中,WndProc()函数的原型声明如下所示。
1 typedef LRESULT (CALLBACK* WNDPROC)
(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
LRESULT指定函数返回值类型,这个值实际上是一个整数,表示调用结果是否成功。
CALLBACK是一个声明说明符,表明函数是由操作系统调用的,必须指定这个符号。
函数名可依据实际情况自定。
hWnd:指向窗口的句柄。·
message:指定消息类型。
wParam:指定其余的、消息特定的信息。该参数的内容与message参数值有关。
lParam:指定其余的、消息特定的信息。该参数的内容与message参数值有关。
在窗口过程处理函数的最后,还要调用默认的窗口过程函数DefWindowProc(),为应用程序没有处理的窗口消息提供默认的处理。
我们看一下WndProc函数的一个示例
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { ...case WM_KEYDOWN: { if(wParam == VK_ESCAPE) //当ESC键按下的时候
DestroyWindow(hWnd); //销毁窗口 } break; ... default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }
下面对上述代码进行一个简单的说明:
通过swtich对消息进行对应的处理。WM_KEYDOWN(键盘按键)是系统定义的窗口消息之一。
在WM_KEYDOWN消息的处理逻辑中,我们判断ESC键是否按下,如果按下,就销毁窗口。
七、一个完整的Windows应用程序
1 #ifndef UNICODE 2 #define UNICODE 3 #endif 4 5 #include <windows.h> 6 7 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 8 9 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) 10 { 11 12 // 注册窗口类 13 const wchar_t CLASS_NAME[] = L"Sample Window Class"; 14 15 WNDCLASS wc = { }; 16 17 wc.lpfnWndProc = WindowProc; 18 wc.hInstance = hInstance; 19 wc.lpszClassName = CLASS_NAME; 20 21 RegisterClass(&wc); 22 23 // 创建窗口. 24 HWND hwnd = CreateWindow( 25 CLASS_NAME, //窗口类名 26 L"Demo Window", //窗口标题 27 WS_OVERLAPPEDWINDOW, //窗口样式 28 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, //窗口位置和大小 29 NULL, //父窗口 30 NULL, //菜单 31 hInstance, //Instance句柄 32 NULL //额外的程序数据 33 ); 34 35 if (hwnd == NULL) 36 { 37 return 0; 38 } 39 40 ShowWindow(hwnd, nCmdShow); 41 42 // 消息循环. 43 MSG msg = { }; 44 while (GetMessage(&msg, NULL, 0, 0)) 45 { 46 TranslateMessage(&msg); 47 DispatchMessage(&msg); 48 } 49 50 return 0; 51 } 52 53 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 54 { 55 switch (uMsg) 56 { 57 case WM_DESTROY: 58 PostQuitMessage(0); 59 return 0; 60 61 case WM_PAINT: 62 { 63 PAINTSTRUCT ps; 64 HDC hdc = BeginPaint(hwnd, &ps); 65 66 //在此处添加使用 hdc 的任何绘图代码... 67 FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1)); 68 EndPaint(hwnd, &ps); 69 } 70 return 0; 71 } 72 73 return DefWindowProc(hwnd, uMsg, wParam, lParam); 74 }
在上面的代码中,我们创建了一个最基本的Windows窗口应用程序,运行效果如下
参考资料
https://learn.microsoft.com/zh-cn/windows/win32/learnwin32/learn-to-program-for-windows
https://learn.microsoft.com/zh-cn/windows/win32/LearnWin32/your-first-windows-program