基於Python與Qt的快速GUI程式設計

發表於2016-04-27

An Expression Evaluator in 30 Lines(30行程式碼的表示式計算器)

這個應用程式是採用30行程式碼(不包括空白行與註釋程式碼)編寫的一個完整的“對話方塊樣式”應用程式。“對話方塊樣式”表示這個應用程式沒有選單欄,通常也沒有工具欄或者狀態列,常見的情況是有一些按鈕控制元件,但沒有中心視窗控制元件(central widget)。相應地,“主視窗樣式”應用程式通常有選單欄、工具欄、狀態列,在一些情況下也有按鈕;並且它們擁有中心視窗控制元件(能容納其他控制元件)。我們將在第6章學習“主視窗樣式”應用程式。

基於Python與Qt的快速GUI程式設計
這個應用程式使用了兩個控制元件:QTextBrowser控制元件,是一個具有隻讀屬性的多行文字框,它可以顯示純文字與HTML文字;QLineEdit控制元件,是一個只能顯示純文字的單行文字框。PyQt中控制元件使用的所有文字都採用Unicode編碼,如果有必要,它們可以被轉換成其他編碼。

計算器應用程式(如圖4.3所示)的呼叫方式和其他常規的GUI應用程式一樣,通過滑鼠點選(或者雙擊,取決於作業系統平臺與設定)它的程式圖示。(當然它也可以通過控制檯啟動執行)。一旦應用程式執行,使用者可以在行編輯框中簡單地輸入數學表示式,當使用者按下回車(Enter)鍵,表示式與它的結果會以追加模式顯示在QTextBrowser控制元件中。任何由於無效的表示式或者無效的數學運算(例如被零除)引起的異常將會轉變成錯誤訊息,並追加在QTextBrowser中顯示。

如同上一節一樣,我們將分塊去瀏覽整個程式碼。這個例子所遵循的模式是我們在以後所有的GUI應用程式開發中使用的模式:“窗體”採用一個類來表示,響應使用者互動的行為由“方法”來處理,“主程式”部分儘可能的保持簡潔。

因為我們在做數學計算,並不想遇到任何的諸如除法截斷等意外情況,因此我們採用浮點數除法。通常我們匯入非PyQt模組時採用import moduleName語句,但因為我們想使用所有的math包裡的函式與常量,因此我們簡單地把math包裡所有的內容都匯入到當前名稱空間。與上一節例子中一樣,我們匯入sys模組來獲取sys.argv引數列表,同時我們從QtCoreQtGui模組中匯入所有內容。

正如我們所見,任何一個控制元件都可以作為頂級窗體。但大多數情況下,我們通過子類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()方法將被呼叫。

當呼叫updateUi()時,首先解析QLineEdit中的文字字元,然後將其轉換為unicode物件。接下來我們利用Python自帶的eval()函式來計算字元表示式的結果。如果成功,將追加該字元表示式,等號符號和用粗體顯示的表示式結果到QTextBrowser物件中。儘管我們通常會盡快地將QStrings物件轉換為unicode物件,我們仍然可傳遞QStringsunicodestrs物件到PyQt方法中,該方法期望的物件是QStrings物件,但PyQt會自動完成必要的轉換。如果任何異常發生,相應地,我們追加一個錯誤訊息。使用catch-all-except程式碼塊通常並不是很好的方式,但在這個30行的程式程式碼中它是合理的。

通過使用eval()函式我們可以避免所有的解析與錯誤檢查工作,而如果使用編譯型語言,這部分工作需要我們自己去實現。

現在我們定義好了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按鈕。為對話方塊增加按鈕將在本章的最後一節介紹。

本例子的完整程式碼為:

相關文章