回 目 录
上 一 章 下 一 章

# 第十八章 表单库

  当你看到网页上那些处理用户数据的表单后，你肯定很想在纯文本模式下创建一个那样的表单。用纯ncurses创建那样的表单十分困难。但是表单库（Form Library）为我们提供了一个基础的编写框架，使我们可以很容易地创建和维护表单。它包含了很多的函数，分别用来管理，动态扩展表单域等等。我们将通过后面的内容来掌握它。

  表单就是表单域的集合，也许这些表单域是一个标签，也许是一项数据的输入框。表单库也提供把表单划分为页面的函数。

### 18.1. 基础知识

  表单的创建步骤与菜单的创建大致相同：

  首先，用new_field()创建与表单相关联的表单域。你可以设置表单域的修饰选项，这样的界面显示出来会更加美观。离开表单域以前, 这些对表单域的修饰属性都是有效的。

  然后，表单域添加进表单，整个表单就会显示到屏幕上，准备读取输入信息。与menu_driver()相似，表单由form_driver()操纵。我们可以传递请求给form_driver()来移动显示焦点（校者注：即表示被选定，通常是高亮条等）到某个表单域，或将光标移动到表单域的结尾等等。用户在表单域内输入并确认后结束，表单就不再显示，并释放占用过的内存。

  一个表单程序的流程大致如下：

    	初始化curses
    	通过new_field()创建表单域。你可以指定域的高度，宽度以及它在表单中的位置。
    	指定表单域所作用的表单，并用new_form()创建表单。
    	用form_post()来递送表单，并刷新屏幕。
    	用一个循环来处理用户请求，通过form_driver()对表单做相应的更新。
    	用form_unpost()取消表单的递送。
    	用free_form()释放已分配给表单的内存。
    	释放已分配给菜单项的内存。
    	结束curses

  如你所见，表单库的用法与跟菜单库的用法很像。下面的几个例子将会让我们领略表单处理的其他方面。先让我们从一个简单的例子开始吧：

### 18.2 编译包含表单库的程序

  要使用表单库中的函数，必须要把form.h头文件包含进源程序代码。在编译和连接时要同时添加 –lform 和 –lncurses两个选项。

    #include <form.h>
        .
        .
        .

    编译和链接: gcc <程序文件> -lform –lncurses

例25． 表单库基础知识

    #include <form.h>

    int main()
    {	FIELD *field[3];
    	FORM  *my_form;
    	int ch;

    	/* 初始化curses */
    	initscr();
    	cbreak();
    	noecho();
    	keypad(stdscr, TRUE);

    	/* 初始化表单域 */
    	field[0] = new_field(1, 10, 4, 18, 0, 0);
    	field[1] = new_field(1, 10, 6, 18, 0, 0);
    	field[2] = NULL;

    	/* 设置表单域 */
    	set_field_back(field[0], A_UNDERLINE); 		/* 为选项打印一条下滑线 */
    	field_opts_off(field[0], O_AUTOSKIP);
    	/* 在域（输入框）填满后光标不会自动跳到下一个表单域 */
    	set_field_back(field[1], A_UNDERLINE);
    	field_opts_off(field[1], O_AUTOSKIP);

    	/* 创建并递送表单 */
    	my_form = new_form(field);
    	post_form(my_form);
    	refresh();

    	mvprintw(4, 10, "Value 1:");
    	mvprintw(6, 10, "Value 2:");
    	refresh();

    	/* 用循环获取用户请求 */
    	while((ch = getch()) != KEY_F(1))
    	{	switch(ch)
    		{	case KEY_DOWN:
    				/* 跳至下一个表单域 */
    				form_driver(my_form, REQ_NEXT_FIELD);
    				/* 跳到当前缓冲的末尾 */
    				/* 精确地在输入最后一个后字符跳出这个表单域 */
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			case KEY_UP:
    				/* 移动到前一个表单域 */
    				form_driver(my_form, REQ_PREV_FIELD);
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			default:
    				/* 如果输入的是普通字符, 就把它打印出来 */
    				form_driver(my_form, ch);
    				break;
    		}
    	}
    	/* 取消表单并释放内存 */
    	unpost_form(my_form);
    	free_form(my_form);
    	free_field(field[0]);
    	free_field(field[1]);

    	endwin();
    	return 0;
    }

  上面的程序比较简单易懂。它首先调用new_field()函数创建了两个表单域。new_field()的六个参数分别确定了表单域的高、宽、起始位置的纵坐标、横坐标、不在显示的行数，以及附加的工作缓冲区。其中第五个参数（不显示的行数）决定了表单域哪些部分的是可见的。如果是0，则显示整个表单域。当用户访问的表单域超出了显示范围，那么表单将变成可滚动的。之后，表单库分别为每个表单分配了缓冲区，用来存储用户输入的数据。new_field()的最后一个参数用来分配额外的缓冲区。这些缓冲区可以用来做其它的事情。

  当创建表单域后，用set_field_back()函数可以增添背景修饰效果。选项AUTOSKIP已用field_opts_off()关闭。如果这个选项是打开的，一旦当前的表单域被输入的数据填满，则光标自动跳转到下一个表单域。

  给表单添加完表单域后，就把表单递送出去。接着，通过对form_driver()发送相应的操作请求， while循环里将处理用户输入的信息。有关form_driver()的细节将在后面的部分详细解释。

### 18.3．玩转表单域

  每个表单域关联了大量的属性。那样可意使你定制它们从而得到你想要的效果。是不是很有趣？那还等什么呢？

