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控制台屏幕缓冲区的句柄,标准输出句柄即可
dwCursorPositionCOORD 结构,用于指定新的光标位置(以字符为单位)
返回值如果函数成功,则返回值为非零值。

参考代码:

//定义句柄
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;

结构解释:

参数参数含义
dwMousePositionCOORD 结构,用来记录光标位置
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 结构的指针
nLengthlpBuffer参数指向的数组的大小,以数组元素为单位。
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语言必做小项目---扫雷(简易版)

C语言实现简易版 扫雷 步骤及代码

C语言实现简易版 扫雷 步骤及代码

如何用C语言快速实现初级版扫雷(步骤详细)

C语言的扫雷简化版

C语言版扫雷(纯代码)