大多数商业化软件产品一般会通过实现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过于底层,我们直接使用相对繁琐,对于简单的界面实现,还是推荐使用更为上层的封装库。