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())