0x00前言
文章中的文字可能存在语法错误以及标点错误,请谅解;
如果在文章中发现代码错误或其它问题请告知,感谢!
0x01 概述
最近在学习windows程序设计的创建窗口这方面的知识,有一些收获,现在将我所学到的内容记录下来,供大家参考。
本例将做一个类似txt文档窗口,可以对用户的键盘输入打印到该窗口上,并且有菜单栏可以单击菜单栏中的退出来退出窗口。
0x02 创建一个完整的窗口程序的步骤
要想创建一个窗口程序,需要以下5个步骤:
(1)注册窗口类(RegisterClassEX);
(2)创建窗口(CreateWindowsEX);
(3)在桌面上显示窗口(ShowWindow);
(4)更新窗口客户区(UpdateWindow);
(5)进入消息获取以及处理循环,直到获取的消息为WM_QUIT时,结束消息循环。
下面分节介绍各步骤的代码实现。
0x03 创建Win32工程
我们一般练习时运行的程序都是使用控制台界面来接收和显示我们的运行结果,但是我们要想使用窗口界面与用户交互的话就应该创建Win32工程。
选择“新建项目”,然后选择“已安装模板”和“Visual C++”,最后选择“Win32项目”。在“名称”框中,键入项目名称,例如“窗口程序”,然后单击“确定”:
在接下来的对话框中选择默认选项即可。
进入到工程之后,发现.cpp文件里有写好的demo程序,我们为了练习,将这个demo程序代码全部注释掉或删掉,然后再继续进行。
0x04 注册窗口类
窗口类是系统在创建窗口时作为模板使用的属性集合。注册窗口类的API函数是ATOM RegisterClassEX(CONST WNDCLASSEXA *Ipwcx),其参数是WNDCLASSEXA结构体指针,该结构体定义了窗口的一些主要的属性,比如光标、图标、背景色和要处理的消息窗口函数等。
WNDCLASSEXA结构体定义如下:
typedef struct tagWNDCLASSEXA {
UINT cbSize; //WNDCLASSEXA结构的大小
UINT style; //从这个窗口类派生的窗口具有的风格
WNDPROC lpfnWndProc; //即window procedure, 窗口消息处理函数指针
int cbClsExtra; //指定紧跟在窗口类结构后的附加字节数
int cbWndExtra; //指定紧跟在窗口事例后的附加字节数
HINSTANCE hInstance; //本模块的实例句柄
HICON hIcon; //窗口左上角图标的句柄
HCURSOR hCursor; //光标的句柄
HBRUSH hbrBackground; //背景画刷的句柄
LPCSTR lpszMenuName; //菜单名
LPCSTR lpszClassName; //该窗口类的名称
HICON hIconSm; //小图标句柄
} WNDCLASSEXA, *PWNDCLASSEXA, NEAR *NPWNDCLASSEXA, FAR *LPWNDCLASSEXA;
各字段含义在注释中已经说明,其中需要注意的是lpfnWndProc字段(即window procedure窗口程序,重要),该字段存放消息处理函数的指针,当窗口收到消息时windows会自动调用这个函数进行处理,处理内容由程序设计者自定,比如本例中当接收到键盘输入字符后就让它即时显示在窗口上。
本例所填充的WNDCLASSEXA结构体如下:
char szClassName[] = "MainWClass";
WNDCLASSEX wndclass; //定义的结构体名
wndclass.cbSize = sizeof(wndclass); //结构的大小
wndclass.style = CS_HREDRAW | CS_VREDRAW; //指定如果大小改变就重画
wndclass.lpfnWndProc = MainWndProc; //窗口的函数指针
wndclass.cbClsExtra = 0; //没有额外的类内存
wndclass.cbWndExtra = 0; //没有额外的窗口内存
wndclass.hInstance = hInstance; //句柄实例
wndclass.hIcon = ::LoadIcon(hInstance, (LPSTR)IDM_EXIT); //使用指定的图标
wndclass.hCursor = ::LoadCursorA(NULL, IDC_ARROW); //使用预定义光标
wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);//使用白色背景刷
wndclass.lpszMenuName = (LPSTR)IDC_MY; //使用指定的菜单
wndclass.lpszClassName = szClassName; //窗口类的名称
wndclass.hIconSm = NULL; //没有类的小图标
其中wndclass.lpfnWndProc、wndclass.hIcon和wndclass.lpszMenuName,这三个字段我们不使用默认字段而是自己定义。三个字段含义分别是:窗口消息处理、指定窗口图标、指定窗口菜单。
填充完该结构体之后,就可以使用RegisterClassEX函数进行注册:
::RegisterClassEX(&wndclass);
0x05创建窗口
创建窗口,调用CreateWindowEx()函数:
HWND hwnd = ::CreateWindowExA(
0, //dwExStyle,扩展样式
szClassName, //lpClassName,类名
NULL, //lpWindowName,标题
WS_OVERLAPPEDWINDOW,//dwstyle,窗口风格
CW_USEDEFAULT, //X,初始x坐标
CW_USEDEFAULT, //Y,初始y坐标
CW_USEDEFAULT, //nWidth,宽度
CW_USEDEFAULT, //nHeight,高度
NULL, //hWndParent,父窗口句柄
NULL, //hMenu,菜单句柄
hInstance, //hInstance,程序实例句柄
NULL); //lpParam,用户数据
// lpParam,用户数据
该函数成功调用返回窗口句柄,若失败返回NULL。第4个参数指定了窗口标题栏、系统菜单、可以改变边框大小等。该参数也可以使用“|”来构建窗口的风格 ,例如将使用“WS_OVERLAPPEDWINDOW | WS_VSCROLL”该窗口将带有最大最小化以及垂直滚动条等风格。
0x06 在桌面上显示
在桌面上显示,则使用BOOL ShowWindow(HWND hWnd, int nCmdShow)函数。该函数用于指定窗口的显示状态,参数nCmdShow是系统传给WinMain函数的参数。如果窗口之前可见,则返回值为非零。如果窗口之前被隐藏,则返回值为零。
本例中调用的ShowWindow():
ShowWindow(hwnd, nCmdShow);
0x07更新窗口客户区
更新窗口客户区使用BOOL UpdateWindow(HWND hWnd );如果函数调用成功,返回值为非零值。如果函数调用不成功,返回值为零。如果指定的窗口更新区域不为空的话,则该函数会发送一个VM_PAINT消息(刷新窗口客户区)到窗口函数(Window Procedure)处理 。注意,所有消息都在WINUSER.H中被标识为数字:
0x08 进入无限消息循环
经过前几个步骤之后,下面就应该进入无限循环等待用户的键盘鼠标输入。Windows中,每个线程都会被分配一个消息队列,当有一个输入发生后,Windows就会把用户的输入翻译成消息存放在消息队列中。使用GetMessage()函数可以从线程的消息队列中取出一个消息来填充MSG结构,例如:
::GetMessage(&msg, NULL, 0,0);
MSG结构体定义:
typedef struct tagMSG {
HWND hwnd; //消息要发送的窗口句柄
UINT message; //消息标识符,以WM_靠头的预定义值
WPARAM wParam; //消息参数一
LPARAM lParam; //消息参数二
DWORD time; //消息放入消息队列的时间
POINT pt; //这是一个POINT数据结构,标识消息放入消息队列时鼠标位置
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
GetMessage()函数从消息队列中取得的消息,如果不是WM_QUIT,则返回非0值,否则返回0。一般将GetMessage()函数加入while()中判断是否为FALSE,以确定是否结束消息循环:
while(::GetMessage(&msg, NULL, 0, 0))
{
//处理逻辑
}
本例中需要把键盘输入的虚拟键消息转换为字符消息(WM_CHAR消息),所以还要使用::TranlateMessage()函数把上面填充后的MSG结构体“翻译”一下:
::TranslateMessage(&msg);
然后使用DispatchMessage()函数将翻译后的消息分发到对应的窗口函数中(0x04小节中WNDCLASSEXA结构体实例中的MainWndProc):
::DispatchMessage(&msg);
0x09 使用窗口函数进行消息处理
前面的小节完成了窗口类注册、创建窗口、显示在屏幕上,整个程序已经进入消息循环,开始从消息队列中取消息,但是真正的工作是在窗口函数(本例的MainWndProc)中完成,该函数的作用就是对传进来的消息按照设计者的意愿进行处理,例如本例中将接收到键盘输入字符后让它即时显示在窗口上。
通常在窗口函数中使用switch和case结构决定怎么处理消息。另外,窗口函数接收到的消息有很多,对于不关心的消息将它们传送至窗口函数中的DefWindowProc()函数处理。一般情况下,窗口函数中消息处理的结构如下:
switch(uMsg)
{
case WM_PAINT:
//处理WM_PAINT消息逻辑
return 0;
case WM_DESTROY;
//处理WM_DESTROY消息逻辑
return 0;
......
}
return ::DefWindowProc(hwnd, message, wParam, lParam);//交由windows默认处理
注意,必须要把不处理的消息交给DefWindowProc()处理,也要把该函数的返回值交给windows,否则windows失去了和应用程序通信的途径,系统不能再控制窗口行为。
0x07小节中说明了VM_PAINT消息作用是刷新客户区,除了使用UpdateWindow()发送该消息之外,当出现窗口第一次创建、窗口大小改变、最小化窗口时,窗口函数等也会收到VM_PAINT消息。处理VM_PAINT消息时总以调用BeginPaint()函数开始,以ENDPaint()函数结束:
hdc = ::BeginPaint(hwnd, &ps);
//处理逻辑
::EndPaint(hwnd, &ps);
当用户关闭窗口,窗口函数会受到一个VM_DESTROY消息,一般收到这个消息之后,设计者都会调用PostQuitMessage()函数响应此消息,该函数会向消息队列发送VM_QUIT消息。在0x08小节提过,当主函数中的GetMessage()函数如果从消息队列中收到VM_QUIT,则返回0值,从跳出while循环,执行接下的代码。
同样在0x08小节中,键盘输入键码后会被TranslateMessage()函数转化为WM_CHAR消息,然后发送到窗口函数,当窗口函数收到WM_CHAR消息后,会把对应的字符添加到字符串变量中,然后使用InvalidateRect()函数使客户区无效,迫使windows再次发送WM_PAINT消息,这样字符就能显示在客户区中了。
0x10 创建窗口的完整代码
创建窗口的完整代码如下:
#include "stdafx.h"
#include<string.h>
#include "第一个窗口程序.h"
#include<string>
//窗口函数的函数原型
LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
char szClassName[] = "MianWClass";
WNDCLASSEX wndclass;
//用描述主窗口的参数填充WNDCLASSEX结构
wndclass.cbSize = sizeof(wndclass); //结构的大小
wndclass.style = CS_HREDRAW | CS_VREDRAW; //指定如果大小改变就重画
wndclass.lpfnWndProc = MainWndProc; //窗口的函数指针
wndclass.cbClsExtra = 0; //没有额外的类内存
wndclass.cbWndExtra = 0; //没有额外的窗口内存
wndclass.hInstance = hInstance; //句柄实例
//wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);//使用预定义图标
wndclass.hIcon = ::LoadIcon(hInstance, (LPSTR)IDI_ICON2);
wndclass.hCursor = ::LoadCursorA(NULL, IDC_ARROW);//使用预定义光标
wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);//使用白色背景刷
wndclass.lpszMenuName = (LPSTR)IDC_MY;
wndclass.lpszClassName = szClassName; //窗口类的名称
wndclass.hIconSm = NULL; //没有类的小图标
//注册这个窗口类
::RegisterClassEx(&wndclass);
//创建主窗口
HWND hwnd = ::CreateWindowExA(
0, //dwExStyle,扩展样式
szClassName, //lpClassName,类名
NULL,//lpWindowName,标题
WS_OVERLAPPEDWINDOW | WS_VSCROLL,//dwstyle,窗口风格
CW_USEDEFAULT, //X,初始x坐标
CW_USEDEFAULT, //Y,初始y坐标
CW_USEDEFAULT, //nWidth,宽度
CW_USEDEFAULT, //nHeight,高度
NULL, //hWndParent,父窗口句柄
NULL, //hMenu,菜单句柄
hInstance, //hInstance,程序实例句柄
NULL); //lpParam,用户数据
if(NULL == hwnd)
{
::MessageBoxA(NULL, "创建窗口出错", "error", MB_OK);
return -1;
}
//显示窗口,刷新窗口客户区
::ShowWindow(hwnd,nCmdShow);
::UpdateWindow(hwnd);
//从消息对列中取出消息,交给窗口函数处理,直到getmessage返回FALSE,结束消息循环
MSG msg;
while(::GetMessage(&msg, NULL, 0, 0))
{
//转化键盘消息
::TranslateMessage(&msg);
//将消息发送到相应的窗口函数
::DispatchMessageA(&msg);
}
//当GetMessage返回FALSE时结束
return msg.wParam;
}
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//str对象用于保存窗口客户区显示的字符串
static std::string str;
switch(message)
{
case WM_CREATE:
{
//设置窗口标题
::SetWindowTextA(hwnd, "最简陋的打字程序");
return 0;
}
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case IDM_EXIT:
{
//向hwnd指定的窗口发送一个WM_CLOSE消息
::SendMessage(hwnd, WM_CLOSE, 0, 0);
break;
}
}
case WM_PAINT://窗口客户区需要重画
{
PAINTSTRUCT ps;
//使无效的客户区变的有效,并取得设备环境句柄
HDC hdc = ::BeginPaint(hwnd, &ps);
// 设置输出文本的背景颜色和文字颜色
::SetTextColor(hdc, RGB(255, 0, 0));
::SetBkColor(hdc, ::GetSysColor(COLOR_3DFACE));
//显示文字
::TextOutA(hdc, 0, 0, str.c_str(), str.length());
::EndPaint(hwnd, &ps);
return 0;
}
case WM_CHAR:
{
// 保存ansi码
str = str + char(wParam);
// 使整个客户区无效
::InvalidateRect(hwnd,NULL,0);
return 0;
}
case WM_DESTROY://正在销毁窗口
{
//向消息队列投递一个WM_QUIT消息,初始GetMessage函数返回0,结束消息循环
::PostQuitMessage(0);
return 0;
}
}
}
//将我们不处理的消息交给系统做默认处理
return ::DefWindowProcA(hwnd, message, wParam, lParam);
}
需要注意的是本例中我们在程序里自定义了窗口的左上角图标,添加方法为
右键“资源管理器栏”的“源文件”,选择“添加”和“资源”,选择“icon”后按“导入”把本地的.icon文件(可自行从网上下载.icon文件)导入到工程中:
选择“资源管理器栏”的“第一个窗口程序.rc”文件,然后选择“Icon”文件夹,删掉以前的icon(因为我们是在demo程序下修改,该icon为demo程序的),将刚导入的icon通过右侧改ID号为“IDI_ICON2”:
最后进入到“第一个窗口文件.cpp”文件中将wndclass.hIcon = ::LoadIcon()第二个参数为IDI_ICON2:
wndclass.hIcon = ::LoadIcon(hInstance, (LPSTR)IDI_ICON2);
这样就将本地图标添加到程序中了。
另外,本例中自定义了菜单,右键“资源管理器栏”的“源文件”,选择“添加”和“资源”,选择“Menu”后新建菜单:
接着,同样选择“资源管理器栏”的“第一个窗口程序.rc”文件,选择“Menu”文件夹,删掉以前的demo留下来的文件,将刚新建的menu通过右侧改ID号为“IDR_MENU1”:
双击“IDR_MENU1”,进入菜单制作场景,然后添加主菜单“文件”和子菜单“退出”,把退出的ID改为“IDM_EXIT”:
回到“第一个窗口文件.cpp”文件在case WM_COMMAND添加退出逻辑,这样在窗口运行的时候单击文件中的退出按钮窗口就会退出:
case IDM_EXIT:
{
//向hwnd指定的窗口发送一个WM_CLOSE消息
::SendMessage(hwnd, WM_CLOSE, 0, 0);
break;
}
0x11运行结果
在运行之前,需要修改项目属性的字符集为多字节,这样程序运行时不会出现"LPWSTR" 类型的实参与 “LPCSTR” 类型的形参不兼容问题:
运行结果:
以上。
参考文档:
1.https://blog.csdn.net/u011583927/article/details/54896961
2.https://blog.csdn.net/qq_32823595/article/details/79450832
3.https://blog.csdn.net/NNNNNNNNNNNNY/article/details/46394207
4.张铮,孙宝山,周立天.Windows程序设计(第3版)[M].北京;人民邮电出版社,2018.7.