Win32 API 控制台鼠标操作、坐标获取与相关函数介绍

一、前置介绍

使用鼠标操作前需要简单介绍 Win32 API 的一些函数。

读取控制台输入缓冲区数据 ReadConsoleInput 函数

从控制台输入缓冲区读取数据,并将其从缓冲区删除。

BOOL WINAPI ReadConsoleInput(
  _In_  HANDLE        hConsoleInput,			// 句柄
  _Out_ PINPUT_RECORD lpBuffer,					// 输出的信息
  _In_  DWORD         nLength,					// lpBuffer 参数指向的数组大小(以数组元素表示)
  _Out_ LPDWORD       lpNumberOfEventsRead		// 指向接收所读取输入记录数量的变量
);

详细信息请参考:ReadConsoleInput 函数

ReadConsoleInput 函数 用于读取操作,但是只能读取一种操作。例如读取的是鼠标操作,输入键盘操作时,接受鼠标操作的变量将置零。

  1. 第一个参数为输入句柄 关于句柄介绍可参考:GetStdHandle 函数

  2. 第二个参数为记录输入的信息 PINPUT_RECORD 结构体,下部分将会介绍。

  3. 后两个参数是 Windows 对内置类型封装的数据类型,这里不做介绍,有兴趣研究的读者我这里放上官方链接 Windows 数据类型

控制台输入缓冲区中的输入事件 INPUT_RECORD 结构

描述控制台输入缓冲区中的输入事件。 可以使用 ReadConsoleInput 或 PeekConsoleInput 函数从输入缓冲区读取这些记录,或使用 WriteConsoleInput 函数写入输入缓冲区。

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;

详细信息请参考:INPUT_RECORD 结构

INPUT_RECORD 结构体中记录操作使用了共用体(联合体),这也就是 ReadConsoleInput 函数只能读取一种操作的原因。

鼠标输入事件 MOUSE_EVENT_RECORD 结构

描述控制台 INPUT_RECORD 结构中的鼠标输入事件。

注意:
当控制台处于鼠标模式 (ENABLE_MOUSE_INPUT) 时,鼠标事件将放置在输入缓冲区中。

每当用户移动鼠标或者按下或释放鼠标按钮之一时,就会生成鼠标事件。 仅当控制台组具有键盘焦点且游标位于控制台窗口边框内时,鼠标事件才会放置在控制台的输入缓冲区中。

typedef struct _MOUSE_EVENT_RECORD {
  COORD dwMousePosition;	// 包含鼠标位置的 COORD 结构,以控制台屏幕缓冲区的字符单元坐标为单位。
  DWORD dwButtonState;		// 鼠标按钮的状态
  DWORD dwControlKeyState;	// 控制键的状态
  DWORD dwEventFlags;		// 鼠标事件的类型
} MOUSE_EVENT_RECORD;

详细信息请参考:MOUSE_EVENT_RECORD 结构

  1. MOUSE_EVENT_RECORD 结构体 记录了鼠标的位置 dwMousePosition,这个位置最小单位是控制台屏幕的一个字符距离,为 Windows 数据类型中的 COORD 类型。

  2. dwButtonState 记录了鼠标的按钮,如鼠标左键和右键等。

  3. dwControlKeyState 记录控制键的状态,这里忽略。

  4. dwEventFlags 记录鼠标事件类型,如 按下或释放了鼠标按钮、发生了鼠标位置更改、垂直鼠标滚轮已移动等等。

这里要注意,想要获取鼠标输入事件信息,必须要使用 SetConsoleMode 函数 更改输入模式。

更改输入模式 SetConsoleMode 函数

设置控制台输入缓冲区的输入模式或控制台屏幕缓冲区的输出模式。

BOOL WINAPI SetConsoleMode(
  _In_ HANDLE hConsoleHandle,	// 控制台输入缓冲区或控制台屏幕缓冲区的句柄
  _In_ DWORD  dwMode			// 要设置的输入或输出模式
);

详细信息请参考:SetConsoleMode 函数

SetConsoleMode 函数可以更改输入输出模式,这里只介绍鼠标输入模式。

二、鼠标坐标获取(以下代码环境为 VS2022 C语言)

鼠标输入模式更改

在 SetConsoleMode 中传入参数:

SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_EXTENDED_FLAGS | ENABLE_MOUSE_INPUT);
  1. ENABLE_MOUSE_INPUT 表示将鼠标信息放置在输入缓冲区中,由 ReadConsoleInput 获取,官方解释为:
    在这里插入图片描述
  2. ENABLE_EXTENDED_FLAGS 表示禁用鼠标选择和编辑文本,官方解释为:
    在这里插入图片描述
    注意,一定要禁用此模式也就是添加 ENABLE_EXTENDED_FLAGS,不然控制台鼠标坐标获取会异常,虽然我不知道原理是什么。

为什么要使用 ’ | ’ 在它俩中间,这实际上是借用了位段的思想,关于位段解释可参考这篇文章 (学习总结6)C语言结构体的内存对齐和位段实现

位段思想简单运用

一个操作是开启状态还是关闭状态,可以用 bool 类型记录,使用 1 字节存储,但只利用了一比特位表示。如果有一堆相关操作,每个用一字节(8个比特位)表示就太浪费空间了,使用 ’ | ’ ’ & ’ 两个按位操作符可以节省空间:

例如,现在有四个开关,鼠标操作开关、键盘操作开关、颜色显示开关、声音开关,若使用 bool 类型会使用 4 字节,这里我们使用位段思想处理只要 1 字节空间,但开关的数值应该为 2 的整数次幕

将所有开关打开
#include <stdio.h>

#define MOUSE_OPERATE 1
#define KEYBOARD_OPERATE 2
#define OPEN_COLOR 4
#define OPEN_SOUND 8

void test2()
{
	unsigned char operate = 0;		// 1 字节空间

	operate |= MOUSE_OPERATE;		// 等价 operate = operate | MOUSE_OPERATE;
	printf("%d\n", operate);		// 打印数值

	operate |= KEYBOARD_OPERATE;
	printf("%d\n", operate);

	operate |= OPEN_COLOR;
	printf("%d\n", operate);

	operate |= OPEN_SOUND;
	printf("%d\n", operate);

	// 将所有开关打开也可以直接表示为:
	//operate = MOUSE_OPERATE | KEYBOARD_OPERATE | OPEN_COLOR | OPEN_SOUND;
}

int main()
{
	test2();

	return 0;
}

在这里插入图片描述

检测是否打开开关

同理于 &,只需要 开关变量 & 具体开关的值 就可以检查是否打开了。

#include <stdio.h>

#define MOUSE_OPERATE 1
#define KEYBOARD_OPERATE 2
#define OPEN_COLOR 4
#define OPEN_SOUND 8

void test1()
{
	unsigned char operate = 0;					// 1 字节

	operate = MOUSE_OPERATE | OPEN_SOUND;		// 打开 鼠标操作 和 声音

												// 按位与操作检查是否打开对应开关
	printf("是否开启了鼠标操作:%d\n", operate & MOUSE_OPERATE);
	printf("是否开启了键盘操作:%d\n", operate & KEYBOARD_OPERATE);
	printf("是否开启了颜色显示:%d\n", operate & OPEN_COLOR);
	printf("是否开启了声音开关:%d\n", operate & OPEN_SOUND);
}

int main()
{
	//test2();
	test1();

	return 0;
}

在这里插入图片描述

鼠标坐标获取代码参考

知道方法后我们可以用变量记录鼠标坐标且打印在控制台上:

#include <stdio.h>
#include <stdbool.h>
#include <Windows.h>

void getMouseOperate()			// 鼠标操作获取
{
	SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_EXTENDED_FLAGS | ENABLE_MOUSE_INPUT);
}

void SetPos(short x, short y)	// 设置光标位置
{
	COORD pos = { x, y };
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

void HideCursor()				// 隐藏光标
{
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &CursorInfo);
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &CursorInfo);
}

void test()
{
	getMouseOperate();
	HideCursor();

	INPUT_RECORD mouse;
	DWORD ret;

	while (true)
	{
		ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &mouse, 1, &ret);

		int x = mouse.Event.MouseEvent.dwMousePosition.X;
		int y = mouse.Event.MouseEvent.dwMousePosition.Y;

		SetPos(0, 0);
		printf("                          ");
		SetPos(0, 0);
		printf("x = %d, y = %d", x, y);
	}
}

int main()
{
	test();

	return 0;
}

关于 设置光标位置隐藏光标 函数来源请参考:Win32 API 光标隐藏定位和键盘读取等常用函数

三、鼠标操作(以下代码环境为 VS2022 C语言)

