18. 表单库

好的。如果你见过网页上那些从用户那里获取输入并执行各种操作的表单,你可能想知道如何在文本模式显示中创建这样的表单。用纯粹的 ncurses 编写那些漂亮的表单相当困难。表单库试图提供一个基本框架,以便轻松构建和维护表单。它有许多功能(函数),可以管理验证、字段的动态扩展等等。让我们全面了解一下。

表单是字段的集合;每个字段可以是标签(静态文本)或数据输入位置。表单库还提供将表单划分为多个页面的功能。

18.1. 基础知识

表单的创建方式与菜单非常相似。首先,使用 new_field() 创建与表单相关的字段。你可以为字段设置选项,以便可以使用一些精美的属性显示它们,在字段失去焦点之前进行验证等等。然后将字段附加到表单。之后,可以发布表单以显示并准备接收输入。与 menu_driver() 类似,表单使用 form_driver() 进行操作。我们可以向 form_driver 发送请求,以将焦点移动到某个字段,将光标移动到字段末尾等等。在用户在字段中输入值并完成验证后,可以取消发布表单并释放已分配的内存。

表单程序的一般控制流程如下所示。

  1. 初始化 curses

  2. 使用 new_field() 创建字段。你可以指定字段的高度和宽度,以及其在表单上的位置。

  3. 通过指定要附加的字段,使用 new_form() 创建表单。

  4. 使用 form_post() 发布表单并刷新屏幕。

  5. 使用循环处理用户请求,并使用 form_driver 对表单进行必要的更新。

  6. 使用 form_unpost() 取消发布菜单

  7. 通过 free_form() 释放分配给菜单的内存

  8. 通过 free_field() 释放分配给项目的内存

  9. 结束 curses

正如你所见,使用表单库与处理菜单库非常相似。以下示例将探讨表单处理的各个方面。让我们从一个简单的示例开始。

18.2. 使用表单库编译

要使用表单库函数,你必须包含 form.h,并且为了将程序与表单库链接,应该按照 -lncurses 的顺序添加标志 -lform。

    #include <form.h>
    .
    .
    .

    compile and link: gcc <program file> -lform -lncurses

示例 25. 表单基础知识

#include <form.h>

int main()
{	FIELD *field[3];
	FORM  *my_form;
	int ch;
	
	/* Initialize curses */
	initscr();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize the fields */
	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 options */
	set_field_back(field[0], A_UNDERLINE); 	/* Print a line for the option 	*/
	field_opts_off(field[0], O_AUTOSKIP);  	/* Don't go to next field when this */
						/* Field is filled up 		*/
	set_field_back(field[1], A_UNDERLINE); 
	field_opts_off(field[1], O_AUTOSKIP);

	/* Create the form and post it */
	my_form = new_form(field);
	post_form(my_form);
	refresh();
	
	mvprintw(4, 10, "Value 1:");
	mvprintw(6, 10, "Value 2:");
	refresh();

	/* Loop through to get user requests */
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case KEY_DOWN:
				/* Go to next field */
				form_driver(my_form, REQ_NEXT_FIELD);
				/* Go to the end of the present buffer */
				/* Leaves nicely at the last character */
				form_driver(my_form, REQ_END_LINE);
				break;
			case KEY_UP:
				/* Go to previous field */
				form_driver(my_form, REQ_PREV_FIELD);
				form_driver(my_form, REQ_END_LINE);
				break;
			default:
				/* If this is a normal character, it gets */
				/* Printed				  */	
				form_driver(my_form, ch);
				break;
		}
	}

	/* Un post form and free the memory */
	unpost_form(my_form);
	free_form(my_form);
	free_field(field[0]);
	free_field(field[1]); 

	endwin();
	return 0;
}

上面的示例非常简单明了。它使用以下命令创建了两个字段new_field()。 new_field() 接受高度、宽度、starty、startx、屏幕外行数和附加工作缓冲区数作为参数。第五个参数屏幕外行数指定要显示多少字段。如果为零,则始终显示整个字段,否则当用户访问未显示的字段部分时,表单将是可滚动的。表单库为每个字段分配一个缓冲区来存储用户输入的数据。使用 new_field() 的最后一个参数,我们可以指定它分配一些额外的缓冲区。这些缓冲区可以用于你喜欢的任何目的。

