荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: jjk (kernel), 信区: Linux
标  题: [LINUXFOCUS]Ncurses介绍(转寄)
发信站: 荔园晨风BBS站 (Fri Mar 15 13:22:03 2002), 转信

【 以下文字转载自 jjk 的信箱 】
【 原文由 jjk.bbs@apue.dhs.org 所发表 】
发信人: zeratul (zeratul), 信区: TecMisc
标  题: [LINUXFOCUS]Ncurses介绍(转寄)
发信站: UNIX编程 (2002年03月14日21:21:09 星期四), 站内信件

http://main.linuxfocus.org/ChineseGB/March2002/article233.shtml

摘要:
Ncurses是一个能提供功能键定义(快捷键),屏幕绘制以及基于文本终端的图形互动功能
的动态库
什么是Ncurses?
您希望您的程序有一个彩色的界面吗?Ncurses是一个能提供基于文本终端窗口功能的动
态库. Ncurses可以:
只要您喜欢,您可以使用整个屏幕
创建和管理一个窗口
使用8种不同的彩色
为您的程序提供鼠标支持
使用键盘上的功能键
Ncurses可以在任何遵循ANSI/POSIX标准的UNIX系统上运行,除此之外,它还可以从系统数
据库中检测终端的属性, 并且自动进行调整,提供一个不受终端约束的接口.因此,Ncurs
es可以在不同的系统平台和不同的终端上工作的非常好.
mc工具集就是一个用ncurses写的很好的例子,而且在终端上系统核心配置的界面同样是
用ncurses编写的. 下面就是它们的截图:
哪里可以下载?
Ncurses是基于GNU/Linux发展的,请访问 http://www.gnu.org/software/ncurses/以获
得最新的更新版本或者其他详细信息以及相关链接 .
基础知识
为了能够使用ncurses库,您必须在您的源程序中将curses.h包括(include)进来,而且在
编译的需要与它连接起来. 在gcc中您可以使用参数-lcurses进行编译.
在使用ncurses的时候,您有必要了解它的基础数据结构.它有一个WINDOW结构,从名字就
很容易知道,它是用来描述 您创建的窗体的,所有ncurse库中的函数都带有一个WINDOW指
针参数.
在ncurses中使用最多的组件是窗体.即使您没有创建自己的窗体,当前屏幕会认为是您自
己的窗体. 如同标准输入输出系统提供给屏幕的文件描述符stdout一样(假设没有管道转
向),ncurses提供一个 WINDOW指针stdscr做相同工作.除了stdscr外,ncurses还定义了一
个WINDOW指针curscr. 和stdscr描述当前屏幕一样,curscr描述当前在库中定义的屏幕,
您可以带着"他们有什么区别?"这个问题继续阅读.
为了在您的程序中使用ncurses的函数和变量,您必须首先调用initscr函数(初始化工作
),它会给一些变量比如 stdscr,curscr等分配内存,并且让ncurses库处于准备使用状态
,换句话说,所有ncurses函数必须跟在initscr后面. 同样的约定,您在结束使用ncurses
后,应该使用endwin来释放所有ncurses使用的内存.在使用endwin后,您将不能在使用 任
何ncurses的函数,除非您再一次调用initscr函数.
在initscr和endwin之间,请不要使用标准输入输出库的函数输出结果到屏幕上,否则,您
会看到屏幕会被您的输出 弄的乱七八糟,这可不是您期望的结果.当ncurses处在激活状
态时,请使用它自己的函数来把结果输出到屏幕.在调用initscr之前或者 endwin之后,您
就可以随便使用了.
刷新屏幕:refresh
WINDOW结构不会经常保持同一高度宽度以及在窗体中的位置,但是会保持在窗体中的内容
.当您向窗体写入数据时, 会改变窗体中的内容,但并不意味着在屏幕中会立即显示出来
,要更新屏幕内容,必须调用refresh或者wrefres函数.
这里介绍了stdscr和curscr两者之间的区别.curscr保存着当前屏幕的内容,在调用ncur
se的输出函数后,stdscr和curscr可能会有不同的内容, 如果您想在最近一次屏幕内容改
变后让stdscr和curscr保持一致,您必须使用refresh函数.换句话说, refresh是唯一一
个处理curscr的函数.千万不要弄混淆了curscr和stdscr,应该在refresh函数中更新cur
scr中的内容
refresh有一个能尽可能快的更新屏幕的机制,当调用refresh时,它只更新窗体中内容改
变的行,这节省了CPU的处理时间 ,防止程序往屏幕上写相同的信息(译者注:在屏幕的同
一位置不用重新显示同样的内容.)这种机制就是为什么同时使用ncurses的函数 和标准
的输入输出函数会造成屏幕内容错位的原因.当调用ncurses的输出函数时,它会设置一个
标志,能让refresh 知道是哪一行改变了.但是您调用标准输入输出函数时,就不会产生这
种结果.
refresh和wrefresh其实做了同样的事情.wrefresh需要一个WINDOW的指针参数,它仅仅刷
新该窗体的内容. refresh()等同于wrefresh(stdscr).我在后面会提到,和wrefresh一样
,ncursers的许多函数都有许多这种为stdscr定义的宏函数.
定义一个新的窗体
下面我们来谈谈能定义新窗体的subwin和newwin函数.他们都需要一个来定义新窗体的高
度,宽度以及 左上角位置的参数,并且返回一个WINDOW指针来指向该窗体.您可以使用它
来作为wrefresh的参数或者一些其他我将要 谈到的函数.
您可能会问:"如果他们做同样的事情,为什么要有两个函数?",您是对的,他们之间有一些
细微的差别.subwin创建一个 窗体的子窗体,它将继承了父窗体的所有属性.但如果在子
窗体中改变了这些属性的值,它将不会影响父窗体.
除此之外,父窗体和子窗体之间还有一些联系.父窗体和子窗体中的内容将彼此共享,彼此
影响.换句话说, 在父窗口和子窗体重叠的区域的字符会被任意一个窗体改变.如果父窗
体写入了数据到这块区域,子窗体中这块区域同样 改变了,反之也是如此.
和subwin不同的是,newwin创建一个独有的窗体.这样的窗体,在没有他们的子窗体之前,
是不会和其他窗体共享 任何文本数据的.使用subwin的好处是可以使用较少的内存就可
以方便的共享字符数据了.但是如果您担心窗体数据会互相影响 那么就应该使用newwin
.
您可以创建任意多层的子窗体,每一个子窗体又可以有它自己的子窗体,但是一定要记住
,窗体的字符内容是被两个以上 的窗体共享的.
当您调用完您定义的窗体后,您可以使用delwin函数来删除该窗体.我建议您使用man pa
ges来得到这些函数的详细参数.
向窗体写数据和从窗体读数据
我们谈到了stdscr,curscr,以及刷新屏幕和定义一个新窗体,但是我们怎样向一个窗体写
入数据?我们怎样从一个窗体中读入数据?
实现以上目的函数如同标准输入输出库中的一些函数一样,我们使用printw来替换print
f输出内容,scanw替换scanf接受输入, addch替换putc或者putchar,getch替换getc或者
getchar.他们用法一样,仅仅名字不同,类似的,addstr可以用来向窗体 写入一个字符串
,getstr用来从窗体中读入一个字符串.所有这些函数都是以一个"w"字母开头,后面再跟
上函数的字, 如果需要操作另外一个窗体内容,第一个参数必须是该窗体的WINDOWS结
构指针,举个例子,printw(...)和wprintw(stdscr,...) 是相同的,就如同refresh()和w
refresh(stdscr)一样.
如果要写这些函数的详细说明,这篇文章将会变的很长.要得到他们的述,原型以及返回
值或者其他信息,man pages是一个不错的选择. 我建议您对照man pages检查您使用的
一个函数.他们提供了详细和非常有用的信息.在这篇文章的最后一节,我提供了 一个示
例程序,可以当作是一个ncurses函数的使用指南.
物理指针和逻辑指针
在讲完写入数据和从窗体读出数据后,我们需要解释一下物理指针和逻辑指针 物理指针
是一个常用指针,它只有一个,从另一个方面讲,逻辑指针属于ncurses窗体, 每一个窗体
都只有一个物理指针,但是他们可以有多个逻辑指针.
当窗体准备写入和读出的时候,逻辑指针会指向窗体中将要进行操作的区域.因此, 通过
移动逻辑指针,您可以任何时候向窗体中的任意位置写入数据.这个是区别与标准输入输
出库的优势之处.
移动逻辑指针的函数是move或者另外一个您非常容易猜出来的函数wmove.move是wmove的
一个宏函数,专门用来处理 stdscr的.
另外一个需要确认的是物理指针和逻辑指针的协作关系,物理指针的位置将会在一段写入
程序后无效,但是我们通过可以 通过WINDOW结构的_leave标志定位它.如果设置了_leav
e标志,在写操作结束后,逻辑指针将会移动到物理指针指向窗体中最后写入的区域. 如果
没有设置_leave位,在写操作结束后,物理指针将返回到逻辑指针指向窗体的第一个字符
写入位置._leave标志是由leaveok函数控制的.
移动物理指针的函数是mvcur,不象其他的函数,mvcur在不用等待refresh动作就会立即生
效.如果您想隐藏物理指针, 您可以使用curs_set函数,使用man pages来获得详细信息.

