Unix/Linux下的Curses库开发指南——第一章 Curses库开发简介

来源:互联网 发布:mac邮件连接失败 编辑:程序博客网 时间:2024/04/29 04:57

 

1.1什么是curses 

curses实际上是一个函数开发包,专门用来进行UNIX下终端环境下的屏幕界面处理以及I/O处理。通过这些函数库,CC++程序就可以控制终端的视频显示以及输入输出。使用curses包中的函数,用户可以非常方便的创建和操作窗口,使用菜单以及表单,而且最为重要的一点是使用curses包编写的程序将独立于各种具体的终端,这样的一个直接的好处就是程序具有良好的移植性。这一点在网络上显得尤其重要,因为你面对的可能是上百种终端,如果为每一个终端都专门重新编写一套新的程序,那么复杂程度出乎想象,而且几乎不可能。为了能够达到这样的目的,curses包使用了终端描述数据库(Terminal Description Databases)terminfo(TERMinal INFOrmation database)或者termcapTERMinal CAPabilitie database,这两个数据库里存放了不同终端的操作控制码和转义序列以及其余相关信息,这样当使用每一个终端的时候,curses将首先在终端描述数据库中查找是否存在该类型的终端描述信息,如果找到则进行适当的处理。如果数据库中没有这种终端信息,则程序无法在该终端上运行,除非用户自己增加新的终端描述。具体的如何在终端描述数据库中增加自定义终端在第八章“terminfo数据库”中有详细的介绍。

 

1.1.1 curses发展历史

curses是怎么来的?curses的名称起源于“cursor optimization”,即光标优化的意思。它最早是由巴克利大学的Bill JoyKen Arnold发展而来,主要是处理游戏rogue的屏幕界面。rogue是一个古老的基于文本的的冒险类游戏。在当时,仅仅控制游戏屏幕的外观显示就需要编写大量的代码,因为它们使用的是古老的termios甚至是tty接口。巨大的工作量迫使Bill JoyKen Arnoldrogue游戏中的所有的屏幕处理和光标移动的函数汇集到一个函数库中。这就形成了最早的也是最简单的curses处理库的雏形。它最终随着BSD UNIX的早期版本发行开来。在这个版本中使用的是当时业已存在的termcap数据库来描述终端信息。

 

后来贝尔实验室的Mark HortonSystem III UNIX中重新编写了curses。它相对以前的版本有了很大的扩展和提高,增加了一些非常新的特性。它首先将termcap数据库改进为 terminfo数据库。terminfo数据库完全由Horton开发编写,它是从termcap发展而来,而且更为中要重要的是其中引进了参数化性能的概念,这样使得描述多视频属性以及彩色终端成为可能。在后来的AT&T System V 版本中,curses就扩展了更多功能和性能,包括了对窗体、菜单、面板、表单等组件以及对鼠标的支持。这时候的curses内容以及设计与最初的BSD版本的 curses在功能和复杂性上已经相去甚远。

1.1.2 curses包内容

本书的cursesSystem V UNIX的版本为主,curses包主要包括下面的四个开发库,如表1.1所示。在后面的章节中我们会针对每一个库进行详细深入的探讨。

表1.1 curses包内容

库名

描述

curses

最早的curses包只包含这一部分,主要控制屏幕的输入和输出,光标的操作,窗口的创建和操作等。

panel

类似于窗口堆栈,不同的窗口可以存放于其中,并且可以在其中进行移动。

menu

新增的部分,主要包括创建菜单并且与之交互的函数,主要用来接受用户的选择。

form

包括创建表单以及与之进行交互的函数,主要用来接受用户数据输入

 

1.1.3 curses包移植性

正如前言部分我们曾经提到过,使用curses包与使用低层终端函数编写的程序最主要的差别在于curses程序是独立于具体终端的,也就是说在某个终端上编写的程序可以完整的移植到另外的终端上而不需要进行任何改动。curses包的可移植性是curses包的最大特性。curses包的这种终端独立性归根于终端描述数据库terminfotermcapterminfo termcap数据库中包含了所有终端的描述信息。termcap数据库是在最早的的BSD UNIX中使用,在后来的System III中则使用terminfo数据库。terminfo数据库是从termcap数据库发展而来,组织方式相对于termcap来说有了进一步的优化,而且描述的终端信息有了进一步的增加。需要使用的数据库可以在程序编译的时候通过cc命令指定,具体的细节在这一章的末尾会有探讨。

 