创建字段后,将它们两个的背景属性设置为下划线,使用 set_field_back()。使用 field_opts_off() 关闭 AUTOSKIP 选项。如果此选项已打开,则一旦活动字段完全填满,焦点将移动到表单中的下一个字段。

将字段附加到表单后,将发布该表单。从这里开始,在 while 循环中处理用户输入,通过向 form_driver 发出相应的请求。所有 form_driver() 请求的详细信息将在后面解释。

18.3. 玩转字段

每个表单字段都与许多属性相关联。可以操作它们以获得所需的效果并获得乐趣!!!所以还等什么?

18.3.1. 获取字段的大小和位置

我们在创建字段时给出的参数可以使用 field_info() 检索。它将高度、宽度、starty、startx、屏幕外行数和附加缓冲区数返回到给定的参数中。它有点像是 new_field() 的逆函数。

int field_info(     FIELD *field,              /* field from which to fetch */
                    int *height, *int width,   /* field size */ 
                    int *top, int *left,       /* upper left corner */
                    int *offscreen,            /* number of offscreen rows */
                    int *nbuf);                /* number of working buffers */

18.3.2. 移动字段

字段的位置可以使用 move_field() 移动到不同的位置。

int move_field(    FIELD *field,              /* field to alter */
                   int top, int left);        /* new upper-left corner */

像往常一样,可以使用 field_infor() 查询更改后的位置。

18.3.3. 字段对齐

可以使用函数 set_field_just() 固定字段要完成的对齐方式。

    int set_field_just(FIELD *field,          /* field to alter */
               int justmode);         /* mode to set */
    int field_just(FIELD *field);          /* fetch justify mode of field */

这些函数接受和返回的对齐模式值是 NO_JUSTIFICATION、JUSTIFY_RIGHT、JUSTIFY_LEFT 或 JUSTIFY_CENTER。

18.3.4. 字段显示属性

正如你所见,在上面的示例中,可以使用 set_field_fore() 和 setfield_back() 设置字段的显示属性。这些函数设置字段的前景和背景属性。你还可以指定一个填充字符,该字符将填充字段的未填充部分。填充字符是通过调用 set_field_pad() 设置的。默认填充值是空格。函数 field_fore()、field_back、field_pad() 可用于查询字段的当前前景、背景属性和填充字符。以下列表给出了这些函数的使用方法。

int set_field_fore(FIELD *field,        /* field to alter */
                   chtype attr);        /* attribute to set */ 

chtype field_fore(FIELD *field);        /* field to query */
                                        /* returns foreground attribute */

int set_field_back(FIELD *field,        /* field to alter */
                   chtype attr);        /* attribute to set */ 

chtype field_back(FIELD *field);        /* field to query */
                                        /* returns background attribute */

int set_field_pad(FIELD *field,         /* field to alter */
                  int pad);             /* pad character to set */ 

chtype field_pad(FIELD *field);         /* field to query */  
                                        /* returns present pad character */

虽然上面的函数看起来很简单,但在开始时使用 set_field_fore() 设置颜色可能会令人沮丧。让我首先解释一下字段的前景和背景属性。前景属性与字符相关联。这意味着字段中的字符将使用你使用 set_field_fore() 设置的属性打印。背景属性是用于填充字段背景的属性,无论那里是否有字符。那么颜色呢?由于颜色始终成对定义,那么显示彩色字段的正确方法是什么?这是一个示例,澄清了颜色属性。

示例 26. 表单属性示例

#include <form.h>