鼠标操作的开关借用了位段思想,我们想要读取鼠标操作就需要找到 相应开关的数值或宏定义,这里再次放上官方链接:MOUSE_EVENT_RECORD 结构

这里只介绍两个数值:

  1. FROM_LEFT_1ST_BUTTON_PRESSED 0x0001,最左侧的鼠标按钮,检查它是否打开了需要获取 dwButtonState 变量的值,进行 & 运算。
    在这里插入图片描述
  2. MOUSE_MOVED 0x0001,鼠标位置更改,介绍它是防止用户鼠标移动过快,在没有选择区域快速点击时移动到选择区域导致的误操作。
    在这里插入图片描述

获得了鼠标坐标 和 鼠标操作,就可以制作简单的鼠标操作界面了,这里放上一个简易菜单,仅供参考:

#include <stdio.h>
#include <stdbool.h>
#include <Windows.h>

void getMouseOperate()			// 鼠标操作获取
{
	SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_EXTENDED_FLAGS | ENABLE_MOUSE_INPUT);
}

void SetPos(short x, short y)	// 输出光标位置
{
	COORD pos = { x, y };
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

void HideCursor()				// 隐藏光标
{
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &CursorInfo);
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &CursorInfo);
}

void menu1()
{
	getMouseOperate();
	HideCursor();

	INPUT_RECORD mouse;
	DWORD ret;

	int y = 0;
	int x = 0;
	int exitgame = 0;

	while (true)
	{
		SetPos(0, 0);

		//读取输入事件
		ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &mouse, 1, &ret);

		DWORD mouseButton = mouse.Event.MouseEvent.dwButtonState;	// 鼠标按键记录
		DWORD mouseFlag = mouse.Event.MouseEvent.dwEventFlags;		// 鼠标事件记录

		//获取鼠标当前位置
		y = mouse.Event.MouseEvent.dwMousePosition.Y;
		x = mouse.Event.MouseEvent.dwMousePosition.X;

		char arr[3][13] = { "开始游戏", "调整画面", "退出游戏" };
		printf("*************************\n");
		if ((y == 1) && (4 <= x && x <= 20))						// 鼠标坐标在这个范围就打印
		{
			printf("****\033[41m     %-10s  \033[0m****\n", &arr[0][0]);	// \033改变颜色
		}
		else
		{
			printf("****   %-12s  ****\n", &arr[0][0]);
		}

		if ((y == 2) && (4 <= x && x <= 20))
		{
			printf("****\033[41m     %-10s  \033[0m****\n", &arr[1][0]);
		}
		else
		{
			printf("****   %-12s  ****\n", &arr[1][0]);
		}

		if ((y == 3) && (4 <= x && x <= 20))
		{
			printf("****\033[41m     %-10s  \033[0m****\n", &arr[2][0]);
		}
		else
		{
			printf("****   %-12s  ****\n", &arr[2][0]);
		}

		printf("*************************\n");

		if ((mouseButton & FROM_LEFT_1ST_BUTTON_PRESSED)	// 按位与检查左键按下
		  && !(mouseFlag & MOUSE_MOVED))					// 按位与检查鼠标未移动
		{
			if ((y == 1) && (4 <= x && x <= 20))
			{
				system("cls");					// 先清屏
				//game();
				getMouseOperate();
			}
			if ((y == 2) && (4 <= x && x <= 20))
			{
				//getMouseOperate();
			}
			if ((y == 3) && (4 <= x && x <= 20))
			{
				system("cls");					// 先清屏
				printf("%s\n", "退出游戏");
				exitgame = 1;
			}
		}

		Sleep(10);				// 刷新频率
		if (exitgame)
		{
			break;
		}
	}
}

int main()
{
	menu1();

	return 0;
}

这段代码源自:C语言两种鼠标操作与扫雷实例

四、注意事项

  1. 使用 system(“cls”) 清屏会导致鼠标操作与坐标获取失效,这个原因暂时还不清楚,使用 它 时只需再次获取鼠标操作即可。
  2. 非鼠标操作会让鼠标操作短暂失效,如此时使用键盘敲击(键盘操作),ReadConsoleInput 函数读取输入缓冲区数据,但不是鼠标数据,在 其 读取下一段数据会有停滞时间,且会随输入的键盘数据次数延长。
  3. 我个人写了一份 Windows C++ 控制台的简易菜单库小项目,方便处理鼠标操作或者键盘操作,有兴趣的读者可以看看:Windows C++控制台菜单库开发与源码展示
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值