同样存在一些宏函数简化了上述的移动和写入等函数.您可以在addch,addstr,printw,g
etch,getstr,scanw等函数 的man pages页得到更多的解释.
清除窗体
当我们向窗体写完内容后,我们怎么样清除窗体,行和字符?
在ncurses中,清除意味着用空白字符填充整块区域,整行或者整个窗体的内容. 下面我介
绍的函数将会使用空白字符填充必要的区域,达到我们清屏的目的.
首先我们谈到能清楚字符和行的函数,delch和wdelch能删除掉窗体逻辑指针指向的字符
,下一个字符和一直到行末的字符都会左移 一个位置.deleteln和wdeleteln能删除掉逻
辑指针指向的行,并且上移下一行.
clrteol和wclrtoeol能清除掉从逻辑指针指向位置右边字符开始到行末的所有字符.clr
tbot和wclrtobot 首先清除掉从逻辑指针所在位置右边字符开始到行末的所有字符,接着
删除下面所有行.
除了这些,还有一些函数能清除整个屏幕和窗体.有两种方法可以清除掉整个屏幕.第一个
是先用空白字符填充 屏幕所有区域,然后再调用refresh函数.另外一种方法是用固定的
终端控制字符清除.第一种方法比较慢,因为它需要重写 当前屏幕.第二种能迅速清除整
个屏幕内容.
erase和werase用空白字符替换窗体的文本字符,在下一次调用refresh后屏幕内容将会被
清除掉.但是如果窗体 需要清掉整个屏幕, 这将一个比较苯的办法.您可以使用上面讲的
第一种方法来完成.当窗体需要被清除的是一个屏幕那么宽, 您可以使用下面讲的函数来
非常好的完成您的任务.
在涉及到其他函数之前,我们先来讨论一下_clear标志位.如果设置了该标志,那么它会存
在WINDOW结构中. 当调用它时,它会用refresh来发送控制代码到终端,refresh检查窗体
的宽度是否是屏幕的宽度(使用_FULLWIN标志位). 如果是的话,它将用内置的终端方法刷
新屏幕,它将写入除了空白字符外的文本字符到屏幕,这是一种非常快速清屏的方法. 为
什么仅仅当窗体的宽度和屏幕宽度相等时才用内置的终端方法清屏呢?那是因为控制终端
代码不仅仅只清除窗体自身 ,它还可以清除当前屏幕._clear标志位由clearok函数控制
.
函数clear和wclear被用来清除和屏幕宽度一样的窗体内容.实际上,这些函数等同与使用
werase和clearok. 首先,它用空白字符填充窗体的文本字符.接着,设置_clear标志位,如
果窗体宽度和屏幕宽度一样,就使用内置的终端方法 清屏,如果不一样就用空白字符填充
窗体所有区域再用refresh刷新.
总而言之,如果您知道窗体的宽度和屏幕宽度一样,就使用clear或者wclear,这个速度将
非常快.如果窗体宽度 不是和屏幕宽度一样,那么使用wclear和werase将没有任何分别.

