6.5 窗口
到目前为止,你一直将终端用作为一个全屏幕的输出介质。对短小、简单的程序来说,这样做一般已足够了,但curses函数库的功能远不止如此。你可以用curses函数库在物理屏幕上同时显示多个不同尺寸的窗口。本节中介绍的许多函数只被X/Open规范定义的扩展curses函数库支持,但因为ncurses函数库也支持它们,所以在大多数平台中使用它们并不会出现问题。现在是时候开始学习多窗口的使用方法了。你还将看到如何将所使用的这些函数通用化,并应用到多窗口的情况下。
6.5.1 WINDOW结构
虽然前面己介绍过标准屏幕stdscr,但目前为止,你几乎没有使用它的必要。因为,几乎所有我们前面讨论过的函数都假设它们工作在stdscr之上,因此,stdscr无需作为一个参数传递给这些函数。
标准屏幕stdscr只是WINDOW结构的一个特例,就像标准输出stdout是文件流的一个特例一样。WINDOW结构通常定义在头文件curses.h中,虽然研究该结构是有意义的,但程序应该永远都不要直接访问它,因为该结构在不同的curses版本中的实现方式不同。
你可以用函数调用newwin和delwin来创建和销毁窗口:
newwin函数的作用是创建一个新窗口,该窗口从屏幕位置(start_y, start_x)开始,行数和列数分别由参数num_of_lines和num_of_cols指定。它返回一个指向新窗口的指针,如果新窗口创建失败则返回null。如果想让新窗口的右下角正好落在屏幕的右下角上,你可以将该函数的行、列参数设为0。所有的窗口范围都必须在当前屏幕范围之内,如果新窗口的任何部分落在当前屏幕范围之外,则newwin函数调用将失败。通过newwin函数创建的新窗口完全独立于所有已存在的窗口。默认情况下,它被放置在任何已有窗口之上,覆盖(但不是改变)它们的内容。
delwin函数的作用是删除一个先前通过newwin函数创建的窗口。因为调用newwin函数可能会给新窗口分配内存,所以当不再需要这些窗口时,不要忘记通过delwin函数将其删除。
注意,千万不要尝试删除curses自己的窗口stdscr和curscr!
创建新窗口后,怎样才能对它们进行写操作呢?答案是,几乎所有你已见过的函数都有对应特定窗口进行操作的通用版本,并且为方便用户的使用,它们还都具备光标移动的功能。
6.5.2 通用函数
你已使用过函数addch和printw在屏幕上增加字符。这两个函数,包括其他一些函数,都可以通过加上一些前缀变为通用函数。前缀w用于窗口、mv用于光标移动、mvw用于在窗口中移动光标。如果查看大多数curses函数库实现中的curses头文件,你会发现你所使用过的许多函数都只是调用这些通用函数的简单的宏定义(#define语句)。
如果给函数增加了w前缀,就必须在该函数的参数表的最前面增加一个WINDOW指针参数。如果给函数增加的是mv前缀,则需要在函数的参数表的最前面增加两个参数,分别是纵坐标y和横坐标x,这两个坐标值指定了执行操作的位置。坐标值y和x是相对于窗口而不是相对于屏幕的,坐标(0,0)代表窗口的左上角。
如果给函数增加了mvw前缀,就需要多传递3个参数,它们分别是一个WINDOW指针、y和x坐标值。让人困惑的是,WINDOWS指针参数总是出现在屏幕坐标值之前,虽然从前缀的写法来看,y和x参数应是首先出现的。
作为一个例子,下面列出了函数addch和printw的所有原型定义集:
其他许多函数,例如inch,也有加上诸如mv和w前缀的通用函数。
6.5.3 移动和更新窗口
通过下面这些函数,你可以移动和重新绘制窗口:
mvwin函数的作用是在屏幕上移动一个窗口。因为不允许窗口的任何部分超出屏幕范围,所以如果在调用mvwin函数时,将窗口的某个部分移动到屏幕区域之外,mvwin函数调用将会失败。
wrefresh、wclear和werases函数分别是前面介绍的refresh、clear和erases函数的通用版本。它们只是多了一个WINDOW指针参数,从而可针对特定的窗口进行操作,而不仅仅局限于stdscr。
touchwin函数非常特殊,它的作用是通知curses函数库其指针参数指向的窗口内容已发生改变。这就意味着,在下次调用wrefresh函数时,curses必须重新绘制该窗口,即使用户实际上并未修改该窗口中的内容。当屏幕上重叠着多个窗口时,你可以通过该函数来安排要显示的窗口。
两个scroll函数控制窗口的卷屏。如果传递给scrollok函数的是布尔值true(通常是非零值),则允许窗口卷屏。而默认情况下,窗口是不能卷屏的。scroll函数的作用只是把窗口内容上卷一行。一些curses函数库的实现版本中还有函数wsctl,它有一个指定卷行行数的参数,而且该参数还可以指定为负值。我们将在本章的稍后部分再次讨论卷屏问题。
实 验 管理多窗口
现在,你已知道如何管理多个窗口了,接下来,你可以把刚学到的这些新函数应用在程序multiw1.c中。为简洁起见,在程序中忽略了错误检查。
(1)与往常一样,我们先安排好各种定义:
(2)然后,用字符填充基本窗口,填充完逻辑屏幕后就开始刷新物理屏幕:
(3)现在,创建一个尺寸为10×20的新窗口,为它添加一些文本,然后将该窗口绘制到屏幕上:
(4)接下来,对背景窗口中的内容做些修改。当再次刷新屏幕时,new_window_ptr指向的窗口将被遮盖住:
(5)此时,如果调用wrefresh来刷新新窗口,则什么也不会发生,因为你并未对新窗口做过改动:
(6)但如果先对新窗口调用一次touchwin函数,让curses误以为新窗口中的内容已发生变化,则下一个wrefresh函数调用将再次把新窗口调到屏幕的最前面:
(7)接下来,再增加另一个加框的重叠窗口:
(8)然后,在清屏和删除这两个新窗口之前在屏幕上轮流显示它们:
遗憾的是,我们无法让读者在书中看到这一切发生的过程。图6-4显示了绘制第一个弹出窗口后的屏幕截图。
图 6-4
在改变背景窗口后,在屏幕上又绘制了一个弹出窗口,这时屏幕的显示如图6-5。
图 6-5
实验解析
在通常的初始化过程之后,程序使用字母填充标准屏幕,以便用户看到添加在其上的新curses窗口。然后,程序演示了如何在背景之上添加一个新窗口,以及新窗口中文本的折行效果。你还看到了如何使用touchwin来强制curses重新绘制窗口,即使窗口内容未发生任何改变。
接着,程序添加了第二个窗口,该窗口覆盖了第一个窗口的内容,这演示了curses是如何管理重叠窗口的。最后,程序关闭curses函数库并退出。
从上面的示例代码中可以看出,为了让窗口在屏幕上以正确的顺序显示,你必须在刷新窗口时非常小心。因为curses函数库并不存储关于窗口之间层次关系的任何信息,所以如果要求curses刷新多个窗口,你必须自己管理窗口之间的层次关系。
为确保curses能够以正确的顺序绘制窗口,你必须以正确的顺序对它们进行刷新。其中一个办法就是,将所有窗口的指针存储到一个数组或列表中,你通过这个数组或列表来维护它们应该显示在屏幕上的顺序。
6.5.4 优化屏幕刷新
从上一节的例子中可以看出,对多个窗口进行刷新需要一定的技巧,但还不至于太麻烦。但当要更新的终端是通过慢速链路连接到主机时,这个潜在的问题就会变得非常严重。幸运的是,现在这种情况已经很少见了,但实际上处理这个问题非常简单,所以,为了内容的完整性,我们在这里介绍这个问题的解决方法。
我们的目标是尽量减少需要在屏幕上绘制的字符数目,因为在慢速链路上,屏幕绘制的速度可能会慢得让人难以忍受。curses函数库为此提供了一种特殊手段,这需要用到下面两个函数:wnoutrefresh和doupdate:
wnoutrefresh函数用于决定把哪些字符发送到屏幕上,但它并不真正地发送这些字符,真正将更新发送到终端的工作由doupdate函数来完成。如果只是调用wnoutrefresh函数,然后立刻调用doupdate函数,则它的效果与直接调用wrefresh完全一样。但如果想重新绘制多个窗口,你可以为每个窗口分别调用wnoutrefresh函数(当然要按正确的顺序来操作),然后只需在调用最后一个wnoutrefresh之后调用一次doupdate函数即可。这允许curses依次为每个窗口执行屏幕更新计算工作,最后仅把最终的更新结果输出到屏幕上。这种做法可以最大限度地减少curses需要发送的字符数目。