曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 20088|回复: 27
打印 上一主题 下一主题

[显示] [专题]通过 curses 管理基于文本的屏幕控制

[复制链接]

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
跳转到指定楼层
楼主
发表于 2009-3-16 21:49:56 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

http://www.groad.net/bbs/read.php?tid=620
http://www.groad.net/bbs/read.php?tid=624
http://www.groad.net/bbs/read.php?tid=628
中,介绍了如何更精细的控制字符的输入以及怎样以一个与具体终端无关的形式提供字符输出。
但是,这种通过使用“通用终端接口”(GTI或 termios)和用用来操控转义字符的 tarm() 及其相关函数需要许多低等级的代码。然在许多程序中,更多需要的是一个高等级的接口。我们希望能够简单的在屏幕上进行简单的绘制以及通过一个函数库来自动的处理终端的相关性问题。

curses 库在简单的文本行程序(line-based)和全图形化(一般更难以编程)X 视窗系统程序间的重要性犹如一道过渡的桥梁(halfway house),如 GTK+/GNOME 和 Qt/KDE.

Linux 确实也有个 svgalib 库(超级 VGA 库,一个底层的图形库),但这并不是一个 UNIX 标准库,所以在其他的类 UNIX 系统里并不通用。

尽管是是编写基于字符的全屏幕程序,curses 库同样显得相当的简单并且与终端无关。用 curses 总比直接使用转义序列要来得简单。curses 也可以管理键盘,并提供一个使用简单且无阻塞(nonblocking)的字符输入模式。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
沙发
 楼主| 发表于 2009-3-17 02:14:37 | 只看该作者

使用 curses 编译

curses 库的名字来自于它的能力---优化光标的移动以及使屏幕上所需要的更新最小化,也就是说(and hence),减少了需要发送到基于文本的终端上的字符数量。尽管字符输出量比起以前的被动式终端/基本终端(dumb terminal)和低速调制解调器来说已经显得不太重要,但是 curses 库还是为作为一个挺有用的附加工具包保留了下来。

curses 是一个函数库,我们必须从一个适当的系统库文件里把所有有关的头文件、函数和宏定义等添加到我们的程序中去,这样才能享受它给我们带来的便利。

对于 curses 库,其实已经存在几个不同的实现版本。其原始版本见 BSD UNIX;在标准化 X/Open 之前,这个版本然后被集成进 system V 系列的 UNIX 里面。 Linux 使用 ncurses("new curses"), 这是为 linux 开发的自由软件函数库。这个实现对于其他的 UNIX 版本具有极高的可移植性。甚至是也有 MS-DOS 和 Windows 的 curses 版本。如果你发现在你喜欢的 UNIX 版本中所捆绑的 curses 没有支持一些特性,那可以尝试用 ncurses 的一个拷贝来替换之。Linux 用户通常都能在他们的系统里找到已安装好的 ncurses 库,或者至少也会包含有一些基于curses(curses-based)的软件所需要的组件。如果你的发行版系统的开发库里并没有预装curses(没有 curses.h 头文件或者没有 curses 库链接),那么通常也可以在大多主要的发行版的标准软件包里找到它,在里面可能的命名为: libncurses5-dev .

X/Open 技术规格定义了 curses 的两个等级:基础的和扩展的。扩展的 curses 包含了一些附加的子程序,这里面包括了在一定范围内可以处理多栏字符和颜色操作的子程序

在编译 curses 程序时,你必须包含头文件 curses.h,并且要用 -lcurses 选项来来链接 curses 库。在许多 Linux 系统里,你可以简单的使用 curses,但你也将发现你实际上正在使用的是更优秀,更新的 ncurses 实现!

检查一下本机中 curses 的建立情况
[root@localhost ~]# ls -l /usr/include/*curses.h
lrwxrwxrwx 1 root root 16 12-02 21:39 /usr/include/curses.h -> ncurses/curses.h
lrwxrwxrwx 1 root root 17 12-02 21:39 /usr/include/ncurses.h -> ncurses/ncurses.h