#### 18.3.1． 获取域的大小和位置

  我们可以用函数field_info()得到已创建表单域的参数。该函数返回表单域的高、宽、起始位置的纵坐标、横坐标、不显示的行数和附加的缓冲区。它的顺序跟new_field()的相反：

    int field_info( FIELD *field,           		/* 要获取信息的表单域*/
                   int *height, *int width, 		/* 域的高、宽 */
                   int *top, int *left,     		/* 起点的y坐标，起点的x坐标 */
                   int *offscreen,          		/* 不在显示范围内的行数 */
                   int *nbuf);              		/* 附加缓冲区的大小 */

#### 18.3.2．移动表单域

  通过函数move_field()可以移动表单域的位置。

    int move_field(  FIELD *field,          		/* 要移动的表单域 */
                    int top, int left);      	/* 新位置的起点坐标（先行后列）*/

  同样的，改变后的位置也可以通过field_info()查询到。

#### 18.3.3．设置表单域的对齐方式

  输入表单域数据的对齐方式可以用函数set_field_just()设置。

    int set_field_just(FIELD *field,      		/* 要更改的表单域 */
                   int justmode);     			/* 要设置的对齐方式 */

    int field_just(FIELD *field);     			/* 查询表单域的对齐方式 */

  这两个函数可以用来设置或者返回相应表单域对齐方式的值，分别为为NO_JUSTIFICATION（没有对齐方式），JUSTIFY_RIGHT（右对齐），JUSTIFY_LEFT（左对齐） 或者JUSTIFY_CENTER（居中对齐）。

#### 18.3.4．表单域的显示修饰

  正像前面提到的那样，在上面的例子中，表单域的修饰效果可以用set_field_fore()和set_field_back()两个函数来设置。这两个函数分别设置表单域的前景和背景修饰。你也可以指定用字符填充该表单域的空白处。通过调用函数set_field_pad()你就可以设置背景填充字符了。默认背景填充字符为空格。函数field_fore()，field_back()，field_pad()可以用来查询当前表单域的前景、背景修饰和背景填充字符。以下是这些函数的原形：

    int set_field_fore(FIELD *field,       		/* 要设置的表单域 */
                       chtype attr);     		/* 被设置表单域的前景修饰属性 */

    chtype field_fore(FIELD *field);       		/* 要查询的表单域 */
                                      			/* 返回该表单域的前景修饰属性 */

    int set_field_back(FIELD *field,        		/* 要设置的表单域 */
                       chtype attr);       		/* 被设置表单域的背景修饰属性 */

    chtype field_back(FIELD *field);       		/* 要查询的表单域 */
                                       		/* 返回该表单域的背景修饰属性 */

    int set_field_pad(FIELD *field,         		/* 要设置的表单域*/
                      int pad);           		/* 要设置表单域的背景填充字符 */
    chtype field_pad(FIELD *field);    		/* 要查询的表单域*/
                                       		/* 返回该表单域的背景填充字符 */

  尽管上面的函数看起来很简单，但用set_field_fore()来设置颜色对初学者来说可能很困难。我先解释一下表单域的前景和背景修饰。前景修饰与字符有关。即在表单域内以set_field_for()设置字符的修饰效果。背景修饰用来填充表单域的背景，不管表单域内是否有字符。由于颜色通常是成对定义，怎样才能正确显示包含颜色设置的表单域呢？下面的例子很清楚地解释了颜色定义。

例26．一个有关表单域属性的例子

    #include <form.h>
    int main()
    {
    	FIELD *field[3];
    	FORM *my_form;
    	int ch;

    	/* 初始化curses */
    	initscr();
    	start_color();
    	cbreak();
    	noecho();
    	keypad(stdscr, TRUE);

    	/* 初始化颜色 */
    	init_pair(1, COLOR_WHITE, COLOR_BLUE);
    	init_pair(2, COLOR_WHITE, COLOR_BLUE);

    	/* 初始化表单域 */
    	field[0] = new_field(1, 10, 4, 18, 0, 0);
    	field[1] = new_field(1, 10, 6, 18, 0, 0);
    	field[2] = NULL;

    	/* 设置表单域选项 */
    	set_field_fore(field[0], COLOR_PAIR(1));		/* 设置表单域背景色为蓝色 */
    	set_field_back(field[0], COLOR_PAIR(2));		/* 白色为前景色（字符为白色）*/
    	field_opts_off(field[0], O_AUTOSKIP);
    	/* 当这个域填满时不自动跳到下一个表单域*/

    	set_field_back(field[1], A_UNDERLINE);
    	field_opts_off(field[1], O_AUTOSKIP);

    	/* 创建表单并递送 */
    	my_form = new_form(field);
    	post_form(my_form);
    	refresh();

    	set_current_field(my_form, field[0]); 		/* 把此表单域设为当前的活动域 */
    	mvprintw(4, 10, "Value 1:");
    	mvprintw(6, 10, "Value 2:");
    	mvprintw(LINES - 2, 0, "Use UP, DOWN arrow keys to switch between fields");
    	refresh();

    	/* 用循环获取用户请求 */
    	while((ch = getch()) != KEY_F(1))
    	{	switch(ch)
    		{	case KEY_DOWN:
    				/* 跳到下一个表单域 */
    				form_driver(my_form, REQ_NEXT_FIELD);
    				/* 跳到当前缓冲区的末尾 */
    				/* 在输入最后一个字符后跳出 */
    				form_driver(my_form, REQ_END_LINE);
    				break;

    			case KEY_UP:
    				/* 跳到前一个表单域 */
    				form_driver(my_form, REQ_PREV_FIELD);
    				form_driver(my_form, REQ_END_LINE);
    				break;

    			default:
    				/* 如果是正常的字符，则读入并打印 */
    				form_driver(my_form, ch);
    				break;
    		}
    	}

    	/* 取消表单递送并释放内存 */
    	unpost_form(my_form);
    	free_form(my_form);
    	free_field(field[0]);
    	free_field(field[1]);

    	endwin();
    	return 0;
    }

  仔细研究这些颜色变量对并深刻的理解前景和背景修饰。在我的程序里面，若要设置颜色属性，我一般只用set_field_back()来设置背景修饰。cureses不允许自定义颜色。

