curses函数库简介及使用
- 导语
- curses简介
- 屏幕
- 输出
- 读取
- 清除
- 移动
- 字符
- 键盘
- 键盘模式
- 输入
- 窗口
- WINDOW
- 常用函数
- 屏幕刷新优化
- 子窗口
- keypad
- 彩色显示
- pad
- 总结
- 参考文献
导语
curses函数库主要用来实现对屏幕和光标的操作,它的功能定位处于简单文本行程序和完全图形化界面之间,在目前图形化界面已经蓬勃发展的现在可能显得有些过时,但是其中很多实现思想和操作仍然值得学习和借鉴,并且curses目前仍然是linux图形化编程的选择之一
curses简介
curses是一个函数库,它提供了许多对光标和终端屏幕的接口函数,在使用的时候必须包括对应的头文件函数声明和宏定义,并且用-lcurses进行定义,有些linux系统因为没有带相关的包,还需要安装类似libncurses5-dev的包
curses工作在屏幕、窗口和子窗口上,对于一个curses窗口,它一般被称为stdscr,与物理屏幕的尺寸一样,当然也可以创造一些别的尺寸小于当前屏幕的窗口,curses用两个ds来映射终端屏幕,stdscr和curscr,前者和stdout非常类似,是curses程序中的默认输出窗口,curscr和stdscr类似,对应的时当前屏幕的样子,需要注意的是,在程序调用refresh之前,输出到stdscr的内容不会显示在屏幕上,stdscr更像一个缓存,暂时存储一些改变
在调用refresh时,curses会比较stdscr和curscr(屏幕预状态和当前状态)之间的异同,之后根据差异刷新屏幕
一般来说,curses刷新逻辑屏幕的频率比刷新物理频率高,这很好理解,可以用计组中cache和内存之间的关系来类比,只有在程序执行的某些阶段,用户需要看到全部结果时,curses才会通过refresh计算出逻辑和物理之间对应的最佳途径,换句话说,curses只把最后的逻辑结果传递给物理屏幕
逻辑屏幕的布局是一个二维数组,每个位置不仅包含字符,还包含它的属性(例如粗体和下划线等),由于curses函数在使用的时候需要创建和删除一些临时ds,所以所有curses程序在开始使用前必须初始化,然后在结束使用之后恢复,通过initscr和endwin函数实现
屏幕
所有调用curses函数的程序必须以initscr和endwin结束,前者只能调用一次,成功则返回stdscr指针,否则返回错误信息,后者成功返回OK,失败返回ERR
输出
curses函数库有很多刷新屏幕的基本函数,书上很多,这里摘录并解释一些常用的函数,需要注意的是chtype是curses自己的字符类型,比标准char有更多位
int addch(const chtype char_to_add);
//在当前位置添加指定的字符
int printw(char *format);
//在光标位置输出,用法和printf一样
int refresh(void);
//刷新屏幕
int box(WINDOW *win_ptr, chtype vertical_char, chtype horizontal_char);
//围绕一个窗口画框,横竖用给定的字符
int insch(chtype char_to_insert);
//插入字符,将已有字符右移,准确来说是头插
读取
从屏幕上读取字符并不常用,但curses还是提供了对应的函数
chtype inch(void);
//返回光标当前位置字符和属性信息
int instr(char *string);
//返回字符串,写入string
int innstr(char *string,int number_of_characters);
//同上,但指定长度
清除
curses提供了四种清楚区域的方法,具体如下
int erase(void);
//在屏幕每个位置写上空白字符
int clear(void);
//彻底清除屏幕
int clrtobot(void);
//清除光标到屏幕尾
int clrtoeol(void);
//清除光标到当前行尾
移动
curses对光标的操作给的很少,只有两个函数,具体如下
int move(int new_y,int new_x);
//将逻辑光标移到指定地点,有外部整数LINES和COLUMNS
//并不会移动物理光标,需要移动则要用refresh
int leaveok(WINDOW *window_ptr,bool leave_flag);
//设置标志,控制屏幕刷新后物理光标的位置,0则和逻辑光标一样否则随机
字符
先前已经提到过,cureses中的每个字符是有属性的,这些属性是用宏来定义的,当需要修改时,直接使用这些宏作为对应参数给到函数即可,一些相关函数具体如下
int attron/attroff/attrset(chtype attribute);
//set是设置,on和off是开关
int standout/standend(void);
//反色显示
书上给出了一个例子,下面是代码和运行结果
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <curses.h>
int main()
{
const char one[]=" First Witch ";
const char two[]=" Second Witch ";
initscr();
move(5,15);
attron(A_BOLD);//设置模式
printw("%s","Bold test");//加粗输出
attroff(A_BOLD);
refresh();//刷新
sleep(1);
move(8,15);
attron(A_STANDOUT);//设置模式
printw("%s","test light");
attroff(A_STANDOUT);//关闭反色
refresh();
sleep(1);
move (10,10);//设置位置输出
printw("%s","test sentence 1");
move (11,13);
printw("%s","test sentence 2");
move(13,10);
printw("%s","test sentence 3");
move (14,23);
printw("%s","test sentence 4");
refresh();
sleep(1);
attron(A_DIM);
int len=strlen(one);
for(int i=len-1;i>=0;i--)//倒序插入
{
move(10,10);
insch(one[i]);
}
len=strlen(two);
for(int i=len-1;i>0;i--)
{
move(13,10);
insch(two[i]);
}
attroff(A_DIM);
refresh();
sleep(1);
move(LINES-1,COLS-1);
refresh();
sleep(5);
endwin();
return 0;
}
可以看到加粗、反色和其他效果的字符串是什么样的
键盘
除了屏幕之外,curses还有针对键盘的一系列接口
键盘模式
在curses中有许多模式,例如预处理,默认输入等,在使用initscr时,输入模式处于预处理模式,只有在用户按下回车之后,输入的数据才会被传给程序(键盘的特殊字符被启用,组合键可产生信号),如果程序调用cbreak就可以将输入模式设置为cbreak模式,字符一经键入就被立刻传递给程序(特殊字符启用,一些简单字符直传)
一些相关的函数如下
int echo/noecho(void);
//启用/不启用回显
int cbreak/nocbreak(void);
//启用/不启用cbreak
int raw/noraw(void);
//启用/不启用特殊字符的处理
输入
一些读取键盘输入的函数具体如下
int getch(void);
//类似getchar
int getstr(char* string);
//类似gets
int getnstr(char* string,int number_of_characters);
//类似getnstr
int scanw(char* format,...);
//类似scanf
书上给出的一个具体的例子如下
#include <stdio.h>
#include <stdlib.h>
#include <curses.h>
#include <string.h>
#include <unistd.h>
int main()
{
char name[256],pw[256];
const char* rpw="1111";//真正的密码,用来检测
initscr();//开模式
move(5,10);
printw("%s","login:");
move(7,10);
printw("%s","user name:");
getstr(name);//拿用户名
move(8,10);
printw("%s","password:");
cbreak();
noecho();//关闭回显
memset(pw,0,sizeof(pw));
for(int i=0; i<256; i++)
{
pw[i]=getch();
if(pw[i]=='\n')break;
move(8,20+i);
addch('*');//把输入的对应位置插入*
refresh();
}
echo();
nocbreak();
move(11,10);
if(strncmp(rpw,pw,strlen(rpw)))
printw("%s","wrong");
else
printw("%s","correct");
refresh();
sleep(2);
endwin();
return 0;
}
可以看到,这个例子实现了一个简单的密码隐藏和检测的功能,通过调用curses和设置光标的位置来实现
窗口
curses可以在物理屏幕上同时显示多个不同尺寸的窗口,而不仅仅是对单一窗口进行操控
WINDOW
stdscr是WINDOW的一个特例,是默认存在的,除此之外,在WINDOW中可以使用newwin和delwin来创建和销毁窗口,具体函数如下
WINDOW *newwin(int num_of_lines,int num_of_cols,int start_y,int start_x);
//创建一个新窗口,行数列数和开始位置
//新窗口完全独立于已存在窗口,并且覆盖它们的内容
int delwin(WINDOW *window_to_delete);
//删除,不能删除stdscr和curscr
常用函数
先前已经使用过一些屏幕上的函数,例如addch、printw等,但是这些函数还可以加上一些前缀变成通用函数,前缀w用于窗口,mv用于光标移动,mvw用于在窗口中移动光标,需要注意的是,当加上了对应的前缀时,函数的参数也会发生对应的变化,下面给出一些加上前缀后的具体函数
int mvwaddch(WINDOW* window_pointer, int y, int x, const chtype char);
int mvwprint(WINDOW *window_pointer, int y, int x, const chtype char);
int mvwin(WINDOW *window_to_move, int new_y, int new_x, int new_x);
int touchwin(WINDOW *window_ptr);
//通知curses窗口内容已改变,下次刷新必须重新绘制
int scrollok(WINDOW *window_ptr, bool scroll_flag);
//控制卷屏,传递给函数是布尔值则允许卷
int scroll(WINDOW *window_ptr);
//窗口内容上卷一行
书上给出的一个运行的例子,代码和部分运行结果如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <curses.h>
int main()
{
WINDOW *nw_ptr,*pw_ptr;//新窗口和弹出创刊
char ch='a';
initscr();//开模式
move(5,5);//移动光标
printw("%s","Multiple windows");
refresh();
for(int i=0;i<255;i++)
for(int j=0;j<255;j++)
{
mvwaddch(stdscr,i,j,ch);//把数据插入整个屏幕
ch=(++ch-'a')%26+'a';//循环小写字母
}
refresh();
sleep(2);
nw_ptr=newwin(10,20,5,5);//新窗口
mvwprintw(nw_ptr,2,2,"%s","Hello World");
mvwprintw(nw_ptr,5,2,"%s","1111111111111111111111111111111111111");
wrefresh(nw_ptr);//刷新以显示
sleep(2);
ch='0';
for(int i=0;i<255;i++)
for(int j=0;j<255;j++)
{
mvwaddch(stdscr,i,j,ch);
ch=(++ch-'0')%10+'0';//循环数字
}
refresh();
sleep(2);
touchwin(nw_ptr);//通知curses窗口内容已变,调用刷新要重写窗口
wrefresh(nw_ptr);
sleep(2);
pw_ptr=newwin(10,20,8,8);//创建新窗口
box(pw_ptr,'|','-');//加上框
mvwprintw(pw_ptr,5,2,"%s","Pop up Window!");
wrefresh(pw_ptr);
sleep(2);
touchwin(nw_ptr);//显示
wrefresh(nw_ptr);
sleep(2);
wclear(nw_ptr);//清屏
wrefresh(nw_ptr);
sleep(2);
delwin(nw_ptr);
touchwin(pw_ptr);
wrefresh(pw_ptr);
sleep(2);
delwin(pw_ptr);
touchwin(stdscr);
refresh();
sleep(2);
endwin();
return 0;
}
运行的时候可以看到,创建的新窗口覆盖了原有的背景窗口不分,然后新的两个窗口彼此相互覆盖,需要注意的是,如果要用curses刷新多个窗口,只能人为的管理这些窗口之间的先后关系
屏幕刷新优化
对屏幕的优化刷新需要一定技巧,当要更新的终端是通过慢速链路连接到主机时,就可能因为设备之间的不同步出问题,在慢速链路上,屏幕绘制的速度会非常慢,curses提供了相对应的手段,具体如下
int wnoutrefresh(WINDOW *window_ptr);
//决定把哪些字符发到屏幕上,但是只是缓存
int doupdate(void);
//把最后的更新结果输出到屏幕上
//如果想重新绘制多个窗口,可以为每个窗口调用wnoutrefresh,然后最后调用doupdate即可
//这是利用了缓存的思路
子窗口
子窗口和窗口的关系有点类似父进程和子进程,它的创建和删除的相关函数如下
WINDOW *subwin(WINDOW *parent, int num_of_lines,int num_of_cols, int start_y, int start_x);
//类似newwin,子窗口和父窗口共享同一字符存储空间
int delwin(WINDOW *window_to_delete);
子窗口最主要的用途是,提供一种简洁方式卷动另一窗口的部分内容,在使用子窗口的时候,刷新屏幕必须先对父窗口调用touchwin,下面是书上给的一个例子代码和运行结果
#include <unistd.h>
#include <stdlib.h>
#include <curses.h>
#include <stdio.h>
int main()
{
WINDOW *sub;//窗口指针
char ch='1';
initscr();
for(int i=0;i<255;i++)
for(int j=0;j<255;j++)
{
mvwaddch(stdscr,i,j,ch);//在对应位置插入字符
ch=(++ch-'0')%9+'1';
}
sub=subwin(stdscr,10,20,10,10);//创建子窗口
scrollok(sub,1);
touchwin(stdscr);//刷新屏幕前调用父窗口
refresh();
sleep(1);
werase(sub);//删除父窗口的对应区域
mvwprintw(sub,2,0,"%s","scrolling");//输出
wrefresh(sub);//刷新窗口
sleep(1);
for(int i=1;i<10;i++)//重新输出并滚动
{
wprintw(sub,"%s","wapping and scrolling");
wrefresh(sub);
sleep(1);
}
delwin(sub);
touchwin(stdscr);
refresh();
sleep(1);
endwin();
return 0;
}
可以看到父窗口的一个小区域被清空,然后生成了一个子窗口,对子窗口进行输出并滚动
keypad
对于键盘上的按键,并不是所有的都可以通过ascall码来表示,比如insert、delete等,这些键在实际输入的时候往往是以escape字符开头的字符串序列,但这样就出现一个问题,系统需要识别单独按下escape键和以该键为首的字符串序列,curses提供了区分它们的实现,curses在启动时会关闭转义序列与逻辑键间的转换功能,通过keypad实现
int keypad(WINDOW *window_ptr, bool keypad_on);
//为了区分不同,curses会在检测到escape之后等待一小段时间,特别是启用keypad后
书上给出了一个keypad的实例,下面是代码和运行结果
#include <unistd.h>
#include <stdlib.h>
#include <curses.h>
#include <string.h>
#include <ctype.h>
int main()
{
initscr();
crmode();
keypad(stdscr,1);//启用keypad模式
noecho();//不回显
clear();
mvprintw(5,5,"q to quit");//打印字符
move(7,5);//移动光标
refresh();
int key=getch();
while(key!=ERR&&key!='q')
{
move(7,5);
clrtoeol();//清除当前位置到行尾
if(isalpha(key))//如果是字符
printw("Key was %c", key);
else
{
switch(key)//用特殊的宏来判断
{
case 27: printw("%s","Escape key");break;
case KEY_END: printw("%s","END key");break;
case KEY_BEG: printw("%s","BEGINNING key");break;
case KEY_RIGHT: printw("%s","RIGHT key");break;
case KEY_LEFT: printw("%s", "LEFT key");break;
case KEY_UP: printw("%s","UP key");break;
case KEY_DOWN: printw("%s","DOWN key");break;
default: printw("Unmatched - %d",key);break;
}
}
refresh();
key=getch();
}
endwin();
return 0;
}
可以看到程序可以识别所有的字符,以及一些设定好的特殊按键
彩色显示
curses对颜色的支持是通过组合来实现的,对于每个想使用的颜色组合,用户必须同时定义一个字符的前景和背景,相关函数具体如下
bool has_colors(void);
//判断终端是否支持彩色显示
int start_color(void);
//初始化颜色显示
int init_pair(short pair_number, short foreground, short background);
//初始化一个颜色组合,用pair_number来替代
int COLOR_PAIR(int pair_number);
//把颜色组合作为属性返回
int pair_content(short pair_number, short *foreground, short *background);
//获得已定义的颜色组合信息
int init_color(short color_number, short red, short green, short blue);
//把一个已有的颜色重新定义
书上给出的例子和运行结果如下
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <curses.h>
int main()
{
initscr();
if(!has_colors())
{
endwin();
fprintf(stderr,"ERROR1\n");
exit(1);
}
if(start_color()!=OK)
{
endwin();
fprintf(stderr,"ERROR2\n");
exit(2);
}
clear();
mvprintw(5,5,"Colors types: %d, Color pairs: %d",COLORS,COLOR_PAIRS);
refresh();
init_pair(1,COLOR_RED,COLOR_BLACK);//初始化几个组合
init_pair(2,COLOR_RED,COLOR_GREEN);
init_pair(3,COLOR_GREEN,COLOR_RED);
for(int i=1;i<=3;i++)
{
attroff(A_BOLD);//关闭加粗
attrset(COLOR_PAIR(i));//设置颜色组合
mvprintw(5+i,5,"COLOR pair %d", i);
attrset(COLOR_PAIR(i)|A_BOLD);//设置颜色组合+加粗
mvprintw(5+i,25,"Bold color pair %d", i);
refresh();
sleep(3);
}
endwin();
return 0;
}
可以看到以不同的颜色组合和加粗显示了字符串
pad
有时在使用curses时需要先建立一个逻辑屏幕,然后再把其部分内容投射到物理屏幕上,这个逻辑屏幕的尺寸可能会大于实际屏幕,curses提供了一个数据结构pad,它可以控制尺寸大于正常窗口的逻辑屏幕,具体函数如下
WINDOW *newpad(int number_of_lines, int number_of_columns);
int prefresh(WINDOW *pad_ptr, int pad_row, int pad_column,
int screen_row_min, int screen_col_min, int screen_row_max, int screen_col_max);
//将pad从指定坐标开始的区域写到屏幕上指定的显示区域,后两个坐标是显示范围
书上给出的例子和运行结果如下
#include <unistd.h>
#include <stdlib.h>
#include <curses.h>
int main()
{
WINDOW*pad;
initscr();
int lines=LINES+50,cols=COLS+50;
pad=newpad(lines,cols);//创建新pad
char ch='a';
for(int i=0;i<lines;i++)
for(int j=0;j<cols;j++)
{
mvwaddch(pad,i,j,ch);
ch=(++ch-'a')%26+'a';
}
prefresh(pad,5,7,2,2,9,9);//刷新显示
sleep(1);
prefresh(pad,LINES+5,COLS+7,5,5,21,19);//移动,再刷新显示一遍
sleep(1);
delwin(pad);
endwin();
return 0;
}
总结
可以看到,linux通过curses实现了对屏幕、键盘的读取,以及对子窗口,pad等的调用,为C语言实现可交互的应用程序提供了很好的实现方法和思路
参考文献
- 《Linux程序设计(第四版)》