ConsoleUI4
0. 前言
作为一名 Oier,在闲暇时刻,不免会打开 C++,写一个简易的、基于控制台的项目,然后传到网上,供大家学习、参考,甚至游玩。但很多时候,不少人会被黑白的控制台界面劝退,或者是被简陋的 cin,scanf 交互搞得体验感全无。
如何把一个单纯的控制台项目玩出花样——即用简单的字符,展示着较为美观的界面,并有着容易上手的交互操作,这便是本文的主题。
前排提示:本文提到的所有函数均在 <windows.h> 或 <conio.h> 中。使用 #include 即可。
1. 美化界面
1.1. 给字符加上颜色
对于一个没有图画,只有字符的程序而言,字符的颜色以及色彩的搭配直接决定了整个程序界面的美观程度。
在 C++ 里,如何使得在控制台中输出的字符拥有颜色呢?其实很简单。
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),__color__);
注意:在本文展示的代码中,像 __color__ 这样用下划线括起来的部分,表示你需要用合适的代码块来填充它。
__color__:表示输出在控制台上的颜色。这是一个两位的 16 进制数,第一位表示背景色,第二位表示前景色(即字符颜色)。其中:
0 黑色
1 深蓝色
2 深绿色
3 深青色
4 深红色
5 深紫色
6 深黄色
7 浅灰色
8 灰色
9 蓝色
a 绿色
b 青色
c 红色
d 紫色
e 黄色
f 白色
比如 __color__=0x0c 时,表示在此之后输出的字符均会变成红色,同时该字符的背景色为黑色。
1.2. 清空屏幕 & “按任意键继续” & 等待
清空屏幕自然是 system("cls")。注意:如果你在之前修改过字体的背景色(就是第一位 16 进制数),必须先修改回来再清屏。
按任意键继续虽然有 system("pause") 的手段,但我个人更喜欢 _getch()。_getch() 是一个中断型的函数,如果你一直不按键盘,则程序会一直等待。当你按下键盘的时候,该函数会返回你按下的是哪一个键。
比如:char c=_getch(),就是一个典型的交互案例(更多的交互内容会在第二节阐述)。如果只是单纯地把它当做一个 pause 来用,只需要单独使用 _getch() 即可。
类似的函数还有一个 kbhit(),它是一个即时的函数,不会使你的程序进入等待状态。但它只会返回 true 或 false,表示你是否按下了键盘上的任意键。
while (true){
//do something
if (kbhit()){
char c=_getch();
//do something
break;
}
}
这也是一个比较经典的应用,在等待你按键的同时,程序还能进行一些运算和操作。
当然有一个更 NB 的函数,会在第三节讲到,这里先埋个伏笔。
等待自然是 Sleep(__time__),其中 __time__ 是一个整数,表示你需要等待的时间(毫秒记)。
1.3. 杂七杂八的设置
- 显示光标
void showCursor(){
HANDLE fd=GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cinfo;
cinfo.bVisible=1;
cinfo.dwSize=1;
SetConsoleCursorInfo(fd,&cinfo);
}
- 隐藏光标
void hideCursor(){
HANDLE fd=GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cinfo;
cinfo.bVisible=0;
cinfo.dwSize=1;
SetConsoleCursorInfo(fd,&cinfo);
}
- 窗口大小 & 窗口标题
void setWindowSize(){
system("title __name__");
char cmd[30];
sprintf(cmd,"mode con cols=%d lines=%d",__width__,__height__);
system(cmd);
}
__name__:你要显示的标题。
__width__,__height__:宽度、高度。
1.4. 光标操作
- 移动光标位置
void gotoxy(int x,int y){
COORD pos;
pos.X=y-1;
pos.Y=x-1;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos);
}
x 表示行数,y 表示列数。gotoxy(1,10) 表示将光标移动至第一行的第十列。
- 获取光标位置
pair <int,int> getxy(){
HANDLE hStdout;
CONSOLE_SCREEN_BUFFER_INFO pBuffer;
hStdout=GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleScreenBufferInfo(hStdout,&pBuffer);
return make_pair(pBuffer.dwCursorPosition.Y+1,pBuffer.dwCursorPosition.X+1);
}
返回 pair 的 first 是行数,second 是列数。如果你的光标在第一行第十列,则会返回 {1,10}。
上述两个函数可以优化程序更新界面的操作,如果只需要修改一小部分格子上的字符,只需要将光标移动到指定的地方输出更新后的字符即可。同理,我们也可以通过上述两个函数实现清行操作,请读者自行思考。
2. 键盘交互
键盘交互本质上其实就是 _getch() 和 kbhit() 的灵活应用。这里主要给出几个比较优雅的键盘交互模板。
2.1. 更方便的输出
为了让模板更加优雅,我采用了自定义的 tellraw 输出格式,用一些简单的特殊字符来表示特殊操作,比如 &a 表示把输出的前景色改成绿色等。具体的,下面是我自己的一个 tellraw 模板。
方便起见,我们把一些函数封装一下:
void setColor(int colorID){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),colorID);
}
void clrscr(){
setColor(0x0f);system("cls");
}
int charToHex(char c){
if (c>='0' && c<='9') return c-'0';
else return c-'a'+10;
}
然后就是我们的 tellraw 了:
void tellraw(string s,...){
va_list ap;va_start(ap,s);
int len=s.size();setColor(0x0f);
int foreColor=15,backColor=0,crlf=getxy().second;
bool change=false;
for (int i=0;i<len;++i){
if (s[i]=='%'){
if (change) setColor(foreColor+backColor*16),change=false;
if (s[i+1]=='s') cout<<va_arg(ap,char*);
else if (s[i+1]=='d') cout<<va_arg(ap,int);
i++;
}else if (s[i]=='&'){
foreColor=charToHex(s[i+1]);
change=true;i++;
}else if (s[i]=='#'){
backColor=charToHex(s[i+1]);
change=true;i++;
}else if (s[i]=='/'){
if (s[i+1]=='/') putchar('/');
else if (s[i+1]=='#') putchar('#');
else if (s[i+1]=='%') putchar('%');
else if (s[i+1]=='&') putchar('&');
else if (s[i+1]=='n'){
pair <int,int> tmp=getxy();
gotoxy(tmp.first,crlf-1);
}
else if (s[i+1]=='c') clrscr();
i++;
}else{
if (change) setColor(foreColor+backColor*16),change=false;
putchar(s[i]);
}
}
setColor(0x0f);va_end(ap);
}
- 具体用法:
# + 0..9,a..f 修改背景色
& + 0..9,a..f 修改前景色
% + d,s 和 printf 类似地输出数字/字符串
/ + #,&,% 输出特殊字符
/ + c 清屏
/ + n 换行(换行后光标的列数将与第一个字符的列数相同)
- 例子:
tellraw("/c&8%s &fget &c%d &fpoint in the contest on &e%s %d&f, &e%d&f./n","zombie462",0,"August",7,2020);
- 效果(图床的图片可能有些失真):
2.2. 选择框
int chosenbox(string s){
int len=s.size(),i=0;
pair <int,int> tmp=getxy();
string t="";
while (s[i]!='@' && i<len) t=t+s[i],i++;
tellraw(t);
int j=i;
t="";
int item=0;
for (int i=j+1;i<len;++i){
if (s[i]=='^'){
j=i;
break;
}else if (s[i]=='@'){
item++;
gotoxy(tmp.first+item,tmp.second);
tellraw(" &b%d. &f"+t+"/n",item);
t="";
}else{
t=t+s[i];
}
}
t="";
for (int i=j+1;i<len;++i){
t=t+s[i];
}
gotoxy(tmp.first+item+1,tmp.second);
tellraw(t);
gotoxy(tmp.first+1,tmp.second);
tellraw("&f-->");
char c=_getch();
int chosen=1;
while (c!=' '){
if (c=='W' || c=='w'){
if (chosen>1){
gotoxy(tmp.first+chosen,tmp.second);
chosen--;
printf(" ");
gotoxy(tmp.first+chosen,tmp.second);
tellraw("&f-->");
}
}else if (c=='S' || c=='s'){
if (chosen<item){
gotoxy(tmp.first+chosen,tmp.second);
chosen++;
printf(" ");
gotoxy(tmp.first+chosen,tmp.second);
tellraw("&f-->");
}
}else if (c>='1' && c<='9' && c-48<=item){
gotoxy(tmp.first+chosen,tmp.second);
chosen=c-48;
printf(" ");
gotoxy(tmp.first+chosen,tmp.second);
tellraw("&f-->");
}else if (c=='A' || c=='a'){
gotoxy(tmp.first+chosen,tmp.second);
chosen=1;
printf(" ");
gotoxy(tmp.first+chosen,tmp.second);
tellraw("&f-->");
}else if (c=='D' || c=='d'){
gotoxy(tmp.first+chosen,tmp.second);
chosen=item;
printf(" ");
gotoxy(tmp.first+chosen,tmp.second);
tellraw("&f-->");
}
c=_getch();
}
gotoxy(tmp.first+item+2,tmp.second);
return chosen;
}
- 调用方式:
传入的字符串由三部分组成:
- 标题部分;表示选择框的标题。以字符
@结束。 - 选项部分;表示具体的选项。每个选项以
@结束。最后以^结束。 - 提示部分;表示选项下方的文字。
函数的返回值为你选择的选项标号。
- 例子:
chosenbox("&fPlease Choose:@&aFirst@&bSecond@&cThird@&fHello^");
- 效果:
- 交互方式:
使用 a,d 直接跳到第一个/最后一个选项。
使用 w,s 移动到上一个/下一个选项。
使用数字键直接跳转到指定选项。
3. 鼠标交互
鼠标交互的核心代码是这一句:
#define press(VK_NONAME) ((GetAsyncKeyState(VK_NONAME)&0x8000)?1:0)
press(VK_LBUTTON) 为真表示按下了左键,press(VK_RBUTTON) 同理。另外,press(__char__) 也能够判断是否按下键盘上的某一个按键。和 kbhit() 一样,它不会中断程序。此外,像 press('A') && press('B') 的形式可以判断你是否同时按下 A,B 两个按键。
当然,只获取鼠标是否按下的信息是不够的,我们还要获得鼠标的位置。
void getpos(POINT &pt){
HWND hwnd=GetForegroundWindow();
GetCursorPos(&pt);
ScreenToClient(hwnd,&pt);
pt.y=pt.y/16+1;
pt.x=pt.x/8+1;
}
使用方式:
POINT pt;getpos(pt);
pt.x 为鼠标所在的行号,pt.y 为鼠标所在的列号。
使用这种方式,我们可以维护按钮,即:
while (true){
//do something
POINT pt;
getpos(pt);
if (pt.x>=button.x && pt.x<=button.x+button.width-1 && pt.y>=button.y && button.y+button.height-1){
//do something
if (press(VK_LBUTTON)){
//do something
break;
}
}
}
其中 button 是一个自定义的按钮类。
当然,鼠标交互还有相当多的技巧和手法,这里由于篇幅原因,不加赘述,相信你一定能够发现新大陆。
4. 后记
ConsoleUI5 已经发布。在您完全掌握本文档的内容后可以前去 这里 看看。