正如前面所说,curses正是通过使用terminfo数据库使得程序可以在不同的终端上可以移植,那么系统是如何做到这一点的呢?

 

从第一章的图0.1可以看出,对于使用curses进行处理的程序员来说他实际上处理的是虚拟终端。curses完成了物理终端到虚拟终端的“映射”。用curses编写的程序在它们每次被调用的时候都需要引用终端描述数据库。数据库中的终端描述信息包括了终端的一系列的性能参数,在curses包中我们定义了很多的变量与这些性能参数对应。当程序执行的时候,程序首先获取终端类型,然后根据终端类型获取终端描述数据库中具体的性能,最后将这些性能参数读进curses中预定义的相应的变量中。当程序与终端进行交互从而需要调用相应的函数的时候,它将从头文件的性能变量中为终端获取必要的控制码,一旦需要某个性能参数,只要找到相应的变量即可,从而达到以不变应万变的效果。例如在curses包中我们定义了LINESCOLS变量对应终端能够显示的最大行数和最大列数这两个性能,不同的终端的LINESCOLS的值可能不同,比如通常的终端的行数为39行,如果使用了软标签,行数将减一变为38。但这种变化都由curses幕后自动完成,用户完全不需要理会,用户需要记住的仅是LINESCOLS以及它们代表的含义。这样,程序就可以运行在各种不同的终端上,唯一的缺陷就是这种终端首先必须在终端信息描述库中存在,否则就无法直接使用curses包,弥补的办法就是需要自己在终端信息描述库中增加终端描述信息。

1.2使用curses包示例

1.2.1简单的curses应用程序

现在我们先看一个简单的curses应用程序1-1,这个程序中包含了curses包中最常使用的一些函数,也许开始看不懂,我们会在后面进行详细的讲解。

程序1-1 简单的curses程序

程序名称 bullseye.c

编译命令 cc –o bullseye  bullseye.c –lcurses

#include <curses.h>

#include <signal.h>

static void finish(int sig)

main(int argcchar **argv)

{

    (void)sigaction(SIGINTfinish)

    initscr()//初始化curses

    keypad(stdscrTRUE)//允许键盘映射

    (void)nonl()

    (void)cbreak()

    (void)noecho()

    //判断是否支持彩色

    if(has_colors())

    {

         start_color()

         //初始化颜色配对表

         init_pair(0COLOR_BLACKCOLOR_BLACK)

         init_pair(1COLOR_GREENCOLOR_BLACK)

         init_pair(2COLOR_REDCOLOR_BLACK)

         init_pair(3COLOR_CYANCOLOR_BLACK)

         init_pair(4COLOR_WHITECOLOR_BLACK)

         init_pair(5COLOR_MAGENTACOLOR_BLACK)

         init_pair(6COLOR_BLUECOLOR_BLACK)

         init_pair(7COLOR_YELLOWCOLOR_BLACK)

    }

    attron(A_BLINK|COLOR_PAIR(2))

    move(LINES/2+1COLS-4)

    addstr(Eye)

    refresh()

    sleep(2)

    

    move(LINES/2 3COLS/2-3)

    addstr(Bulls”)

    refresh()

    sleep(2)

 

    finish(0)

}

static void finish(int sig)

{

    endwin()

    exit(0)

}

在上面的程序1-1中我们只是简单的将光标移动到屏幕中央附近的两个不同位置,然后在这两个位置上输出单词BlueEyeBulls,字体的颜色分量分别为(GreenGreenBlack),并同时进行闪烁。我们通过函数move()进行光标移动以及函数addstr()输出单词。下面我们详细讨论这个程序所涉及到的问题,这些问题对所有的使用curses包的程序都是非常重要的。

 

1.2.2开始使用curses

1.2.2.1头文件

每一个使用curses包的程序都必须在程序中包括相应库所使用的头文件。头文件中定义了各种各样的数据类型以及宏,同时声明了各种能够在程序中引用的常量和函数。我们通常所用到的头文件如表1.2所示。

表1.2  curses包以及其对应的头文件

库名

头文件

curses

curses.h

panel

panel.h

menu