看一下相关库文件
[root@localhost ~]# ls -l /usr/lib/lib*curses*
lrwxrwxrwx 1 root root     12 12-02 21:39 /usr/lib/libcurses.a -> libncurses.a
lrwxrwxrwx 1 root root     17 12-02 21:39 /usr/lib/libcurses.so -> libncurses.so.5.5
lrwxrwxrwx 1 root root     13 12-02 21:39 /usr/lib/libcursesw.a -> libncursesw.a
lrwxrwxrwx 1 root root     18 12-02 21:39 /usr/lib/libcursesw.so -> libncursesw.so.5.5
-rw-r--r-- 1 root root 469236 2007-01-06 /usr/lib/libncurses.a
-rw-r--r-- 1 root root 123506 2007-01-06 /usr/lib/libncurses++.a
-rw-r--r-- 1 root root 584508 2007-01-06 /usr/lib/libncurses_g.a
lrwxrwxrwx 1 root root     15 12-02 21:39 /usr/lib/libncurses.so -> libncurses.so.5
lrwxrwxrwx 1 root root     17 12-02 21:36 /usr/lib/libncurses.so.5 -> libncurses.so.5.5
-rwxr-xr-x 1 root root 297464 2007-01-06 /usr/lib/libncurses.so.5.5
-rw-r--r-- 1 root root 123506 2007-01-06 /usr/lib/libncurses++w.a
-rw-r--r-- 1 root root 522346 2007-01-06 /usr/lib/libncursesw.a
-rw-r--r-- 1 root root 663886 2007-01-06 /usr/lib/libncursesw_g.a
lrwxrwxrwx 1 root root     16 12-02 21:39 /usr/lib/libncursesw.so -> libncursesw.so.5
lrwxrwxrwx 1 root root     18 12-02 21:36 /usr/lib/libncursesw.so.5 -> libncursesw.so.5.5
-rwxr-xr-x 1 root root 330428 2007-01-06 /usr/lib/libncursesw.so.5.5

如上所示,若在系统里找到了 curses.h 和 ncurses.h 已经存在并有了相应链接,并且看到 ncurses 库文件也已存在,那么可以使用以下命令进行编译:
gcc program.c -o promgram.exe -lcurses

如果你的 curses 配置情况不能自动使用 ncurses,那么你必须通过包含 ncurses.h 而不是 curses.h 明确强制使用 ncurses 库,通过一下命令来实现:
gcc -I/usr/include/ncurses program.c -o program -lncurses
这里,-I 选项的作用是指定检索头文件时将要使用的子目录路径。可下载代码形式的制作文件(makefile)会假设用户缺省使用的是 ncrses 函数库,如果用户必须对此做出修改,说不定在你的系统上还不得不手工完成编译工作。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
板凳
 楼主| 发表于 2009-3-17 23:25:03 | 只看该作者

curses 术语及概念

curses 例程工作在屏幕,视窗,以及子窗口上。“屏幕”就是我们这在写的设备,它占据了这个设备上所有可用的显示面积。当然,如果终端窗口在一个 X 窗口中,那么所谓的“屏幕”就是该终端窗口中所有可用的字符位置。

然而,至少总有一个 curses 视窗 stdscr ,它和物理屏幕一样大小。你可以额外创建一些小于屏幕的窗口。视窗可以互相重叠并且可以有很多个子窗口,但是每一个子窗口必须总是包含在父窗口中。

