ConsoleUI4

zombie462

2020-08-07 14:45:23

Personal

## 0. 前言 作为一名 Oier,在闲暇时刻,不免会打开 C++,写一个简易的、基于控制台的项目,然后传到网上,供大家学习、参考,甚至游玩。但很多时候,不少人会被黑白的控制台界面劝退,或者是被简陋的 `cin,scanf` 交互搞得体验感全无。 如何把一个单纯的控制台项目玩出花样——即用简单的字符,展示着较为美观的界面,并有着容易上手的交互操作,这便是本文的主题。 **前排提示:本文提到的所有函数均在 `<windows.h>` 或 `<conio.h>` 中。使用 `#include` 即可。** ## 1. 美化界面 ### 1.1. 给字符加上颜色 对于一个没有图画,只有字符的程序而言,字符的颜色以及色彩的搭配直接决定了整个程序界面的美观程度。 在 C++ 里,如何使得在控制台中输出的字符拥有颜色呢?其实很简单。 ```cpp 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`,表示你是否按下了键盘上的任意键。 ```cpp while (true){ //do something if (kbhit()){ char c=_getch(); //do something break; } } ``` 这也是一个比较经典的应用,在等待你按键的同时,程序还能进行一些运算和操作。 当然有一个更 NB 的函数,会在第三节讲到,这里先埋个伏笔。 等待自然是 `Sleep(__time__)`,其中 `__time__` 是一个整数,表示你需要等待的时间(毫秒记)。 ### 1.3. 杂七杂八的设置 + 显示光标 ```cpp void showCursor(){ HANDLE fd=GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cinfo; cinfo.bVisible=1; cinfo.dwSize=1; SetConsoleCursorInfo(fd,&cinfo); } ``` + 隐藏光标 ```cpp void hideCursor(){ HANDLE fd=GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cinfo; cinfo.bVisible=0; cinfo.dwSize=1; SetConsoleCursorInfo(fd,&cinfo); } ``` + 窗口大小 & 窗口标题 ```cpp 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. 光标操作 + 移动光标位置 ```cpp 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)` 表示将光标移动至第一行的第十列。 + 获取光标位置 ```cpp 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` 模板。 方便起见,我们把一些函数封装一下: ```cpp 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` 了: ```cpp 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 换行(换行后光标的列数将与第一个字符的列数相同) ``` + 例子: ```cpp 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); ``` + 效果(图床的图片可能有些失真): ![](https://cdn.luogu.com.cn/upload/image_hosting/ht6ehlal.png) ### 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; } ``` + 调用方式: 传入的字符串由三部分组成: 1. 标题部分;表示选择框的标题。以字符 `@` 结束。 2. 选项部分;表示具体的选项。每个选项以 `@` 结束。最后以 `^` 结束。 3. 提示部分;表示选项下方的文字。 函数的返回值为你选择的选项标号。 + 例子: ``` chosenbox("&fPlease Choose:@&aFirst@&bSecond@&cThird@&fHello^"); ``` + 效果: ![](https://cdn.luogu.com.cn/upload/image_hosting/6j08hlnt.png) + 交互方式: 使用 `a,d` 直接跳到第一个/最后一个选项。 使用 `w,s` 移动到上一个/下一个选项。 使用数字键直接跳转到指定选项。 ## 3. 鼠标交互 鼠标交互的核心代码是这一句: ```cpp #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` 两个按键。 当然,只获取鼠标是否按下的信息是不够的,我们还要获得鼠标的位置。 ```cpp void getpos(POINT &pt){ HWND hwnd=GetForegroundWindow(); GetCursorPos(&pt); ScreenToClient(hwnd,&pt); pt.y=pt.y/16+1; pt.x=pt.x/8+1; } ``` 使用方式: ```cpp 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 已经发布。在您完全掌握本文档的内容后可以前去 [这里](https://crystal302.github.io/2021/05/08/consoleUI5/) 看看。