int main()
{	FIELD *field[3];
	FORM  *my_form;
	int ch;
	
	/* Initialize curses */
	initscr();
	start_color();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize few color pairs */
	init_pair(1, COLOR_WHITE, COLOR_BLUE);
	init_pair(2, COLOR_WHITE, COLOR_BLUE);

	/* Initialize the fields */
	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 options */
	set_field_fore(field[0], COLOR_PAIR(1));/* Put the field with blue background */
	set_field_back(field[0], COLOR_PAIR(2));/* and white foreground (characters */
						/* are printed in white 	*/
	field_opts_off(field[0], O_AUTOSKIP);  	/* Don't go to next field when this */
						/* Field is filled up 		*/
	set_field_back(field[1], A_UNDERLINE); 
	field_opts_off(field[1], O_AUTOSKIP);

	/* Create the form and post it */
	my_form = new_form(field);
	post_form(my_form);
	refresh();
	
	set_current_field(my_form, field[0]); /* Set focus to the colored field */
	mvprintw(4, 10, "Value 1:");
	mvprintw(6, 10, "Value 2:");
	mvprintw(LINES - 2, 0, "Use UP, DOWN arrow keys to switch between fields");
	refresh();

	/* Loop through to get user requests */
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case KEY_DOWN:
				/* Go to next field */
				form_driver(my_form, REQ_NEXT_FIELD);
				/* Go to the end of the present buffer */
				/* Leaves nicely at the last character */
				form_driver(my_form, REQ_END_LINE);
				break;
			case KEY_UP:
				/* Go to previous field */
				form_driver(my_form, REQ_PREV_FIELD);
				form_driver(my_form, REQ_END_LINE);
				break;
			default:
				/* If this is a normal character, it gets */
				/* Printed				  */	
				form_driver(my_form, ch);
				break;
		}
	}

	/* Un post form and free the memory */
	unpost_form(my_form);
	free_form(my_form);
	free_field(field[0]);
	free_field(field[1]); 

	endwin();
	return 0;
}

尝试使用颜色对,并尝试理解前景和背景属性。在我的程序中使用颜色属性时,我通常只使用 set_field_back() 设置背景。Curses 根本不允许定义单个颜色属性。

18.3.5. 字段选项位

还有大量的字段选项位可以设置,以控制表单处理的各个方面。你可以使用以下函数操作它们

int set_field_opts(FIELD *field,          /* field to alter */
                   int attr);             /* attribute to set */ 

int field_opts_on(FIELD *field,           /* field to alter */
                  int attr);              /* attributes to turn on */ 

int field_opts_off(FIELD *field,          /* field to alter */
                  int attr);              /* attributes to turn off */ 

int field_opts(FIELD *field);             /* field to query */ 

函数 set_field_opts() 可用于直接设置字段的属性,或者你可以选择使用 field_opts_on() 和 field_opts_off() 选择性地打开和关闭一些属性。你可以随时使用 field_opts() 查询字段的属性。以下是可用选项的列表。默认情况下,所有选项都处于打开状态。

O_VISIBLE

控制字段是否在屏幕上可见。可以在表单处理期间使用,以根据父字段的值隐藏或弹出字段。

O_ACTIVE

控制字段在表单处理期间是否处于活动状态(即,是否被表单导航键访问)。可以用于使标签或派生字段的缓冲区值可由表单应用程序更改,而不是由用户更改。

O_PUBLIC

控制在字段输入期间是否显示数据。如果在字段上关闭此选项,库将接受和编辑该字段中的数据,但它不会显示,并且可见的字段光标不会移动。你可以关闭 O_PUBLIC 位来定义密码字段。

O_EDIT

控制是否可以修改字段的数据。当此选项关闭时,除了以下编辑请求之外的所有编辑请求REQ_PREV_CHOICEREQ_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;
	
	/* Initialize curses */
	initscr();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize the fields */
	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 options */
	set_field_back(field[1], A_UNDERLINE); 	/* Print a line for the option 	*/
	
	field_opts_off(field[0], O_ACTIVE); /* This field is a static label */
	field_opts_off(field[1], O_PUBLIC); /* This filed is like a password field*/
	field_opts_off(field[1], O_AUTOSKIP); /* To avoid entering the same field */
					      /* after last character is entered */
	
	/* Create the form and post it */
	my_form = new_form(field);
	post_form(my_form);
	refresh();
	
	set_field_just(field[0], JUSTIFY_CENTER); /* Center Justification */
	set_field_buffer(field[0], 0, "This is a static Field"); 
						  /* Initialize the field  */
	mvprintw(STARTY, STARTX - 10, "Field 1:");
	mvprintw(STARTY + 2, STARTX - 10, "Field 2:");
	refresh();

	/* Loop through to get user requests */
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case KEY_DOWN:
				/* Go to next field */
				form_driver(my_form, REQ_NEXT_FIELD);
				/* Go to the end of the present buffer */
				/* Leaves nicely at the last character */
				form_driver(my_form, REQ_END_LINE);
				break;
			case KEY_UP:
				/* Go to previous field */
				form_driver(my_form, REQ_PREV_FIELD);
				form_driver(my_form, REQ_END_LINE);
				break;
			default:
				/* If this is a normal character, it gets */
				/* Printed				  */	
				form_driver(my_form, ch);
				break;
		}
	}

	/* Un post form and free the memory */
	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,      /* field to alter */
                   int status);         /* status to set */