cruses 库维护了两个数据结构,此数据接够有点类似于一个终端屏幕的地图(用这两个数据结构来映射屏幕):
stdscr 与 curscr
这两个结构中,stdscr 相对来说比较重要一点,这个结构在 curses 函数输出时被更新。stdscr 数据结构是“标准屏幕”。它和 stdout 在很多方面都非常相似。在 curses 程序里,它是个默认的输出窗口。curscr 结构与之类似,但是它保持屏幕所显示的实际上看起来就像是发生在当前时刻。写到 stdscr 的输出不会立即出现在屏幕上,知道程序调用了 refresh() -- 这时候,curses 库会比较 stdscr 和 curscr (屏幕当前的样子)中的内容;然后,curses 会使用这两个结构的不同来更新屏幕。

一些 curses 程序需要了解 cruses 所维护的 stdscr 结构,所以几个函数需要它这样的一个参数。然而,实际上的 stdscr 结构与函数库的具体实现密切相关(implementation-dependent)并且永远不能直接去访问。curses 程序也永远无须用到 curscr 结构。

因此,在 curses 程序里处理输出字符的过程如下
[Plain Text] 纯文本查看 复制代码
使用 curses 函数去更新一个逻辑屏幕;
请求 curses 通过 refresh 来刷新物理屏幕。


这个两级跳的方法(tow-level approach)的好处,除了对程序设计方面更加容易外(in addition to 除了..之外),还有就是 curses 屏幕刷新得非常有效率。另外(in addtion),尽管这在一个控制台屏幕上看来不太重要,但如果你正在运行的程序是在一个低速网络链接上(低速串行口或调制解调器)时,这种差距看起来就很客观了。

一个 curses 程序会对逻辑屏幕输出函数做多次的调用,比如说可能会在整个屏幕上移动光标使之找到要写文本的位置以及划线与画框框。

在某个阶段(at some stage),用户需要查看全部的输出。这种情况一般发生在调用 refresh 期间,curses 将计算出物理屏幕对影逻辑屏幕的一个最佳方法。通过使用合适终端工具(性能指标)以及优化光标移动动作,curses 可以经常以更少的字符输出来更新屏幕,而不需要立即的去一次性的写一整屏的东西。

逻辑屏幕的布局是一个字符数组,数组下标由行号和列号组成,开始坐标(0,0)在屏幕的左上角。

所有的 curses 函数使用(y,x)为坐标表示,y表示行,x表示列。每一个位置不仅容纳这个屏幕位置上的字符而且还保存有他的属性。属性是否能被显示出来还依赖于物理终端的性能指标,但通常至少会有粗题和下划线这两种属性可用。

因为 curses 库需要创建和破坏同一个临时数据结构,所有 curses 程序在使用之前必须初始化库,以便用后还原。这里有两个函数可以做这件工作:initscrendwin

测试代码
#include <unistd.h>
#include <stdlib.h>
#include <curses.h>

int main()
{
        initscr();

        move(5, 15);
        printw("%s", "hello,wrold");
        refresh();
        sleep(3);  /*休息3秒以看到效果*/

        endwin();
        exit(EXIT_SUCCESS);
}

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
地板
 楼主| 发表于 2009-3-18 21:48:38 | 只看该作者

屏幕

所有 curses 程序必须以 initscr 开始 并且以 endwin 结束。
如果 initscr 函数执行成功,则返回一个指向 stdscr 结构的指针;假如失败,它就简单的打印出诊断信息并使程序退出。
endwin 函数成功时返回 OK, 错误时返回 ERR.你可以调用 endwin 函数离开 cruses, 然后可以通过调用 clearok(stdscr,1)与 refresh 来复原 curses 操作.这有效的使得 curses 忘记了自己的显示情况,而强制它执行了完整的一次原文重现操作.

测试代码:
#include <unistd.h>
#include <stdlib.h>
#include <curses.h>

int main()
{
        initscr();

        move(5, 15);
        printw("%s", "hello,wrold");
        refresh();
        sleep(3);

        endwin();
        sleep(3);

        clearok(stdscr, 1);
        refresh();
        sleep(3);
        endwin();
        exit(EXIT_SUCCESS);
}

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
5#
 楼主| 发表于 2009-3-19 01:44:20 | 只看该作者