menu.h

form

form.h

我们在使用curses编写程序的时候可能会用到上面的一个以上的库。但是curses库是每一个程序都必须包含的,它定义的一些公共的函数和变量是每一个curses程序都需要的。另外,在程序进行编译的时候我们必须将使用到的所有的库都一起编译进去,否则程序将无法编译通过。如果用户在AT&T UNIX PC上使用终端访问方法(TAM Terminal Access Method)进行编程的话,则还需要包括TAM库。一旦程序正确编译,它就可以执行并进行调试。同时系统中的环境变量必须设置正确,这样编译程序才能找到终端描述数据库。

 

示例程序的一开始我们就包括了头文件curses.hcurses.h中定义了LINESCOLS两个变量。程序中通过这两个变量来计算光标的位置从而能够通过move函数将光标放置在屏幕的中央附近。由于这两个值是与具体的终端的尺寸关联的,因此不管我们的程序运行在什么样的终端上,光标的位置都是处于屏幕的相同的位置。

 

另一方面curses.h中也定义了refresh(),实际上它是一个宏定义,具体的定义如下:

#define refresh()  wrefresh(stdscr)

从上面的定义可以看出,窗口中调用refresh()实际上是调用函数wrefresh()来对标准屏幕进行刷新。不仅refresh(),事实上curses中的很多函数都是这种伪函数。它们之间遵循一定的命名规范,我们在第二章将详细讨论。

为了能够在程序意外中断的时候对curses包进行必要的处理,我们对中断信号进行适当处理,因此必须包含信号处理的头文件signal.h。同时我们定义了信号处理函数finish()

1.2.2.2 curses初始化

在主函数中设置了信号处理函数之后我们就调用了initscr(),一般情况下在其余的curses函数被调用之前我们就必须首先调用initscr()initscr()curses包进行一些初始化的工作,而且在每一个程序里面,这个函数只能调用一次。它的作用主要包括下面几个方面:

■ 通过读取TERM环境变量的值来决定当前使用的终端类型,开启终端模式。

■ 根据终端的具体情况将终端的一些性能参数读进相关变量中,完成对相关数据结构的初始化工作,例如示例程序1-1正是在initscr()中获取了LINESCOLS的值。

■ 创建和初始化标准屏幕stdscr和当前屏幕curscr,同时为它们分配必要的存储空间。

■ 通知refresh()函数首次调用的时候能够清除屏幕

如果在终端初始化的过程中遇到错误,比如程序当前运行的终端在终端信息描述库中并没有描述,那么程序将会在stderr上输出错误信息,同时退出程序。另一方面,由于initscr()涉及到窗口空间分配,因此可能导致内存溢出,虽然这种情况极少发生。一旦发生,initscr()将中断处理同时返回错误信息。

 

需要强调的一点是,initscr()必须在所有其它的操作stdscrcurscr的函数之前调用,否则一旦引用到窗口的地方程序将会由于应用程序段寻址错误而“core dump”。因此大部分情况下我们总是在程序开始就调用initscr()。其余一些函数如果不涉及到窗口方面就可以在initscr()之前调用,比如slk_init()filter()ripofflines()use_env()等等。

 

如果你的程序使用的是多个终端,那么我们将使用newterm()代替initscr()。对于每一个你希望与之交互的终端设备,都调用一次newterm()newterm()返回一个SCREEN结构,用来引用某个终端。在需要从某个终端接受输入或者进行输出时候,必须通过set_term()将它设置为当前终端,所有的curses函数操作的仅仅是当前终端。

 

1.2.2.3终端模式设置

程序使用initscr()进行初始化之后,程序对终端的模式进行了一些设置。终端模式实际上是一系列的开关属性,它们直接影响着终端如何处理输入以及输出。具体的关于终端设置模式的细节在第二章的终端模式一节中会讨论,这里仅仅一带而过。

keypad()用来控制是否将键盘上的特殊字符比如上下左右键等转换成curses包中定义的对应的特殊键。比如将“↓”对应成KEY_DOWN,“”转换成KEY_RIGHT。程序1-1中将建立这种映射关系。

nonl()用来控制程序将回车键不要转换为换行符。

cbreak()用来控制程序一一读取除了DELETE或者CTRL等特殊字符以外的所有字符。

