Qt 工具箱是一个使用广泛的跨平台 GUI 工具箱,可用于 Windows、Linux、Mac OSX 和许多手持平台。QT 具有良好结构化(但灵活)的面向对象的结构、清晰的文档以及直观的 API。本文中,David Mertz 和 Boudewijn Rempt 探讨了 Qt 库,集中讨论了允许 Python 程序员访问 Qt 功能的 PyQt 绑定。
一般来说,选择用于应用程序的 GUI 工具箱会是一件棘手的事。使用 Python(许多语言也一样)的程序员可以选择的 GUI 工具箱种类繁多,而每个工具箱都有各自的优缺点。有些速度比其它工具箱快,有些比较小;有些易于安装,有些更适合于跨平台使用(对于这一点,还要指出,有些支持您需要满足的特定特性)。当然,各种库都相应具有各种许可证。
对于 Python 程序员而言,缺省的 GUI 选择是 Tk(通过 Tkinter 绑定)― 其原因显而易见。Tkinter 和闲置的 IDE 是由 Python 创始人编写的,它们是作为大多数 Python 分发版的缺省选择而出现的。标准 Python 文档讨论了 Tkinter,但没有涉及任何其它 GUI 绑定。这是故意的!至少可以这么认为,如果 Tk 和 Tkinter 不是这么糟糕,程序员就没有理由去寻找替代品了。要诱导 Python 程序员放弃缺省选择,那么工具箱必须提供额外的东西。PyQt 就是这样一个工具箱。
PyQt 所具有的优点远远超过了 Tkinter(它也有几个缺点)。Qt 和 PyQt 速度都很快;Qt 和 PyQt 的设计完全是面向对象的;Qt 提供了一个设计良好的窗口构件集合,它比 Tk 所提供的要大得多。就其缺点而言,Qt 的许可证受到的限制比许多工具箱(至少在非 Linux 平台方面)都多;正确安装 Qt 和 PyQt 常常会很复杂;另外,Qt 是一个相当大的库。PyQt 应用程序的用户将需要设法完成安装 Qt 和 PyQt,这使分发变得很困难。(请阅读本文后面的用于其它语言的 Qt 绑定。)
PyQt 严格遵循 Qt 的发放许可。特别是,它可用于 UNIX/X11 平台上的 GPL,并可用于 Zaurus 上的 Qt Palmtop Environment 环境,还存在用于较老的 Qt 版本的免费(free-as-in-free-beer)Windows 软件包。PyQt 的商业许可证可用于 Windows。
对于本文而言,PyQt 有一个方面优于许多其它工具箱,它值得我们特别关注。Qt 使用一种称为信号/插槽(signals/slots)的机制在窗口构件(以及其它对象)之间传递事件和消息。这种机制完全不同于包括 Tkinter 在内的大多数工具箱所用的回调(callback)机制。使用信号/插槽以灵活且可维护的方式控制对象间通信要比使用脆弱的回调风格容易得多。应用程序越大,Qt 的这个优势就越重要。
本文的作者之一 Boudewijn Rempt 已经出版了一本有关使用 PyQt 进行应用程序开发的书籍。GUI Programming with Python: QT Edition(请参阅参考资料)显示了如何设计和开发完整的 GUI 应用程序,其中包括从最初的构思到分发的全过程。
样本应用程序
要显示信号/插槽和回调之间的反差,我们提供了一个写着玩玩的应用程序,它使用 Tkinter 和 PyQt。尽管实际上 PyQt 版本对于这个基本程序并不更简单,但是它已经演示了 PyQt 应用程序更好的模块性和可维护性。
应用程序包括四个窗口构件:
“Quit”按钮(用来与整个应用程序通信)
“Log Timestamp”按钮(用于窗口构件间的消息)
文本区域,显示可滚动的已记录日志的时间戳记列表
消息窗口构件,显示已记录日志的时间戳记数
在 Tkinter 中,我们可以这样实现应用程序:
清单 1. Logger.py Tkinter 应用程序
#!/usr/bin/python
import sys, time
from Tkinter import *
class Logger(Frame):
def __init__(self):
Frame.__init__(self)
self.pack(expand=YES, fill=BOTH)
self.master.title("Timestamp logging application")
self.tslist = []
self.tsdisp = Text(height=6, width=25)
self.count = StringVar()
self.cntdisp = Message(font=('Sans',24),
textvariable=self.count)
self.log = Button(text="Log Timestamp",
command=self.log_timestamp)
self.quit = Button(text="Quit", command=sys.exit)
self.tsdisp.pack(side=LEFT)
self.cntdisp.pack()
self.log.pack(side=TOP, expand=YES, fill=BOTH)
self.quit.pack(side=BOTTOM, fill=BOTH)
def log_timestamp(self):
stamp = time.ctime()
self.tsdisp.insert(END, stamp+"")
self.tsdisp.see(END)
self.tslist.append(stamp)
self.count.set("% 3d" % len(self.tslist))
if __name__=='__main__':
Logger().mainloop()
这个 Tk 版本使用了 log_timestamp() 方法作为按钮的 command= 参数。这个方法需要依次单独操作它要影响的所有窗口构件。如果我们想更改按钮按下的效果(例如还要记录时间戳记),那么这个风格就很脆弱。通过继承您可以实现这一点:
清单 2. StdOutLogger.py Tkinter 增强
class StdOutLogger(Logger):
def log_timestamp(self):
Logger.log_timestamp(self)
print self.tslist[-1]
但是这个子类的作者需要相当精确地理解 Logger.log_timestamp() 已经做了什么;而且除非通过在子类中完全重写 .log_timestamp() 方法并且不调用父方法,否则没有办法除去消息。
一个非常基本的 PyQt 应用程序总有一些样本代码,这些代码在哪里都相同,Tkinter 代码也是这样。但是,当我们进一步研究设置应用程序所需的代码,以及显示窗口构件的代码时,区别就显现出来了。
清单 3. logger-qt.py PyQt 应用程序
#!/usr/bin/env python
import sys, time
from qt import * # Generally advertised as safe
class Logger(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.setCaption("Timestamp logging application")
self.layout = QGridLayout(self, 3, 2, 5, 10)
self.tsdisp = QTextEdit(self)
self.tsdisp.setMinimumSize(250, 300)
self.tsdisp.setTextFormat(Qt.PlainText)
self.tscount = QLabel("", self)
self.tscount.setFont(QFont("Sans", 24))
self.log = QPushButton("&Log Timestamp", self)
self.quit = QPushButton("&Quit", self)
self.layout.addMultiCellWidget(self.tsdisp, 0, 2, 0, 0)
self.layout.addWidget(self.tscount, 0, 1)
self.layout.addWidget(self.log, 1, 1)
self.layout.addWidget(self.quit, 2, 1)
self.connect(self.log, SIGNAL("clicked()"),
self.log_timestamp)
self.connect(self.quit, SIGNAL("clicked()"),
self.close)
def log_timestamp(self):
stamp = time.ctime()
self.tsdisp.append(stamp)
self.tscount.setText(str(self.tsdisp.lines()))
if __name__ == "__main__":
app = QApplication(sys.argv)
app.connect(app, SIGNAL('lastWindowClosed()'), app,
SLOT('quit()'))
logger = Logger()
logger.show()
app.setMainWidget(logger)
app.exec_loop()
通过创建布局管理器,Logger 类开始工作了。布局管理器在任何 GUI 系统中都是一个很复杂的主题,但是 Qt 的实现使之变得简单。在大多数情况下,您会使用 Qt Designer 创建一般的 GUI 设计,随后可将它用于生成 Python 或 C++ 代码。然后您可以使生成的代码生成子类,以添加功能。
但是在这个示例中,我们选择手工创建布局管理器。窗口构件被置于网格的各个单元中,或者可以跨多个单元放置。在 Tkinter 需要命名参数的地方,PyQt 就不允许它们。这是一个很重要的差异,它经常会使在两种环境中工作的人们无所适从。
所有 Qt 窗口构件都可以和 QString 对象很自然地一起工作,而不能和 Python 字符串或 Unicode 对象一起工作。幸运的是,转换是自动的。如果您在 Qt 方法中使用了字符串或 Unicode 参数,那么它将自动转换成 QString。不能进行反向转换:如果您调用了一个返回 QString 的方法,那么您获得的是 QString。
应用程序中最有趣的部分是我们将 clicked 信号连接到功能的位置。一个按钮连接到了 log_timestamp 方法;另一个连接到了 QWidget 类的 close 方法。
图 1. logger-qt 的屏幕快照
现在我们想将日志记录添加到这个应用程序的标准输出。这十分容易。我们可以使 Logger 类生成子类,或者为了演示,创建简单的独立函数:
清单 4. logger-qt.py PyQt 增强
def logwrite():
print(time.ctime())