大家好,很高兴又和大家见面了!!! 在开始今天的内容前,咱们先闲聊一下。博主是从2023.8.19号晚上23:28左右正式开始接触C语言,在此之前,我也只是一个对编程一窍不通的小白,我的本科专业是给排水科学与工程,一个就业前景还不错但是不太适合我本人的专业。
在经历了一些事情之后,我对继续从事本专业的工作已经失去了坚持下去的动力,为了改变现在,于是我便下定决心跨专业进行学习。
在学习编程的这段时间里,相信大部分朋友和我一样接触最多的就是一些枯燥乏味的理论知识,比如分支语句、循环语句、函数、指针、结构体、枚举、动态内存管理、链表……平时做的最多的可能就是在力扣网或者牛客网上刷刷题来巩固相关的知识点,长期处于这样的学习状态下,大家的学习热情可能会大打折扣。
为了帮助大家更好的运用前面所学的理论知识,提高自身的代码能力,以及帮助大家重拾对编程的兴趣,今天我们将分享一份干货内容——贪吃蛇实战项目程序编写。
为了很好的完成这个项目的编写以及对各个知识点的理解,我们需要先具备一下知识点的储备:
这些知识点除了WIN32 API对我们目前来说比较陌生以外,其它的内容我相信大部分朋友都已经没什么问题了。想要完成贪吃蛇游戏能够在Windows系统下的运行,WIN32 API的知识点是必不可少的,因此我们会在今天的篇章中详细介绍相关的知识点。
既然要学习WIN32 API的相关内容,那么我们就需要先了解什么是WIN32 API;
Windows作为一个多作业操作系统,它除了能够协调应用程序的执行、内存的分配、以及资源的管理之外,它同时还是一个很大的服务中心。
调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、秒回图形、使用周边设备等目的。
由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programing Interface,简称API函数。
WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。
简单的理解就是我们通过调用WIN32 API的各个函数,就能够将对应的程序在Windows32位平台下正常运行。 这里要注意一定是Windows环境下,对应的程序不能再Linux环境下使用。
相信大家现在看完这些介绍还是有一点似懂非懂的状态,没关系,我们现在只需要知道一件事——我们今天要学习的是如何调用WIN32 API中的各个函数。
在介绍完WIN32 API后,我们还需要介绍一个非常关键的内容——控制台。
那什么是控制台呢?
其实控制台就是我们平时在运行VS时生成的一个运行窗口,如下所示:
其实Windows也有自己的控制台,我们可以通过win + R
打开运行窗口,再输入cmd并点击确认打开控制台,如下所示:
在完成上述操作后,我们将会得到一个黑框框,如下所示:
这就是Windows下的控制台,我们可以在这个控制台中输入Windows下对应的指令来完成相应的操作。
如果我要修改控制台的大小,我就可以通过mode
指令来进行修改,这个指令的用法如下所示:
这里的介绍我们简单点理解就是可以通过mode con cols=c lines=c
来修改控制台的窗口大小,这里的c指的是一个整型常量,不能是浮点型,也不能是字符型,如下所示:
所以如果我们要将窗口大小设置为30行,50列时,我们就可以通过mode con cols=50 lines=30
来完成设置,如下所示:
在完成输入后,我们通过回车进行确认,就能得到修改之后的控制台窗口,如下所示:
可以看到,因为控制台窗口默认值是25行,80列,我们进行修改之后的控制台比原先的控制台要长度要小一点,宽度要大一点。
知道了如何修改控制台的大小就,就意味着我们可以在程序运行后得到一个我们需要的指定大小的运行窗口,但是我们在程序运行后还等通过窗口的标题知道我们运行的是什么程序才行呀!Windows中同样也提供了一个指令——title指令,具体的用法如下所示:
从介绍中我们可以看到,我们只需要在这个指令后面加上我们需要修改的标题内容即可,如下所示:
现在修改前的控制台窗口的标题为C:\Windows……,当我们通过title指令修改后,结果如下所示:
可以看到此时的控制台窗口的标题就已经被修改了。
刚才上述的操作都是在Windows的控制台下完成的,那我们应该如何在VS中来调用这些指令呢?这里就需要借助C语言提供的一个库函数——system函数。函数的介绍如下所示:
这里的介绍很多,我们只需要提取几个关键信息就行:
<stdlib.h>
简单的理解就是我们可以通过system这个函数来调用当前操作系统下的对应的指令。
就比如我想在VS中来修改控制台的窗口大小以及控制台的标题,那我们就可以通过system这个函数来进行对应的指令的调用,为了方便大家看到这个过程,我们通过监视窗口来观察一下不同指令的执行结果:
此时我们刚刚开始运行程序,可以看到此时的控制台窗口很大,我们先通过调用mode指令来修改窗口大小,如下所示:
可以看到在执行完system函数后,窗口就变小了很多,下面我们再修改一下窗口的标题,如下所示:
可以看到,在执行完system函数后,标题也成功的被修改了,因此我们是可以通过system函数来调用Windows的相关指令的。
这时可能有朋友在尝试过后会发现标题并未被修改,如下所示:
为什么会这样呢?
这是因为此时我们的程序已经运行结束了,因此,对应的控制台标题就变成了结束后的内容了。那我们应该如何解决呢?
其实在上述的调试过程中我们之所以能发现它修改的过程,这是因为我们是在一步一步的运行,也就是说,我只要让程序在修改完标题后能够停下来,不是直接介绍,那是不是就能看到它的修改过程了呢?
这里让程序停下来的方式有很多,比如通过Sleep函数、通过输入函数……但是这里我要介绍的是Windows系统下的一个暂停指令——pause。我们先来看一下它的用法:
它的用法很简单,只需要在命令行输入该指令即可完成暂停,如下所示:
可以看到在Windows的控制台窗口中它的标题上会显示pause
,屏幕缓冲区中会显示请按任意键继续 ...
,下面我们再通过system函数在VS中调用一下这个指令看看会是什么结果:
可以看到,在VS的控制台窗口中,它同样显示的是请按任意键继续 ...
,只不过在VS中它的控制台标题并未显示pause。下面我们在再看一下按下任意键后它们两个控制台又会有什么样的表现:
通过对比我们可以看到,因为此时我们在VS中测试的程序已经结束,所以VS的窗口标题是显示的结束后的标题,但是在Windows的控制台下,我们是可以继续进行操作的,所以标题任然是贪吃蛇。
到这里对于控制台窗口的操作我们就介绍完了,我们这一部分了解到了4个操作:
下面我们就要开始介绍WIN32 API中的各个函数的使用了,这里要注意的是,如果我们需要使用Windows中的函数的话,那我们需要引用头文件<Windows.h>
。我们接着往下看:
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
函数的具体介绍如下所示:
从这个介绍中我们可以得到以下信息:
HANDLE WINAPI GetStdHandle(
_In_ DWORD nStdHandle
);
从函数的原型中我们可以看到函数的返回类型是HANDLE
类型;
_In_DWORD
类型的参数,这个参数类型是什么我们目前可以不需要深究,我们只需要知道这个函数的参数有三种:STD_INPUT_HANDLE——标准输入设备。 最初,这是输入缓冲区 CONIN$ 的控制台。
STD_OUTPUT_HANDLE——标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$。
STD_ERROR_HANDLE——标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$。
这个函数我们可以简单的理解为它是用来操作输入缓冲区和控制台屏幕缓冲区的,所谓的控制台屏幕缓冲区就是控制台中的黑色部分。
这个函数的使用就比较简单,我们只需要通过HANDLE创建一个变量,再通过这个变量来接收函数的参数即可,如下所示:
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
现在我们对函数传入的参数为输出设备,这里我们可以认为就是屏幕缓冲区,接下来我们就可以通过handle来对屏幕缓冲区中的内容进行一系列的操作了。
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
我们先来看一下这个函数的详细介绍:
从这个介绍中我们我可以得到以下信息:
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
从函数的原型中我们可以看到这个函数的返回类型是布尔类型;
_In_ HANDLE
_Out_ PCONSOLE_CURSOR_INFO
第一个类型是不是有点熟悉,没错就是我们前面介绍的HANDLE
类型,因此我们能够确定是的这个函数需要我们将前面创建好的变量作为参数传入,并且这个函数指定的是屏幕缓冲区;第二个类型是_Out_ PCONSOLE_CURSOR_INFO
,从介绍中我们可以看到它实质上是一个指针类型,而且还是一个结构体指针,指向的是CONSOLE_CURSOR_INFO
这个结构体,这个结构体具体是什么,我们暂时还不太清楚;
这个函数具体是怎么用的貌似我们还是不太清楚,为了更加详细的了解这个函数,我们需要来了解一下CONSOLE_CURSOR_INFO
这个结构体,下面我们一起来看一下,它具体是一个什么结构体;
我们先来看一下这个结构体的介绍:
从介绍中我们可以看到,这个结构体的原型是:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
在这个结构体中,它有两个成员,一个是DWORD
类型的dwSize
,一个是布尔类型的bVisible
,这两个类型它们分别控制的是游标的百分比以及游标的可见性。
现在问题来了,这个游标是个什么东西呢?
我们可以简单的理解就是控制台中一直在闪烁的一个小的横线,如下所示:
这个游标我们也可以把它称为光标,那也就是说这个结构体实际上是来描述这个光标的比列以及可见性的。从描述中我们可以看到,如果光标是可见的,那么它的值就是true,也就是1。
也就是说GetConsoleCursorInfo
这个函数是用来查找光标的相关信息的,检索的位置是在屏幕缓冲区内,函数会将查找好的光标信息放入结构体变量中,我们在进行结构体变量传参时,需要传入结构体指针变量。下面我们就来测试一下;
通过上面的介绍我们可以得到结论,GetStdHandle
与GetConsoleCursorInfo
这两个函数是需要配合使用的,因此我们可以尝试着编写下列代码:
//win32API函数测试
void test4() {
//GetStdHandle函数测试
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
//CONSOLE_CURSOR_INFO结构
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
//获取屏幕缓冲区中的光标属性
GetConsoleCursorInfo(handle, &cursor_info);
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
}
我们来看看测试结果如何:
当我们将结构体变量初始化时,结构体中的两个成员的值都为0,当时当我们获取光标的相关信息后我们发现,控制光标百分比的成员的值变为了25,而控制光标可见性的值变为了1,也就是说此时的光标占完整光标的25%,此时的光标是可见的。
那我们可不可以设置光标为100%或者让光标不可见呢?我们接着往下看;
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
我们先来看一下函数的详细介绍:
从介绍中我们可以得到以下信息:
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
从函数的原型中我们可以看到这个函数的返回类型是布尔类型
HANDLE
const CONSOLE_CURSOR_INFO*
也就是和获取光标信息的函数参数一样,都是输出设备以及结构体指针,只不过这个函数的结构体指针是不可修改的结构体指针;
这个函数简单的理解就是可以修改光标的相关信息,比如光标的百分比或者光标的可见性,修改成功后返回true,失败则返回false;
在了解了这个函数后,我们就可以使用一下这个函数来修改相关的操作,代码如下所示:
//win32API函数测试
void test4() {
//GetStdHandle函数测试
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
//CONSOLE_CURSOR_INFO结构
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
system("pause");
//获取屏幕缓冲区中的光标属性
GetConsoleCursorInfo(handle, &cursor_info);
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
system("pause");
//修改光标的百分比
cursor_info.dwSize = 100;
SetConsoleCursorInfo(handle, &cursor_info);
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
system("pause");
//修改光标的可见性
cursor_info.bVisible = false;
SetConsoleCursorInfo(handle, &cursor_info);
//光标的属性打印
printf("dwSize = %d\nbVisible = %d\n", cursor_info.dwSize, cursor_info.bVisible);
system("pause");
}
为了更好的观察,我们通过暂停指令来看一下经过不同的操作后会发生什么光标会发生什么变化:
现在第一次暂停时,我们还在初始化阶段,此时并未获取光标信息,所以光标虽然是可见的但是结构体成员的值都为0;
此时我们完成了光标信息的获取,可以看到,现在的光标大小占总大小的25%,光标是可见的;
可以看到经过函数的修改后此时的光标变大了,因为我们只修改了光标比列,因此光标此时依然是可见的;
在修改完光标的可见性之后我们可以看到,此时的屏幕中已经无法看到光标了。
经过上面的介绍,相信大家对光标的信息获取与大小以及可见性的修改已经没问题了,我们来思考一下,如果我要写一个贪吃蛇的话我希望相关的内容都能在屏幕中央显示,或者说在屏幕的其它地方显示,不要在左上角显示,这个能不能做到呢?我们继续往下看;
如果我们想修改光标出现的位置的话,那我们就需要先了解一下屏幕的坐标——COORD
;
我们先来看一下COORD
的介绍:
从介绍中可以看到,COORD
实际上也是一个结构体,这个结构体是用来描述控制台屏幕缓冲区中字符单元的坐标的;
坐标系的原点(0,0)位于缓冲区的顶部左侧单元格,也就是屏幕左上角;
结构体中的两个成员都是short
类型,并且X表示的是水平坐标也就是列值,Y表示的是垂直坐标也就是行值,具体的单位取决于函数的调用;
通过这些信息,我们就可以将控制台看做一个坐标系,如下所示:
既然这个结构体描述的是字符单元的坐标,也就是说如果我们将其看做一个一个小格子的话,那么X轴表示的就是一个字符的宽度,而Y轴表示的是一个字符的高度,那么我们就可以得到对应的网格坐标系,如下图所示:
看到这两张图,相信大家都应该能够理解COORD
这个结构体了,那是不是说我只要修改了对应的对标值,就能改变光标的位置呢?下面我们可以测试一下:
从输出结果来看,我们貌似并没有完成对光标位置的修改,那就说明只靠结构体变量是无法进行光标位置的修改的,那我们应该怎么办才能修改光标位置呢?我们接着往下看;
设置指定控制台屏幕缓冲区中的光标位置。
我们先来看一下这个函数的详细介绍:
从这个介绍中我们可以得到以下信息:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
可以看到这个函数也是一个布尔类型的函数;
HANDLE
COORD
也就是说这个函数的参数分别是指定的设备以及对应的坐标:
COORD
定义的结构体变量中赋值的坐标,该坐标需要再屏幕缓冲区的坐标范围之内;那也就是说,我们只有通过结构体变量确定好坐标后,再通过这个函数将指定的对象设置在对应的起点放在对应的坐标位置。那具体是不是这样呢?还需要我们进一步的验证;
为了验证我们的猜想,下面我们通过printf
函数来观察一下这个函数的使用,代码如下:
//WIN32API函数测试2
void test5() {
//COORD结构体测试
COORD pos = { 15,20 };
//输出呵呵
printf("1.hehe\n");
//将光标的横坐标设置为15,纵坐标设置为20
system("pause");
//确定指定设备
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
//设置坐标位置信息
SetConsoleCursorPosition(handle, pos);
//输出呵呵
printf("2.hehe\n");
system("pause");
//更改位置信息
pos.X = 10, pos.Y = 10;
//设置坐标位置信息
SetConsoleCursorPosition(handle, pos);
//输出呵呵
printf("3.hehe\n");
system("pause");
}
下面我们来测试一下,看看两次更改后的结果如何:
从3次呵呵的打印位置可知,SetConsoleCursorPosition
这个函数确实是用来设置光标的起始位置的,我们在通过这个函数设置好光标的起始位置之后,需要输出的信息就会从设置的坐标处开始进行输出。
不知道有没有朋友注意到一个点,我们在输出时会发现一个数字加上一个标点符号也就是两个字符的宽度才是一个汉字的宽度。在今天的内容中我们就不展开讨论了,在下一个篇章中我们再好好的探讨一下;
有朋友可能会说,如果我想获取坐标的位置信息我又该如何操作呢?在函数的介绍中给了我们明确的回复:要确定光标的当前位置,请使用 GetConsoleScreenBufferInfo
函数。
也就是说GetConsoleScreenBufferInfo
这个函数是专门用来获取光标的位置信息的,因为咱们本次的贪吃蛇游戏编写中不会涉及这个函数,因此这里我就不继续展开了,以后有机会我们再来分享;
在介绍完了控制台窗口的设置、光标的设置以及光标位置的设置之后,接下来我们要介绍一个非常重要的API函数——GetAsyncKeyState
——确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState
后是否按下了该键。可以简单的理解就是用来检测键盘的各个按键的使用情况。
我们先来看一下它的函数原型:
SHORT GetAsyncKeyState(
[in] int vKey
);
可以看到这个函数的返回类型是short
类型,函数的参数是整型,这里的vKey
表示的是虚拟键码。所谓的虚拟键码指的是键盘上看到的各个按键的一个虚拟整型值,这里我们主要介绍以下几个虚拟键码:
这里代表的分别是键盘上的ESC键、空格键以及方向键,我们可以通过给函数传入相应的虚拟键码来完成相应的操作,比如我们通过方向键来控制蛇的移动,通过空格键来暂停游戏,通过ESC键来退出游戏等等,具体操作的实现到后面我们会详细介绍。我们来继续看这个函数的介绍:
这里的函数介绍感觉不是那么好理解,我们可以简单的理解为该函数返回的是一个短整型的值,如果该值的二进制位最高位为1则表示当前的键是按下的,如果为0则表示当前的键是抬起的,也就是没有按下; 如果最低位的值为1则表示当前的键在上一次调用该函数后有使用过,如果为0则表示未被使用;
因此我们可以设想一下,如果我要通过这个函数来控制蛇的移动,那我是不是只需要监测方向键是否又被使用过,如果有使用,那就移动蛇,如果未被使用,那就不做任何操作。
那我们有应该如何使用这个函数呢?
这个函数的使用我们需要借助预处理指令来完成。
&
操作符来完成,如:GetAsyncKeyState(VK) & 1
;GetAsyncKeyState(VK) & 1? 1 : 0
;#define
定义一个宏用来监测按键是否又被使用过,如下所示:#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 1? 1 : 0)
这样我们在函数中只需要使用这个宏就能根据宏的结果来判断按键是否又被使用过,如下所示:
可以看到,此时我们可以通过这个宏来检测方向键左键在上一次调用前有没有被使用过。
在今天的篇章中我们详细介绍了需要实现贪吃蛇项目的话需要掌握的Win32 API中的部分指令与函数:
到这里咱们今天的内容就全部介绍完了,希望这篇内容能够帮助大家学习并理解WIN32 API中的这些指令与函数的使用,最后感谢大家的翻阅,咱们下一篇再见!!!