基于Python与Qt的快速GUI编程,PythonQtGUI编程,“对话框样式”表示这个应


An Expression Evaluator in 30 Lines(30行代码的表达式计算器)

这个应用程序是采用30行代码(不包括空白行与注释代码)编写的一个完整的“对话框样式”应用程序。“对话框样式”表示这个应用程序没有菜单栏,通常也没有工具栏或者状态栏,常见的情况是有一些按钮控件,但没有中心窗口控件(central widget)。相应地,“主窗口样式”应用程序通常有菜单栏、工具栏、状态栏,在一些情况下也有按钮;并且它们拥有中心窗口控件(能容纳其他控件)。我们将在第6章学习“主窗口样式”应用程序。

这个应用程序使用了两个控件:QTextBrowser控件,是一个具有只读属性的多行文本框,它可以显示纯文本与HTML文本;QLineEdit控件,是一个只能显示纯文本的单行文本框。PyQt中控件使用的所有文本都采用Unicode编码,如果有必要,它们可以被转换成其他编码。

计算器应用程序(如图4.3所示)的调用方式和其他常规的GUI应用程序一样,通过鼠标点击(或者双击,取决于操作系统平台与设置)它的程序图标。(当然它也可以通过控制台启动运行)。一旦应用程序运行,用户可以在行编辑框中简单地输入数学表达式,当用户按下回车(Enter)键,表达式与它的结果会以追加模式显示在QTextBrowser控件中。任何由于无效的表达式或者无效的数学运算(例如被零除)引起的异常将会转变成错误消息,并追加在QTextBrowser中显示。

如同上一节一样,我们将分块去浏览整个代码。这个例子所遵循的模式是我们在以后所有的GUI应用程序开发中使用的模式:“窗体”采用一个类来表示,响应用户交互的行为由“方法”来处理,“主程序”部分尽可能的保持简洁。

from __future__ import division
import sys
from math import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *

因为我们在做数学计算,并不想遇到任何的诸如除法截断等意外情况,因此我们采用浮点数除法。通常我们导入非PyQt模块时采用import moduleName语句,但因为我们想使用所有的math包里的函数与常量,因此我们简单地把math包里所有的内容都导入到当前命名空间。与上一节例子中一样,我们导入sys模块来获取sys.argv参数列表,同时我们从QtCoreQtGui模块中导入所有内容。

class Form(QDialog):
    def __init__(self,parent=None):
        super(Form,self).__init__(parent)
        self.browser = QTextBrowser()
        self.lineedit = QLineEdit('Type an express and press Enter')
        self.lineedit.selectAll()
        layout = QVBoxLayout()
        layout.addWidget(self.browser)
        layout.addWidget(self.lineedit)
        self.setLayout(layout)
        self.lineedit.setFocus()
        self.connect(self.lineedit,SIGNAL('returnPressed()'),self.updateUi)
        self.setWindowTitle('Calculate')

正如我们所见,任何一个控件都可以作为顶级窗体。但大多数情况下,我们通过子类QDialog或者QMainWindow创建顶级窗体,有时候我们也采用QWidgetQDialogQMainWindow,以及所有的PyQt控件,都是从QWidget派生出来的,它们都属于Python中的新式类。通过继承QDialog类我们可以得到一个空白的窗口,即一个灰色的矩形窗口,它只具有一些简单的行为或者方法。例如,如果用户点击关闭的X按钮,对话框会关闭。默认情况下,当控件被关闭时,它仅仅只是隐藏了,当然,我们也可以改变这一行为,这将在以后的章节中介绍。

我们传递给Form类的初始化方法__init__()默认的参数parentNone值,并采用super()方法进行初始化。如果一个窗体控件没有父类,那么这个控件就是一个顶级窗体,在这个例子中正是我们希望的。接下来我们创建两个控件,并且保存它们的引用以便以后我们在__init__()外部可以访问它们。因为我们没有传递父类给这两个控件,似乎它们会成为顶级窗体——这是没有意义的。我们将简单地介绍它们是如何在初始化过程中获取父类的。我们给QLineEdit提供一个初始显示文本,并选取全部文本,这样做可以保证当用户开始输入时,我们提供的初始文本被替换。

我们希望窗体控件垂直的显示,一个在另一个上方。这可以通过创建一个QVBoxLayout布局并添加这两个窗体控件实现,然后设置好窗体的布局。如果你运行程序并改变窗体的大小,会发现增加的垂直空间被分配给了QTextBrowser控件,两个控件都会沿水平方向增长。这是通过布局管理器自动实现的,也可以通过设置布局进行调整。

使用布局的一个重要的作用是PyQt自动代理所有被展示的控件。因此,尽管我们没有定义控件的父类为主窗体,但当我们调用setLayout()函数时,布局管理器会拥有控件的所有权,同时将自己的所有权给主窗体,此外,它也会拥有它内部的布局管理器的所有权。这表示所有被展示的控件都不是顶级窗体,它们都拥有父类,这也是我们希望看到的。所以当主程序被释放时,它的所有的子控件和布局管理器都会以正确的顺序被释放。

窗体中的控件的布局可以有多久方式。我们可以使用resize()或者move()函数来定义它们的绝对大小和位置;也可以重新执行resizeEvent()函数来自动计算它们的大小和位置,或者使用PyQt的布局管理器。使用绝对大小或者位置非常的麻烦。一方面,我们需要进行大量的人工计算,另一方面,如果我们改变了窗体的布局,我们需要重新进行所有的计算。自动计算控件的大小与位置是一个更好的方式,但我们同样需要编写大量的计算代码。

