本系列文章系本人原创,欢迎转载,转载请注明出处
注:这篇文章所建立的所有文件(源文件,库文件,可执行文件等)均可在这里下载。下载的是chapter5.tar.gz文件,可以在Linux系统终端中用如下命令解压到当前目录:
tar zxvf chapter5.tar.gz
在Linux系统下,你可能会与终端(命令行)有很多的交互,因此,编写能够控制终端的程序也是值的探讨的。现在我们就来讲解一下Linux下控制终端的函数库curses。
一、用curses函数库在终端打印 “Hello World!”
用Hello World来作为一个语言的学习过程中的第一个程序几乎已成为一种惯例。同样的,我们用curses函数库做的第一件事,也是打印Hello World。
在此之前,因为我们操作的是终端(屏幕),所以我们先来简单的介绍下字符是如何显示在屏幕上的。
首先,curses函数库有两个数据结构来对应终端屏幕:stdscr和curscr。stdscr是curses的默认输出屏幕,curscr对应的是当前屏幕,在程序调用refresh函数之前,输出到stdscr上的内容是不会显示出来的。而refresh函数的功能就是比较stdscr和curscr的不同,然后根据结果来刷新屏幕。
既然是要将内容输出到屏幕上,那么我们就要知道内容输出到屏幕上的什么位置。在curses函数库中,用形如(行,列)的方式来表示屏幕上的位置,如 (5,2)就表示第五行第二列。坐标原点 (0,0)在终端屏幕的左上角。
好了,接下来我们就开始第一个用curses函数库在屏幕上输出的例子吧,源码如下:
#include <curses.h>
int main()
{
//在进行屏幕的操作前需要先初始化
initscr();
//移到第二行,第五列
move(2, 5);
//在当前位置输出字符串
printw("%s","Hello World!");
//refresh的作用我们在上面讲过
refresh();
//停顿3秒
sleep(3);
//我们的屏幕操作结束了
endwin();
return 0;
}
回到终端,编译源码。由于curses函数库不是C语言标准库,在编译的时候需要指定库文件:
gcc helloWorld.c -lcurses
编译无误后,执行文件,效果如图,会在指定的位置输出 “Hello World!”,停顿3秒,然后程序退出。
二、一些关于终端屏幕的基本操作
光标的移动
这个比较简单,我们呢在上面的 “Hello World”的例子里已经见到过:
#include <curses.h>
int move(int row, int column);
执行该函数,将光标移动到到指定的(行,列)。这里再介绍两个值LINES和COLS,它们分别对应row和column能指定的最大值。
键盘相关的操作
1) 控制模式
#include <curses.h>
//开启回显
int echo(void);
//关闭回显
int noecho(void);
//开启cbreak模式,字符一经输入便立刻传给程序处理
int cbreak(void);
//关闭cbreak模式
int nocbreak(void);
//开启raw模式,字符一经输入便立刻传给程序,并且关闭特殊字符的处理
int raw(void);
//关闭raw模式
int noraw(void);
2)键盘输入
#include <curses.h>
int getch(void)
int getstr(char *string);
int getnstr(char *string, int number_of_characters);
int scanw(char *format, ...);
例子
接下来我们用一个例子来展示curses函数库。这个例子是一个简单的扫雷程序。它会在终端屏幕上打印10*10的数组,用?表示未知的格子,用F表示用户标记,用_表示空格子,用*表示雷。用户用方向键进行光标的移动,用空格来作标记,用回车来探索一个未知的格子,当用户标记了所有雷或者触发任意雷的时候游戏结束。下面是源码:
#include <curses.h>
#include <time.h>
//定义行/列
#define ROWS 10
#define COLUMNS 10
//mine数组/矩阵就代表了屏幕上显示的雷区
bool mine[ROWS][COLUMNS];
//flag数组/矩阵用来记录雷区对应格子的状态,0:未标记,1:已被用户标记,2:已探索
int flag[ROWS][COLUMNS];
//初始化mine数组/矩阵,值为true的是雷,值为false的不是雷
void init_mine()
{
int i,j;
srand((unsigned)time(NULL));
for(i=0;i<ROWS;++i)
{
for(j=0;j<COLUMNS;++j)
{
int value=rand()%5;
if(value==4)
{
mine[i][j]=true;
}else{
mine[i][j]=false;
}
}
}
}
void init_flag()
{
int i,j;
for(i=0;i<ROWS;++i)
for(j=0;j<COLUMNS;++j)
flag[i][j]=0;
}
//获取当前格子周围8个格子中共有多少雷
int get_mines(const int current_row, const int current_col)
{
int offset[]={-1,0,1};
int i,j;
int count=0;
for(i=0;i<3;++i)
{
for(j=0;j<3;++j)
{
if(offset[i] || offset[j])
{
int tmp_row=current_row+offset[i];
int tmp_col=current_col+offset[j];
if(tmp_row>=0 && tmp_row<ROWS && tmp_col>=0 && tmp_col<COLUMNS)
{
count+=mine[tmp_row][tmp_col];
}
}
}
}
return count;
}
//探索指定格子,如果它周围有雷,则显示周围的雷的个数,否则在它周围8个格子中进行递归的探索
void check_mines(const int current_row, const int current_col)
{
//如果该格子已经被探索过,则不再重复探索
if(flag[current_row][current_col]==2)
{
return;
}
flag[current_row][current_col]=2;
int count=get_mines(current_row,current_col);
if(count)
{
move(current_row,current_col);
printw("%d",count);
}else {
move(current_row,current_col);
printw("%s","_");
int offset[]={-1,0,1};
int i,j;
for(i=0;i<3;++i)
{
for(j=0;j<3;++j)
{
if(offset[i] || offset[j])
{
int tmp_row=current_row+offset[i];
int tmp_col=current_col+offset[j];
if(tmp_row>=0 && tmp_row<ROWS && tmp_col>=0 && tmp_col<COLUMNS)
{
check_mines(tmp_row,tmp_col);
}
}
}
}
}
}
//检查当前格子,如果是雷则返回2,如果所有雷都已被用户标记,则返回1,否则探索格子并返回0
int scan_mines(const int current_row, const int current_col)
{
if(mine[current_row][current_col])
{
return 2;
}
int i,j;
bool is_all_scanned=true;
for(i=0;i<ROWS;++i)
{
for(j=0;j<COLUMNS;++j)
{
if(mine[i][j])
{
if(!flag[i][j])
{
is_all_scanned=false;
break;
}
}
}
}
if(is_all_scanned)
{
return 1;
}
check_mines(current_row, current_col);
return 0;
}
int main()
{
init_mine();
init_flag();
initscr();
noecho();
//向屏幕输出ROWS * COLUMNS的矩阵,用?表示未知区域,用_表示空区域,用*表示雷,用数组表示该位置周围有多少雷
int i,j;
for(i=0;i<ROWS;++i)
{
for(j=0;j<COLUMNS;++j)
{
move(i,j);
printw("%s","?");
}
}
//将光标移到矩阵的左上角
move(0,0);
refresh();
//开始与用户交互,直到没有未知区域(用?表示)或用户触雷结束。
int status=0;//0:正常游戏中,1:用户扫了所有的雷结束,2:用户触雷结束
int current_row=0;
int current_col=0;
while(!status)
{
//获取用户击键
int value=getch();
switch (value)
{
case 65:
current_row=(current_row-1)<0?0:(current_row-1);
break;
case 66:
current_row=(current_row+1)>(ROWS-1)?(ROWS-1):(current_row+1);
break;
case 67:
current_col=(current_col+1)>(COLUMNS-1)?(COLUMNS-1):(current_col+1);
break;
case 68:
current_col=(current_col-1)<0?0:(current_col-1);
break;
case 32:
if(flag[current_row][current_col]==1)
{
printw("%s","?");
flag[current_row][current_col]=0;
}else if (flag[current_row][current_col]==0){
printw("%s","F");
flag[current_row][current_col]=1;
}
break;
case 10:
status=scan_mines(current_row,current_col);
break;
}
move(current_row,current_col);
refresh();
}
if(status==2)
{
for(i=0;i<ROWS;++i)
{
for(j=0;j<COLUMNS;++j)
{
move(i,j);
printw("%s",mine[i][j]?"*":"-");
}
}
}
move(ROWS+1, COLUMNS+1);
printw("%s\n",(status==2)?"Game Over.":"You won!");
refresh();
sleep(10);
endwin();
return 0;
}
效果如下:
失败的图片:
胜利的图片: