大多数商业化软件产品一般会通过实现GUI(Graphical User Interface)或者TUI(Text-based User Interface/Textual User Interface/Terminal User Interface)来降低软件的使用难度。本文简要介绍一个TUI库: newt。
newt是由RedHat开发,主要用在RatHat的Linux发行版本(RHEL, Fedora和CentOS)的安装程序项目Anaconda中。NEWT的全称是: Not Erik’s Windowing Toolkit,它基于S-Lang库实现。
因为newt是用于Linux发行版本的安装程序中,因而它的运行空间非常有限,需要保证程序尽可能小,所以在设计时选择使用C语言进行开发,也尽量不支持多余特性。于是在设计之初就不支持事件驱动。在支持事件驱动的窗口库中,有一个事件循环监听事件发生,根据发生的事件展示不同的窗口。在newt中,窗口创建和销毁基于栈模式,新创建的窗口位于之前的窗口之上,且只能展示最上层窗口,当最上层窗口销毁之后才能展示下边的窗口。这也就是说所有窗口都为模态窗口(model windows)。这种特性限制了newt库本身只适合用于完成顺序的程序流程,一个典型的场景就是程序的安装向导。SHELL程序往往也是顺序的流程,从SHELL程序转换为newt窗口程序完全不涉及程序流程的改变,实现非常简单。
newt中的主要结构为组件(components), 它和其他窗口库中所称的widget为相同概念。newt组件有许多, 常用的包括:
- Button
- CompactButton
- EntryBox
- CheckBox
- RadioButton
- Textbox
- Scrollbars
- Listbox
具体可以参考这篇tutorail文章
。这篇文章写于2003年, 当时newt版本为v0.31,略有些老,但内容上在现在的版本(v0.52)上依然适用。
Form是一种特殊的组件,它将其他组件组合在一起。newt应用程序使用form做为与用户交互的媒介。newt应用程序与用户交互的的简要逻辑为: 当newt应用需要从用户获取输入时,它运行一个form。此时,newt应用开始等待用户输入。用户输入信息到form中所包含的组件后,将控制权交还newt应用(如通过触发按钮等行为),newt应用再从组件中获取到用户输入的内容继续执行。form可以包含任意组件,也包括其他form。form嵌套可以用于改变TAB键触发的移动顺序、控制窗口不同区域的背景色,滚动特定窗口区域等。
newt代码中将所有的组件都用一种变量类型:newtComponent来表示。我们可以给组件注册回调函数,回调函数何时被触发取决于组件类型。回调函数原型和注册函数原型如下:
1 | typedef void (*newtCallback)(newtComponent, void *); |
newt所有组件都绘制在窗口(window)上,newt默认创建了一个特殊的背景窗口,叫做root窗口。我们能够在该窗口上放置组件,也可以使用API创建新的窗口。newt库的坐标系统原点坐于屏幕左上角,函数原型中,left参数表示x坐标,从原点出发从左至右增大,top参数表示y坐标,从上到下增大。left可以对应为列数,top可以对应为行数,两个坐标都可以为负数,表示从反方向开始计算。如:
1 | newtDrawRootText(10, -5, "Hello NEWT"); |
表示在距屏幕左上角的第10列,倒数第5行的位置输出字符串Hello NEWT。
下边我们以一个实例来说明newt的使用。
1 |
|
CentOS发行版默认已带有newt库,我们需要安装newt-devel库:
1 | yum install newt-devel |
编译该程序:
1 | gcc -o newtdemo newtdemo.c -lnewt |
执行newtdemo, 界面如下图:
在密码框中输入123456后,移动焦点选择OK按钮,回车后显示另一个窗口:
我们简单说明下源代码。main函数中首先调用了:
1 | newtInit(); |
newtInit()用于初始化newt库的内部数据结构,并将终端设置为raw模式。接着调用newtCls()来清空屏幕。newt窗口的最后一行用于显示帮助信息,如每个快捷键所对应的功能。代码中调用newtPushHelpLine(NULL)显示默认的帮助信息。接下来通过调用newtOpenWindow()创建了一个新窗口,后续的组件将绘制在该窗口中。接下来,在窗口上创建了Username和Password两个输入框用于接收用户输入,以及两个按钮。
从组件定义的代码行中可以看到所有的组件都是newtComponent类型:
1 | newtComponent form, btn_ok, btn_cancel, label_username, label_password, |
我们使用一个Form表单将这些组件组合起来:
1 | newtFormAddComponents(form, label_username, entry_username, |
当执行newtFormRun()时,newt应用将等待用户操作。我们通过exit_status结构体接收用户的操作信息来判断哪个按钮被触发。如果用户触发了Cancel按钮则程序结束。如果触发的为OK按钮,则检测输入的密码是否为123456。如果输入错误,则显示一个错误信息窗口。密码验证通过则显示一个长消息Textbox窗口。
这里我们也可以通过注册回调函数给两个按钮组件,根据哪个回调函数被执行来判断哪个按钮被触发。
newt整体使用非常简单,然而比较遗憾的是,它基本没有文档,最好的参考资料为官方源码中的几个示例:
- https://pagure.io/newt/blob/master/f/test.c
- https://pagure.io/newt/blob/master/f/testgrid.c
- https://pagure.io/newt/blob/master/f/textbox.c
官方newt库中还提供了Python封装库,名称为snack。具体用法和C库类似,官方也提供了两个示例:
除此之外,newt库中还提供了一个whiptail的命令行程序,用在SHELL脚本中显示对话框。比如,我们在BASH中执行:
1 | whiptail --yesno "Continue?" 10 50 |
这将显示如下窗口:
当选择YES按钮时,程序退出码为0,选择NO按钮,程序退出码为1。我们选择YES之后,查看退出码:
1 | [root@centos2 newt]# echo $? |
具体信息可以参考:
类似的TUI工具还有CDK和dialog。[CDK](Curses Development Kit)是一个程序库,能够链接进我们自己开发的应用程序,dialog为一个命令行程序,与whiptail类似,可以在SHELL脚本中实现TUI界面。它们都是基于ncurses开发的,ncurses是目前最为广泛使用的TUI库。不过它的API过于底层,我们直接使用相对繁琐,对于简单的界面实现,还是推荐使用更为上层的封装库。