16. 面板库

既然您已经精通 curses,您肯定想做一些大事。您创建了许多重叠的窗口,以实现专业的 Windows 类型的外观。但不幸的是,很快就变得难以管理。多次刷新和更新会让您陷入噩梦。每当您忘记以正确的顺序刷新窗口时,重叠的窗口就会产生污点。

不要绝望。面板库中提供了一个优雅的解决方案。用 ncurses 开发人员的话来说:

当您的界面设计使得窗口可能深入到可见性堆栈中或在运行时弹出到顶部时,由此产生的簿记工作可能会变得繁琐且难以正确完成。因此,就有了面板库。

如果您有很多重叠的窗口,那么面板库是您的最佳选择。它消除了执行一系列 wnoutrefresh()、doupdate() 的需要,并减轻了正确(自下而上)执行它们的负担。该库维护有关窗口顺序、它们的重叠的信息,并正确更新屏幕。还在等什么?让我们仔细看看面板。

16.1. 基础知识

面板对象是一个窗口,它被隐式地视为包含所有其他面板对象的一个甲板的一部分。该甲板被视为一个堆栈,顶部面板完全可见,其他面板可能根据其位置而被遮挡或不被遮挡。因此,基本思想是创建一个重叠面板的堆栈,并使用面板库来正确显示它们。有一个类似于 refresh() 的函数,当调用它时,会以正确的顺序显示面板。提供了用于隐藏或显示面板、移动面板、更改其大小等的函数。在所有对这些函数的调用期间,重叠问题都由面板库管理。

面板程序的一般流程如下:

  1. 创建要附加到面板的窗口(使用 newwin())。

  2. 使用选择的可见性顺序创建面板。根据所需的可见性堆叠它们。函数 new_panel() 用于创建面板。

  3. 调用 update_panels() 将面板以正确的可见性顺序写入虚拟屏幕。执行 doupdate() 以在屏幕上显示它。

  4. 使用 show_panel()、hide_panel()、move_panel() 等操作面板。利用诸如 panel_hidden() 和 panel_window() 之类的辅助函数。利用用户指针为面板存储自定义数据。使用函数 set_panel_userptr() 和 panel_userptr() 来设置和获取面板的用户指针。

  5. 当您完成面板操作后,使用 del_panel() 删除面板。

让我们通过一些程序来弄清楚这些概念。以下是一个简单的程序,它创建了 3 个重叠的面板并在屏幕上显示它们。

16.2. 使用面板库编译

要使用面板库函数,您必须包含 panel.h,并且要将程序与面板库链接,应添加标志 -lpanel 以及 -lncurses,顺序如此。

    #include <panel.h>
    .
    .
    .

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

示例 14. 面板基础知识

#include <panel.h>

int main()
{	WINDOW *my_wins[3];
	PANEL  *my_panels[3];
	int lines = 10, cols = 40, y = 2, x = 4, i;

	initscr();
	cbreak();
	noecho();

	/* Create windows for the panels */
	my_wins[0] = newwin(lines, cols, y, x);
	my_wins[1] = newwin(lines, cols, y + 1, x + 5);
	my_wins[2] = newwin(lines, cols, y + 2, x + 10);

	/* 
	 * Create borders around the windows so that you can see the effect
	 * of panels
	 */
	for(i = 0; i < 3; ++i)
		box(my_wins[i], 0, 0);

	/* Attach a panel to each window */ 	/* Order is bottom up */
	my_panels[0] = new_panel(my_wins[0]); 	/* Push 0, order: stdscr-0 */
	my_panels[1] = new_panel(my_wins[1]); 	/* Push 1, order: stdscr-0-1 */
	my_panels[2] = new_panel(my_wins[2]); 	/* Push 2, order: stdscr-0-1-2 */

	/* Update the stacking order. 2nd panel will be on top */
	update_panels();

	/* Show it on the screen */
	doupdate();
	
	getch();
	endwin();
}

正如您所见,上面的程序遵循了如上所述的简单流程。窗口使用 newwin() 创建,然后使用 new_panel() 将它们附加到面板。当我们一个接一个地附加面板时,面板堆栈会更新。要将它们显示在屏幕上,需要调用 update_panels() 和 doupdate()。

16.3. 面板窗口浏览

下面给出了一个稍微复杂的示例。此程序创建了 3 个窗口,可以使用 Tab 键循环浏览。看看代码。