int field_status(FIELD *field);         /* fetch status of field */

最好仅在离开字段后检查字段的状态,因为数据缓冲区可能尚未更新,因为验证仍在进行中。为了保证返回正确的状态,请在以下位置调用 field_status():(1)在字段的退出验证检查例程中,(2)从字段或表单的初始化或终止挂钩中,或(3)在表单驱动程序处理 REQ_VALIDATION 请求之后。

18.3.7. 字段用户指针

每个字段结构都包含一个指针,用户可以将该指针用于各种目的。表单库不会触及它,用户可以将其用于任何目的。以下函数设置和获取用户指针。

int set_field_userptr(FIELD *field,   
           char *userptr);      /* the user pointer you wish to associate */
                                /* with the field    */

char *field_userptr(FIELD *field);      /* fetch user pointer of the field */

18.3.8. 可变大小字段

如果你想要一个宽度可变的动态变化的字段,这就是你想要充分利用的功能。这将允许用户输入比字段原始大小更多的数据,并让字段增长。根据字段方向,它将水平或垂直滚动以包含新数据。

要使字段动态可增长,应关闭选项 O_STATIC。这可以使用

    field_opts_off(field_pointer, O_STATIC);

但通常不建议允许字段无限增长。你可以使用以下命令设置字段增长的最大限制

int set_max_field(FIELD *field,    /* Field on which to operate */
                  int max_growth); /* maximum growth allowed for the field */

可以由以下命令检索动态可增长字段的字段信息

int dynamic_field_info( FIELD *field,     /* Field on which to operate */
            int   *prows,     /* number of rows will be filled in this */
            int   *pcols,     /* number of columns will be filled in this*/
            int   *pmax)      /* maximum allowable growth will be filled */
                              /* in this */
虽然 field_info 像往常一样工作,但建议使用此函数来获取动态可增长字段的正确属性。

回想一下库例程 new_field;高度设置为 1 的新字段将被定义为单行字段。高度大于 1 的新字段将被定义为多行字段。

O_STATIC 关闭(动态可增长字段)的单行字段将包含单个固定行,但是如果用户输入的数据超过初始字段的容纳量,则列数可以增加。显示的列数将保持固定,并且额外的数据将水平滚动。

O_STATIC 关闭(动态可增长字段)的多行字段将包含固定数量的列,但是如果用户输入的数据超过初始字段的容纳量,则行数可以增加。显示的行数将保持固定,并且额外的数据将垂直滚动。

以上两段几乎描述了动态可增长字段的行为。表单库的其他部分的运行方式如下所述

  1. 如果选项 O_STATIC 关闭并且未指定字段的最大增长量,则字段选项 O_AUTOSKIP 将被忽略。当前,当用户在字段的最后一个字符位置键入时,O_AUTOSKIP 会生成自动 REQ_NEXT_FIELD 表单驱动程序请求。在未指定最大增长量的可增长字段上,没有最后一个字符位置。如果指定了最大增长量,则如果字段已增长到其最大大小,则 O_AUTOSKIP 选项将像往常一样工作。

  2. 如果选项 O_STATIC 关闭,则字段对齐将被忽略。当前,set_field_just 可用于 JUSTIFY_LEFT、JUSTIFY_RIGHT、JUSTIFY_CENTER 单行字段的内容。可增长的单行字段,根据定义,将水平增长和滚动,并且可能包含超出可对齐范围的数据。field_just 的返回值将保持不变。

  3. 如果字段选项 O_STATIC 关闭并且未指定字段的最大增长量,则过载的表单驱动程序请求 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 来查询缓冲区的当前大小。

