在 C++ 中创建图形窗口
b9113fced86a32cad0d8 · · 科技·工程
0 前言
控制台软件实在是太低级了!分辨率低的要命!也就输出一点文字了!根本没法好好画图!对于游戏作者一点也不友好!
但是 EasyX 根本不会安装!
有没有 Windows 自带的画图工具啊?
有的,兄弟,有的,我们可以使用 Windows 提供的 GDI(图形设备接口)来实现这一切。
1 准备工作
既然是 Windows API,那肯定少不了 #include<windows.h>,接着,肯定要导入一些来自 Windows 的库文件。如果你使用的是 MinGW64,那么你使用 #pragma 是无效的,你需要指定编译指令 -luser32 -lgdi32 -lkernel32。(至于怎么添加编译指令?难道您不会百度么?)
2 创建窗口
在创建窗口之前,因为编写图形窗口的特殊性,我们同样需要讲解一下 main 函数的写法。
这次不是简单的使用 int main();,而是使用 int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow);。
这些是干什么的呢?
WinMain()函数是 GUI 程序的入口点,类似于控制台程序的main(),长成这样是历史遗留的结果,具体的可以去查;HINSTANCE hInstance参数是用来标识当前应用程序的实例的,是操作系统分配给程序的唯一标识符。创建各种窗口是都要使用;HINSTANCE hPrevInstance参数是 16 位操作系统遗留下来的遗产,现在,其值始终为NULL,当它不存在就行;LPSTR lpCmdLine参数就是运行程序时的命令行参数,例如控制台运行test.exe -a -b,则该参数为-a -b;int nCmdShow就是指定主窗口的初始显示方式,通常由操作系统指定。
很好!你已经完成了主函数的定义,你现在可以创建窗口了。至于创建窗口,直接套模板就行,实在是没什么可以写的,也没什么大的变化:
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){
// 1. 注册窗口类
const wchar_t CLASS_NAME[] = L"Sample Window Class";// 窗口类名
WNDCLASSW wc = {};
wc.lpfnWndProc = WindowProc; // 指定窗口过程函数
wc.hInstance = hInstance; // 当前实例句柄
wc.lpszClassName = CLASS_NAME; // 窗口类名
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 默认箭头光标
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 窗口背景色
RegisterClassW(&wc);
// 2. 创建窗口实例
HWND hwnd = CreateWindowExW( // 返回值是窗口句柄
0, // 扩展样式(无)
CLASS_NAME, // 窗口类名
L"Windows GUI test", // 窗口标题
WS_OVERLAPPEDWINDOW, // 普通窗口样式
CW_USEDEFAULT, CW_USEDEFAULT, // 默认位置
800, 600, // 窗口大小
NULL, // 无父窗口
NULL, // 无菜单
hInstance, // 实例句柄
NULL // 无额外参数
);
if (hwnd == NULL){
return 0;
}
// 3. 显示和更新窗口
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
}
容易发现各种字符串前面都有一个 L,放心,这不会导致编译错误,这表示使用宽字符(Unicode),这是一个十分现代化的字符集,是 Windows GDI 编程推荐的编码。这也是预防多语言字符串乱码最方便快捷的方法。
现在,发生了编译错误,编译器提示 WindowProc 未定义,不用担心,我们马上就开始定义该函数了。
3 窗口过程函数
编写窗口过程函数,一般是在 WinMain() 函数前声明,其后定义,不过具体还是看大家自己的意愿与习惯。
窗口过程函数是用于处理窗口信息的,通常,在主函数里有一个用于处理窗口消息的消息循环,会由它将消息分发到窗口过程函数。(必须将其写在创建窗口后)
// 4. 消息循环:处理窗口消息
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg); // 翻译键盘消息
DispatchMessage(&msg); // 分发消息到窗口过程
}
return msg.wParam; // 结束运行时的主函数返回值
现在,定义窗口过程函数,因为前面有 wc.lpfnWndProc = WindowProc; 所以,我们的窗口过程函数就叫做 WindowProc 了。
窗口过程函数的原型是 LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam); ,其各个参数与返回值代表了不同的含义:
LRESULT返回值是代表消息处理的结果,正常情况下是0;HWND hwnd表示消息所属的窗口的句柄;UINT uMsg表示消息类型;WPARAM wParam和LPARAM lParam表示的是消息的附加参数。
在函数中,可以通过 switch(){case:} 或 if(){}else if(){}else{} 来针对每一种消息类型给出不同的处理方法。(不过我自认为 switch(){case:} 写起来更好看一点)
这里给出常见的 UINT uMsg 的含义:
|UINT uMsg|含义|
|:-|:-|
|WM_CREATE|窗口创建|
|WM_PAINT|窗口需要重绘|
|WM_SIZE|窗口大小改变|
|WM_CLOSE|请求关闭窗口|
|WM_DESTROY|窗口销毁|
|WM_KEYDOWN|键盘按键按下|
|WM_LBUTTONDOWN|鼠标左键按下|
|WM_RBUTTONDOWN|鼠标右键按下|
该套模板了!
// 窗口过程函数:处理所有发送到窗口的消息
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
switch (uMsg){
case WM_DESTROY: // 窗口关闭时
{
PostQuitMessage(0); // 通知消息循环退出
return 0;
}
case WM_PAINT: // 窗口需要重绘时
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd,&ps);// 这里的 hdc 十分重要!
// 在这里画图
EndPaint(hwnd,&ps);
return 0;
}
case WM_SIZE: // 窗口大小改变时
{
// 可添加处理窗口大小变化的代码
return 0;
}
}
// 处理未显式处理的消息(必须调用默认处理函数)
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
所以,现在你可以在 case WM_PAINT:{} 中指定画图内容,但是画图的函数是什么呢?我们将在下一部分列举所有常见的画图函数。
4 画图函数
在 GUI 编程中,有许多神奇的函数,通过把这些函数放入窗口过程函数的 case WM_PAINT 部分,便可以画图了。
画图函数里面有一个 HDC hdc 的参数,它指的是设备上下文句柄,是所有画图操作的基础。
一般情况下,调用函数时的参数用上面 HDC hdc = BeginPaint(hwnd,&ps); 得到的 hdc 就行了。
接下来,我们将列举常见的画图函数:
绘制直线
函数原型
BOOL MoveToEx(HDC hdc,int x,int y,LPPOINT lpPoint);
BOOL LineTo(HDC hdc,int x,int y);
解释:
MoveToEx移动到(x,y) ;LineTo画直线到(x,y) 。使用示例
MoveToEx(hdc,114,514,NULL); LineTo(hdc,1919,810);解释: 从
(114,514) 画直线到(1919,810) 。绘制矩形
函数原型
BOOL Rectangle(HDC hdc,int left,int top,int right,int bottom);解释: 画一个矩形,使得其左侧在直线
x=left ,顶部在直线y=top ,右侧在直线x=right ,底部在直线y=bottom 。使用示例
Rectangle(hdc,114,514,1919,810);解释: 画一个矩形,使得其左侧在直线
x=114 ,顶部在直线y=514 ,右侧在直线x=1919 ,底部在直线y=810 。绘制椭圆形
函数原型
BOOL Ellipse(HDC hdc,int left,int top,int right,int bottom);解释: 画一个椭圆形,使得其同时与
x=left 、y=top 、x=right 、y=bottom 相切。使用示例
Ellipse(hdc,114,514,1919,810);解释: 画一个椭圆形,使得其同时与
x=114 、y=514 、x=1919 、y=810 相切。绘制多边形
函数原型
BOOL Polygon(HDC hdc,const POINT *apt,int cpt);解释: 画一个
cpt 边形,其每一个顶点存储在apt数组中。使用示例
POINT points[] = {{100,50}, {200,80}, {250,150}, {150,200}}; Polygon(hdc,points,4);解释: 画一个 4 边形,其4个顶点坐标依次为
(100,50) 、(200,80) 、(250,150) 、(150,200) 。绘制折线
函数原型
BOOL Polyline(HDC hdc,const POINT *apt,int cpt);解释: 画
cpt-1 条线段,第i 条线段连接cpt_{i-1} 和cpt_{i} 。使用示例
POINT points[] = {{100,50}, {200,80}, {250,150}, {150,200}}; Polyline(hdc,points,4);解释: 画一个 3 条线段,它们的端点分别为:
(100,50) 、(200,80) 、(250,150) 、(150,200) 。绘制文本
函数原型
int DrawTextW(HDC hdc,LPCWSTR lpchText,int nCount,LPRECT lpRect,UINT uFormat);解释: 画一个内容为
lpchText的字符串,长度为nCount,并把字符限制在lpRect的范围内。使用示例
RECT rc = {100, 100, 300, 200}; DrawTextW(hdc,L"Centered Text",-1,&rc,DT_CENTER | DT_VCENTER);解释: 画一个内容为
Centered Text的字符串,长度自动计算,并把字符限制在rc的范围内。补充信息
rc是一个结构体,它的结构就像Ellipse的参数一样。uFormat参数是一个位掩码,通过多个二的幂次进行或运算得到结果。它们有以下值:
| 标志 | 效果 |
|---|---|
DT_TOP |
顶部对齐(默认) |
DT_LEFT |
左对齐(默认) |
DT_CENTER |
水平居中 |
DT_RIGHT |
右对齐 |
DT_VCENTER |
垂直居中 |
DT_BOTTOM |
底部对齐 |
它们还有更多,可以自己上网去查。
5 有用但不是画图的函数
(呃其实只有两个,但我不知道为什么又放了一个大章)
这里面的窗口句柄就是窗口过程函数的 HWND hwnd 参数。
强制重绘(步骤 1)
函数原型
InvalidateRect(HWND hWnd,CONST RECT* lpRect,BOOL bErase);
解释:
在句柄为 hWnd 的窗口,标记 lpRect 范围内为无效,并根据 bErase 决定是否擦除背景。
使用示例
InvalidateRect(hwnd,NULL,TRUE);
解释:
在句柄为 hwnd 的窗口,标记整个窗口为无效,并擦除背景。
强制重绘(步骤 2)
函数原型
UpdateWindow(HWND hWnd);
解释:
在句柄为 hWnd 的窗口,立即触发重绘。
使用示例
UpdateWindow(hwnd);
解释:
在句柄为 hwnd 的窗口,立即触发重绘。
注意:两者有区别,一个是标记无效,另一个是立即触发重绘。两者通常一起使用。
6 一个完整的示例代码
#include<windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){ //1. 注册窗口类 const wchar_t CLASS_NAME[] = L"Sample Window Class";// 窗口类名
WNDCLASSW wc = {};
wc.lpfnWndProc = WindowProc; // 指定窗口过程函数
wc.hInstance = hInstance; // 当前实例句柄
wc.lpszClassName = CLASS_NAME; // 窗口类名
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 默认箭头光标
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 窗口背景色
RegisterClassW(&wc);
// 2. 创建窗口实例
HWND hwnd = CreateWindowExW( // 返回值是窗口句柄
0, // 扩展样式(无)
CLASS_NAME, // 窗口类名
L"Windows GUI test", // 窗口标题
WS_OVERLAPPEDWINDOW, // 普通窗口样式
CW_USEDEFAULT, CW_USEDEFAULT, // 默认位置
800, 600, // 窗口大小
NULL, // 无父窗口
NULL, // 无菜单
hInstance, // 实例句柄
NULL // 无额外参数
);
if (hwnd == NULL){
return 0;
}
// 3. 显示和更新窗口
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
// 4. 消息循环:处理窗口消息
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg); // 翻译键盘消息
DispatchMessage(&msg); // 分发消息到窗口过程
}
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg){ case WM_DESTROY: // 窗口关闭时 { PostQuitMessage(0); // 通知消息循环退出 return 0; } case WM_PAINT: // 窗口需要重绘时 { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd,&ps);// 这里的 hdc 十分重要! // 画直线 MoveToEx(hdc,114,514,NULL); LineTo(hdc,1919,810); // 画矩形 Rectangle(hdc,114,514,1919,810); // 画椭圆形 Ellipse(hdc,114,514,1919,810); // 画四边形 POINT points[] = {{100,50}, {200,80}, {250,150}, {150,200}}; Polygon(hdc,points,4); // 画字符串 RECT rc = {100, 100, 300, 200}; DrawTextW(hdc,L"Centered Text",-1,&rc,DT_CENTER | DT_VCENTER); EndPaint(hwnd,&ps); return 0; }
case WM_SIZE: // 窗口大小改变时
{
// 可添加处理窗口大小变化的代码
return 0;
}
}
// 处理未显式处理的消息(必须调用默认处理函数)
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
# 7 结语
图形化软件实在是太好用了!分辨率也很不错!各种图形都能很好地输出!完全可以好好画图!对于游戏作者十分的友好!
而且是 Windows 自带的!
有没有更高级的画图工具或函数啊?
有的,兄弟,有的,我们很快就能看到《在 C++ 中创建图形窗口(进阶)》了。~~(然而我也不知道要花多长时间)~~