向屏幕输出数据

提供了几个几本的函数来刷新屏幕
#include <curses.h>

int addch(const chtype char_to_add);
int addchstr(chtype *const string_to_add);
int printfw(char *format, ...);
int refresh(void);
int box(WINDOW *win_ptr, chtype vertical_char, chtype horizontal_char);
int insch(chtype char_to_insert);
int insertln(void);
int delch(void);
int beep(void);
int flash(void);

curses 有自己的字符类型--chtype,它比标准的 char 类型还有更多的二进制位。在标准 Linux 的 ncurses 版本里,chtype 实际上是一个 unsigned long 的 typedef 定义。

add... 函数增加字符或字符串到指定的当前位置。
printw 函数与 printf 一样对一个字符串进行格式化,并且把它打印到当前位置。
refresh 函数使物理屏幕被刷新,成功时返回OK,错误发生时返回ERR。
box 函数允许绕着窗口话一个框子。

在标准的 curses 里,你可能仅用到“正常”字符。而在扩展的 curses 中,你可以使用 ACS_VLINEACS_HLINE 这两个定义画出更好看的框子。基于此,你的终端需要支持画线字符(line-drawing characters)。通常的,这些在 xterm 窗口里会有更好的表现,而标准的控制台就差些。然而对这两者的支持差距却趋向日益变大,所以如果对移植性看得比较重要,那么还是要避免使用它们。

insch 函数的功能是插入一个字符,把现有的字符向右移,但在行尾进行这个操作造成的后果没有定义,具体的情况还是确定于你所使用的终端。
insertln 插入一个空行,把现有的行依次向下移动一行。
两个 delete 函数是类似(analogous)于两个 insert 函数。

如果要弄出声音,你可以调用 beep 函数。只有很少一部分的终端无法弄出声来,所以一些 curses 设置就让闪屏来代替 beep 调用。如果你工作在一个繁忙的办公室里,你就会非常乐见机器能够发出声来提醒你。正如大家猜到的那样,flash 函数能够引起屏幕闪屏,但如果无法产生闪屏效果的话,它就试图发出声来提醒你。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
6#
 楼主| 发表于 2009-3-20 00:57:45 | 只看该作者

从屏幕中读取数据

可以从屏幕中读取数据,尽管这一功能并不经常用(因为对于写过的东西还是很容易跟踪的)。如果确实需要,可以使用以下函数:
#include <curses.h>
chtype inch(void);
int instr(char *string);
int innstr(char *string, int number_of_characters);
inch() 函数总是可用的,但 instr() 和 innstr() 却不是总能被支持。
inch() 函数返回一个当前屏幕光标位置处那个字符以及它的属性信息。注意,inch 并不是返回一个字符,而是一个 chtype 类型的数据。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
7#
 楼主| 发表于 2009-3-20 02:06:55 | 只看该作者

清屏

有以下 4 种主要的方法来清除一个屏幕中的区域:
#include <curses.h>
int erase(void);
int clear(void);
int clrtobot(void);
int clrtoeol(void);

erase 函数在屏幕的每个位置都写上空格。clear 函数和 erase 函数类似,也是进行清屏动作,但还会通过调用函数 clearok 可以强制原文重现操作。clearok 强制进行清屏操作,但在下一次调用 refresh 时会进行原文重现操作。

clear 函数通常使用一个中断命令来擦除整个屏幕,而不是简单的试图擦除当前屏幕中的非空区域。这样,对于完整的屏幕擦除 clear 函数就成了一个相当可靠彻底的方法。在屏幕显示因为某些原因变得混乱或者被破坏了,那么在 clear 函数后面跟着 refresh 函数这样的组合操作,提供了一个非常有用的重新绘制屏幕手段。

clrtobot 函数从屏幕上光标所在的当前位置清除到屏幕末尾;
clrtoeol 从光标的当前位置清除到所在行的末尾。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
8#
 楼主| 发表于 2009-3-20 10:20:41 | 只看该作者