使用布局管理器让一切事情变得简单。布局管理器非常的智能:它们自动地适应调整大小事件和内容改变。任何人如果使用过不同版本的Windows的对话框,会很感谢对话框可改变大小所带来的好处,因为当用户的内容太大时,使用小的而且不可改变大小的对话框会带来非常多的不方便。对于国际化应用程序需要根据语言调整内容时,布局管理器也可以让事情变得简单,而不用担心如果翻译过后的语言长度大于原始的语言时被截断。

PyQt提供三种布局管理器:垂直布局管理器,水平布局管理器以及网格布局管理器。布局管理器可以嵌套,这让非常复杂的布局也变成可能。还有其他的布局方式,例如使用splitter或者tab控件。所有的这些方式都会在第9章更深入地介绍。

出于对用户使用上的方便,我们在程序运行的开始将焦点设置为QLineEdit控件;可以通过调用setFocus()函数实现,这必须在设置好布局管理器后调用。

函数connect()的调用我们将在以后的章节中深入地介绍。目前只要知道每一个控件(或者一些其他的QObject)通过发送一个“信号(signals)”来声明它们的状态改变就足够了。这些信号(与Unix系统的信号没有任何关系)通常被忽略。然而,当我们选择关注我们感兴趣的那些信号时,我们可以通过这个方式识别我们想要知道的那些QObject对象,它们发送出的信号是我们感兴趣的,当信号发出时,我们希望执行哪些函数或者方法。

在这个例子中,当用户在QLineEdit中按下“回车(Enter)”键时,会触发returnPressed()信号,因为调用了connect()函数,当触发信号时,用调用updateUi()方法。我们将在一会看到发生了什么改变。

最后一件事是通过__init__()函数来设置窗体的标题。

正如我们一会将看到,窗体被创建并且调用了show()方法。一旦事件循环开始,窗体会显示出来——没有更多的事情发生。应用程序只是简单地执行事件循环,等待用户点击鼠标或者按下按键。一旦用户开始交互,它们交互的结果将被处理。因此如果用户输入一个表达式,QLineEdit控件会关注用户输入的内容,一旦用户按下“回车(Enter)“键,我们定义的updateUi()方法将被调用。

    def updateUi(self):
        try:
            text = unicode(self.lineedit.text())
            self.browser.append('%s = %s' % (text, eval(text)))
        except:
            self.browser.append('%s is invalid!' % text)

当调用updateUi()时,首先解析QLineEdit中的文本字符,然后将其转换为unicode对象。接下来我们利用Python自带的eval()函数来计算字符表达式的结果。如果成功,将追加该字符表达式,等号符号和用粗体显示的表达式结果到QTextBrowser对象中。尽管我们通常会尽快地将QStrings对象转换为unicode对象,我们仍然可传递QStringsunicodestrs对象到PyQt方法中,该方法期望的对象是QStrings对象,但PyQt会自动完成必要的转换。如果任何异常发生,相应地,我们追加一个错误消息。使用catch-all-except代码块通常并不是很好的方式,但在这个30行的程序代码中它是合理的。

通过使用eval()函数我们可以避免所有的解析与错误检查工作,而如果使用编译型语言,这部分工作需要我们自己去实现。

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

现在我们定义好了Form类,在calculate.pyw文件的最后,我们创建一个QApplication对象以及一个Form类的实例form,增加一个绘图的计划,并开始事件循环。

这就是完整的应用程序。然而一切并没有结束。我们并没有说明如何去终止应用程序,但因为我们的form是从QDialog对象派生出来的,它继承了某些行为。例如,如果用户点击窗体的关闭按钮X,或者用户按下了Esc按键,窗体form会关闭。当窗体form关闭时,它是隐藏着的。PyQt会自动检查应用程序是否有非隐藏的窗体,以及是否有进一步交互的可能。如果没有,那么会释放掉整个窗体form并终止应用程序。

在一些情况下,我们希望在窗体不可见的情况下让应用程序继续运行——例如,服务器。在这些情况下,我们可以调用QApplication.setQuitOnLastWindowClosed(False)函数来实现。

在Mac OS X,或者一些X Windows的窗口管理器例如twm中,本节中的例子并没有关闭按钮,在Mac平台上,选择菜单栏的Quit菜单也不起作用。在这个情况下,可以按下Esc键来终止应用程序,或者使用Command+.来终止。考虑到这一点,对于在Mac或者使用如twm作为窗口管理器的操作系统平台上开发GUI应用程序时,最好提供一个Quit按钮。为对话框增加按钮将在本章的最后一节介绍。

本例子的完整代码为:

from __future__ import division
import sys
from math import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class Form(QDialog):
    def __init__(self,parent=None):
        super(Form,self).__init__(parent)
        self.browser = QTextBrowser()
        self.lineedit = QLineEdit('Type an express and press Enter')
        self.lineedit.selectAll()
        layout = QVBoxLayout()
        layout.addWidget(self.browser)
        layout.addWidget(self.lineedit)
        self.setLayout(layout)
        self.lineedit.setFocus()
        self.connect(self.lineedit,SIGNAL('returnPressed()'),self.updateUi)
        self.setWindowTitle('Calculate')

    def updateUi(self):
        try:
            text = unicode(self.lineedit.text())
            self.browser.append('%s = %s' % (text, eval(text)))
        except:
            self.browser.append('%s is invalid!' % text)

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

评论关闭