创建npyscreen应用程序

对象概览

Npyscreen 应用程序是由3种主要类型的对象构建出来的.

Form Objects 窗口
窗体对象(通常是整个终端的大小,但有时大一些或者 -用于菜单之类的时候会小一些)可以提供出一个区域容纳其他控件.它们可能提供一些额外功能,比如菜单的控制系统,或是在当用户选择”ok”按钮时启动的协程.它们可能还会定义一些在用户按键间隙,或是用户在窗口中移动时要进行的操作.
Widget Objects 控件
这些是窗体上独立的控制部件, -文本框,标签,滚动条等等.
Application Objects 应用
这些对象提供一种更方便的管理你的应用程序运行的方式. 虽然有时也能不用应用对象来写一些简单的应用,但不建议这样. 讲道理,应用对象在多屏幕应用的管理上更不容易出错(在那些随时让应用崩溃的bug上). 另外,使用这些对象让你能利用 npyscreen 开发好的高级特性.

立取可用的程序结构

大多数新应用程序看起来都像下面这样:

import npyscreen

# 这里应用充当了 curses 初始化封装器的角色
# 同时也管理着应用的实际形态.

class MyTestApp(npyscreen.NPSAppManaged):
    def onStart(self):
        self.registerForm("MAIN", MainForm())

# 这个窗口类定义了展示给用户的显示内容.

class MainForm(npyscreen.Form):
    def create(self):
        self.add(npyscreen.TitleText, name = "Text:", value= "Hellow World!" )

    def afterEditing(self):
        self.parentApp.setNextForm(None)

if __name__ == '__main__':
    TA = MyTestApp()
    TA.run()

应用程序结构细节(教程)

第一次使用的用户可能会对上面的代码比较困惑. 要是这样,下面的教程就来详细解释一个npyscreen程序的结构.哪怕你很不了解底层的curses系统,你也应该可以跟上我们的节奏.

窗体, 控件和应用

使用封装器

切换到和切出 curses 环境是个非常枯燥的任务. python 的 curses 模块提供了一个封装器来完成这些. 这在 npyscreen 中以 wrapper_basic 向外暴露. 一个很简单的应用程序的基本框架看起来应该是这样的:

import npyscreen

def myFunction(*args):
    pass

if __name__ == '__main__':
    npyscreen.wrapper_basic(myFunction)
    print( "Blink and you missed it!" )

没啥复杂的. curses 环境启动并且啥也没干就退出了.但这只是一个开始.

注意, npyscreen 还提供其他封装器来完成一些稍有不同的东西.

使用窗体

现在我们来放点东西到屏幕上.为此我们需要一个 Form 实例:

F = npyscreen.Form(name='My Test Application')

这应该就够了,让我们把他放到封装器里面:

import npyscreen

def myFunction(*args):
    F = npyscreen.Form(name='My Test Application')

if __name__ == '__main__':
    npyscreen.wrapper_basic(myFunction)
    print( "Blink and you missed it!" )

这看起来好像还是啥也没干 – 因为我们实际还没有把窗体显示出来. F.display() 可以把它放到屏幕上, 但是我们实际上是希望用户可以玩一下, 所以让我们用 F.edit() 换掉它:

import npyscreen

def myFunction(*args):
    F = npyscreen.Form(name='My Test Application')
    F.edit()

if __name__ == '__main__':
    npyscreen.wrapper_basic(myFunction)
    print( "Blink and you missed it!" )

但还是没运行, 因为当你想要尝试编辑窗体的时候 npyscreen 会发现没有控件可编辑. 我们把它改正过来.

添加第一个控件

我们把一个带标题的文本框放到里面. 我们用下面的代码来做到这点:

F.add(npyscreen.TitleText, name="First Widget")

The full code is:

import npyscreen

def myFunction(*args):
    F = npyscreen.Form(name='My Test Application')
    F.add(npyscreen.TitleText, name="First Widget")
    F.edit()

if __name__ == '__main__':
    npyscreen.wrapper_basic(myFunction)
    print( "Blink and you missed it!" )

好多了! 这样我们就有了一个有应用样子的东西了. 加上3点小调整我们就可以把关闭显示的信息改成用户输的随便什么内容:

import npyscreen

def myFunction(*args):
    F = npyscreen.Form(name='My Test Application')
    myFW = F.add(npyscreen.TitleText, name="First Widget")   # <------- Change 1
    F.edit()
    return myFW.value   # <------- Change 2

if __name__ == '__main__':
    print( npyscreen.wrapper_basic(myFunction) ) # <---- and change 3

让我们更加面向对象一点

我们现在用的这个方法在简单程序上还可以. 一旦我们开始在窗体上创建大量控件,还是把那些代码收进对象里面更好. 不再过程化的用基础的 Form() 类, 让我们创建一个自己的窗口类. 我们会重写 Form 类的 create() 方法, 只要窗口被创建都会调用它:

class myEmployeeForm(npyscreen.Form):
    def create(self):
        super(myEmployeeForm, self).create()  # This line is not strictly necessary: the API promises that the create method does nothing by default.
                                              # I've ommitted it from later example code.
        self.myName        = self.add(npyscreen.TitleText, name='Name')
        self.myDepartment  = self.add(npyscreen.TitleText, name='Department')
        self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

我们可以用前面的封装器的代码来利用这个特性:

import npyscreen

class myEmployeeForm(npyscreen.Form):
    def create(self):
        self.myName        = self.add(npyscreen.TitleText, name='Name')
        self.myDepartment  = self.add(npyscreen.TitleText, name='Department')
        self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