移动光标

移动光标只需要一个函数,还有一个辅助函数用来控制刷新屏幕后 curses 应该把光标放在什么地方:
#include <curses.h>
int move(int new_y, int new_x);
int leaveok(WINDOW *window_ptr, bool leave_flag);

move 函数简单的把逻辑光标位置移动到指定地点。在绝大部分的 curses 版本里,有两个 extern 整形:LINESCOLUMS 包含着物理屏幕的尺寸,这可以确定 new_y 和 new_x 的最大值。
就其调用 move 本身而言,这并不会引起物理光标 位置的移动,它仅改变的是光标在屏幕上的逻辑位置,而紧接着的输出就会在这个位置上出现。如果希望调用 move 后马上就看到效果,那就在其后调用 refresh() 函数。

在一次屏幕刷新后,curses需要把物理光标放在某个位置上,控制这一位置的标志由 leaveok 函数设置。默认情况下,标志为 false,在刷新后,硬件光标将停留在和逻辑光标所在的地点。 如果flag 设置为 ture,则硬件光标随机的停留在屏幕可能的任何一个地方。一般来说,人们更喜欢用缺省选项。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
9#
 楼主| 发表于 2009-3-20 11:54:55 | 只看该作者

字符属性

每一个 curses 字符都有一个确定的属性,属性控制字符在屏幕上的显示方式,当然前提是显示硬件要能支持所请求的显示属性。
预定义的属性有:
A_BLINK A_BOLDA_DIMA_REVERSE A_STANDOUTA_UNDERLINE
可以利用以下函数设置一个或多个这样的属性:
#include <curses.h>

int attron(chtype attribute);
int attroff(chtype attribute);
int attrset(chtype attribute);
int standout(void);
int standend(void);

attrset  函数设置 curses 的属性;
attron 和 attroff 在不影响打开或者关闭指定的属性;
standoutstandend 提供了一种更为通用的 “突出” 模式,在大多数终端上,它通常被映射为 ”反显“ 。

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
10#
 楼主| 发表于 2009-3-20 23:25:46 | 只看该作者

应用举例一

测试代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <curses.h>

int main()
{
        const char witch_one[] = "First Witch ";
        const char witch_two[] = "Second Witch ";
        const char *scan_ptr;

        initscr(); /*初始化*/

        move(5, 15);
        attron(A_BOLD);  /*打开字体加粗属性*/
        printw("%s", "Macbeth");
        attroff(A_BOLD); /*关闭字体加粗属性*/
        refresh();
        sleep(2);

        move(8, 15);
        attron(A_STANDOUT);  /*突出显示属性,打底色*/
        printw("%s", "Thunder and Lightning");
        attroff(A_STANDOUT);
        refresh();
        sleep(2);

        move(10, 10);
        printw("%s", "When shall we three meet again");
        move(11, 23);
        printw("%s", "In thunder, lightning, or in rain?");
        move(13, 10);
        printw("%s", "When the hurlyburly's done,");
        move(14,23);
        printw("%s", "When the battle's lost and won.");
        refresh();
        sleep(2);

        attron(A_DIM);
        scan_ptr = witch_one + strlen(witch_one) - 1;
        while(scan_ptr != witch_one) {
                move(10, 10);
                insch(*scan_ptr--);
        }
        insch(*scan_ptr);

        scan_ptr = witch_two + strlen(witch_two) - 1;
        while(scan_ptr != witch_two) {
                move(13, 10);
                insch(*scan_ptr--);
        }
        insch(*scan_ptr);
        attroff(A_DIM);
        refresh();
        sleep(2);

        move(LINES - 1, COLS - 1); /*把光标移动到窗口的右下角*/

        refresh();
        sleep(2);

        endwin();
        exit(EXIT_SUCCESS);
}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2025-5-3 11:28 , Processed in 0.074984 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表