noecho()使得键盘输入的字符不需要直接在屏幕上显示出来,这通常在将按键作为控制键时候非常有用。

1.2.2.4颜色处理

为了能够使得显示的字符为彩色,我们必须设置色彩属性。在设置色彩属性之前我们必须能够判断终端是否支持彩色,为此调用函数has_colors()来判断。一旦终端支持彩色,我们将使用init_pair()开始初始化颜色配对表,颜色配对表用来设置字符的前景色和背景色。关于它的细节在第二章后详细的讨论。

 

示例程序中我们在颜色配对表中设置了八个条目,它们的背景色都是黑色。这样在使用的时候我们只要指定颜色配对表中的条目索引就可以设置字符的色彩。示例程序中我们使用attron(A_BLINK|COLOR_PAIR(2))将需要显示的字符设置为闪烁,同时颜色为颜色配对表的索引号为2的条目中指定的颜色,即为红色前景,黑色背景。

1.2.2.5使用refresh()wrefresh()进行屏幕更新

为了能够使屏幕更新时候取得最佳效果,即使我们已经对屏幕进行了更改,比如输出了一个字符,curses函数也不会立即更新到终端屏幕上,它将每一次的更新累积起来,只有在程序中调用了函数refresh()或者wrefresh()之后,屏幕才会真正进行更新,从而将终端上的各个窗口之间的相互变化的累积结果反映出来。refresh()更新默认窗口,wrefresh()用来更新程序自定义窗口而不是curses的默认窗口。

 

默认窗口通常称之为stdscr,即标准屏幕,它的尺寸即是终端屏幕的大小。如果程序中使用curses,那么它将由curses自动产生。stdscr是屏幕更新后的逻辑屏幕,它的显示内容在终端屏幕上不一定能够立即看的到。我们在终端屏幕上看到的是curses中提供的另外一个默认窗口curscr,即当前窗口。它用来跟踪在物理屏幕上的当前显示结果。stdscrcurscr的内容并不完全相同。当refresh() 调用的时候,stdscr将与curscr进行对比,然后仅仅更新它们之间的不同之处,一旦更新,curscr将显示stdscr上的内容,这样当前屏幕上反映内容就与标准屏幕上的一样。下面的图1.1和图1.2演示了当你执行程序1-1在屏幕上输出单词 EyeBulls的时候所发生的一切,从图中可以看出stdscrcurscr的不同之处。

 

屏幕刷新是curses包中非常重要的一个部分,为了能够理解refresh(),我们从下面的两个非常相近的程序1-21-3的运行结果来看refresh()对程序执行结果的影响。

程序1-2 refresh()示例程序

程序名称 refresh1.c

编译命令 cc –o refresh1  refresh1.c –lcurses

#include <stdio.h>

#include <curses.h>

 

int main()

{

          int line

          int i

          char c

 

          initscr()

          refresh()

          for(line=0line<LINESline++){

                  move(lineline)

                  c=line+'0'

                  addch(c)

                 for(i=0i<500000i++)

          }

          endwin()

          refresh(); /* notice! refresh() is here! */

}

   }

 

程序1-3 refresh()示例程序

程序名称 refresh2.c

编译命令 cc –o refresh2  refresh2.c –lcurses

#include <stdio.h>

#include <curses.h>

 

int main()

{

        int line

        int i

        char c

 

        initscr()

        refresh()

        for(line=0line<LINESline++){

                move(lineline)

                c=line+'0'

                addch(c)

                for(i=0i<500000i++)

                refresh(); /* refresh() is moved to here!! */

        }

        endwin()

}

为了更好了理解refresh()的作用以及它对程序执行效率的影响,我们必须的比较这两个程序的差别。可以看出,它们的不同之处就在与它们的refresh()的位置的不同。程序1-2refresh()在循环的外面,而程序1-3refresh()在循环的里面。分别执行这两个程序我们可以看出对于程序1-2,执行结果是一次性显示出来,而对于程序1-3,执行结果是一个一个的显示出来的,即每调用一次refresh()即更新一次屏幕。

 

另一方面,从程序的执行速度上可以看出,程序1-2的速度远比程序1-3快,从理论上来计算:程序1-3中一共refresh()LINES次,而程序1-2仅更新了一次。如果我们将LINES替换成一个更大的数,那么程序1-3中的速度可想而知。