def myFunction(*args):
    F = myEmployeeForm(name = "New Employee")
    F.edit()
    return "Created record for " + F.myName.value

if __name__ == '__main__':
    print( npyscreen.wrapper_basic(myFunction) )

提供选项

实际上,我们可能就是不太想要任何旧部门名称输进来 - 我们希望给出一列选项. 让我们来用 TitleSelectOne 控件. 这是一个多行控件, 我们需要注意,让它只占用屏幕的几行就好(如果让它自己定,它会占用屏幕上剩余的所有空间):

self.myDepartment = self.add(npyscreen.TitleSelectOne, max_height=3,
                                name='Department',
                                values = ['Department 1', 'Department 2', 'Department 3'],
                                scroll_exit = True  # 让用户移通过按下下方向键而不是 tab 来移出控件.
                                                    # 可以试试看看它们的不同.
                                )

Putting that in context:

import npyscreen

class myEmployeeForm(npyscreen.Form):
    def create(self):
        self.myName        = self.add(npyscreen.TitleText, name='Name')
        self.myDepartment = self.add(npyscreen.TitleSelectOne, scroll_exit=True, max_height=3, name='Department', values = ['Department 1', 'Department 2', 'Department 3'])
        self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

def myFunction(*args):
    F = myEmployeeForm(name = "New Employee")
    F.edit()
    return "Created record for " + F.myName.value

if __name__ == '__main__':
    print( npyscreen.wrapper_basic(myFunction) )

更彻底面相对象一点

到现在我们都做得还不错,但还是比较糙. 我们还是手动调用 F.edit() 方法, 这对单窗体的应用还过得去,但是以后有递归深度的时候,一不小心就会有问题. 这也让一些这个库本身的更巧妙的特性无法操作了. 更好的办法是使用 NPSAppManaged 类来管理你的应用程序.

我们还是放弃这个支持我们这么久的旧框架,然后以另一个基础开始我们的应用程序吧:

import npyscreen

class MyApplication(npyscreen.NPSAppManaged):
    pass

if __name__ == '__main__':
    TestApp = MyApplication().run()
    print( "All objects, baby." )

这样其实会异常退出, 因为你没有一个 ‘MAIN’ 窗口, 这是 NPSAppManaged 程序的起始点.

我们把它改过来. 我们会用一下前面的窗口类:

import npyscreen

class myEmployeeForm(npyscreen.Form):
    def create(self):
       self.myName        = self.add(npyscreen.TitleText, name='Name')
       self.myDepartment = self.add(npyscreen.TitleSelectOne, scroll_exit=True, max_height=3, name='Department', values = ['Department 1', 'Department 2', 'Department 3'])
       self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

class MyApplication(npyscreen.NPSAppManaged):
    def onStart(self):
       self.addForm('MAIN', myEmployeeForm, name='New Employee')

if __name__ == '__main__':
    TestApp = MyApplication().run()
    print( "All objects, baby." )

如果你运行上面的代码,你可能觉得自己有点挫败, 因为这个程序会一直显示那个要你编辑的窗口,然后你不得不按下 “^C”(Control C) 来退出.

这是因为 NPSAppManaged 类一直会显示任何以它的 NEXT_ACTIVE_FORM 属性(这个例子里, 默认是 – ‘MAIN’)命名的窗口. 旧版的教程会建议直接去设定它,但是你得用 setNextForm(formid) 方法.

让我们改一下 myEmployeeForm 来告诉它, 在 NPSAppManaged 上下文中运行之后,它应该通知它的父对象 NPSAppManaged 停止显示窗口. 我们通过创建一个叫做 afterEditing 的特殊方法来实现:

class myEmployeeForm(npyscreen.Form):
    def afterEditing(self):
        self.parentApp.setNextForm(None)

    def create(self):
        self.myName        = self.add(npyscreen.TitleText, name='Name')
        self.myDepartment  = self.add(npyscreen.TitleSelectOne, scroll_exit=True, max_height=3, name='Department', values = ['Department 1', 'Department 2', 'Department 3'])
        self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

如果喜欢的话,我们还可以通过在 MyApplication 类中定义一个特殊的 onInMainLoop 方法来实现同样的结果 – 这个方法会在每个窗口被编辑完之后被调用.

我们的代码现在看起来是这样的:

import npyscreen

class myEmployeeForm(npyscreen.Form):
    def afterEditing(self):
        self.parentApp.setNextForm(None)

    def create(self):
        self.myName        = self.add(npyscreen.TitleText, name='Name')
        self.myDepartment = self.add(npyscreen.TitleSelectOne, scroll_exit=True, max_height=3, name='Department', values = ['Department 1', 'Department 2', 'Department 3'])
        self.myDate        = self.add(npyscreen.TitleDateCombo, name='Date Employed')

class MyApplication(npyscreen.NPSAppManaged):
    def onStart(self):
        self.addForm('MAIN', myEmployeeForm, name='New Employee')
        # A real application might define more forms here.......

if __name__ == '__main__':
    TestApp = MyApplication().run()

处理方式的选择

上面最后一个例子,对于只是写个很简单的小应用可能有点杀鸡焉用牛刀了. 但是这样能提供一个健壮的多的可以构建更大型的应用程序框架, 比我们在教程一开始用的那个也就只多写了几行而已. 如果你要显示不止一个屏幕,或者一直运行一个应用的话,这就是你该用的处理方法.