#### 18.3.5．表单域属性

  表单的处理是通过对表单域设定属性实现的，有大量的表单域属性对应表单处理的各个方面。你可以用下面的函数为表单域设定属性：

    int set_field_opts(FIELD *field,			/* 要变更的表单域 */
                       		int attr);		/* 要设定的属性 */

    int field_opts_on(FIELD *field,          	/* 要改变选项的表单域 */
                      		int attr);		/* 要启用的属性 */

    int field_opts_off(FIELD *field,			/* 要改变选项的表单域 */
                      		int attr);		/* 要关闭的属性 */

    int field_opts(FIELD *field);				/* 要查询的表单域 */

  函数set_field_opts()可以直接用来设置表单域属性。要启用或关闭某些属性的话，你可以用函数field_opts_on()或field_opts_off()来完成。你可以随时用函数field_opts()来查询某个表单域的属性。下面列出了所有可用的属性，默认情况下，它们都是已启用的。

O_VISIBLE

  控制表单域是否在屏幕上可见。可以用来显示或隐藏某些表单域以及附加表单域。

O_ACTIVE

  控制表单域在表单处理时是否是激活状态（如：类似Tab键在域之间跳转访问）。这样可以使应用程序创建带有可变缓冲区的标签或起始表单域。

O_PUBLIC

  控制输入时是否在表单域中显示输入数据。如果关闭了某个表单域的这个属性，表单系统仍然允许编辑这个表单域的数据。但不会在这个可视表单域中显示出来，同时光标也不会移动。你可以通过关闭O_PUBLIC选项来定义一个密码域。

O_EDIT

  控制表单域的数据能否被修改。当这个选项关闭时，除了REQ_PREV_CHOICE 和REQ_NEXT_CHOICE以外，其它所有的编辑请求都将失效。可以设置类似这样的只读表单域用来显示帮助信息。

O_WRAP

  控制多行表单域的自动换行。通常，当一个单词（用空格作为单词之间的间隔）的任意一个字符到了当前行的末尾时，整个单词会自动换行显示（如果有下一行的话）。当这个选项关闭时，这个单词会在行尾处断开。

O_BLANK

  清空表单域。当此选项打开后，在表单域的起始位置输入字符时，程序会自动清空整个域(除了刚输入的这个字符)。

O_AUTOSKIP

  控制当前表单域填满时是否自动跳转到下一个表单域。通常，当用户的输入的数据超出了的一个表单域所能容纳的数据时，光标会自动跳转到下一个表单域。如果这个选项已关闭，则光标停在表单域的末尾。对于没有大小限制的动态表单域，这个选项会被忽略。

O_NULLOK

  是否检查空白表单域的有效性，默认为不检查。用户可以在退出时让一个表单域为空，而不用像通常那样检查这个表单域是否有效。如果某个表单域的这个选项是关闭的，退出程序时就要检查表单域的有效性了。

O_PASSOK

  控制表单有效性检查发生的时间。默认是在表单域的数据被修改后检查。设置以后，用户在退出表单域后检查表单域的有效性。当你需要在处理表单时同时更改表单域的有效性检查函数，设置O_PASSOK选项就对了。

O_STATIC

控制表单域的长度是否固定为初始长度。如果关闭这个选项，则表单域就变成动态的。系统会根据输入数据的长度自动调整表单域大小。

  如果当前的表单域为选定状态时，表单域的属性不能修改。但是，可以在表单域递送后修改属性。

  这些属性都是位掩码，可以用“或”逻辑运算符组合属性。你已经看见了关闭O_AUTOSKIP属性后的效果。下面的这个例子说明了更多属性的用法。其他的属性会在后面适当的地方说明。

