基于Python与Qt的快速GUI编程(4.1)

998 查看

写在前面的话:

对于我而言,一直都希望自己写的程序有一个漂亮的界面,尽管有华而不实之嫌,但是看上去的确是很酷的一件事。从delphi,C#到开始使用Python,在wxPython与PyQt中首先选择了前者,使用了一段时间,总觉得“所想即所得(WYSIWYG)”仍然不太适应,当然wxPython已经能够满足我的需求了。这次希望尝试下PyQt,希望能从中学习到些新的思想。我不太确定什么样的记录方式是好的,第一次就以翻译为主吧,以《Rapid GUI Programming with Python and Qt (2008)》为参考书,尝试将其翻译成中文。当然英语水平与编程水平都有限,希望能得到各位朋友的批评指正。因为前三章内容为Python基础,所以直接从第四章开始,So let's code.

第四章 GUI编程介绍

本章首先简要地回顾三个采用PyQt语言编写的尽管小巧但实用的GUI应用程序。我们希望借此来强调在GUI编程中遇到的一些问题,但是大部分具体的细节会在以后的章节中详细介绍。一旦我们对PyQt的GUI编程有了一定的感觉,我们将讨论PyQt的“信号与槽”机制——这是一个响应用户交互同时不需要用户考虑额外不相关的细节的高级通讯机制。

尽管PyQt商业上被用来开发小到几百行大到数十万行的应用程序,本章中我们所编写的所有应用程序都不超过100行,它们仅仅用来说明通过少量的代码能完成多少事情。

此外,在本章中我们通过纯粹的编写代码来设计应用程序的用户接口,但在第7章中,我们会学习如何使用Qt自带的可视化设计工具(Qt Designer)来创建用户接口。

Python控制台应用程序与模块文件的扩展名通常为.py,但对于Python GUI应用程序,我们通常使用.pyw扩展。.py文件与.pyw文件在Linux平台上都可以很好地运行,但在Windows平台上,.pyw可以让系统自动地选用pythonw.exe解释器,而不是python.exe,这可以保证当我们运行Python GUI应用程序时,不会出现不必要的控制台窗口。在Mac OS X平台上,采用.pyw作为文件的扩展名同样是有必要的。

PyQt文档是以一系列HTML文件提供的,这些文件独立于Python文档。最常用的文档是那些包含PyQt API接口的文档。这些文档是通过原始的C++/Qt文档转换而来,它们的索引页称为classes.html;Windows用户可以通过“开始”菜单下的“PyQt”子菜单找到相应的连接。阅读这些索引页来获得所有可用的类的概述是非常值得的,同时研读那些可用的类也是非常有趣的事。

我们见到的第一个应用程序是一个不太常见的混合情况:这个GUI应用程序必须从控制台启动,因为它需要用户提供命令行参数。我们使用它因为它可以帮助我们更简单地解释PyQt的事件循环机制,而不必引入任何其他的GUI细节。第二个与第三个应用程序都是代码非常短但是是标准的GUI应用程序。它们都展示了怎样去创建窗体控件(Widgets)以及怎样对这些控件进行布局——包括标签、按钮、复选框以及其他用户可见或者可以交互的控件。同时,这两个应用程序也展示了如何去响应用户的交互——例如,当用户执行特定的动作时,如何调用指定的函数或者方法。

在这一章的最后一节,我们将更深入地介绍如何处理用户的交互,在下一章节中,我们会更系统地介绍窗口布局及对话框。本章的目的主要是让读者对GUI编程有初步的感觉,而不用担心具体的细节:以后的章节中我们将会填补所有的细节,使读者熟悉更多的标准PyQt编程技巧。

A Pop-Up Alert in 25 Lines

我们的第一个GUI应用程序有点古怪。首先,它必须从控制台中运行,其次,该应用程序没有任何的“装饰”——没有标题栏,没有菜单栏,没有关闭程序的X按钮。图4.1显示了整个应用程序的运行界面。


图4.1 The Alert program

为了让输出显示,我们需要进入命令行并输入如下命令:

C:\>cd c:\pyqt\chap04
C:\pyqt\chap04>alert.pyw 12:15 Wake Up

