An Expression Evaluator in 30 Lines(30行程式碼的表示式計算器)
這個應用程式是採用30行程式碼(不包括空白行與註釋程式碼)編寫的一個完整的“對話方塊樣式”應用程式。“對話方塊樣式”表示這個應用程式沒有選單欄,通常也沒有工具欄或者狀態列,常見的情況是有一些按鈕控制元件,但沒有中心視窗控制元件(central widget)。相應地,“主視窗樣式”應用程式通常有選單欄、工具欄、狀態列,在一些情況下也有按鈕;並且它們擁有中心視窗控制元件(能容納其他控制元件)。我們將在第6章學習“主視窗樣式”應用程式。
QTextBrowser
控制元件,是一個具有隻讀屬性的多行文字框,它可以顯示純文字與HTML文字;QLineEdit
控制元件,是一個只能顯示純文字的單行文字框。PyQt中控制元件使用的所有文字都採用Unicode
編碼,如果有必要,它們可以被轉換成其他編碼。計算器應用程式(如圖4.3所示)的呼叫方式和其他常規的GUI應用程式一樣,通過滑鼠點選(或者雙擊,取決於作業系統平臺與設定)它的程式圖示。(當然它也可以通過控制檯啟動執行)。一旦應用程式執行,使用者可以在行編輯框中簡單地輸入數學表示式,當使用者按下回車(Enter)鍵,表示式與它的結果會以追加模式顯示在QTextBrowser
控制元件中。任何由於無效的表示式或者無效的數學運算(例如被零除)引起的異常將會轉變成錯誤訊息,並追加在QTextBrowser
中顯示。
如同上一節一樣,我們將分塊去瀏覽整個程式碼。這個例子所遵循的模式是我們在以後所有的GUI應用程式開發中使用的模式:“窗體”採用一個類來表示,響應使用者互動的行為由“方法”來處理,“主程式”部分儘可能的保持簡潔。
1 2 3 4 5 |
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
引數列表,同時我們從QtCore
與QtGui
模組中匯入所有內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
建立頂級窗體,有時候我們也採用QWidget
。QDialog
和QMainWindow
,以及所有的PyQt控制元件,都是從QWidget
派生出來的,它們都屬於Python中的新式類。通過繼承QDialog
類我們可以得到一個空白的視窗,即一個灰色的矩形視窗,它只具有一些簡單的行為或者方法。例如,如果使用者點選關閉的X按鈕,對話方塊會關閉。預設情況下,當控制元件被關閉時,它僅僅只是隱藏了,當然,我們也可以改變這一行為,這將在以後的章節中介紹。
我們傳遞給Form
類的初始化方法__init__()
預設的引數parent
為None
值,並採用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()
方法將被呼叫。
1 2 3 4 5 6 |
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
物件,我們仍然可傳遞QStrings
、unicode
和strs
物件到PyQt方法中,該方法期望的物件是QStrings
物件,但PyQt會自動完成必要的轉換。如果任何異常發生,相應地,我們追加一個錯誤訊息。使用catch-all-except
程式碼塊通常並不是很好的方式,但在這個30行的程式程式碼中它是合理的。
通過使用eval()
函式我們可以避免所有的解析與錯誤檢查工作,而如果使用編譯型語言,這部分工作需要我們自己去實現。
1 2 3 4 |
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
按鈕。為對話方塊增加按鈕將在本章的最後一節介紹。
本例子的完整程式碼為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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_() |