示例 15. 面板窗口浏览示例

#include <panel.h>

#define NLINES 10
#define NCOLS 40

void init_wins(WINDOW **wins, int n);
void win_show(WINDOW *win, char *label, int label_color);
void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string, chtype color);

int main()
{	WINDOW *my_wins[3];
	PANEL  *my_panels[3];
	PANEL  *top;
	int ch;

	/* Initialize curses */
	initscr();
	start_color();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize all the colors */
	init_pair(1, COLOR_RED, COLOR_BLACK);
	init_pair(2, COLOR_GREEN, COLOR_BLACK);
	init_pair(3, COLOR_BLUE, COLOR_BLACK);
	init_pair(4, COLOR_CYAN, COLOR_BLACK);

	init_wins(my_wins, 3);
	
	/* Attach a panel to each window */ 	/* Order is bottom up */
	my_panels[0] = new_panel(my_wins[0]); 	/* Push 0, order: stdscr-0 */
	my_panels[1] = new_panel(my_wins[1]); 	/* Push 1, order: stdscr-0-1 */
	my_panels[2] = new_panel(my_wins[2]); 	/* Push 2, order: stdscr-0-1-2 */

	/* Set up the user pointers to the next panel */
	set_panel_userptr(my_panels[0], my_panels[1]);
	set_panel_userptr(my_panels[1], my_panels[2]);
	set_panel_userptr(my_panels[2], my_panels[0]);

	/* Update the stacking order. 2nd panel will be on top */
	update_panels();

	/* Show it on the screen */
	attron(COLOR_PAIR(4));
	mvprintw(LINES - 2, 0, "Use tab to browse through the windows (F1 to Exit)");
	attroff(COLOR_PAIR(4));
	doupdate();

	top = my_panels[2];
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case 9:
				top = (PANEL *)panel_userptr(top);
				top_panel(top);
				break;
		}
		update_panels();
		doupdate();
	}
	endwin();
	return 0;
}

/* Put all the windows */
void init_wins(WINDOW **wins, int n)
{	int x, y, i;
	char label[80];

	y = 2;
	x = 10;
	for(i = 0; i < n; ++i)
	{	wins[i] = newwin(NLINES, NCOLS, y, x);
		sprintf(label, "Window Number %d", i + 1);
		win_show(wins[i], label, i + 1);
		y += 3;
		x += 7;
	}
}

/* Show the window with a border and a label */
void win_show(WINDOW *win, char *label, int label_color)
{	int startx, starty, height, width;

	getbegyx(win, starty, startx);
	getmaxyx(win, height, width);

	box(win, 0, 0);
	mvwaddch(win, 2, 0, ACS_LTEE); 
	mvwhline(win, 2, 1, ACS_HLINE, width - 2); 
	mvwaddch(win, 2, width - 1, ACS_RTEE); 
	
	print_in_middle(win, 1, 0, width, label, COLOR_PAIR(label_color));
}

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();
}

16.4. 使用用户指针

在上面的示例中,我使用了用户指针来找出循环中的下一个窗口。我们可以通过指定用户指针将自定义信息附加到面板,该用户指针可以指向您想要存储的任何信息。在本例中,我存储了指向循环中下一个面板的指针。可以使用以下函数设置面板的用户指针:set_panel_userptr()。可以使用以下函数访问它:panel_userptr()它将返回作为参数给出的面板的用户指针。找到循环中的下一个面板后,它会通过函数 top_panel() 将其带到顶部。此函数将作为参数给出的面板带到面板堆栈的顶部。

16.5. 移动和调整面板大小

函数move_panel()可用于将面板移动到所需位置。它不会更改面板在堆栈中的位置。请确保您在与面板关联的窗口上使用 move_panel() 而不是 mvwin()。

调整面板大小稍微复杂。没有直接的函数可以仅调整与面板关联的窗口的大小。调整面板大小的解决方案是创建一个具有所需大小的新窗口,使用 replace_panel() 更改与面板关联的窗口。不要忘记删除旧窗口。可以使用函数 panel_window() 找到与面板关联的窗口。

以下程序以一个据说是简单的程序展示了这些概念。您可以像往常一样使用 <TAB> 键循环浏览窗口。要调整活动面板的大小或移动它,请按“r”键进行调整大小,按“m”键进行移动。然后使用箭头键调整大小或将其移动到所需的方式,然后按 Enter 键结束调整大小或移动。此示例利用用户数据来获取执行操作所需的数据。

