Win32 API 控制台鼠标操作、坐标获取与相关函数介绍
- 一、前置介绍
- 读取控制台输入缓冲区数据 ReadConsoleInput 函数
- 控制台输入缓冲区中的输入事件 INPUT_RECORD 结构
- 鼠标输入事件 MOUSE_EVENT_RECORD 结构
- 更改输入模式 SetConsoleMode 函数
- 二、鼠标坐标获取(以下代码环境为 VS2022 C语言)
- 鼠标输入模式更改
- 位段思想简单运用
- 将所有开关打开
- 检测是否打开开关
- 鼠标坐标获取代码参考
- 三、鼠标操作(以下代码环境为 VS2022 C语言)
- 四、注意事项
一、前置介绍
使用鼠标操作前需要简单介绍 Win32 API 的一些函数。
读取控制台输入缓冲区数据 ReadConsoleInput 函数
从控制台输入缓冲区读取数据,并将其从缓冲区删除。
BOOL WINAPI ReadConsoleInput(
_In_ HANDLE hConsoleInput, // 句柄
_Out_ PINPUT_RECORD lpBuffer, // 输出的信息
_In_ DWORD nLength, // lpBuffer 参数指向的数组大小(以数组元素表示)
_Out_ LPDWORD lpNumberOfEventsRead // 指向接收所读取输入记录数量的变量
);
详细信息请参考:ReadConsoleInput 函数
ReadConsoleInput 函数 用于读取操作,但是只能读取一种操作。例如读取的是鼠标操作,输入键盘操作时,接受鼠标操作的变量将置零。
-
第一个参数为输入句柄 关于句柄介绍可参考:GetStdHandle 函数
-
第二个参数为记录输入的信息 PINPUT_RECORD 结构体,下部分将会介绍。
-
后两个参数是 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 结构
-
MOUSE_EVENT_RECORD 结构体 记录了鼠标的位置 dwMousePosition,这个位置最小单位是控制台屏幕的一个字符距离,为 Windows 数据类型中的 COORD 类型。
-
dwButtonState 记录了鼠标的按钮,如鼠标左键和右键等。
-
dwControlKeyState 记录控制键的状态,这里忽略。
-
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);
- ENABLE_MOUSE_INPUT 表示将鼠标信息放置在输入缓冲区中,由 ReadConsoleInput 获取,官方解释为:
- 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 结构
这里只介绍两个数值:
- FROM_LEFT_1ST_BUTTON_PRESSED 0x0001,最左侧的鼠标按钮,检查它是否打开了需要获取 dwButtonState 变量的值,进行 & 运算。
- 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语言两种鼠标操作与扫雷实例
四、注意事项
- 使用 system(“cls”) 清屏会导致鼠标操作与坐标获取失效,这个原因暂时还不清楚,使用 它 时只需再次获取鼠标操作即可。
- 非鼠标操作会让鼠标操作短暂失效,如此时使用键盘敲击(键盘操作),ReadConsoleInput 函数读取输入缓冲区数据,但不是鼠标数据,在 其 读取下一段数据会有停滞时间,且会随输入的键盘数据次数延长。
- 我个人写了一份 Windows C++ 控制台的简易菜单库小项目,方便处理鼠标操作或者键盘操作,有兴趣的读者可以看看:Windows C++控制台菜单库开发与源码展示