例27． 表单域属性用法示例

    #include <form.h>

    #define STARTX 15
    #define STARTY 4
    #define WIDTH 25

    #define N_FIELDS 3

    int main()
    {	FIELD *field[N_FIELDS];
    	FORM  *my_form;
    	int ch, i;
    	/* 初始化curses */
    	initscr();
    	cbreak();
    	noecho();
    	keypad(stdscr, TRUE);

    	/* 初始化表单域 */
    	for(i = 0; i < N_FIELDS - 1; ++i)
    		field[i] = new_field(1, WIDTH, STARTY + i * 2, STARTX, 0, 0);
    	field[N_FIELDS - 1] = NULL;

    	/* 设置表单域选项 */
    	set_field_back(field[1], A_UNDERLINE); 			/* 为选项打印下划线 */
    	field_opts_off(field[0], O_ACTIVE); 			/* 这个表单域是静态标签 */
    	field_opts_off(field[1], O_PUBLIC); 			/* 这个表单域类似密码域 */
    	field_opts_off(field[1], O_AUTOSKIP);
    /* 防止在输入完最后一个字符后还是在同一个域输入*/

    	/* 创建表单并递送 */
    	my_form = new_form(field);
    	post_form(my_form);
    	refresh();

    	set_field_just(field[0], JUSTIFY_CENTER); 			/* 表单域输入时居中对齐 */
    	set_field_buffer(field[0], 0, "This is a static Field "); /* 初始化表单域 */
    	mvprintw(STARTY, STARTX - 10, "Field 1:");
    	mvprintw(STARTY + 2, STARTX - 10, "Field 2:");
    	refresh();

    	/* 循环读取用户请求 */
    	while((ch = getch()) != KEY_F(1))
    	{	switch(ch)
    		{	case KEY_DOWN:
    				/* 跳到下一个表单域 */
    				form_driver(my_form, REQ_NEXT_FIELD);
    				/* 跳到当前缓冲的末尾 */
    				/* 在最后一个字符处跳出 */
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			case KEY_UP:
    				/* 跳到前一个表单域 */
    				form_driver(my_form, REQ_PREV_FIELD);
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			default:
    				/* 若是普通的字符，则打印出来 */
    				form_driver(my_form, ch);
    				break;
    		}
    	}

    	/* 取消表单传递并释放内存 */
    	unpost_form(my_form);
    	free_form(my_form);
    	free_field(field[0]);
    	free_field(field[1]);

    	endwin();
    	return 0;
    }

  这个例子虽然用处不大，但它演示了这些属性的用法和效果。如果恰到好处地使用这些属性，可以在有效地在表单中显示信息。第二个表单域关闭了O_PUBLIC选项，所以你看不到输入的字符。

#### 18.3.6．获取表单域的状态

  表单域的状态用来标记表单域的是否被修改。初始值为FALSE，当用户在表单域中输入数据，同时也修改了数据缓冲区后，该值就变为TRUE。因此，可以通过查询一个表单域的状态来看检测它是否被修改了。下面的函数支持以上的功能。

    int set_field_status(FIELD *field,      		/* 要改变状态的表单域 */
                       		int status);     /* 要设置的表单域状态 */

    int field_status(FIELD *field);        		/* 要取得状态的表单域*/

  你的程序最好能够在离开一个表单域的时候检查一下表单域的状态。因为表单域的有效性还没有确定，所以数据缓冲区可能还没有更新。为确保返回正确的表单域状态，最好在下面三种情况调用field_status()函数：（1）、退出表单域并检查有效性时；（2）、在表单域或表单初始化或退出时；（3）、Form Driver处理REQ_VALIDATION请求后。

#### 18.3.7. 用户指针

  每个表单域的结构都包含一个用户指针，这个指针可以使用户做很多事情。它不能被表单库直接访问，但可以被用户任意使用。下面的函数可以设置和读取用户指针。

    int set_field_userptr(FIELD *field,
               		char *userptr);      		/* 与这个表单域关联的用户指针 */
    char *field_userptr(FIELD *field);   		/* 要获取指针的表单域 */

#### 18.3.8. 动态表单域

  假如你需要一个可以动态改变大小的表单域的话，那么这正是你需要用到的功能。当用户输入的数据大小超过该表单域的原大小时，文本框的尺寸就会动态的改变。根据文本框的位置，文本框可以自动的水平或垂直滚动，以适应新增添的数据。

  为使一个表单域的宽度可以动态地改变，应该先关闭O_STATIC选项。这可以通过调用下面函数完成：

    	field_opts_off(field_pointer, O_STATIC);

  但是通常不允许一个表单域无限制地增长。可以设置表单域动态增长的最大宽度：

    int set_max_field(FIELD *field,    		/* 要进行设置的表单域 */
    			int max_growth); 			/* 表单域动态增长的最大宽度 */

  可以通过下面的函数访问动态增长的表单域：

    int dynamic_field_info( FIELD *field,  		/* 要进行查询的表单域 */
    					int	*prows,   	/* 表单域的行数 */
    					int	*pcols,  	/* 表单域的列数 */
    					int  *pmax)		/* 表单域动态增长的最大宽度 */

  作者建议使用这个函数来检查动态表单域的属性。

  再次调用new_field()函数，函数将会自动创建高度为一个字符的单行表单域。创建初始的高度大于一个字符的表单域自动被定义为多行表单域。

  一个关闭了O_STATIC选项的单行表单域（动态增长的表单域）只包含一行，但是如果用户输入的数据的大小超过初始的大小的话，表单域宽度可以自动增长。显示的宽度仍然是固定的，但是可以通过水平滚动来查看超出部分的数据。

  如同单行表单域一样，一个关闭了O_STATIC选项的多行表单域（动态增长的表单域）包含固定的宽度，但是如果用户输入的数据大小超过初始的大小的话，行数可以自动增长。可显示的宽度仍然是固定的，但可以通过纵向滚动来查看超出部分的数据。

  上面两段非常形象的描述了表单域的动态增长行为。

  下面介绍表单库的其他行为：

  1、 如果某个表单域的O_STATIC选项关闭，也没有设置表单域动态增长的最大限度，O_AUTOSKIP选项将被忽略。通常情况下，当用户在表单域的最后一个位置输入数据，O_AUTOSKIP会自动向Form Driver发送REQ_NEXT_FIELD请求。但是，在一个没有设定最大增长值的动态表单域里，就没有最末的位置。但是如果定义了最大增长值，如果表单域自动增长到最大限度，O_AUTOSKIP选项也将正常工作。

  2、 如果O_STATIC选项关闭，表单域的对齐请求也将被忽略。通常的，set_field_just 可以通过JUSTIFY_LEFT，JUSTIFY_RIGHT，JUSTIFY_CENTER来设置单行域的对齐方式。但是从定义中我们知道：一个动态增长的单行表单域包含比初始长度更多的数据，并且可以通过水平滚动查看这些数据。因此，这些都超过了set_field_just作用的范围。所以函数field_jus不会改变返回的结果。

  3、 如果O_STATIC选项关闭，并且没有为表单域定义最大动态增长范围，对Form Driver发送的REQ_NEW_LINE请求将会忽略，但O_NL_OVERLOAD选项正常执行。这时，如果表单选项O_NL_OVERLOAD是打开的，如果在表单域的最后一行发送REQ_NEW_LINE请求系统将会隐式地生成并发送一个REQ_NEXT_FIELD请求。如果一个表单域可以无限制地动态增长，则没有最后一行。因此发送REQ_NEW_LINE请求就不会隐式生成REQ_NEXT_FIELD请求了。如果指定了最大动态增长的范围，并且O_NL_OVERLOAD选项是打开的。如果在表单域自动增长到最大，且用户停留在最后一行的时候就可以发送REQ_NEW_LINE请求，系统会隐式的产生并发送REQ_NEXT_FIELD请求。

  4、 调用库函数dup_field()仍然会正常工作；它将复制这个表单域，包括当前缓冲区的大小和表单域的内容。如果指定了表单域的最大动态增长值，也将会一起被复制。

  5、 调用库函数link_field()仍然会正常工作；它将复制表单域的所有修饰属性和所连接表单域的共享缓冲区。如果O_STATIC选项被随后的某个表单域的共享缓冲区修改了，并且输入的数据超出了当前缓冲区能够容纳的数据，那么系统对此的反应将取决于当前域的属性。

  6、 调用库函数field_info()仍然会正常工作；变量nrow将包含开始调用new_field时的值。用户应该用上面叙述的dynamic_field_info函数来查询缓冲区的当前大小。

  上面的有些要点只有详细了解释Form Driver后才好理解。我们会在后面的几个部分深入讲解Form Driver。

