先上效果演示(点击播放此GIF大图):
没错,这就是CLI命令行程序,
相信老师在评分时,看厌了只能使用键盘按键来选择的程序后
看到这个定能眼前一亮吧!
(之所以用这个场景是因为实在想不到现在除了应付考试的学生还有谁会开发基于命令行窗口的程序了╮(╯_╰)╭)
废话不说直接上代码:
//#include <iostream> // 能用C++还是建议做个界面呗
#include <stdio.h>
#include <Windows.h> // 主角
//#include <string.h>
//#include <conio.h> // 为了在VS中_getch能正常使用
DWORD WINAPI SelectEventThread(LPVOID pM);
//int scanf_opt(int* optPtr, int optMin, int optMax);
int main()
{
int option;
system("color e1");
printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
printf("\t\t\t\t ○●○●○● 欢迎登录学生选课管理系统 ●○●○●○\n");
printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
printf("\n\t\t\t\t\t\t请选择您的身份:\n\n");
printf("\t\t\t\t\t ┏━━━━━━━━━━━━━━━━━━━━━━━┓\n");
printf("\t\t\t\t\t ┃ ① - 学生 ┃\n");
printf("\t\t\t\t\t ┗━━━━━━━━━━━━━━━━━━━━━━━┛\n\n");
printf("\t\t\t\t\t ┏━━━━━━━━━━━━━━━━━━━━━━━┓\n");
printf("\t\t\t\t\t ┃ ② - 老师 ┃\n");
printf("\t\t\t\t\t ┗━━━━━━━━━━━━━━━━━━━━━━━┛\n\n");
printf("\t\t\t\t\t ┏━━━━━━━━━━━━━━━━━━━━━━━┓\n");
printf("\t\t\t\t\t ┃ ③ - 退出程序 ┃\n");
printf("\t\t\t\t\t ┗━━━━━━━━━━━━━━━━━━━━━━━┛\n\n\n");
printf("\t\t\t\t\t 请点击相应标题(或输入1,2,3):");
//这里第四个参数为可触发坐标数组字符串,格式:第零选项y起始-y终止,x起始-x终止|第一选项y起始-y终止,x起始-x终止|....目前最大支持0-9的10个选项
HANDLE handle = CreateThread(NULL, 0, SelectEventThread, (char*)
"0--1,0-0|6-8,44-67|10-12,44-67|14-16,44-67|" // 每个菜单修改我,这里的意思是第0行到第-1行为选项0触发区间(设为-1就不可能被触发)|第6行到第8行的第44-67列为选项1触发区间|以此类推
, 0, NULL); // 创建线程
//scanf_opt(&option, 1, 3);
scanf("%d", &option);
TerminateThread(handle, 1); // 强行结束线程,防止后续菜单新建的线程重复输入
printf("\n你的选择:%d\n", option);
system("pause");
return 0;
}
DWORD WINAPI SelectEventThread(LPVOID pM)
{
CONSOLE_SELECTION_INFO selectionInfo;
int p[10][4]; // 定义可触发坐标数组,目前最大输入10组
char* s = (char*)pM; // 第二次强制转换,把LPVOID参数转换到字符串
int i=0, r, n;
while (1)
{
// 格式化输入的参数文本,不具体注释了
r = sscanf(s, "%d-%d,%d-%d|%n", &p[i][0], &p[i][1], &p[i][2], &p[i][3], &n);
if (4 == r)
{
s += n;
i++;
}
else if (0 == r)
s++;
else
break;
}
int x, y;
while (1)
{
GetConsoleSelectionInfo(&selectionInfo); // 获取选择的信息,存到x,y里方便比较
y = selectionInfo.dwSelectionAnchor.Y;
x = selectionInfo.dwSelectionAnchor.X;
for (int c = 0; c < i; c++) if (y >= p[c][0] && y <= p[c][1] && x <= p[c][3] && x >= p[c][2]) {
// 因为个位数字的输入无需其他转换,直接加上0的ASCII码即可,若有需求可自改循环使之能接受两位甚至更多的选项输入
// 若想菜单为英文输入,可以把48改为a的ASCII码(97)
PostMessageA(GetConsoleWindow(), 258, (WPARAM)(48 + c), 0);
PostMessageA(GetConsoleWindow(), 258, (WPARAM)13, 0); // 此行传递回车,若使用getch接受选项则无需此行
}
Sleep(100); // 线程的休眠时间,理论越小点按越流畅,但占用CPU资源越多
}
}
/* 这里有需要的再自取,代替scanf输入,无需回车的好方法
// 菜单选项输入,避免只读取空格前内容造成错误输入可以正常运行的BUG
// 输入格式:选项option指针,最小选项,最大选项
int scanf_opt(int* optPtr, int optMin, int optMax) {
int flag;
int i = 0;
do
{
flag = 0;
i = _getch();
i -= 48; // 0的ASCII码为48
if (i > optMax || i < optMin)
{
printf("\n输入无效,请您重新输入:");
flag = 1;
continue;
}
else
*optPtr = i;
} while (flag);
return 1;
}*/
实现原理
功能实现的核心是Windows.h
的GetConsoleSelectionInfo()
API与多线程API
前者的官方文档在此:GetConsoleSelectionInfo function - Windows Console | Microsoft Docs
Windows自带的CLI界面“命令提示符”默认是开启“快速编辑模式”的,
在该模式下,鼠标点选命令行内的文本会使程序暂停,
并可以像编辑文本一样复制当前窗口内显示的内容。
而利用Windows API内的GetConsoleSelectionInfo()
函数,
程序可以获取到当前命令行窗口被选择的信息。
我们需要先声明一个CONSOLE_SELECTION_INFO
类型的变量
用于存储选择信息struct(第49行),
将该变量的指针传至函数即可
选择信息的结构如下,或参看官方API文档:CONSOLE_SELECTION_INFO structure - Windows Console | Microsoft Docs
typedef struct _CONSOLE_SELECTION_INFO
{
DWORD dwFlags;
COORD dwSelectionAnchor;
SMALL_RECT srSelection;
} CONSOLE_SELECTION_INFO, *PCONSOLE_SELECTION_INFO;
其中我们需要关心的就是COORD类型的dwSelectionAnchor
,
它包含了当前选中的区域尾部坐标
COORD.X
就是坐标的行数,COORD.Y
为列数,
我们可以用自定义的变量来保存坐标(第71,72行),方便后续比对
调用函数后,会持续获取,若没有选中则会返回0,0;
有选中进行后续比对即可通过PostMessageA()
函数向窗口模拟输入该选中所代表的值即可
后者多线程API目的是:避免while(1)循环将窗口卡死以至于不接受用户的按键输入
多线程的具体原理这里就不展开了,
我们关心的是,只需要将循环获取选择信息并处理的逻辑,放在新创建的线程里即可。
适用环境
开发环境
在VS2019,预处理器定义添加_CRT_SECURE_NO_WARNINGS的x86、x64环境下、
MinGW,Dev-C++,TDM-GCC 4.9.2环境下均测试编译、运行正常;
C-Free 5下编译不通过
运行环境
Windows 10 专业版、家庭普通版测试运行正常,
Win8.1及以下无条件进行测试,欢迎反馈~
只要开启了控制台属性中的“快速编辑模式”,即开启了选择功能,理论应正常。
Comments | NOTHING