使用颜色
您在屏幕上看到的颜色其实都是颜色对,因为每一个区域都有一个背景色和一个前景色.
使用ncurses显示彩色 意味着您定义自己的颜色对并且将这些颜色对写入到窗体.
如同使用ncurses函数必须先调用initscr一样,start_color需要首先调用以初始化色素
. 您用来定义自己的颜色对的函数是init_pair,当您使用它定义了一个颜色对后,它将会
和您在函数中的设置的第一个参数联系起来. 在程序中,无论您什么时候需要用该颜色对
,您只需用COLOR_PAIR调用该参数就可以了.
除了定义颜色对,您还必须使用函数来保证写入的使用是用不同的颜色对,attron和watt
ron可以满足您的要求. 使用这些函数将会用您选择的颜色对写入数据到相应的屏幕上,
直到调用了attroff或者wattroff函数.
bkgd和wbkgd函数可以改变相应的整个窗体的颜色对,调用时,它将会改变窗体所有区域的
前景色和背景色.也就 是说,在下一个刷新动作前,窗体上所有的区域将会使用新的颜色
对重写.
使用刚才提到的那些函数man pages来得到详细的关于颜色资料和信息.
窗体的边框
您可以给您的程序里面的窗体一个很好看的边框,在库中有一个box宏函数可以替您做到
这一点,和其他函数所 不同的是,没有wbox函数.box需要一个WINDOW指针来作为参数.
您可以在box的man pages页轻松获得详细的帮助,这里有一些需要注意的是,给一个窗体
设置边框其实只是 在窗体的相应边框区域写入了一些字符.如果您在边框区域一下写如
了某些数据,边框将会被中断. 解决的办法就是在您在原始窗体里面再建一个子窗体,将
原始窗体放入到边框里面然后使用里面的子窗体作为需要的输入数据窗体.
功能键
为了能够使用功能键,必须在我们需要接受输入的窗体中设置_use_keypad标志位,keypa
d是一个能设置 _use_keypad值的函数,当您设置了_use_keypad后,您就可以使用键盘的
功能键(快捷键),如同普通输入一样.
在这里,如果您想使用getch来作个简单接受数据输入的例子,您需要注意的是要将数据赋
给整形变量(int)而不是 字符型(char).这是因为整形变量能容纳的功能键比字符型更多
.您不需要知道这些功能键的值,您只需要使用库中定义的 宏名就可以了,在getch的man
 page中有这些数值的列表.