### 18.4、表单窗口

  表单窗口的概念与菜单窗口的很相似。每个表单与一个主窗口和一个子窗口关联。主窗口用来显示标题、边框以及其他的东西。子窗口包含所有的表单域并且根据它们的位置显示它们。这使得创建样式灵活的表单变得很容易。

  由于这些操作与菜单窗口的创建很相似。所以使用的函数大致相同，工作原理也大致相同。所以这里只给出一个例子，并不做过多的解释。

例28．一个表单窗口例子

    #include <form.h>

    void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string, chtype color);
    int main()
    {
    	FIELD *field[3];
    	FORM  *my_form;
    	WINDOW *my_form_win;
    	int ch, rows, cols;

    	/* 初始化curses */
    	initscr();
    	start_color();
    	cbreak();
    	noecho();
    	keypad(stdscr, TRUE);

    	/* 初始化颜色对 */
       	init_pair(1, COLOR_RED, COLOR_BLACK);

    	/* 初始化域 */
    	field[0] = new_field(1, 10, 6, 1, 0, 0);
    	field[1] = new_field(1, 10, 8, 1, 0, 0);
    	field[2] = NULL;

    	/* 设置表单域选项*/
    	set_field_back(field[0], A_UNDERLINE);
    	field_opts_off(field[0], O_AUTOSKIP);
    	/* 如果当前表单域已满，不跳自动跳到下一个表单域 */

    	set_field_back(field[1], A_UNDERLINE);
    	field_opts_off(field[1], O_AUTOSKIP);

    	/* 创建表单并传递 */
    	my_form = new_form(field);

    	/* 计算表单所需要的面积大小 */
    	scale_form(my_form, &rows, &cols);

    	/* 创建与表单相关联的窗口 */
    	my_form_win = newwin(rows + 4, cols + 4, 4, 4);
    	keypad(my_form_win, TRUE);

    	/* 设置主窗口和子窗口 */
    	set_form_win(my_form, my_form_win);
    	set_form_sub(my_form, derwin(my_form_win, rows, cols, 2, 2));

    	/* 在窗口的四周显示边框和标题 */
    	box(my_form_win, 0, 0);
    	print_in_middle(my_form_win, 1, 0, cols + 4, "My Form", COLOR_PAIR(1));
    	post_form(my_form);
    	wrefresh(my_form_win);
    	mvprintw(LINES - 2, 0, "Use UP, DOWN arrow keys to switch between fields");
    	refresh();

    	/* 循环以获取用户请求 */
    	while((ch = wgetch(my_form_win)) != KEY_F(1))
    	{	switch(ch)
    		{	case KEY_DOWN:
    				/* 跳到下一个域 */
    				form_driver(my_form, REQ_NEXT_FIELD);
    				/* 跳到当前缓冲区的末尾 */
    				/* 在最后一个字符处跳出 */
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			case KEY_UP:
    				/* 跳到上一个域 */
    				form_driver(my_form, REQ_PREV_FIELD);
    				form_driver(my_form, REQ_END_LINE);
    				break;
    			default:
    				/* 如果是一个普通的字符，则打印出来 */
    				form_driver(my_form, ch);
    				break;
    		}
    	}
    	/* 取消表单递送并释放内存 */
    	unpost_form(my_form);
    	free_form(my_form);
    	free_field(field[0]);
    	free_field(field[1]);
    	endwin();
    	return 0;
    }

    void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string, chtype color)
    {	int length, x, y;
    	float temp;
    	if(win == NULL)
    		win = stdscr;
    	getyx(win, y, x);
    	if(startx != 0)
    		x = startx;
    	if(starty != 0)
    		y = starty;
    	if(width == 0)
    		width = 80;
    	length = strlen(string);
    	temp = (width - length)/ 2;
    	x = startx + (int)temp;
    	wattron(win, color);
    	mvwprintw(win, y, x, "%s", string);
    	wattroff(win, color);
    	refresh();
    }

