C语言项目扫雷-鼠标版
Posted Fly-bit
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言项目扫雷-鼠标版相关的知识,希望对你有一定的参考价值。
前言
你的「扫雷」💣 还不能用「鼠标」玩❓ 还在输入「坐标」❓ 已经都 「2021」了 ❗️ 谁还会玩你的「扫雷」呢❓
❤️「看过来」❤️,本文将 手把手 💪带你写出「新的扫雷」😎
提示:该项目基于C语言编写,由 VisualStudio 2019 所实现
注:游戏中所有操作均通过鼠标实现
一、游戏效果展示:
下图以三倍速展示
二、界面函数
若想实现控制台的基本操作,需先了解以下内容
注:下列函数,均须引用头文件 <Windows.h>
2.0 句柄
要想对界面进行一系列的「操作」,则离不开「句柄」这一重要概念。「句柄」是Windows最常用的概念。它通常用来标识Windows资源(如菜单、图标、窗口等)和设备等对象。虽然可以把句柄理解为是一个指针变量类型,但它不是对象所在的地址指针,而是作为Windows系统内部表的索引值来使用的。
其声明为:
typedef void *HANDLE;
从上面可以看出,句柄「 HANDLE 」是一个无类型指针。
参考代码:
HANDLE out_put = NULL;
点此深入了解👉深入了解Windows句柄到底是什么
2.0.1 GetStdHandle() 函数
函数结构:
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);
功能: | 获取指定标准设备的句柄(标准输入,标准输出或标准错误) |
---|---|
参数:nStdHandle | 标准设备。此参数可以是以下值之一。 |
STD_INPUT_HANDLE | (DWORD)-10 标准输入设备。 |
STD_OUTPUT_HANDLE | (DWORD)-11 标准输出设备。 |
STD_ERROR_HANDLE | (DWORD)-12 标准错误设备。 |
返回值: | 不同情况下,返回值有以下三种情况 |
指定设备的句柄 | 函数成功 |
INVALID_HANDLE_VALUE | 函数失败 |
NULL | 无相关句柄 |
注意:
该函数仅有上述三种参数。默认情况下,标准输出句柄和标准错误句柄都是对应的屏幕(显示器)
参考代码:
//定义句柄
HANDLE out_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
2.1 控制台
2.1.1 COORD结构体
若要进行相关窗口操作,则必须先了解 「COORD」结构,结构如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
说明:
X | 水平坐标或列值 |
---|---|
Y | 垂直坐标或行值 |
注:单位取决于函数调用
2.1.2 SMALL_RECT 结构
结构如下:
typedef struct _SMALL_RECT {
SHORT Left;
SHORT Top;
SHORT Right;
SHORT Bottom;
} SMALL_RECT;
结构说明:
功能 | 定义矩形的左上角和右下角的坐标 |
---|---|
Left | 矩形左上角的x坐标 |
Top | 矩形左上角的y坐标 |
Right | 矩形右下角的x坐标 |
Bottom | 矩形右下角的y坐标 |
2.1.3 SetConsoleTitle() 函数
函数结构:
BOOL WINAPI SetConsoleTitle(_In_ LPCTSTR lpConsoleTitle);
功能 | 设置当前控制台窗口标题 |
---|---|
参数:lpConsoleTitle | 要在控制台窗口的标题栏中显示的字符串 |
返回值 | 如果函数成功,则返回值为非零值。反之为0 |
参考代码:
SetConsoleTitle("扫雷");
注意:在实际使用过程中,出现了控制台乱码的情况
解决方法:右击 当前解决方案👉 属性 👉 配置属性 👉 高级 👉 字符集 👉 使用更多字符集
点击了解更多👉官方参考网址
2.1.4 SetConsoleWindowInfo() 函数
函数结构:
BOOL WINAPI SetConsoleWindowInfo(
_In_ HANDLE hConsoleOutput,
_In_ BOOL bAbsolute,
_In_ const SMALL_RECT *lpConsoleWindow
);
功能 | 设置控制台屏幕缓冲区窗口的当前大小和位置 |
---|---|
参数 | 参数含义 |
hConsoleOutput | 可理解为标准输出句柄 |
bAbsolute | 如果此参数为TRUE,则坐标指定窗口的新左上角和右下角。如果为FALSE,则坐标相对于当前窗口角坐标 |
lpConsoleWindow | 指向SMALL_RECT结构的指针,该结构指定窗口的新左上角和右下角 |
返回值 | 如果函数成功,则返回值为非零值。反之为0 |
点击了解更多👉官方参考网址
2.2 隐藏光标
扫雷过程中,为使界面更加美观,隐藏光标是必须的。在使用相关函数前,让我们先了解下列结构
2.2.1 CONSOLE_CURSOR_INFO 结构
结构说明:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
参数 | 参数含义 |
---|---|
dwSize | 光标填充的字符单元格的百分比。通常该值介于1和100之间。光标外观会发生变化,从完全填充单元格到显示为单元格底部的水平线。 |
bVisible | 该参数为bool 类型,表示光标的可见性。如果光标可见,则此成员为TRUE,反之为 FALSE |
2.2.2 GetConsoleCursorInfo() 函数
函数结构:
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
函数解释:
功能: | 获取光标相关信息 |
---|---|
参数: | 参数解释 |
hConsoleOutput | 控制台屏幕缓冲区的句柄,且该句柄必须具有GENERIC_READ访问权限。 |
lpConsoleCursorInfo | 指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关控制台光标的信息。 |
返回值 | 成功返回非0,反之为0 |
参考代码:
//定义句柄
HANDLE out_put;
//获取标准输出句柄
//注意:获取的为标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
//创建光标信息
CONSOLE_CURSOR_INFO cursor;
//隐藏光标
cursor.bVisible = false;
cursor.dwSize = 1;
SetConsoleCursorInfo(out_put, &cursor);
点击了解更多👉官方参考网址
2.3 光标跳转
若想指定文本输出位置,可以通过函数 GetConsoleScreenBufferInfo() 与 FillConsoleOutputCharacter() 实现指定位置填充,但上述函数结构复杂,因此本文通过「 光标跳转」来实现。
2.3.1 SetConsoleCursorPosition() 函数
函数结构:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
函数解释:
功能 | 设置指定控制台屏幕缓冲区中的光标位置 |
---|---|
参数 | 参数解释 |
hConsoleOutput | 控制台屏幕缓冲区的句柄,标准输出句柄即可 |
dwCursorPosition | COORD 结构,用于指定新的光标位置(以字符为单位) |
返回值 | 如果函数成功,则返回值为非零值。 |
参考代码:
//定义句柄
HANDLE out_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
//用于存储鼠标当前坐标
COORD pos = { 5,5 };
SetConsoleCursorPosition(out_put, pos);
printf("当前鼠标位置X:%d Y:%d",pos.X,pos.Y);
pos.X = 10;
pos.Y = 10;
SetConsoleCursorPosition(out_put, pos);
printf("当前鼠标位置X:%d Y:%d",pos.X,pos.Y);
结果如下:
点击了解更多👉官方参考网址
2.4 文本颜色函数
若要实现前文游戏界面,用不同颜色区分「 边界 」和「 雷区 」,则需使用 相关函数来实现。
2.4.1 SetConsoleTextAttribute() 函数
函数结构:
BOOL WINAPI SetConsoleTextAttribute(HANDLE hConsoleOutput, WORD wAttributes);
函数解释:
功能 | 设置控制台文本属性(颜色),可以设置前景色FOREGROUND(文本颜色)和背景色BACKGROUND |
---|---|
参数 | 参数解释 |
hConsoleOutput | 控制台屏幕缓冲区的句柄。此处获取标准输出句柄即可 |
wAttributes | 字符属性 |
返回值 | 如果函数成功,则返回值为非零值。 |
点击了解更多👉官方参考网址
2.4.2 字符属性
字符属性可以分为两类:颜色和DBCS
字符属性 | 含义 |
---|---|
FOREGROUND_BLUE | 文字颜色包含蓝色 |
FOREGROUND_GREEN | 文字颜色包含绿色 |
FOREGROUND_RED | 文字颜色包含红色 |
FOREGROUND_INTENSITY | 文字颜色加强 |
BACKGROUND_BLUE | 背景颜色包含蓝色 |
BACKGROUND_GREEN | 背景颜色包含绿色 |
BACKGROUND_RED | 背景颜色包含红色 |
BACKGROUND_INTENSITY | 背景颜色加剧 |
COMMON_LVB_LEADING_BYTE | 前导字节 |
COMMON_LVB_TRAILING_BYTE | 尾随字节 |
COMMON_LVB_GRID_HORIZONTAL | 顶部水平 |
COMMON_LVB_GRID_LVERTICAL | 左垂直 |
COMMON_LVB_GRID_RVERTICAL | 正确的垂直 |
COMMON_LVB_REVERSE_VIDEO | 反转前景和背景属性 |
COMMON_LVB_UNDERSCORE | 下划线 |
2.4.3 颜色对照表
除上述以外,还有以下颜色可供选择
2.4.4 参考代码
//定义句柄
HANDLE out_put, in_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
for (int i = 0; i < 7; i++)
{
SetConsoleTextAttribute(out_put, 144 + 15 * i);
printf("第%d次打印\\n", i);
}
SetConsoleTextAttribute(out_put, FOREGROUND_INTENSITY);
SetConsoleTextAttribute(out_put, FOREGROUND_BLUE);
printf("第7次打印\\n");
SetConsoleTextAttribute(out_put, BACKGROUND_GREEN);
printf("第8次打印\\n");
打印结果如下:
由此可见, SetConsoleTextAttribute() 函数的使用仅此而已。
三、鼠标事件
仅仅掌握上述函数,只能美化你的界面,未能达到 「 鼠标操作 」 的目的。因此,还需学习以下内容
3.1 INPUT_RECORD结构
功能:描述控制台输入缓冲区中的输入事件
结构说明:
typedef struct _INPUT_RECORD {
WORD EventType;
union {
KEY_EVENT_RECORD KeyEvent;
MOUSE_EVENT_RECORD MouseEvent;
WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
MENU_EVENT_RECORD MenuEvent;
FOCUS_EVENT_RECORD FocusEvent;
} Event;
} INPUT_RECORD;
结构解释:
EventType | 输入事件类型的句柄和存储在Event成员中的事件记录。 |
---|---|
值 | 含义 |
FOCUS_EVENT | 该事件成员包含一个FOCUS_EVENT_RECORD结构 |
KEY_EVENT | 该事件成员包含一个KEY_EVENT_RECORD结构有关键盘事件的信息 |
MENU_EVENT | 该事件成员包含一个MENU_EVENT_RECORD结构 |
MOUSE_EVENT | 所述事件构件包含MOUSE_EVENT_RECORD结构用约鼠标移动或按键按压事件的信息 |
WINDOW_BUFFER_SIZE_EVENT | 该事件成员包含一个WINDOW_BUFFER_SIZE_RECORD结构有关控制台屏幕缓冲区的新大小信息 |
3.2 MOUSE_EVENT_RECORD 结构
由于本文仅用鼠标操作,故仅介绍该结构
结构说明:
typedef struct _MOUSE_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;
结构解释:
参数 | 参数含义 |
---|---|
dwMousePosition | COORD 结构,用来记录光标位置 |
dwButtonState | 鼠标按键的状态 |
值 | 含义 |
FROM_LEFT_1ST_BUTTON_PRESSED | 鼠标左键 |
RIGHTMOST_BUTTON_PRESSED | 鼠标右键 |
FROM_LEFT_2ND_BUTTON_PRESSED | 鼠标滚轮 |
FROM_LEFT_3RD_BUTTON_PRESSED | 鼠标左起第三个按键(前进键) |
FROM_LEFT_4TH_BUTTON_PRESSED | 鼠标左起第四个按键(后退键) |
dwControlKeyState | 控制键状态(因本文用不到,故不在此展开) |
wEventFlags | 鼠标事件类型 |
值 | 含义 |
DOUBLE_CLICK | 双击的第二次单击发生,第一次单击作为常规按钮事件返回 |
MOUSE_MOVED | 鼠标位置发生变化 |
注:上述仅提供了本项目可能用到的值。
3.3 ReadConsoleInput() 函数
函数结构:
BOOL WINAPI ReadConsoleInput(
_In_ HANDLE hConsoleInput,
_Out_ PINPUT_RECORD lpBuffer,
_In_ DWORD nLength,
_Out_ LPDWORD lpNumberOfEventsRead
);
结构说明:
功能 | 从缓冲区读取数据并删除 |
---|---|
参数 | 含义 |
hConsoleInput | 标准输入句柄 |
lpBuffer | 指向INPUT_RECORD 结构的指针 |
nLength | lpBuffer参数指向的数组的大小,以数组元素为单位。 |
lpNumberOfEventsRead | 指向LPDWORD 结构的指针,该结构用来存储读取记录 |
点击了解更多👉官方参考网址
3.4 参考示例
//定义句柄
HANDLE in_put;
//获取标准输入句柄
in_put = GetStdHandle(STD_INPUT_HANDLE);
//用于存储鼠标当前坐标
COORD pos = { 0,0 };
//定义输入事件结构体
INPUT_RECORD mouse_record;
//用于存储读取记录
DWORD res;
//Game();
while (1)
{
//读取输入事件
ReadConsoleInput(in_put, &mouse_record, 1, &res);
if (mouse_record.EventType == MOUSE_EVENT)
{
//单击鼠标右键
if (mouse_record.Event.MouseEvent.dwButtonState == RIGHTMOST_BUTTON_PRESSED )
printf("单击右键\\n");
//双击
if (mouse_record.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
{
printf("双击\\n");
break;
}
//单击鼠标左键
else if (mouse_record.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
printf("单击左键\\n");
}
}
CloseHandle(in_put);
注:事实上,运行起来并不能直接实现鼠标操作,而需要 「 二次运行 」方可。即,出现图一后,需将光标置于文档中,再次按下 Ctrl + F5,出现图二「 cmd.exe 控制台 」之后进行如下设置,重新打开「 cmd.exe 控制台 」即可
四、游戏代码剖析
框架构建
首先我们需预设「 窗口大小 」、「 雷场大小、坐标」,「 雷的个数 」,为方便后期重新设置、增加代码可读性,在此我们进行宏定义
//设置雷区行数
#define ROW 10
//设置雷区列数
#define COL 10
//设置雷的个数
#define NUM 10
//设置窗口大小
#define WIDTH 50
#define HEIGHT 25
//雷场起始坐标定义
COORD pos_field;
游戏主体函数
首先确定雷区起始坐标,为使界面美观,在此定义 Y 为窗口高的三分之一
创建一个二维数组,用来存储雷场信息,事实上,该二维数组大小创建为 [ROW + 2] [COL + 2]更有利于后期展开,在此为设置成如此,是为了后期鼠标坐标和对应数组坐标的一致性
void Game()
{
//用于存储雷区起始坐标
COORD pos_field = { (WIDTH - (2 + ROW) * 2) / 2 ,(HEIGHT - COL - 2) / 3 - 1 };
pos_field.X = (pos_field.X % 2) == 0 ? pos_field.X : pos_field.X - 1;
//定义雷区
int arr[ROW][COL];
int i;
do
{
//system("CLS");
//界面初始化
InitiaInterface(pos_field);
//布置雷
PlaceMines(arr);
//排雷
MineClearance(arr,pos_field);
//游戏结束,玩家选择
i = ChoiceGet(pos_field) - 2;
} while (i);
//定义句柄
HANDLE out_put, in_put;
//获取标准输出、输入句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
in_put = GetStdHandle(STD_INPUT_HANDLE);
//关闭句柄
CloseHandle(out_put);
CloseHandle(in_put);
}
界面初始化
界面初始化,需要完成「 边界 」、「 雷场 」、「 提示区 」的打印,以及「 光标隐藏 」、「 设置窗口大小 」等操作
值得注意的是:
1.在 cmd 窗口中一个「 方块或中文 」占两个单位的横坐标,一个单位的纵坐标
2.打印雷场时,光标应一次跳 2个单位
为使界面更加美观,应合理安排雷区起始位置,故提前设置好雷区起始坐标(非边界起始坐标)
void InitiaInterface(COORD pos_field)
{
//定义句柄
HANDLE out_put, in_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
//获取标准输入句柄
in_put = GetStdHandle(STD_INPUT_HANDLE);
//设置窗口标题
SetConsoleTitle("扫雷");
//用于存储鼠标当前坐标
COORD pos = { 0,0 };
//创建光标信息
CONSOLE_CURSOR_INFO cursor;
//隐藏光标
cursor.bVisible = false;
cursor.dwSize = 1;
SetConsoleCursorInfo(out_put, &cursor);
//设置控制台屏幕缓冲区窗口的当前大小和位置
SMALL_RECT rect = { 0, 0, WIDTH , HEIGHT };
SetConsoleWindowInfo(out_put, 1, &rect);
//打印边界、雷场
for (int j = 0; j < COL + 2; j++)
{
for (int i = 0; i < ROW + 2; i++)
{
//设置光标位置
pos.X = pos_field.X - 2 + 2 * i;
pos.Y = pos_field.Y + j - 1;
SetConsoleCursorPosition(out_put, pos);
//边界设置为绿色,雷区黄色,以便区分
if (i == 0 || i == ROW + 1 || j == 0 || j == COL + 1)
color(10);
else
color(14);
printf("■");
}
}
//设置分割线
color(FOREGROUND_BLUE);
pos.X = 0;
pos.Y = pos_field.Y + COL + 2;
SetConsoleCursorPosition(out_put, pos);
for (int i = 0; i < WIDTH - 1; i++)
printf("-");
//设置左提示区
color(BACKGROUND_GREEN);
pos.X = (WIDTH / 2 - 5) / 2 ;
pos.Y += 3;
SetConsoleCursorPosition(out_put, pos);
printf("提示");
//提示信息
color(FOREGROUND_GREEN);
pos.X = (WIDTH / 2 - 13) / 2;
pos.Y++;
SetConsoleCursorPosition(out_put, pos);
printf("左键单击选择");
pos.Y++;
SetConsoleCursorPosition(out_put, pos);
printf("右击添加标记");
pos.Y++;
SetConsoleCursorPosition(out_put, pos);
printf("双击取消标记");
//设置右提示区
//打印边界
pos.X = WIDTH / 2;
pos.Y = pos_field.Y + COL + 3;
color(FOREGROUND_GREEN);
SetConsoleCursorPosition(out_put, pos);
for (int i = WIDTH / 2; i < WIDTH - 1; i++)
{
printf("-");
}
pos.Y++;
for (; pos.Y < HEIGHT - 1; pos.Y++)
{
SetConsoleCursorPosition(out_put, pos);
printf("|");
}
pos.X = WIDTH - 2;
pos.Y = pos_field.Y + COL + 4;
for (; pos.Y < HEIGHT - 2; pos.Y++)
{
SetConsoleCursorPosition(out_put, pos);
printf("|");
}
pos.X = WIDTH / 2;
SetConsoleCursorPosition(out_put, pos);
for (int i = WIDTH / 2; i < WIDTH - 1; i++)
{
printf("-");
}
}
为方便颜色设置,设置颜色函数
void color(int x)
{
HANDLE out_put = GetStdHandle(菜鸟也可轻松搞定!)C语言必做小项目---扫雷(简易版)