示例 16. 面板移动和调整大小示例

#include <panel.h>

typedef struct _PANEL_DATA {
	int x, y, w, h;
	char label[80]; 
	int label_color;
	PANEL *next;
}PANEL_DATA;

#define NLINES 10
#define NCOLS 40

void init_wins(WINDOW **wins, int n);
void win_show(WINDOW *win, char *label, int label_color);
void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string, chtype color);
void set_user_ptrs(PANEL **panels, int n);

int main()
{	WINDOW *my_wins[3];
	PANEL  *my_panels[3];
	PANEL_DATA  *top;
	PANEL *stack_top;
	WINDOW *temp_win, *old_win;
	int ch;
	int newx, newy, neww, newh;
	int size = FALSE, move = FALSE;

	/* Initialize curses */
	initscr();
	start_color();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize all the colors */
	init_pair(1, COLOR_RED, COLOR_BLACK);
	init_pair(2, COLOR_GREEN, COLOR_BLACK);
	init_pair(3, COLOR_BLUE, COLOR_BLACK);
	init_pair(4, COLOR_CYAN, COLOR_BLACK);

	init_wins(my_wins, 3);
	
	/* Attach a panel to each window */ 	/* Order is bottom up */
	my_panels[0] = new_panel(my_wins[0]); 	/* Push 0, order: stdscr-0 */
	my_panels[1] = new_panel(my_wins[1]); 	/* Push 1, order: stdscr-0-1 */
	my_panels[2] = new_panel(my_wins[2]); 	/* Push 2, order: stdscr-0-1-2 */

	set_user_ptrs(my_panels, 3);
	/* Update the stacking order. 2nd panel will be on top */
	update_panels();

	/* Show it on the screen */
	attron(COLOR_PAIR(4));
	mvprintw(LINES - 3, 0, "Use 'm' for moving, 'r' for resizing");
	mvprintw(LINES - 2, 0, "Use tab to browse through the windows (F1 to Exit)");
	attroff(COLOR_PAIR(4));
	doupdate();

	stack_top = my_panels[2];
	top = (PANEL_DATA *)panel_userptr(stack_top);
	newx = top->x;
	newy = top->y;
	neww = top->w;
	newh = top->h;
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case 9:		/* Tab */
				top = (PANEL_DATA *)panel_userptr(stack_top);
				top_panel(top->next);
				stack_top = top->next;
				top = (PANEL_DATA *)panel_userptr(stack_top);
				newx = top->x;
				newy = top->y;
				neww = top->w;
				newh = top->h;
				break;
			case 'r':	/* Re-Size*/
				size = TRUE;
				attron(COLOR_PAIR(4));
				mvprintw(LINES - 4, 0, "Entered Resizing :Use Arrow Keys to resize and press <ENTER> to end resizing");
				refresh();
				attroff(COLOR_PAIR(4));
				break;
			case 'm':	/* Move */
				attron(COLOR_PAIR(4));
				mvprintw(LINES - 4, 0, "Entered Moving: Use Arrow Keys to Move and press <ENTER> to end moving");
				refresh();
				attroff(COLOR_PAIR(4));
				move = TRUE;
				break;
			case KEY_LEFT:
				if(size == TRUE)
				{	--newx;
					++neww;
				}
				if(move == TRUE)
					--newx;
				break;
			case KEY_RIGHT:
				if(size == TRUE)
				{	++newx;
					--neww;
				}
				if(move == TRUE)
					++newx;
				break;
			case KEY_UP:
				if(size == TRUE)
				{	--newy;
					++newh;
				}
				if(move == TRUE)
					--newy;
				break;
			case KEY_DOWN:
				if(size == TRUE)
				{	++newy;
					--newh;
				}
				if(move == TRUE)
					++newy;
				break;
			case 10:	/* Enter */
				move(LINES - 4, 0);
				clrtoeol();
				refresh();
				if(size == TRUE)
				{	old_win = panel_window(stack_top);
					temp_win = newwin(newh, neww, newy, newx);
					replace_panel(stack_top, temp_win);
					win_show(temp_win, top->label, top->label_color); 
					delwin(old_win);
					size = FALSE;
				}
				if(move == TRUE)
				{	move_panel(stack_top, newy, newx);
					move = FALSE;
				}
				break;
			
		}
		attron(COLOR_PAIR(4));
		mvprintw(LINES - 3, 0, "Use 'm' for moving, 'r' for resizing");
	    	mvprintw(LINES - 2, 0, "Use tab to browse through the windows (F1 to Exit)");
	    	attroff(COLOR_PAIR(4));
	        refresh();	
		update_panels();
		doupdate();
	}
	endwin();
	return 0;
}