### 18.5、表单域输入的有效性检查

  默认情况下，表单域接收任何用户输入的数据。但也可以为表单域附加输入有效性检查。这样的话，如果域里面有和检查项不相符的数据，用户就跳不出这个域了。有些检查类型也支持即时检查——即在用户输入字符的同时进行检查。

  可以用下面的函数给表单域附加输入有效性检查：

    int set_field_type(FIELD *field,          	/* 要附加输入检查的表单域 */
                       FIELDTYPE *ftype,  		/* 要关联的检查类型 */
                       ...);                  	/* 其他参数 */

  一旦为表单域附加检查类型，表单域的有效性检查类型可以这样查询：

    FIELDTYPE *field_type(FIELD *field);      	/* 要查询的表单域 */

  Form Driver只检查由用户从终端输入数据的有效性。在下面的情况下，不进行有效性检查：

    	?应用程序通过调用set_field_buffer改变表单域的数据；
    	?已连接的表单域间接的被修改了——被其他连接到这个域的表单域修改。

  下面是已定义的检查类型。你也可以自定义检查类型，虽然可以让程序更敏感、更智能，但也很麻烦。

TYPE_ALPHA（字符输入检查模式）

  这个检查模式只接受字母，不接受空格，数字，特殊字符。这个检查模式让表单域在输入时就对输入的字符进行检查。这个检查模式可以这样设置：

    int set_field_type(FIELD *field,          	/* 要附加输入检查的表单域 */
                       TYPE_ ALPHA         		/* 要附加的检查模式 */
                       int width);           	/* 表单域的最大宽度 */

  参数width指定检查数据的最小宽度。用户必须输入至少一个字符，才可以离开这个表单域。通常你会把表单域的宽度设置为这个宽度；但是如果这个最小宽度大于表单域的宽度，那么有效性检查的结果总会是“无效”。把最小宽度设置为0可以使有效性检查在任何时候结束。

TYPE_ALNUM（字符和数字输入检查模式）

  这个检查模式同时接受字母和数字，不接受空格，特殊字符这个检查模式让表单域在输入时就对输入的字符进行检查。这个检查模式可以这样设置：

    int set_field_type(FIELD *field,			/* 要附加输入检查的表单域 */
                       TYPE_ AKLUNM, 			/* 要附加的检查模式 */
                       int width);			/* 表单域的最大宽度 */

  参数width设置所检查数据的最小宽度。同TYPE_ALPHA方式一样，通常你会把表单域的宽度设置为这个宽度；但是如果这个最小宽度大于表单域的宽度，那么有效性检查的结果总会是“无效”。把最小宽度设置为0可以使有效性检查在任何时候结束。

TYPE_ENUM（限定匹配项检查模式）

  这种检查模式检查输入到表单域的数据是否在一个给定限定的匹配项集合内，这个集合可以是一系列的字符串（例如：美国各州的仅有两个字母的邮政代码）。这个检查方式可以这样设置：

    int set_field_type(FIELD *field,          	/* 要附加输入检查的表单域 */
                       TYPE_ENUM,          		/* 要附加的检查模式 */
                       char **valuelist;     	/* 限定值的列表 */
                       int checkcase;         	/* 是否大小写敏感 */
                       int checkunique);      	/* 是否检查匹配项唯一性 */

  参数valuelist必须指向一系列以NULL字符结束的有效的字符串。如果参数checkcase为真，输入到表单域的字符串将区分大小写。

  当用户退出一个附加TYPE_ENUM检查的表单域时，程序将试着自动将缓冲区内的数据补充完整数据之有效。当然，输入完整的供选的字符串是一般的做法。但是也可以输入一个供选字符串的前几个字符，表单会将其自动补充完整为有效数据。

  默认情况下，如果你输入字符串的前几个字符，并且这几个字符在供选集合中有很多个匹配项，这前几个字符将会被匹配为第一个匹配字符串。不过，如果参数checkunique为真的话，要让输入有效，就要求匹配项在集合内是唯一的。

  REQ_NEXT_CHOICE和REQ_PREV_CHOICE请求对这些表单域会有特殊用处。

TYPE_INTEGER

  这种检查方式检查表单域的值是否是一个整数。这个检查方式可以这样设置：

    int set_field_type(FIELD *field,          	/* 要附加输入检查的表单域  */
                       TYPE_INTEGER,        		/* 要附加的检查模式 */
                       int padding,          	/* 填充为0 */
                       int vmin, int vmax);   	/* 有效值的范围 */

  在这种情况下的有效字符为整数，也可以是带“－”号的负整数。当用户离开这个表单域之后才检查输入值是否在规定范围之内。如果限制范围的最大值小于或者等于最小值，整数值的限制范围将被忽略。如果输入的整数通过了范围检查，函数将会自动地在这个值前面填充尽可能多的0，让其变为填充变量。

  通过C库函数atoi（3）可以很方便的解释TYPE_INTEGER类型值的缓冲区。

