目录
- ZERO 、
- 一、使用环境
- 二、说明以及预览图
- 三、制作教程
- 1、打开VS,新建项目,找到windows应用程序模板,如下图所示。
- 2、修改模板
- 1)、`CreateWindowW`函数
- 2)、模板创建的`MyRegisterClass`的函数
- 3)、`GetMessage`消息接受函数
- 四、End......
ZERO 、
>本章是主要任务是不使用外置图形库,做一个自己的窗口
一、使用环境
使用VS2022,需要包含windows桌面应用程序扩展,
因为我们不使用图形库graphics.h。
二、说明以及预览图
说明:demo做得可能有点粗糙,因为使用的只有C/C++的内置函数,但是基本功能还是实现了。
本来还想加入一些道具元素,但能力有限最后还是鸽了。
写这篇文章的目的其实还是复习一下,然后也是希望在写文章的过程中找到错误,也希望读者能够给我一些建议。
下面是预览图
三、制作教程
1、打开VS,新建项目,找到windows应用程序模板,如下图所示。
进入之后点击调试,你就会看到一个窗口,当然是白色的,什么也没有。
2、修改模板
1)、CreateWindowW
函数
这里我会给大家简单说明一下模板代码中一些比较重要的部分,包括以及要修改的部分。
首先是模板自定义的InitInstance(HINSTANCE, int)
函数,你会发现这个函数里面包含着CreateWindowW()
函数,下面简单介绍一下,搞清这个函数再来下一个注册函数。
_In和_opt顾名思义,一个输入,一个输出;
LPCTSTRlpClassName
和LPCTSTRlpWindowName
是窗口类名称和标题,文件开头就有定义,赋值的时候使用的是MyRegisterClass
函数,我们下面再讲;
然后是句柄的含义,详情你们可以去官方API上面寻找,我个人的理解其实就是一个特定的标识符,浅一点来说就是跟人们的身份证雷同。
HWND WINAPI CreateWindow(
_In_opt_ LPCTSTRlpClassName, // 窗口类名称
_In_opt_ LPCTSTRlpWindowName, // 窗口标题
_In_ DWORD dwStyle, // 窗口风格,或称窗口格式
_In_ int x, // 初始 x 坐标
_In_ int y, // 初始 y 坐标
_In_ int nWidth, // 初始 x 方向尺寸
_In_ int nHeight, // 初始 y 方向尺寸
_In_opt_ HWNDhWndParent, // 父窗口句柄
_In_opt_ HMENUhMenu, // 窗口菜单句柄
_In_opt_ HINSTANCEhInstance, // 程序实例句柄
_In_opt_ LPVOIDlpParam // 创建参数
);
前面几个参数很简单,没必要讲;
1.父窗口句柄我们没有,设置为NULL;
2.窗口菜单句柄我们也没有,为NULL;
3.程序实例句柄,可有可无,但我们一般设置为主函数的第一个参数,也就是hInstance
。
4.最后一个没什么用,设置为NULL。
5.最后我们使用的窗口函数就是这样:
6.HWND hwnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WIND_WIDTH, WIND_HEIGHT, nullptr, nullptr, hInstance, nullptr);
其中WIND_WIDTH, WIND_HEIGHT
是你自己定义的参数,表示窗口的大小,其他就是windows自带的类型,具体可以使用CTRL+鼠标左键看源码。
之所以要使用这个函数,除了创造窗口之外,我们还需要函数返回的句柄hwnd,这个相当于唯一标识窗口,以后修改窗口时就需要它传参,因此我们把直接把函数InitInstance(HINSTANCE, int)
去掉,然后把内容复制到主函数即可,贴代码。
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_MODSDEMO, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
hInst = hInstance; // 将实例句柄存储在全局变量中
HWND hwnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, WIND_WIDTH, WIND_HEIGHT, nullptr, nullptr, hInstance, nullptr);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
if (!hwnd)
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_MODSDEMO));
MSG msg = { 0 };
// 主消息循环:
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int)msg.wParam;
}
2)、模板创建的MyRegisterClass
的函数
开始我们就看到一个名为
wcex
的变量,这里的wcex
并不标识唯一的窗口,标识符还是得靠我们上面提到的hwnd。
下面来介绍一些各个参数
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_MODSDEMO));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_MODSDEMO);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
1.
cbSize
:表示窗口大小,使用sizeof即可;
2.style
:窗口类型,自行查阅;
3.lpfnWndProc
:窗口过程函数,非常重要,举个小栗子,接受到的消息经过翻译是要传到这个函数来的,使用这个函数接受到消息才会引发相应的操作,比如我们的键盘和鼠标输入依靠的就是此函数,模板中应该也写出了,看不懂没关系,有一些我也看不懂,跟着我来
4.cbClsExtra
和cbWndExtra
额外参数,没什么用,设置为0;
5.hInstance
:句柄,填主函数第一个参数即可;
6.hIcon
:应用程序图标,你们的左上角的东西,你也可以使用如下代码使用自己喜欢的.ico
图标
(HICON)LoadImage(NULL,L"Nitro.ico",IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE);
7.
hCursor
:鼠标的形式,可以有很多取值;
8.hbrBackground
:背景颜色,此处设置的为白色,所以我们窗口一片白,我们还可以使用如下函数让窗口灰或白
(HBRUSH)GetStockObject(GRAY_BRUSH);
9.
lpszMenuName
和lpszClassName
上面以及解释,这里正好给他俩所在的参数赋值
10.hIconSm
:与hIcon
不同的是,它是加载窗口图像。
然后是返回的RegisterClassExW
的窗口注册函数,这一步必不可少。
3)、GetMessage
消息接受函数
这个也是重点,上面我们提到了窗口过程函数,根据接受的消息做出相应的操作,那么我们也还就需要特定的函数用来接受和翻译消息。
GetMessage
函数的特点是,接受消息的时候才会动,同时接受消息,没有消息的时候他是不会动的,但是作为游戏来说,时时刻刻都在运行着,并且每时每刻都要准备接受消息,因此本函数不适合使用在游戏中。
我们使用另外一个消息接受函数PeekMessage
,此函数每时每刻都在接受消息,没有消息就返回false;
BOOL PeekMessage(
LPMSG IpMsg, //接受消息的结构体
HWND hWnd, //检查消息的句柄,设置为0即使用已创建的句柄,你也可以使用上面的hwnd
UINT wMSGfilterMin,
UINT wMsgFilterMax,//min和max设置为0表示传来的消息照单全收
UINT wRemoveMsg //接受消息的形式,最好是处理消息后然后扔掉
);
因此我们这么使用它
// 主消息循环:
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
如果直接用
while
里面加PeekMessage
,那么如果没收到消息返回false
,这时候就会退出while
循环导致主函数直接返回,窗口闪一下就会结束,因此我们只能while
里面检测是否是WM_QUIT
退出消息,然后使用if
判断,这样就能一直把PeekMessage
锁在循环里了。
其他代码就不需要动了。
#include "framework.h"
#include "ModsDemo.h"
#define WIND_WIDTH 800
#define WIND_HEIGHT 680
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_MODSDEMO, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
hInst = hInstance; // 将实例句柄存储在全局变量中
HWND hwnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, WIND_WIDTH, WIND_HEIGHT, nullptr, nullptr, hInstance, nullptr);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
if (!hwnd)
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_MODSDEMO));
MSG msg = { 0 };
// 主消息循环:
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
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_MODSDEMO));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_MODSDEMO);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
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;
}
四、End…
一个简单的窗口就做好了,不复杂,但是东西多,记住下面这几点
我文件名字是ModDemo
,其中MAKEINTRESOURCE
中的参数与你的文件名字有关,所以不要直接copy代码,比如我的函数就是MAKEINTRESOURCE(IDC_MODSDEMO)
参数正是IDC_{大写的文件名}
。
用浅墨的规划来说:
窗口创建四部曲:
- 窗口设计
MyRegisterClass
。 - 窗口注册
MyRegisterClass
中的RegisterClassExW
- 窗口正式创建
CreateWindowW
- 窗口显示和更新
ShowWindow
,UpdateWindow
;
如果有可能第二章就一起实现Game_Init
和Game_Paint
。