以上某些要点仅在解释表单驱动程序后才有意义。我们将在接下来的几节中研究这一点。

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;
	
	/* Initialize curses */
	initscr();
	start_color();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize few color pairs */
   	init_pair(1, COLOR_RED, COLOR_BLACK);

	/* Initialize the fields */
	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 options */
	set_field_back(field[0], A_UNDERLINE);
	field_opts_off(field[0], O_AUTOSKIP); /* Don't go to next field when this */
					      /* Field is filled up 		*/
	set_field_back(field[1], A_UNDERLINE); 
	field_opts_off(field[1], O_AUTOSKIP);
	
	/* Create the form and post it */
	my_form = new_form(field);
	
	/* Calculate the area required for the form */
	scale_form(my_form, &rows, &cols);

	/* Create the window to be associated with the form */
        my_form_win = newwin(rows + 4, cols + 4, 4, 4);
        keypad(my_form_win, TRUE);

	/* Set main window and sub window */
        set_form_win(my_form, my_form_win);
        set_form_sub(my_form, derwin(my_form_win, rows, cols, 2, 2));

	/* Print a border around the main window and print a title */
        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();

	/* Loop through to get user requests */
	while((ch = wgetch(my_form_win)) != KEY_F(1))
	{	switch(ch)
		{	case KEY_DOWN:
				/* Go to next field */
				form_driver(my_form, REQ_NEXT_FIELD);
				/* Go to the end of the present buffer */
				/* Leaves nicely at the last character */
				form_driver(my_form, REQ_END_LINE);
				break;
			case KEY_UP:
				/* Go to previous field */
				form_driver(my_form, REQ_PREV_FIELD);
				form_driver(my_form, REQ_END_LINE);
				break;
			default:
				/* If this is a normal character, it gets */
				/* Printed				  */	
				form_driver(my_form, ch);
				break;
		}
	}

	/* Un post form and free the memory */
	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,          /* field to alter */
                   FIELDTYPE *ftype,      /* type to associate */
                   ...);                  /* additional arguments*/
一旦设置,可以使用以下命令查询字段的验证类型
FIELDTYPE *field_type(FIELD *field);      /* field to query */

表单驱动程序仅在最终用户输入数据时才验证字段中的数据。在以下情况下不会发生验证

以下是预定义的验证类型。你还可以指定自定义验证,但这有点棘手和繁琐。

TYPE_ALPHA

此字段类型接受字母数据;没有空格,没有数字,没有特殊字符(这在字符输入时进行检查)。它使用以下命令设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_ALPHA,            /* type to associate */
                   int width);            /* maximum width of field */

width 参数设置数据的最小宽度。用户必须至少输入 width 个字符才能离开字段。通常,你希望将其设置为字段宽度;如果它大于字段宽度,则验证检查将始终失败。最小宽度为零使字段完成成为可选。

TYPE_ALNUM

此字段类型接受字母数据和数字;没有空格,没有特殊字符(这在字符输入时进行检查)。它使用以下命令设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_ALNUM,            /* type to associate */
                   int width);            /* maximum width of field */

width 参数设置数据的最小宽度。与 TYPE_ALPHA 一样,通常你希望将其设置为字段宽度;如果它大于字段宽度,则验证检查将始终失败。最小宽度为零使字段完成成为可选。

TYPE_ENUM

此类型允许你将字段的值限制为一组指定的字符串值(例如,美国州的两位字母邮政编码)。它使用以下命令设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_ENUM,             /* type to associate */
                   char **valuelist;      /* list of possible values */
                   int checkcase;         /* case-sensitive? */
                   int checkunique);      /* must specify uniquely? */

valuelist 参数必须指向以 NULL 结尾的有效字符串列表。checkcase 参数,如果为 true,则使与字符串的比较区分大小写。

当用户退出 TYPE_ENUM 字段时,验证过程会尝试将缓冲区中的数据完成为有效条目。如果已输入完整的选择字符串,则当然有效。但是,也可以输入有效字符串的前缀并为你完成。

默认情况下,如果你输入这样的前缀,并且它与字符串列表中的多个值匹配,则该前缀将完成为第一个匹配值。但是,checkunique 参数,如果为 true,则要求前缀匹配是唯一的才能有效。

REQ_NEXT_CHOICE 和 REQ_PREV_CHOICE 输入请求对于这些字段特别有用。

TYPE_INTEGER

此字段类型接受整数。它按如下方式设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_INTEGER,          /* type to associate */
                   int padding,           /* # places to zero-pad to */
                   int vmin, int vmax);   /* valid range */

有效字符包括可选的前导减号和数字。范围检查在退出时执行。如果范围最大值小于或等于最小值,则忽略范围。

如果值通过其范围检查,则会填充尽可能多的前导零数字,以满足 padding 参数。

TYPE_INTEGER 值缓冲区可以方便地使用 C 库函数 atoi(3) 进行解释。

TYPE_NUMERIC

