ConsoleUI4
zombie462
2020-08-07 14:45:23
## 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/) 看看。