1-1(下页面继续)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(接上页

1-2

1.2.2.6使用endwin()函数中断curses程序

使用curses包的程序必须在程序结束之前能够恢复所有的初始终端设置,因为我们在程序执行过程中可能将终端的某些模式设置或者性能更改掉。这些我们可以通过函数endwin()函数来实现。另外在初始化的时候我们为stdscrcurscr分配的空间必须释放,这意味着stdscr不再受影响。endwin()执行后光标移至于屏幕的左下角的位置。

在上面的三个程序中,endwin()是程序最后调用的一个函数,它与initscr()首尾呼应。

1.2.3 terminfotermcap

为了能够在ANSI终端上运行程序1-1,程序就必须知道变量LINESCOLS的值即终端屏幕的总行数和总列数。在terminfo数据库中关于ANSI终端的描述信息中通过下面的一行来描述上面两个性能。

cols#80lines#25

如果终端使用termcap数据库,则相应的性能描述为

co#80:li#25

当程序开始执行的时候,curses将首先根据环境变量$TERM获得终端类型,然后读取terminfo或者termcap数据库根据终端类型获取终端性能描述,最终总行数和总列数这两个性能参数将被导进到相应变量COLSLINES中。这两个变量在curses.h中声明。当我们调用move()函数来移动光标的位置的时候,新的位置将基于COLSLINES的值。由于这些参数是根据终端性能自动设置的,因此程序可以在任何类型的终端上运行而不会导致运行结果有明显的差异。

 

curses中使用的终端类型由TERM环境变量决定,我们可以通过命令echo $TERM查看,也可以通过命令来进行重新设置或者可以在象 .profile 和 .cshrc的文件中进行定义然后在登录的时候自动读入。如果登录shellBshell或者Kshell,则TERM设置如下:     

TERM=terminaltypeexport TERM

如果登录的是C shellTERM的设置为:

setenv TERM terminaltype

在上面的例子中terminaltype是正在使用的终端的名字,一般的终端类型为vt100,它不是在terminfo中定义就是在termcap中定义。比如,对于ANSI终端来说,TERM将被设置成“ansi”。如果使用的是terminfo数据库,使用curses包的程序将自动引用/usr/lib/terminfo/a/ansi文件来找到必须的性能参数。如果使用的是termcap数据库,程序将从/etc/termcap文件中获取终端的相关信息。

 

我们可以指定一个自己的终端数据库来替代terminfotermcap。做到这一点只需要分别简单的修改环境变量TERMINFOTERMCAP就可以了。这对于那些想在终端信息描述库中增加新的终端描述信息或者修改已存在的描述信息而又不想真正改变数据库本身的人是非常有用的。

 

TERMINFO的值通常设置成包含终端描述信息的文件的路径名,这个终端描述信息的格式与正常的terminfo的描述信息格式相同。然后通过命令tic编译这个文件。

TERMCAP的值通常设置为包含终端描述信息而且格式与文件/etc/termcap相同的文件的路径名,与TERMINFO不同,这个文件不需要进行另外的编译就可以直接使用。如果需要额外的终端描述信息,那么唯一需要的仅是与程序使用的终端描述信息数据库对应的环境变量。

这两个数据库我们都可以通过一些curses包之外的比较低级的函数进行直接访问,比如tgetent()tgetnum()等等。:多数情况下我们并不鼓励这么做,因为这样编写的程序不具有curses的移植性。 

 

关于terminfotermcap和低层函数的使用我们在“terminfo数据库”章节中会有详细的讨论和讲解,另外你也可以通过帮助文件获取更多的信息。

 

1.2.4编译curses程序

如果程序中使用了curses包,那么在程序编译的时候它必须链接程序中引用的库。在cc编译命令中通过使用 选项来指定编译所必须的库。通常用来编译curses程序所用的命令如下:

   cc o outputfile file1.c file2.c  [-l X] lcurses SCO UNIX

或者

   gcc o outputfile file1.c file2.c  [-l X] lcurses Linux

-lcurses选项将在编译的时候链接curses包。之所以所有的程序都必须链接curses包是因为它包含了所有的初始化操作,基本的输入和输出操作,以及更新和中断操作。[-l X]指明了一个可选的额外的库,主要包括panelmenuform或者tam。如果我们需要一个以上的额外的库,那么必须将这些库都用-l列举出来。outputfile是输出的可执行文件的名称,file1.cfile2.c是程序包含的文件。

 

需要注意的一点是,如果我们程序中使用了panelmenuform等库,-lcurses选项必须在其余的选项之后,至于X之间的顺序无所谓。在目录/usr/lib的下面我们可以找到这些库。每一个库的名字都类似于libX.aX指定了具体的库名称,比如libcurses.alibpanel.a、等等。cc 命令能够理解这些命名规定。

 

任何使用curses包的程序都必须使用到terminfo或者temcap之中的任何一个数据库。默认的终端描述数据库为terminfo,但是我们可以改变使用的默认数据库。为了能够确定当前使用的数据库,在文件/usr/include/curses.h中搜索下面的几行就可以了。

#if !defined(_M_TERMINFO) && !defined (_M_TERMCAP)

#define _TERMINFO

#endif

从上面的代码中可以看出,如果定义了_M_TERMINFO,则默认的终端描述数据库为terminfo。如果定义了_M_TERMCAP,则termcap为默认的数据库。如果我们所希望使用的数据库也正是当前的默认数据库,那么程序可以使用上面的命令进行编译。否则程序在编译的时候必须使用-D选项来指定数据库。例如,如果默认的数据库为terminfo,那么可以在cc命令中通过包含下面的选项来指定使用数据库termcap:

cc D_M_TERMCAP  -ltcap ltermlib

相反如果默认的数据库为termcap,则可以通过包含下面的选项来指定使用数据库terminfo:

cc D_M_TERMCAP -ltinfo

 

如果我们在AT&T UNIX机器上运行curses程序,那么我们还必须将TAM交易库包括进去。为此除了curses.h头文件我们还必须包含tam.h头文件:

#include <tam.h>

为了能够编译包含TAM交易库的程序,我们使用cc命令的时候还必须进行额外的设置。TAM的头文件在/usr/include/tam目录下,cc命令正常情况下并不会读取这个目录,如果需要指定cc读取这个目录的话,可以通过cc-I选项实现。另一方面我们必须在cc命令行中将tam库链接进去,因此命令行将会如下:

cc -I /usr/include/tam  -ltam 

 

1.2.5运行使用curses编写的程序

使用curses包的程序在运行的时候将读取环境变量以获取程序当前运行的终端的相关信息。首先TERM环境变量必须设置成允许程序使用curses包来决定终端的类型。使用curses包的程序也需要使用到环境变量TERMINFOTERMCAP,如果他们都有定义的话。一旦需要的这些环境变量都已经设置好,程序就可以正常运行了。

 

如果一个curses程序不能获得预想中的结果,那么我们可以通过dbxtra或者codeview进行调试。正常情况下编译器的调试跟踪选项是关闭的。为了启动调试功能,我们必须在cc命令中增加-g选项。关于这两个调试器的具体的帮助信息可以参考有关调试方面的书籍。这两个调试器都会产生一个模拟vt100终端的窗口。它允许curses程序调试的时候好象运行在vt100终端上。当程序运行的时候,这种模仿由codeview自动产生。在dbxtra中在dbxtra启动之后我们必须在提示行输入vt100来进行设置,例如:

dbxtra programname

(dbxtra) vt100

在调试的时候需要主要几个方面。首先,使用curses的程序它会将它的输出缓存起来以使屏幕更新达到最优效果。这些输出并不会立即在屏幕上显示出来而是要等到refresh()或者wrefresh()函数调用之后才会进行显示。这样的一个结果就是程序中的一些代码可能希望屏幕上的某个部分有所改变,但实际上却好象什么都没有发生一样。因此在调试的时候为了能够立即显示更改结果,我们可以增加refresh()wrefresh()函数的调用次数。在程序能够正常运行的时候在把它们再去掉。

 

另外一个问题就是设置在curses包上的一些断点并不会正常工作,原因就是这些函数实际上是引用另外一个函数的宏定义。这个函数的解决方法就是将断点设置在实际函数上而不是宏定义上。例如,refresh()函数实际上是一个wrefresh()的宏定义。如果我们需要在refresh()上设置断点的话,那么真正的断点就必须设在wrefresh()上。

原创粉丝点击