/* Put all the windows */
void init_wins(WINDOW **wins, int n)
{	int x, y, i;
	char label[80];

	y = 2;
	x = 10;
	for(i = 0; i < n; ++i)
	{	wins[i] = newwin(NLINES, NCOLS, y, x);
		sprintf(label, "Window Number %d", i + 1);
		win_show(wins[i], label, i + 1);
		y += 3;
		x += 7;
	}
}

/* Set the PANEL_DATA structures for individual panels */
void set_user_ptrs(PANEL **panels, int n)
{	PANEL_DATA *ptrs;
	WINDOW *win;
	int x, y, w, h, i;
	char temp[80];
	
	ptrs = (PANEL_DATA *)calloc(n, sizeof(PANEL_DATA));

	for(i = 0;i < n; ++i)
	{	win = panel_window(panels[i]);
		getbegyx(win, y, x);
		getmaxyx(win, h, w);
		ptrs[i].x = x;
		ptrs[i].y = y;
		ptrs[i].w = w;
		ptrs[i].h = h;
		sprintf(temp, "Window Number %d", i + 1);
		strcpy(ptrs[i].label, temp);
		ptrs[i].label_color = i + 1;
		if(i + 1 == n)
			ptrs[i].next = panels[0];
		else
			ptrs[i].next = panels[i + 1];
		set_panel_userptr(panels[i], &ptrs[i]);
	}
}

/* Show the window with a border and a label */
void win_show(WINDOW *win, char *label, int label_color)
{	int startx, starty, height, width;

	getbegyx(win, starty, startx);
	getmaxyx(win, height, width);

	box(win, 0, 0);
	mvwaddch(win, 2, 0, ACS_LTEE); 
	mvwhline(win, 2, 1, ACS_HLINE, width - 2); 
	mvwaddch(win, 2, width - 1, ACS_RTEE); 
	
	print_in_middle(win, 1, 0, width, label, COLOR_PAIR(label_color));
}

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();
}

专注于主 while 循环。一旦它找出按下的键的类型,它就会采取适当的措施。如果按下“r”键,则启动调整大小模式。在此之后,当用户按下箭头键时,新大小会更新。当用户按下 <ENTER> 键时,当前选择结束,并通过使用解释的概念来调整面板大小。在调整大小模式下,程序不会显示窗口是如何调整大小的。将其作为练习留给读者,以便在窗口调整到新位置时打印虚线边框。

当用户按下“m”键时,移动模式开始。这比调整大小要简单一些。当按下箭头键时,新位置会更新,按下 <ENTER> 键会导致通过调用函数 move_panel() 来移动面板。

在此程序中,用户数据(表示为 PANEL_DATA)在查找与面板关联的信息方面起着非常重要的作用。正如注释中写的那样,PANEL_DATA 存储面板大小、标签、标签颜色以及指向循环中下一个面板的指针。

16.6. 隐藏和显示面板

可以使用函数 hide_panel() 隐藏面板。此函数仅将其从面板堆栈中删除,从而在您执行 update_panels() 和 doupdate() 后在屏幕上隐藏它。它不会破坏与隐藏面板关联的 PANEL 结构。可以使用 show_panel() 函数再次显示它。

以下程序显示了面板的隐藏。按“a”或“b”或“c”分别显示或隐藏第一个、第二个和第三个窗口。它使用带有小变量 hide 的用户数据,该变量跟踪窗口是否隐藏。由于某种原因,函数panel_hidden()用于判断面板是否隐藏的函数无法正常工作。Michael Andres 也提交了一份错误报告 此处

示例 17. 面板隐藏和显示示例

#include <panel.h>

typedef struct _PANEL_DATA {
	int hide;	/* TRUE if panel is hidden */
}PANEL_DATA;

#define NLINES 10
#define NCOLS 40