当程序运行时,程序会自动隐藏在系统后台中,简单地标记着时间,直到到达用户指定的时间时,该应用程序会弹出一个带文本消息的窗口,持续显示大约一分钟以后,该应用程序会自动终止。

用户指定的时间必须采用24小时制。为了方便测试,我们可以使用刚刚过去的时间,例如:当时间接近12:30时,指定12:15,应用程序会立即弹出带文本消息的窗体。

现在我们知道了这个应用程序是用来干什么的,以及怎样运行它,接下来我们可以回顾整个程序的执行过程。这个程序文件比25行要多几行,因为我们并没有算上注释行——所有的可执行程序代码只有25行。我们首先用引入包开始:

import sys
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *

首先创建一个QApplication对象。每一个PyQt Gui应用程序必须有一个QApplication对象。这个对象提供了获取如应用程序的路径、屏幕尺寸等全局信息的方式。该对象同样提供了事件循环。

当我们创建了QApplication对象,我们向其传递命令行参数;这是因为PyQt可以识别它自身的命令行参数,例如-geometry-style,所以我们应该提供读取命令行参数的机会。如果QApplication识别到一些参数,那么执行它,并从参数列表中去除它。QApplication能够识别的参数列表在QApplication的初始化文档中提供。

try:
    due = QTime.currentTime()
    message = 'Alert!'
    if len(sys.argv)<2:
        raise ValueError
    hours, mins = sys.argv[1].split(':')
    due = QTime(int(hours),int(mins))
    if not due.isValid():
        raise ValueError
    if len(sys.argv)>2:
        message = ' '.join(sys.argv[2:])
except ValueError:
    message = 'Usage: alert.pyw HH:MM [optional message]'

该应用程序需要一个时间,所以我们设置变量due为当前时间。我们同样提供一个默认的消息为"Alert"。如果用户提供的命令行参数小于1,那么抛出一个ValueError异常。这会导致时间为当前时间,显示消息为错误消息“usage"。

如果第一个参数不包含冒号,那么当我们尝试调用split()函数拆分时间参数时,会抛出一个ValueError异常。如果时间参数中的小时或者分钟不是一个有效的数字,那么同样会通过int()抛出一个ValueError异常。如果小时或者分钟超出了各自的范围,那么变量due是一个无效的QTime对象,我们同样设置抛出一个ValueError异常。尽管Python提供了自己的日期和时间类,但是PyQt中的日期和时间类使用起来更为方便(在一些情况下更强大),所以我们更偏向于使用PyQt中的日期或者时间类。

如果时间是有效的,那么如果有额外的命令行参数,我们设置显示消息为将额外参数通过空格连接形成的字符串,否则我们设置显示消息为默认的”Alert!"(当程序通过命令行运行时,它被传递了一系列参数,第一个是程序文件名称,剩余的为一系列非空格字符,即通过命令行输入的每一个单词。这些单词可能会被shell改变——例如,应用通配符扩展时。Python将每个具体给定的单词保存到sys.argv列表中)。

现在我们知道什么时候消息会显示以及会显示什么消息。

while QTime.currentTime() < due:
    time.sleep(20)

程序不断的循环,并比较当前时间与设定的目标时间。如果当前时间比设定的目标时间要迟,那么循环终止。我们可以简单的将pass语句放到循环中,但如果这么做Python会尽可能快的循环,消耗掉所有的处理器周期。time.sleep()命令告诉Python在指定的时间内(这里设定为20秒)暂停处理。这给其他应用程序更多的资源去运行,也更加合理,因为我们在等待设定的目标时间到来的过程中不需要做任何事情。

除了创建一个QApplicaion对象,我们目前所做的仅仅是标准的控制台编程。

label = QLabel('<font color=red size=72><b>'+message+'</b></font>')
label.setWindowFlags(Qt.SplashScreen)
label.show()
QTimer.singleShot(60000, app.quit) # 1 minute
app.exec_()

我们已经创建好了QApplicaion对象,准备好了需要的消息,也设定好了目标时间,现在我们可以开始创建应用程序了。GUI应用程序需要窗体控件,这个例子中我们需要一个label(标签)控件来显示消息。QLabel可以接受HTML文本,因此我们指定一个HTML字符串让其显示粗体红色字体,大小为72磅。