范例
我们将来分析一个非常简单实用的程序.在这个程序中,将使用ncurses定义菜单,菜单中
的一个选择项都会被证明选种. 这个程序比较有意思的一面就是使用了ncurses的窗体
来达到菜单效果.您可以看下面的屏幕截图.
程序开始和普通一样,包括进去了一个头文件.接着我们定义了回车键和escape键的ASCI
I码值.
#include <curses.h>
#include <stdlib.h>
#define ENTER 10
#define ESCAPE 27
当程序的时候,下面的函数会被调用.它首先调用initscr初始化指针接着调用start_col
or来显示彩色. 整个程序中所使用的颜色对会在后面定义.调用curs_set(0)会屏蔽掉物
理指针.noecho()将终止键盘上的输入会在屏幕上显示出来. 您可以使用noecho函数控制
键盘输入进来的字符,只允许需要的字符显示.echo()将会屏蔽掉这种效果. 接着的函数
keypad设置了可以在stdscr中接受键盘的功能键(快捷键),我们需要在后面的程序中定义
F1,F2以及移动的光标键.
void init_curses()
{
    initscr();
    start_color();
    init_pair(1,COLOR_WHITE,COLOR_BLUE);
    init_pair(2,COLOR_BLUE,COLOR_WHITE);
    init_pair(3,COLOR_RED,COLOR_WHITE);
    curs_set(0);
    noecho();
    keypad(stdscr,TRUE);
}
下面定义的这个函数定义了一个显示在屏幕最顶部的菜单栏, 您可以看下面的main段程
序,它看上去好象只是屏幕最顶部的一行,其实实际上是stdscr窗体的一个子窗体,该子窗
体只有 一行.下面的程序将指向该子窗体的指针作为它的参数,首先改变它的背景色,接
着定义菜单的字,我们使用waddstr定义菜单 的字.需要注意的是wattron调用了另外
一个不同的颜色对(序号3)以取代缺省的颜色对(序号2).记住2号颜色对在最开始就 由w
bkgd设置成缺省的颜色对了.wattroff函数可以让我们切换到缺省的颜色对状态.
void draw_menubar(WINDOW *menubar)
{
    wbkgd(menubar,COLOR_PAIR(2));
    waddstr(menubar,"Menu1");
    wattron(menubar,COLOR_PAIR(3));
    waddstr(menubar,"(F1)");
    wattroff(menubar,COLOR_PAIR(3));
    wmove(menubar,0,20);
    waddstr(menubar,"Menu2");
    wattron(menubar,COLOR_PAIR(3));
    waddstr(menubar,"(F2)");
    wattroff(menubar,COLOR_PAIR(3));
}
下一个函数显示了当按下F1或者F2键显示的菜单,定义了一个在蓝色背景上 菜单栏颜色
一样的白色背景窗体,我们不希望这个新窗口会被显示在背景色上的字覆盖掉.它们应该
停留在那里直到 关闭了菜单.这就是为什么菜单窗体不能定义为stdscr的子窗体,下面会
提到,窗体items[0]是用newwin函数定义的, 其他8个窗体则都是定义成items[0]窗体的
子窗体.这里的items[0]被用来绘制一个围绕在菜单旁边的边框,其他的 窗体则用来显示
菜单中选中的单元.同样的,他们不会覆盖掉菜单上的边框.为了区别选中和没选中的状态
,有必要让 选中的单元背景色和其他的不一样.这就是这个函数中倒数第三句的作用了,
菜单中的第一个单元背景色和其他的不一样, 这是因为菜单弹出来后,第一个单元是选中
状态.
WINDOW **draw_menu(int start_col)
{
    int i;
    WINDOW **items;
    items=(WINDOW **)malloc(9*sizeof(WINDOW *));
    items[0]=newwin(10,19,1,start_col);
    wbkgd(items[0],COLOR_PAIR(2));
    box(items[0],ACS_VLINE,ACS_HLINE);
    items[1]=subwin(items[0],1,17,2,start_col+1);
    items[2]=subwin(items[0],1,17,3,start_col+1);
    items[3]=subwin(items[0],1,17,4,start_col+1);
    items[4]=subwin(items[0],1,17,5,start_col+1);
    items[5]=subwin(items[0],1,17,6,start_col+1);
    items[6]=subwin(items[0],1,17,7,start_col+1);
    items[7]=subwin(items[0],1,17,8,start_col+1);
    items[8]=subwin(items[0],1,17,9,start_col+1);
    for (i=1;i<9;i++)
        wprintw(items[i],"Item%d",i);
    wbkgd(items[1],COLOR_PAIR(1));
    wrefresh(items[0]);
    return items;
}
下面这个函数简单的删除了上面函数定义的菜单窗体.它首先用delwin函数删除窗体, 接
着释放items指针的内存单元.
void delete_menu(WINDOW **items,int count)
{
    int i;
    for (i=0;i<count;i++)
        delwin(items[i]);
    free(items);
}
scroll_menu函数允许我们在菜单选择项上上下移动,它通过getch读取键盘上的键值,如
果按下了键盘上的上移或者下移方向键, 菜单选择项的上一个项或者下一个项被选中.回
忆一下刚才所讲的,选中项的背景色将会和没选中的不一样.如果是向左或者向右 的方向
键,当前菜单将会关闭,另一个菜单打开.如果按下了回车键,则返回选中的单元值.如果按
下了ESC键,菜单将会被关闭,并且没有任何选择项 ,下面的函数忽略了其他的输入键.ge
tch能从键盘上读取键值,这是因为我们在程序开始使用了keypad(stdscr,TRUE) 并且将
返回值赋给一个int型变量而不是char型变量,这是因为int型变量能表示比char型更大的
值.
int scroll_menu(WINDOW **items,int count,int menu_start_col)
{
    int key;
    int selected=0;
    while (1) {
        key=getch();
        if (key==KEY_DOWN || key==KEY_UP) {
            wbkgd(items[selected+1],COLOR_PAIR(2));
            wnoutrefresh(items[selected+1]);
            if (key==KEY_DOWN) {
                selected=(selected+1) % count;
            } else {
                selected=(selected+count-1) % count;
            }
            wbkgd(items[selected+1],COLOR_PAIR(1));
            wnoutrefresh(items[selected+1]);
            doupdate();
        } else if (key==KEY_LEFT || key==KEY_RIGHT) {
            delete_menu(items,count+1);
            touchwin(stdscr);
            refresh();
            items=draw_menu(20-menu_start_col);
            return scroll_menu(items,8,20-menu_start_col);
        } else if (key==ESCAPE) {
            return -1;
        } else if (key==ENTER) {
            return selected;
        }
    }
}
最后就是我们的main部分了.它使用了上面所有我们所讲述和编写的函数来使程序合适的
工作. 它同样通过getch读取键值来判断F1或者F2是否按下了,并且用draw_menu来在相应
的菜单窗体上绘制菜单. 接着调用scroll_menu函数让用户选择某一个菜单,当scroll_m
enu返回后,它删除菜单窗体并且显示所选择的单元内容 在信息栏里.
我必须提到的是函数touchwin.如果在菜单关闭后没有调用touchwin而立即刷新,那么最
后打开的菜单将一直停留在 屏幕上.这是因为在调用refresh时,menu函数根本就没有完
全改变stdscr的内容.它没有重新写入数据到stdscr上, 因为它以为窗体内容没有改变.
touchwin函数设置了所有WINDOW结构中的标志位,它通知refresh刷新窗体中所有的行,
值都改变了,这样在下一次刷新整个窗体时,即使窗体内容没有改变也要重新写入一次.在
菜单关闭后,选择的菜单信息会一直停留在 stdscr上面.菜单没有在stdscr上写数据,因
为它是开了一个新的子窗口.
int main()
{
    int key;
    WINDOW *menubar,*messagebar;
    init_curses();
    bkgd(COLOR_PAIR(1));
    menubar=subwin(stdscr,1,80,0,0);
    messagebar=subwin(stdscr,1,79,23,1);
    draw_menubar(menubar);
    move(2,1);
    printw("Press F1 or F2 to open the menus. ");
    printw("ESC quits.");
    refresh();
    do {
        int selected_item;
        WINDOW **menu_items;
        key=getch();
        werase(messagebar);
        wrefresh(messagebar);
        if (key==KEY_F(1)) {
            menu_items=draw_menu(0);
            selected_item=scroll_menu(menu_items,8,0);
            delete_menu(menu_items,9);
            if (selected_item<0)
                wprintw(messagebar,"You haven't selected any item.");
            else
                wprintw(messagebar,
                  "You have selected menu item %d.",selected_item+1);
            touchwin(stdscr);
            refresh();
        } else if (key==KEY_F(2)) {
            menu_items=draw_menu(20);
            selected_item=scroll_menu(menu_items,8,20);
            delete_menu(menu_items,9);
            if (selected_item<0)
                wprintw(messagebar,"You haven't selected any item.");
            else
                wprintw(messagebar,
                  "You have selected menu item %d.",selected_item+1);
            touchwin(stdscr);
            refresh();
        }
    } while (key!=ESCAPE);
    delwin(menubar);
    delwin(messagebar);
    endwin();
    return 0;
}
如果您拷贝了代码到一个文件,假设名字是example.c,并且移走了我所有的注释,您可以
用下面这个方法编译:
gcc -Wall example.c -o example -lcurses
为了测试程序,您可以在参考一章里下载该程序.
总结
我谈到了很多关于ncurses的基础知识,应该足够用来给您的程序创建一个很好看的界面
.还有许多方便的功能 在这里都没有提及,您可以在我经常问到的几个函数的man pages
里面找到很多有用的信息.读完了后,您将回明白 我这里提到的东西和内容仅仅是一个介
绍而已.
参考
参考程序: example.c
ncurses主页地址:http://www.gnu.org/software/ncurses/

--
    洗一个澡,看一朵花,吃一顿饭,假使你觉得快活,并非因为澡洗得
干净,花开得好,或者菜合你的口味,主要是因为你心上没有挂碍,轻松
的灵魂可以专注肉体的感觉,来欣赏,来审定。

※ 来源:·UNIX编程 apue.dhs.org·[FROM: 211.101.140.97] --
※ 转寄:·UNIX编程 apue.dhs.org·[FROM: 202.119.32.102]
--
※ 转载:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.0.146]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店