void init_wins(WINDOW **wins, int n);
void win_show(WINDOW *win, char *label, int label_color);
void print_in_middle(WINDOW *win, int starty, int startx, int width, char *string, chtype color);

int main()
{	WINDOW *my_wins[3];
	PANEL  *my_panels[3];
	PANEL_DATA panel_datas[3];
	PANEL_DATA *temp;
	int ch;

	/* Initialize curses */
	initscr();
	start_color();
	cbreak();
	noecho();
	keypad(stdscr, TRUE);

	/* Initialize all the colors */
	init_pair(1, COLOR_RED, COLOR_BLACK);
	init_pair(2, COLOR_GREEN, COLOR_BLACK);
	init_pair(3, COLOR_BLUE, COLOR_BLACK);
	init_pair(4, COLOR_CYAN, COLOR_BLACK);

	init_wins(my_wins, 3);
	
	/* Attach a panel to each window */ 	/* Order is bottom up */
	my_panels[0] = new_panel(my_wins[0]); 	/* Push 0, order: stdscr-0 */
	my_panels[1] = new_panel(my_wins[1]); 	/* Push 1, order: stdscr-0-1 */
	my_panels[2] = new_panel(my_wins[2]); 	/* Push 2, order: stdscr-0-1-2 */

	/* Initialize panel datas saying that nothing is hidden */
	panel_datas[0].hide = FALSE;
	panel_datas[1].hide = FALSE;
	panel_datas[2].hide = FALSE;

	set_panel_userptr(my_panels[0], &panel_datas[0]);
	set_panel_userptr(my_panels[1], &panel_datas[1]);
	set_panel_userptr(my_panels[2], &panel_datas[2]);

	/* Update the stacking order. 2nd panel will be on top */
	update_panels();

	/* Show it on the screen */
	attron(COLOR_PAIR(4));
	mvprintw(LINES - 3, 0, "Show or Hide a window with 'a'(first window)  'b'(Second Window)  'c'(Third Window)");
	mvprintw(LINES - 2, 0, "F1 to Exit");

	attroff(COLOR_PAIR(4));
	doupdate();
	
	while((ch = getch()) != KEY_F(1))
	{	switch(ch)
		{	case 'a':			
				temp = (PANEL_DATA *)panel_userptr(my_panels[0]);
				if(temp->hide == FALSE)
				{	hide_panel(my_panels[0]);
					temp->hide = TRUE;
				}
				else
				{	show_panel(my_panels[0]);
					temp->hide = FALSE;
				}
				break;
			case 'b':
				temp = (PANEL_DATA *)panel_userptr(my_panels[1]);
				if(temp->hide == FALSE)
				{	hide_panel(my_panels[1]);
					temp->hide = TRUE;
				}
				else
				{	show_panel(my_panels[1]);
					temp->hide = FALSE;
				}
				break;
			case 'c':
				temp = (PANEL_DATA *)panel_userptr(my_panels[2]);
				if(temp->hide == FALSE)
				{	hide_panel(my_panels[2]);
					temp->hide = TRUE;
				}
				else
				{	show_panel(my_panels[2]);
					temp->hide = FALSE;
				}
				break;
		}
		update_panels();
		doupdate();
	}
	endwin();
	return 0;
}

/* Put all the windows */
void init_wins(WINDOW **wins, int n)
{	int x, y, i;
	char label[80];

	y = 2;
	x = 10;
	for(i = 0; i < n; ++i)
	{	wins[i] = newwin(NLINES, NCOLS, y, x);
		sprintf(label, "Window Number %d", i + 1);
		win_show(wins[i], label, i + 1);
		y += 3;
		x += 7;
	}
}

/* Show the window with a border and a label */
void win_show(WINDOW *win, char *label, int label_color)
{	int startx, starty, height, width;

	getbegyx(win, starty, startx);
	getmaxyx(win, height, width);

	box(win, 0, 0);
	mvwaddch(win, 2, 0, ACS_LTEE); 
	mvwhline(win, 2, 1, ACS_HLINE, width - 2); 
	mvwaddch(win, 2, width - 1, ACS_RTEE); 
	
	print_in_middle(win, 1, 0, width, label, COLOR_PAIR(label_color));
}

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();
}

16.7. panel_above() 和 panel_below() 函数

函数panel_above()panel_below()可用于查找面板上方和下方的面板。如果这些函数的参数为 NULL,则它们分别返回指向底部面板和顶部面板的指针。