在PyQt中,所有控件都可以视为顶级窗体,即使是一个按钮或者一个标签。当控件按这种方式使用时,PyQt自动分配一个带标题栏的窗体来容纳该控件。对于这个例子,我们并不需要一个标题栏,因此我们设置标签控件的窗体标记为splash screens,因为它没有标题栏。一旦我们建立了标签为窗体,我们可以调用窗体的show()函数。这个时候,标签窗口并没有显示!show()函数的调用仅仅是安排了一个“绘图事件”,即添加了一个绘制特定控件的事件到QApplicaion对象的事件队列中。

下一步,我们设置一个定时器。Python的时间库time.sleep()函数是以秒为单位,而QTimer.singleShot()函数是以毫秒为单位。我们提供给singleShot()函数两个参数:第一个参数是多久后超时(本例中设定为1分钟),第二个参数是一个函数或者方法,即超时后调用的函数或者方法。

在PyQt的术语中,函数或者方法有一个专有名词叫做“槽(slot)",尽管在PyQt文档中使用术语”callable“,”Python slot“和”Qt slot“来区别Python中的__slot__,即Python语言参考中描述的一种新式类的特征。本书中我们将采用PyQt中的术语,因为我们从来没有用到Python中的__slot__

因此目前我们有两个事件任务计划:一个立即执行绘图事件,一个一分钟后执行的定时器超时事件。

app.exec_()函数的调用标志着QApplicaion对象的事件循环开始。第一个事件是绘图事件,因此带有指定显示消息的标签窗体会在屏幕中弹出。大约一分钟以后,执行计时器超时事件,调用Application.quit()方法退出程序。这个方法将使GUI应用程序干净地终止。它关闭所有的窗体,释放所有占用的资源,并退出应用程序。

GUI应用程序采用事件循环机制。采用伪代码来描述,一个事件循环如下面的代码所示:

while True:
    event = getNextEvent()
    if event:
        if event == Terminate:
            break
        processEvent(event)

当用户与应用程序交互时,或者特定的其他事件发生时,例如计时器超时任务或者应用程序的窗体被覆盖时,在PyQt内部产生一个新的事件并添加到事件队列中。应用程序的事件循环不断地检查是否有事件需要处理,如果有,那么处理它(或者将它传递到与事件相关联的函数或者方法中去处理)。


图4.2 批处理应用程序与GUI应用程序的比较

尽管完成了这个例子,并且如果使用控制台这个例子也非常有用,但这个应用程序只使用了一个控件。此外,我们没有提供给用户任何的交互。它的运行机制更像传统的批处理(batch-processing)应用程序:应用程序被调用,执行一些事件处理任务(等待,显示消息),然后终止。大多数GUI应用程序的工作机制则不同。一旦被调用,则进入事件循环并响应事件。其中一些事件来自用户——例如按键与鼠标点击,一些事件来自系统——例如计时器超时任务和窗体的显示。它们处理诸如按钮点击或者菜单选取等事件的请求,只有当用户请求关闭事件时应用程序才终止。

我们将要看到的下一个应用程序比我们刚刚学习的应用程序更为常见,它可以代表大多数小型的GUI应用程序。

第一个例子的完整代码如下:

import sys
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *

app = QApplication(sys.argv)

try:
    due = QTime.currentTime()
    message = 'Alert!'
    if len(sys.argv)<2:
        raise ValueError
    hours, mins = sys.argv[1].split(':')
    due = QTime(int(hours),int(mins))
    if not due.isValid():
        raise ValueError
    if len(sys.argv)>2:
        message = ' '.join(sys.argv[2:])
except ValueError:
    message = 'Usage: alert.pyw HH:MM [optional message] %d' % len(sys.argv) 

while QTime.currentTime() < due:
    time.sleep(20)

label = QLabel('<font color=red size=72><b>'+message+'</b></font>')
label.setWindowFlags(Qt.SplashScreen)
label.show()
QTimer.singleShot(5000, app.quit) # 1 minute
app.exec_()