TYPE_NUMERIC

  这种检查方式检查表单域的值是否是一个十进制小数。这个检查方式可以这样设置：

    int set_field_type(FIELD *field,          	/* 要附加输入检查的表单域  */
                       TYPE_NUMERIC,    		/* 要附加的检查模式 */
                       int padding,           	/* 要设置的精度 */
                       int vmin, int vmax);   	/* 有效值的范围 */

  在这种情况下的有效字符为实数也可以是带“－”号的负数，也可以带小数点。同样的，当退出这个表单域的时候才检查输入值是否在规定范围之内。如果限制范围的最大值小于或者等于最小值，整数值的限制范围将被忽略。如果输入的整数通过了范围检查，函数将会自动地在这个值后面填充尽可能多的0，让其变为填充变量。

  通过C库函数atoi（3）可以很方便的解释TYPE_NUMERIC类型值的缓冲区

TYPE_REGEXP

  这种检查方式检查表单域的值是否是一个与常规表达式相匹配的数据。这个检查方式可以这样设置：

    int set_field_type(FIELD *field,          	/* 要设置检查方式的域 */
                       TYPE_REGEXP,         		/* 要关联的类型 */
                       char *regexp);         	/* 要匹配的表达式 */

  常规表达式的语法即regcomp(3)。常规表达式是否匹配的检查在退出表单域时执行。

### 18.6、Form Driver：表单系统的灵魂

  和菜单系统一样，form_driver()在表单系统中扮演同样重要的角色。各种类型的请求都是经过form_driver()处理的：

    int form_driver(FORM *form,     			/* 要进行操作的表单 */
                    int request)    			/* 表单请求项 */

  正像你在前面的有些例子中看到的，必须在一个循环中取得用户的输入，然后再确定这是一个数据还是一个表单处理请求。若是表单处理请求，则将信号传递给form_driver()，由它完成相应的工作。这些请求项可以大致地分为下面几类。下面将解释这些不同的请求和它们的用法：

#### 18.6.1、页面导航请求

  这些请求用来在表单里面实现翻页，并控制页面的显示。一个表单可以由多个页面组成。如果你的一个表单中含有很多表单域和逻辑分段，你就可以把这个表单设置为多页的。函数set_new_page()可以为指定的表单域增添一个新页面。

    int set_new_page(FIELD *field,			/* 要设置新页或取消设置的表单域 */
             bool new_page_flag); 			/* 若要设置断点，则应该为TRUE */

  下面的导航请求允许你在不同的表单页面中跳转：

    	REQ_NEXT_PAGE		跳到表单下一页
    	REQ_PREV_PAGE		跳到表单上一页
    	REQ_FIRST_PAGE		跳到表单第一页
    	REQ_LAST_PAGE		跳到表单最后一页

  这些请求把页面列表视作一个循环。也就是说，在最后一页发送REQ_NEXT_PAGE请求使得表单页从最后一页跳到第一页，REQ_PREV_PAGE使得表单页从第一页跳到最后一页。

#### 18.6.2、表单域间移动请求

  下面的这些请求处理光标在不同表单域间移动，这些表单域必须是同一个页面内的。

    	REQ_NEXT_FIELD		跳到下一个表单域
    	REQ_PREV_FIELD		跳到上一个表单域
    	REQ_FIRST_FIELD		跳到第一个表单域
    	REQ_LAST_FIELD		跳到最后一个表单域

    	REQ_SNEXT_FIELD		跳到有序表单域的下一个表单域
    	REQ_SPREV_FIELD		跳到有序表单域的上一个表单域
    	REQ_SFIRST_FIELD		跳到有序表单域的第一个表单域
    	REQ_SLAST_FIELD		跳到有序表单域的最后一个表单域

    	REQ_LEFT_FIELD		向左跳到上一个表单域
    	REQ_RIGHT_FIELD		向右跳到下一个表单域
    	REQ_UP_FIELD		向上跳到上一个表单域
    	REQ_DOWN_FIELD		向下跳到下一个表单域

  这些请求把同一页内的表单域视为一个循环。也就是说，REQ_NEXT_FIELD可以使光标从最后一个表单域跳到第一个表单域，REQ_PREV_FIELD可以使光标从第一个域跳到最后一个域。对于这些请求(包括REQ_FIRST_FIELD和REQ_LAST_FIELD)来说，表单域的顺序就是表单数组中其指针的顺序（用new_form()或set_form_fields()设置）。

  同样也可以按屏幕位置的来依次遍历这些表单域，顺序是自左至右，由上到下。可以通过使用第二组移动请求（即REQ_S开头的请求）完成。

  最后，也可以使用方向键在表单域间移动。要完成这项功能，可以使用上面四组请求中的第三组。注意：为完成这个功能，这些请求引用的表单位置是指表单的左上角。

  例如，假设你有一个多行表单域B和两个与B在同一行的单行表单域A和C。而且A在B的左边，C在B的右边。REQ_MOVE_RIGHT请求只在A,B,C的第一行在同一行的情况下才能使光标从A移动到B；否则将跳过B而移动到C。

