1.1什么是curses
curses实际上是一个函数开发包,专门用来进行U NIX下终端环境下的屏幕界面处理以及
I/O处理。通过这些函数库,C和C++程序就可以控制终端的视频显示以及输入输出。使用curses包中的函数,用户可以非常方便的创建和操作窗口,使用菜单以及表单,而且最为重要的一点是使用curse s包编写的程序将独立于各种具体的终端,这样的一个直接的好处就是程序具有良好的移植性。这一点在网络上显得尤其重要,因为你面对的可能是上百种终端,如果为每一个终端都专门重新编写一套新的程序,那么复杂程度出乎想象,而且几乎不可能。为了能够达到这样的目的,curses包使用了终端描述数据库(Termin al Descri ption Databa ses)termin fo(TERMin al INFOrm ation databa se)或者term cap(TERMin al
CAPabi litie databa se),这两个数据库里存放了不同终端的操作控制码和转义序列以及其余相关信息,这样当使用每一个终端的时候,curses将首先在终端描述数据库中查是否存在该类型的终端描述信息,如果到则进行适当的处理。如果数据库中没有这种终端信息,则程序无法在该终端上运行,除非用户自己增加新的终端描述。具体的如何在终端描述数据库中增加自定义终端在第八章“termin fo数据库”中有详细的介绍。
1.1.1 curses发展历史
curses是怎么来的?curses的名称起源于“cursor optimi zatio n”,即光标优化的意思。它最早是由巴克利大学的B illJoy和Ke n Arnold发展而来,主要是处理游戏rogu e的屏幕界面。rogue是一个古老的基于文本的的冒险类游戏。在当时,仅仅控制游戏屏幕的外观显示就需要编写大量的代码,因为它们使用的是古老的t ermi os甚至是tty接口。巨大的工作量迫使Bil l Joy和Ke n Arnold将rogu e游戏中的所有的屏幕处理和光标移动的函数汇集到一个函数库中。这就形成了最早的也是最简单的curses处理库的雏形。它最终随着B SD UNIX的早期版本发行开来。在这个版本中使用的是当时业已存在的term cap数据库来描述终端信息。
后来贝尔实验室的Mark Horton在Syst em III UNIX中重新编写了c urses。它相对以前的版本有了很大的扩展和提高,增加了一些非常新的特性。它首先将termcap数据库改进为
termin fo数据库。termin fo数据库完全由Horton开发编写,它是从termcap发展而来,而且更为中要重要的是其中引进了参数化性能的概念,这样使得描述多视频属性以及彩终端成为可能。在后来的AT&T System V 版本中,curses就扩展了更多功能和性能,包括了对窗体、菜单、面板、表单等组件以及对鼠标的支持。这时候的curses内容以及设计与最初的BSD版本的curses在功能和复杂性上已经相去甚远。
1.1.2 curses包内容
本书的curses以S ystem V UNIX的版本为主,curses包主要包括下面的四个开发库,如表1.1所示。在后面的章节中我们会针对每一个库进行详细深入的探讨。
表1.1 curses包内容
1.1.3 curses包移植性
正如前言部分我们曾经提到过,使用curses包与使用低层终端函数编写的程序最主要的差别在于c urse s程序是独立于具体终端的,也就是说在某个终端上编写的程序可以完整的移植到另外的终端上而不需要进行任何改动。curses包的可移植性是curses包的最大特性。curses 包的这种终端独立性归根于终端描述数据库t ermin fo和te rmcap。termin fo 和termcap数据库中包含了所有终端的描述信息。termc
a p数据库是在最早的的B SD UNIX中使用,在后来的SystemIII中则使用terminfo数据库。termin fo数据库是从termcap数据库发展而来,组织方式相对于term cap来说有了进一步的优化,而且描述的终端信息有了进一步的增加。需要使用的数据库可以在程序编译的时候通过c c命令指定,具体的细节在这一章的末尾会有探讨。
正如前面所说,curses正是通过使用terminfo数据库使得程序可以在不同的终端上可以移植,那么系统是如何做到这一点的呢?
从第一章的图0.1可以看出,对于使用curses进行处理的程序员来说他实际上处理的是虚拟终端。curses完成了物理终端到虚拟终端的“映射”。用curse s编写的程序在它们每次被调用的时候都需要引用终端描述数据库。数据库中的终端描述信息包括了终端的一系列的性能参数,在curse s包中我们定义了很多的变量与这些性能参数对应。当程序执行的时候,程序首先获取终端类型,然后根据终端类型获取终端描述数据库中具体的性能,最后将这些性能参数读进c urse s中预定义的相应的变量中。当程序与终端进行交互从而需要调用相应的函数的时候,它将从头文件的性能变量中为终端获取必要的控制码,一旦需要某个性能参数,只要到相应的变量即可,从而达到以不变应万变的效果。例如在curses包中我们定义了L INES和COLS变量对应终端能够显示的最大行数和最大列数这两个性能,不同的终端的L INES和COLS的值可能不同,比如通常的终端的行数为39行,如果使用了软标签,行数将减一变为38。但这种变化都由curses幕后自动完成,用户完全不需要理会,用户需要记住的仅是LIN
ES和C OLS以及它们代表的含义。这样,程序就可以运行在各种不同的终端上,唯一的缺陷就是这种终端首先必须在终端信息描述库中存在,否则就无法直接使用curses包,弥补的办法就是需要自己在终端信息描述库中增加终端描述信息。
1.2使用cur ses包示例
1.2.1简单的curses应用程序
现在我们先看一个简单的c urse s应用程序1-1,这个程序中包含了curses包中最常使用的一些函数,也许开始看不懂,我们会在后面进行详细的讲解。
程序1-1 简单的curses程序
程序名称bullseye.c
编译命令 cc –o bullseye bullse ye.c –lcurse s
#include <curses.h>
#include <signal.h>
static void finish(int sig);
main(int argc,char **argv)
{
(void)sigaction(SIGINT,finish);
initscr();//初始化curses包
keypad(stdscr,TRUE);//允许键盘映射
(void)nonl();
(void)cbreak();
(void)noecho();
//判断是否支持彩
if(has_colors())
{
start_color();
//初始化颜配对表
init_pair(0,COLOR_BLACK,COLOR_BLACK);
init_pair(1,COLOR_GREEN,COLOR_BLACK);
init_pair(2,COLOR_RED,COLOR_BLACK);
init_pair(3,COLOR_CYAN,COLOR_BLACK);
init_pair(4,COLOR_WHITE,COLOR_BLACK);
init_pair(5,COLOR_MAGEN TA,COLOR_BLACK);
init_pair(6,COLOR_BLUE,COLOR_BLACK);
init_pair(7,COLOR_YELLO W,COLOR_BLACK);
}
attron(A_BLIN K|COLOR_PAIR(2));
move(LINES/2+1,COLS-4);
addstr(“Eye”);
refresh();
sleep(2);
move(LINES/2 –3,COLS/2-3);
addstr(“Bulls”);
refresh();
sleep(2);
finish(0);
}
static void finish(int sig)
{
endwin();
exit(0);
}
在上面的程序1-1中我们只是简单的将光标移动到屏幕中央附近的两个不同位置,然后在
这两个位置上输出单词BlueEye和Bulls,字体的颜分量分别为(Green,Green,Black),并同时进行闪烁。我们通过函数m ove()进行光标移动以及函数a ddstr()输出单词。下面我们详细讨论这个程序所涉及到的问题,这些问题对所有的使用c urses包的程序都是非常重要的。
1.2.2开始使用curses包
1.2.2.1头文件
每一个使用c urses包的程序都必须在程序中包括相应库所使用的头文件。头文件中定义了各种各样的数据类型以及宏,同时声明了各种能够在程序中引用的常量和函数。我们通常所用到的头文件如表1.2所示。
表1.2 curses包以及其对应的头文件
程序都必须包含的,它定义的一些公共的函数和变量是每一个curses程序都需要的。另外,在程序进行编译的时候我们必须将使用到的所有的库都一起编译进去,否则程序将无法编译通过。如果用户在A T&T UNIX PC上使用终端访问方法(TAM –Termin al Access Method)进行编程的话,则还需要包括T AM库。一旦程序正确编译,它就可以执行并进行调试。同时系统中的环境变量必须设置正确,这样编译程序才能到终端描述数据库。
示例程序的一开始我们就包括了头文件curses.h。curses.h中定义了L INES和COLS两个变量。程序中通过这两个变量来计算光标的位置从而能够通过mo ve函数将光标放置在屏幕的中央附近。由于这两
个值是与具体的终端的尺寸关联的,因此不管我们的程序运行在什么样的终端上,光标的位置都是处于屏幕的相同的位置。
另一方面curses.h中也定义了refre sh(),实际上它是一个宏定义,具体的定义如下:
#define refres h() wrefresh(stdscr)
从上面的定义可以看出,窗口中调用r efres h()实际上是调用函数wrefresh()来对标准屏幕进行刷新。不仅refresh(),事实上curses中的很多函数都是这种伪函数。它们之间遵循一定的命名规范,我们在第二章将详细讨论。
为了能够在程序意外中断的时候对c urses包进行必要的处理,我们对中断信号进行适当处理,因此必须包含信号处理的头文件signal.h。同时我们定义了信号处理函数fini sh();
1.2.2.2 curses初始化
在主函数中设置了信号处理函数之后我们就调用了init scr(),一般情况下在其余的curses函数被调用之前我们就必须首先调用i nitscr()。initsc r()对curse s包进行一些初始化的工作,而且在每一个程序里面,这个函数只能调用一次。它的作用主要包括下面几个方面:
■通过读取TE RM环境变量的值来决定当前使用的终端类型,开启终端模式。
■根据终端的具体情况将终端的一些性能参数读进相关变量中,完成对相关数据结构的初始化工作,例如示例程序1-1正是在in itscr()中获取了LINES和C OLS的值。
■创建和初始化标准屏幕s tdscr和当前屏幕curscr,同时为它们分配必要的存储空间。
■通知refresh()函数首次调用的时候能够清除屏幕
如果在终端初始化的过程中遇到错误,比如程序当前运行的终端在终端信息描述库中并没有描述,那么程序将会在stde rr上输出错误信息,同时退出程序。另一方面,由于init scr()涉及到窗口空间分配,因此可能导致内存溢出,虽然这种情况极少发生。一旦发生,initsc r()将中断处理同时返回错误信息。
需要强调的一点是,initsc r()必须在所有其它的操作s tdscr和curs cr的函数之前调用,否则一旦引用到窗口的地方程序将会由于应用程序段寻址错误而“core dump”。因此大部分情况下我们总是在程序开始就调用i nitsc r()。其余一些函数如果不涉及到窗口方面就可以在initsc r()之前调用,比如slk_init(),filter(),ripoff lines(),use_en v()等等。
如果你的程序使用的是多个终端,那么我们将使用newt erm()代替init scr()。对于每一个你希望与之交互的终端设备,都调用一次n ewter m()。newter m()返回一个SCREEN结构,用来引用某个终端。在需要从某个终端接受输入或者进行输出时候,必须通过set_ter m()将它设置为当前终端,所有的curses函数操作的仅仅是当前终端。
1.2.2.3终端模式设置
程序使用initscr()进行初始化之后,程序对终端的模式进行了一些设置。终端模式实际上是一系列的开关属性,它们直接影响着终端如何处理输入以及输出。具体的关于终端设置模式的细节在第二章的终端模式一节中会讨论,这里仅仅一带而过。
keypad()用来控制是否将键盘上的特殊字符比如上下左右键等转换成c urse s包中定义的对应的特殊键。比如将“↓”对应成KEY_DOWN,“→”转换成KEY_RIGH T。程序1-1中将建立这种映射关系。
nonl()用来控制程序将回车键不要转换为换行符。
cbreak()用来控制程序一一读取除了DELE TE或者C TRL等特殊字符以外的所有字符。noecho()使得键盘输入的字符不需要直接在屏幕上显示出来,这通常在将按键作为控制键时候非常有用。
1.2.2.4颜处理
为了能够使得显示的字符为彩,我们必须设置彩属性。在设置彩属性之前我们必须能够判断终端是否支持彩,为此调用函数h as_colors()来判断。一旦终端支持彩,我们将使用i nit_p air()开始初始化颜配对表,颜配对表用来设置字符的前景和背景。关于它的细节在第二章后详细的讨论。
linux下的sleep函数示例程序中我们在颜配对表中设置了八个条目,它们的背景都是黑。这样在使用的时候我们只要指定颜配对表中的条目索引就可以设置字符的彩。示例程序中我们使用attron(A_BLIN K|COLOR_PAIR(2))将需要显示的字符设置为闪烁,同时颜为颜配对表的索引号为2的条目中指定的颜,即为红前景,黑背景。
1.2.2.5使用refresh()和wrefresh()进行屏幕更新
为了能够使屏幕更新时候取得最佳效果,即使我们已经对屏幕进行了更改,比如输出了一个字符,curses函数也不会立即更新到终端屏幕上,它将每一次的更新累积起来,只有在程序中调用了函数r efre sh()或者wrefresh()之后,屏幕才会真正进行更新,从而将终端上的各
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论