此字段类型接受十进制数。它按如下方式设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_NUMERIC,          /* type to associate */
                   int padding,           /* # places of precision */
                   int vmin, int vmax);   /* valid range */

有效字符包括可选的前导减号和数字,可能包括小数点。范围检查在退出时执行。如果范围最大值小于或等于最小值,则忽略范围。

如果值通过其范围检查,则会填充尽可能多的尾随零数字,以满足 padding 参数。

TYPE_NUMERIC 值缓冲区可以方便地使用 C 库函数 atof(3) 进行解释。

TYPE_REGEXP

此字段类型接受与正则表达式匹配的数据。它按如下方式设置

int set_field_type(FIELD *field,          /* field to alter */
                   TYPE_REGEXP,           /* type to associate */
                   char *regexp);         /* expression to match */

正则表达式的语法与 regcomp(3) 的语法相同。正则表达式匹配的检查在退出时执行。

18.6. 表单驱动程序:表单系统的工作主力

与菜单系统一样,form_driver() 在表单系统中起着非常重要的作用。所有类型的表单系统请求都应通过 form_driver() 传递。

int form_driver(FORM *form,     /* form on which to operate     */
                int request)    /* form request code         */

正如你在上面的一些示例中所见,你必须在一个循环中查找用户输入,然后决定它是字段数据还是表单请求。然后将表单请求传递给 form_driver() 以完成工作。

这些请求大致可以分为以下几类。下面解释了不同的请求及其用法

18.6.1. 页面导航请求

这些请求导致在表单中进行页面级移动,从而触发新表单屏幕的显示。表单可以由多个页面组成。如果你有一个包含大量字段和逻辑部分的大型表单,则可以将表单划分为页面。函数 set_new_page() 用于在指定的字段处设置新页面。

int set_new_page(FIELD *field,/* Field at which page break to be set or unset */
         bool new_page_flag); /* should be TRUE to put a break */

以下请求允许你移动到不同的页面

  • 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() 设置)。

也可以遍历字段,就好像它们已按屏幕位置顺序排序一样,因此序列从左到右,从上到下。要执行此操作,请使用第二组四个排序移动请求。

最后,可以使用视觉方向向上、向下、向右和向左在字段之间移动。要完成此操作,请使用第三组四个请求。但是请注意,对于这些请求而言,表单的位置是其左上角。

例如,假设你有一个多行字段 B,以及与 B 在同一行上的两个单行字段 A 和 C,其中 A 在 B 的左侧,C 在 B 的右侧。仅当 A、B 和 C 都共享同一第一行时,从 A 发出的 REQ_MOVE_RIGHT 才会转到 B;否则它将跳过 B 转到 C。

18.6.3. 字段内导航请求

这些请求驱动编辑光标在当前选定字段内的移动。

  • REQ_NEXT_CHAR 移动到下一个字符。

  • REQ_PREV_CHAR 移动到上一个字符。

  • REQ_NEXT_LINE 移动到下一行。

  • REQ_PREV_LINE 移动到上一行。

  • REQ_NEXT_WORD 移动到下一个单词。

  • REQ_PREV_WORD 移动到上一个单词。

  • 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 字符传递给表单驱动程序时,它将被视为将字符添加到字段数据缓冲区的请求。这是插入还是替换取决于字段的编辑模式(默认值为插入)。

以下请求支持编辑字段和更改编辑模式

  • 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_CLR_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 选项关闭,则此特殊操作被禁用,并且表单驱动程序仅返回 E_REQUEST_DENIED。

18.6.6. 顺序请求

如果你的字段类型是有序的,并且具有用于从给定值获取该类型的下一个值和上一个值的关联函数,则有一些请求可以将该值提取到字段缓冲区中

  • REQ_NEXT_CHOICE 将当前值的后继值放入缓冲区。

  • REQ_PREV_CHOICE 将当前值的前驱值放入缓冲区。

在内置字段类型中,只有 TYPE_ENUM 具有内置的后继函数和前驱函数。当你定义自己的字段类型时(请参阅自定义验证类型),你可以关联我们自己的排序函数。

18.6.7. 应用程序命令

表单请求表示为大于 KEY_MAX 且小于或等于常量 MAX_COMMAND 的 curses 值之上的整数。此范围内的值将被 form_driver() 忽略。因此,应用程序可以将其用于任何目的。它可以被视为特定于应用程序的操作并采取相应的操作。