#### 18.6.3、域内导航请求

  下面的这些请求使光标在当前表单域内移动.

    	REQ_NEXT_CHAR		跳到下一个字符处
    	REQ_PREV_CHAR		跳到前一个字符处
    	REQ_NEXT_LINE		跳到下一行
    	REQ_PREV_LINE		跳到前一行
    	REQ_BEG_FIELD		跳到表单域的开始
    	REQ_END_FIELD		跳到表单域的结尾
    	REQ_BEG_LINE		跳到行首
    	REQ_END_LINE		跳到行尾
    	REQ_LEFT_CHAR		跳到表单域的左边
    	REQ_RIGHT_CHAR		跳到表单域的右边
    	REQ_UP_CHAR			跳到表单域的上边
    	REQ_DOWN_CHAR		跳到表单域的下边

  前后单词之间都用空格来分开。如果要让光标移动到起始处或末尾处（在表单域内或行内），相应的命令会在当前范围内查找第一个字符或最后一个字符。

#### 18.6.4、滚动请求

  增大后的动态表单域和指定显示行数的表单域都是可滚动的。单行表单域以水平方式滚动；多行表单域以垂直方式滚动。一般滚动都是因为光标移动（为使光标可见而滚动）引起的。你也可以使用下面的请求来显式地请求滚动：

    	REQ_SCR_FLINE		纵向向前滚动一行
    	REQ_SCR_BLINE		纵向向后滚动一行
    	REQ_SCR_FPAGE		纵向向前滚动一页
    	REQ_SCR_BPAGE		纵向向后滚动一页
    	REQ_SCR_FHPAGE		纵向向前滚动半页
    	REQ_SCR_BHPAGE		纵向向后滚动半页
    	REQ_SCR_FCHAR		纵向向前滚动一个字符
    	REQ_SCR_BCHAR		纵向向后滚动一个字符
    	REQ_SCR_HFLINE		纵向向前滚动一个域宽
    	REQ_SCR_HBLINE		纵向向后滚动一个域宽
    	REQ_SCR_HFHALF		纵向向前滚动半个域宽
    	REQ_SCR_HBHALF		纵向向后滚动半个域宽

  在执行滚动时，表单域的一页就是它的可视部分的高度。

#### 18.6.5、编辑请求

  当你传递一个ASCII字符给Form Driver时，它被视做一个添加到表单域的数据缓冲区中的字符。是插入模式还是替换模式由表单域的编辑模式决定（默认模式是插入模式）。 下面的请求支持表单域编辑并且可以改变编辑模式：

    	REQ_INS_MODE		设置为插入模式
    	REQ_OVL_MODE		设置为替换模式
    	REQ_NEW_LINE		新行请求（看下面的解释）
    	REQ_INS_CHAR		在当前字符处插入一个空格
    	REQ_INS_LINE		在当前字符处插入一个空行
    	REQ_DEL_CHAR		删除光标所在处的字符
    	REQ_DEL_PREV		删除光标所在处的前一个字
    	REQ_DEL_LINE		删除光标所在行
    	REQ_DEL_WORD		删除光标所在的字
    	REQ_CLR_EOL			清除当前位置到行尾的字符
    	REQ_CLR_EOF			清除当前位置到域尾的字符
    	REQ_CLEAR_FIELD		清空整个域

  REQ_NEW_LINE请求和REQ_DEL_PREV请求比较复杂，而且部分被其他表单选项控制。尤其是在光标处于表单域的开头或最后一行发送这类请求时，这里需要特别的说明：

首先看一下REQ_NEW_LINE：

  通常REQ_NEW_LINE的作用是在插入模式时中断光标当前编辑位置的所在行。把当前光标后面的部分作为一个新行插入，并且光标移到新行的开头（你可以认为是在表单域缓冲区插入一行）。

  通常REQ_NEW_LINE在替换模式下会将当前光标位置到行尾的所有内容清除。并且将光标移动到新行的开头。

  但是，在表单域的开头或最后一行发送REQ_NEW_LINE请求时，它相当于REQ_NEXT_FIELD的功能。若选项O_NL_OVERLOAD关闭，这个特殊行为就会禁止。

接下来看一下REQ_DEL_PREV：

  通常REQ_DEL_PREV是删除前一个字符。如果是在插入模式下并且光标在行首，当前行的内容将会接到上一行上去，这时它完成的功能是把当前行的内容接到上一行，并删除当前行（你可以认为是在表单域缓冲区中删除一新行）。

  但是如果是在表单域的开头，REQ_DEL_PREV则被当成REQ_PREV_FIELD处理。

  如果选项O_BS_OVERLOAD关闭，这个特殊行为就会禁止，Form Driver就会返回E_REQUEST_DENIED。

#### 18.6.6、顺序请求

  如果你的表单域类型是有序的，并且已经关联了取得前一个值和后一个值的函数。以下两个请求可以将前一个值或后一个值放入缓冲区：

    	REQ_NEXT_CHOICE		把当前值的后一个值放在缓冲区
    	REQ_PREV_CHOICE		把当前值的前一个值放在缓冲区

  对于内置域类型，只有TYPE_ENUM有内置的函数用来读取前面一个值或后面一个值。当定义了一个你自己的表单域类型时（参看自定义有效检查类型），你可以关联你自己的函数。

#### 18.6.7、应用程序命令

  表单请求用大于KEY_MAX且小于等于常数MAX_COMMAND的整数表示。在这个范围内的值将被form_driver()忽略。这使得其他应用程序任意使用它们。它可以被看成是应用程序中特定的行为代码，并通过该代码采取相应的行为。
回 目 录
上 一 章 下 一 章