翻譯:《用python和Qt進行GUI編程》——第四章

4 GUI編程簡介 html

 

       這一章,咱們從回顧3段至今仍然有用的GUI程序開始。咱們將利用這個機會去着重強調GUI編程中會包含的一些問題,詳細的介紹會放到後面的章節。一旦咱們創建起PyQt GUI編程的初步感受後,咱們就講討論PyQt的信號槽機制,這是一個高級的通訊機制,他能夠反映用戶的操做而且讓咱們忽略無關的細節。 java

       儘管PyQt在商業上創建的應用程序大小在幾百行到十多萬行都有,可是這章咱們介紹的程序都在100行內,他們展現了使用不多的代碼能夠實現多麼多的功能。 python

       在這章,咱們僅僅使用代碼來構建咱們的用戶界面,在第7章,咱們將學習如何使用可視化圖形工具,Qt Designer. linux

       Python的控制檯應用程序和Python的模塊文件老是使用.py後綴,不過Python GUI應用程序咱們使用.pyw後綴。不無奈是.py仍是.pyw,在linux都表現良好,不過在windows上.pyw確保windows使用pythonw.exe解釋器而不是python.exe解釋器,這確保咱們運行GUI程序的時候沒有沒必要要的控制檯窗口出現。在Mac OS X,必定要使用.pyw後綴。 c++

       PyQt的文檔提供了一系列的HTML文件,這些文件獨立於Python文檔以外。文檔中最經常使用到的是那些轉換過來的PyQt API。這些文件都是從原生的C++/Qt文檔轉換的,他們的索引頁是classes.html;windows用戶能夠從他們PyQt的菜單文件裏找到這些頁面。大致瀏覽那些可用的類是頗有價值的,固然深刻閱讀起來看起來也不錯。 程序員

       咱們將探索的第一個應用程序是一個不一樣尋常的「混血」程序:雖然是一個GUI程序可是它須要從控制檯載入,這是由於它須要一些參數才能運行正確。咱們之因此包涵它是由於這讓咱們解釋PyQt的事件循環機制變得容易了不少,不用多費口舌去講解其餘暫時用不到的GUI細節。第二個和第三個例子都是短小的標準GUI程序。他們都展現瞭如何建立和佈局(標籤、按鈕、下拉菜單和其餘的用戶能夠看到交互的組件)。例子也展現了咱們是怎麼迴應用戶交互的,例如,當用戶進行了一個特殊操做時如何調用一個特殊的函數。 express

       在最後一節,咱們將會咱們將會更加深刻的講解如何處理用戶和程序的交互,在下一張咱們將會更加透徹的覆蓋佈局和對話框的知識。這一章咱們是想讓你創建起GUI編程的初步感受,體會它是怎樣運做的,不須要過多的關注他的細節。以後的章節會深刻講解而且漸漸讓你熟悉標準PyQt編程。 編程

 

一個25行的彈出鬧鐘 小程序

       咱們的第一個GUI應用程序看上去有點兒奇怪。第一,它必須從控制檯啓動;第二,它們有「修飾品」——標題欄、菜單、關閉按鈕。以下圖: windows

    要獲得上面的展現,咱們須要在命令行鍵入以下的命令:

    C:\>cd c:\pyqt\chap04

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

       程序運行後,程序將在後臺默默運行,僅僅是簡單的記錄時間,而當特殊的時間到達時,它會彈出一個信息窗口。以後大約一分鐘,程序會自動終止。

  這個特殊的時間必須使用24小時進制的時鐘。爲了測試的目的,咱們使用剛剛通過的時間,例如如今已經12:30了咱們使用12:15,那麼窗口會理解彈出(好吧,一秒以內)。

       如今咱們知道了這個程序能幹什麼,如何去運行它,讓咱們回顧一下他的實現。這個文件只有幾行,稍微比25行多些,由於裏面會有一些註釋和空白行,可是與執行相關的代碼只有25行,咱們從import開始:

import sys
import time
from PyQt4.QtCore import (QTime, QTimer, Qt, SIGNAL)
from PyQt4.QtGui import (QApplication, QLabel)
    咱們導入了sys模塊是由於咱們想要接受命令行的參數sys.argv。time模塊則是由於咱們須要sleep()函數,PyQt模塊是由於須要使用GUI和QTime類。
app = QApplication(sys.argv)

    開始的時候咱們建立了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]" # 24hr clock

    在比較靠後的地方,這個程序須要一個時間,咱們設定爲如今的時間。咱們提供了一個默認的時間,若是用戶沒有給出任何一個命令行參數,咱們拋出ValueError異常。接將會顯示當前時間以及包含「用法」的錯誤信息。


    若是第一個參數沒有包含冒號,那麼當咱們嘗試調用split()將兩個元素解包時將會觸發ValueError異常。若是小時分鐘的數字不是合法數字,那麼int()將會引起ValueError異常,若是小時和分鐘超過了應有的界限,那麼QTime也會引起ValueError異常。雖然Python提供了本身的date和time類,可是PyQt的date和time類更加方便,因此咱們使用PyQt的。

    若是time經過驗證,咱們就講顯示信息設置爲命令行裏剩餘的參數,若是沒有其他參數的話,則顯示咱們開始時設置的默認信息「Alert!」。

    如今咱們知道了合適信息必須顯示,以及顯示那些信息。

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

    咱們接二連三的循環,同時比較當前時間和目標時間。只有當前時間超過目標時間後循環纔會中止。咱們能夠僅僅把pass放入循環內,若是這樣的話,Python會很是一遍一遍快的執行這個循環,這可不是什麼好現象(資源利用太高)。因此咱們使用time.sleep()來掛起這個進程20秒。這讓機器上其餘的程序能夠獲得更多的運行機會,由於咱們這個程序在等待過程當中並不須要作任何事情。

    拋開建立QApplication對象的那部分,咱們如今作的於標準控制檯程序沒什麼不一樣。

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

    咱們建立了QApplication對象,咱們有了顯示信息,因此如今使咱們建立咱們程序的時候了。一個GUI程序須要widgets,因此咱們須要用label來顯示咱們的信息。一個QLabel能夠接收HTML文本,因此咱們就給了一串HTML string來顯示這段信息。

    在PyQt中,任何widget均可用做頂級窗口,甚至是一個button或者label。當一個widget這麼作的時候,PyQt自動給他一個標題欄。在這個程序裏咱們不須要標題,我因此咱們咱們把label的標籤設置爲了用做窗口分割的flag,由於那些東西沒有標題。當咱們設置完畢後,咱們調用show()方法。今後開始,label窗口顯示出來了!當調用show()方法時,僅僅是計劃執行「重繪事件」,也就是說,把這個產生新重繪事件的QApplication對象添加到事件隊列中去。

    接下來,咱們設置了一下只是用一次的定時器。由於Python標準庫的time.sleep()方法使用秒鐘時間,而QTimer.singleShot()方法使用毫秒。咱們給singleShot()方法兩個參數,多長時間後觸發這個定時器,以及響應這個定時器的方法。

    在PyQt的術語中,一個方法或者函數咱們給他一個稱號「slot」(槽),儘管在PyQt的文檔中,術語 「callable」, 「Python slot」,和「Qt slot」這些來做爲區分Python的__slots__,一個Python類的新特性。在這本書中咱們僅僅使用PyQt的術語,由於咱們歷來沒有用的__slots__。

    因此如今,咱們有兩個時間時間表:一個paint事件咱們想當即執行,還有一個timer的timeout時間咱們想一分鐘後執行。

    調用app.exec_()開始了QApplication對象的事件循環。第一個事件是paint時間,因此label窗口帶着咱們設置的信息顯示在了屏幕上,大約一分鐘後timer觸發timeout時間,調用QApplication.quit()方法。這個方法會結束這個GUI程序,他會關閉全部打開的窗口,清空申請的資源,而後退出程序。

    在GUI程序中使用的是事件循環機制。用僞代碼表示就像這樣:

while True:
    event = getNextEvent()
    if event:
        if event == Terminate:
            break
        processEvent(event)
    當用戶於程序交互的時候,或者有特定事件發生的時候,例如timer或者程序窗口被遮蓋等等,一個PyQt事件就會發生,而且添加到事件隊列中去。應用程序的事件循環持續不斷的堅持是否有事件須要執行,若是有就執行。



    儘管完成這個程序只用了一個簡單的widget,兵器使用控制檯程序看起來確實頗有效,不過咱們如今尚未給這個程序與用戶交互的能力。這個程序的運做效果於傳統的批處理程序也很類似。從他調用開始,他會處理一些進程,例如等待,顯示信息,而後終止。大部分的GUI程序運行卻與之不一樣。一旦開始運行,他們進入事件循環而且響應事件。有些事件是用戶產生的,例如按下鍵盤、點擊鼠標,有些事件則是系統產生的,例如timer定時器到達預約事件,窗口重繪等。這些GUI程序對請求做出處理,僅僅在用戶要求終止時結束程序。

  下一個咱們要介紹的程序比咱們剛剛看到的要更加符合傳統,而且是一個典型的小的GUI程序。 

 

  一個30行的表達式計算器 

  這個程序是一個有對話框風格的用30行寫成的應用程序(刨除註釋和空行)。對話框風格是指這個程序沒有菜單、工具條、狀態欄、中央widget等等,大部分的組建是按鈕。與之對應的誰主窗口風格程序,上面沒有的它都有。第六章咱們將研究主窗口風格的程序。

    這個程序使用了兩種widget:一個QTextBrowser,他是一個只讀的多行文本框,能夠顯示普通的文本或者HTML;一個QLineEdit,,這是一個單行的輸入部件,能夠處理普通文本。在PyQt中全部的text都是Unicode的,固然必要的時候他們也能夠轉碼成其餘編碼。

    這個計算器程序就像任何GUI程序同樣,雙擊圖標運行。程序開始執行後,用戶能夠隨意像輸入框輸入表達式,若是輸入回車時,表達式和他的結果就會顯示在QTextBrowser上。若是有什麼異常的話,QTextBrowser也會顯示錯誤信息。 

    像平時同樣,咱們先瀏覽下代碼。這個例子展現了咱們創建GUI程序的模式:使用一個form類來包含全部須要交互的方法,而程序的「主要」部分則看上去很精悍。

from __future__ import division
import sys
from math import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
    由於在咱們的數學計算裏面並不像要截斷除法計算(計算機的整數除整數的規則),咱們要肯定咱們進行的是浮點型除法。一般,咱們在import非PyQt模塊是使用import 模塊名字的語法;可是由於咱們相用到math模塊裏面不少的方法,因此咱們把math模塊裏面全部的方法導入到了咱們的名字空間裏面。咱們導入sys模塊一般是爲了獲得sys.argv參數列表,而後咱們導入了QtCore和QtGui模塊裏面的全部東西。
class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)
        self.browser = QTextBrowser()
        self.lineedit = QLineEdit("Type an expression 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")

    咱們以前已經看到,任何一個widget均可以看所是頂級窗口。可是大多數狀況下,咱們使用QDialog或者QMainWindow來作頂級窗口,偶而使用QWidget,無論是QDialog或者QMainWindow,以及全部PyQt裏面的widget都是繼承自QWidget。經過繼承QDialog咱們獲得了一個空白的框架,他有一個灰色的矩形,以及一些方便的行爲和方法。例如,若是咱們單擊X按鈕,這個對話框就會關閉。默認狀況下,當一個widget關閉的時候事實上僅僅是隱藏起來了,固然咱們能夠改變這些行爲,下一章咱們就會介紹。

  咱們給予咱們的Form類一個__init__()方法,提供一了一個默認的parent=None,而且使用super()方法進行初始化。一個沒有parent的widget就會變成頂級窗口,這正是咱們所須要的。而後咱們建立了咱們須要的widget,而且保持了他們的引用,這樣以後在__init__()以外咱們就能夠方便的引用到他們。覺得咱們沒有給他們parent,看上去他們會變成頂級窗口,這不是咱們所期待的。別急,在初始化的後面咱們就會看到他們是怎麼獲得parent的了。咱們給予QLineEdit一些信息來最初顯示在程序上,而且將這些信息選中了。這樣就保證咱們的用戶開始輸入信息時這些顯示信息會最快的被擦除掉。

  咱們想要一個接一個的在窗口中垂直顯示咱們的widget,這使得咱們建立了QVBoxLayot而且在裏面添加了咱們的兩個widget,接下來把form的佈局設置爲這個layout。若是咱們運行咱們的程序,而且改變程序大小的時候,咱們會發現有一些多餘的垂直空間添加到了QTextBrowser上,而全部的widget水平都會拉長。這些都會有佈局管理器自動處理,而且能夠很方便的經過佈局策略來調整。

  一個重要的邊際效應是PyQt在使用layout時,會自動將佈局中的widget的父母從新定位。因此儘管咱們沒有對咱們的widget指定父母,可是當咱們調用setLayout()的那個時候咱們已將把這兩個widget的parent設定爲Form的self了。這樣一來,全部的部件都是頂級窗口的一員,他們都有parent,這纔是咱們計劃的。而且,當form刪除的時候,他全部的子widget和layout都會和他一塊兒按照正確的順序刪除。

    form裏面的widget能夠有多種技術佈局。咱們可使用resize()move()方法去去給他們賦予絕對的尺寸與位置;咱們能夠重寫resizeEvent()方法,動態的計算它們的尺寸和座標,或者使用PyQt的佈局管理器。使用絕對尺寸和座標很是不方便。一方面,咱們須要進行大量的計算,另外一方面,若是咱們改變了佈局,咱們還要衝進計算。動態的計算大小和位置是更好的方案,不過這依然須要咱們寫不少冗長無聊的代碼。

    使用佈局管理器使這一切變得容易了不少。而且佈局管理器很是聰明:他們自動的去適應resize事件,而且去知足這些改變。任何使用對話框的人相信都更喜歡大小能夠改變而不是固定了使用小小的不能改變的窗口,由於這樣才能適應用戶的內心需求。佈局管理器也讓你的程序國際化時變得簡單,這樣當翻譯一個標籤的時候就不會因目標語言比源語言要囉嗦的多而被砍掉了。

    PyQt提供了三種佈局管理器:水平、垂直、網格。佈局能夠內嵌,因此你能夠作出很是精緻的佈局。固然也有其餘的佈局方式,例如使用tab widget,或者splitter,這些在第九章會深刻講解。

    處於禮貌,咱們把focus放到QLineEdit上,咱們能夠調用setFocus()達到這一點。這一點必須在佈局完成後才能實施。

    Connect()的調用咱們將在本章稍後的地方深刻講解。能夠說,任何一個widget(以及某些QObject對象)均可以經過發出信號聲明狀態改變了。這些信號一般被忽略了,然而咱們選擇咱們感興趣的信號,咱們經過QObject的聲明來讓咱們知道這些咱們感心情信號被髮出了,而且這個信號發出的時候能夠調用咱們想用的方法。

    在這個例子裏,當用戶在QLineEdit上按下回車鍵的時候,returnPress()信號將會被髮射,不過由於咱們的connect()方法,當這個信號發出的時候,咱們調用updateUi()這個方法。立刻咱們就會看到發生了什麼。

    我在在__init__方法作的最後一件事情是設置了窗口的標題。

    咱們簡短的看一下,咱們創造了form,而且調用了上面的show()方法。一旦事件循環開始,form顯示出來,好像沒有其餘什麼發生。程序僅僅是運行事件循環,等待用戶去按下鼠標或者鍵盤。因此當用戶輸入一個表達式的時候QLineEdit將會顯示用戶輸入的表達式,當用戶按下回車鍵時,咱們的updateUi方法將會被調用。

def updateUi(self):
        try:
            text = unicode(self.lineedit.text())
            self.browser.append("{0} = <b>{1}</b>".format(text,
                                eval(text)))
        except:
            self.browser.append("<font color=red>{0} is invalid!</font>"
                                .format(text))

當updateUi()這個方法被調用的時候,他會檢索QLineEdit的信息,而且馬上將他轉換爲unicode對象。咱們使用Python的eval()方法來直接運算表達式的值。若是成功的話,咱們將計算的結果添加到QTextBrowser裏面,這時候咱們會將unicode字符轉換爲QString,在須要QString參數PyQt模塊中,咱們能夠傳入QString,unicode,str這些字符串,PyQt會自動進行轉換工做。若是產生了異常,咱們就把錯誤信息添加到QTextBrowser裏面,一般使用一個抓取全部異常的except塊在實際編程時並非一個很好的用法,不過在咱們這個只有30行的程序裏面看上去說的過去。

不過使用eval()方法時,咱們應該自行進行語法和解析方面的檢查,誰讓python是個解析語言。

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

如今咱們的Form類已經定義完了,基本上也到了calculate.pyw文件的最後,咱們建立了QApplication對象,開始了繪製工做,啓動了事件循環。

這就是所有的程序,不過這還不是故事的結局。咱們尚未說用戶是怎麼終結這個程序的。由於咱們的程序繼承自QDialog,他繼承了不少有用的行爲。例如,若是用戶點擊了X按鈕,或者是按下了Esc鍵,那麼form就會關閉。當一個form關閉的時候,他僅僅是隱藏起來了。當form隱藏起來的時候,PyQt將會探測咱們的程序,看看是否還有可見的窗口或者是否還有能夠交互的行爲,若是全部窗口都隱藏起來了,那麼PyQt就會終止程序而且delete這個form。

有些狀況下,咱們但願咱們的程序就算在隱藏狀態下依然可以運行,例如一個服務器。這種狀況下,咱們調用QApplication.setQuitOnLast-WindowClosed(False)。雖然這不多見,可是他能夠保證窗口關閉的時候程序能夠繼續運行。

在Mac OS X以及某些Windows窗口管理器中(像twm),程序是沒有關閉按鈕的,而在Mac上從菜單欄選擇退出是沒有效果的。這種狀況下,咱們就須要按下Esc來終止程序,在Mac上你還可使用Command +。因此,若是這個程序有可能在Mac或者twm中使用的時候,最好在dialog中添加一個退出按鈕。

如今咱們已經準備好去看本章最後一個完整的小程序了,他擁有更多的用戶行爲,有一個更復雜的佈局,以及更加複雜的處理方式,不過基本的結構域咱們的計算器程序很像,不過添加了更多PyQt對話框的特性。

一個70行的貨幣轉換工具

貨幣轉換工具是一個試用的小工具。可是因爲兌換匯率時常變換,咱們不能簡單的像前幾章同樣使用一個靜態的字典來存儲這些信息。一般,加拿大銀行都會在網上提供這些銀行匯率,而且這個文件的格式咱們能夠很是容易的讀取更改。這些匯率與最新的數據可能有幾天的時差,可是這些信息對於有國際合同須要估算預付款的時候是足夠使用了。

這個程序要想使用首先下載這些匯率數據,而後他纔會建立用戶界面。 一般咱們從import開始看代碼:

import sys
import urllib2
from PyQt4.QtCore import (Qt, SIGNAL)
from PyQt4.QtGui import (QApplication, QComboBox, QDialog,
        QDoubleSpinBox, QGridLayout, QLabel)

不管是python仍是PyQt都提供了和網絡有關的類。第18章,咱們會使用PyQt的類,不過在這一章咱們使用Python的urllib2模塊,由於這個模塊提供了從網上抓取文件的一些很是方便的工具。

class Form(QDialog):

def __init__(self, parent=None):
    super(Form, self).__init__(parent)

    date = self.getdata()
    rates = sorted(self.rates.keys())

    dateLabel = QLabel(date)
    self.fromComboBox = QComboBox()
    self.fromComboBox.addItems(rates)
    self.fromSpinBox = QDoubleSpinBox()
    self.fromSpinBox.setRange(0.01, 10000000.00)
    self.fromSpinBox.setValue(1.00)
    self.toComboBox = QComboBox()
    self.toComboBox.addItems(rates)
    self.toLabel = QLabel("1.00")

在使用super()初始化咱們的form後,咱們調用getdata()方法。不久咱們就會看到這個方法從網上下載讀取匯率數據,並把它們存儲到self.rates這個字典裏,而且必須返回一個有date的字符串。字典的key是貨幣的幣種,value是貨幣轉換的係數。

咱們將字典中的key排序後獲得了一組copy,這樣在咱們的下拉列表裏就以使用排序後的貨種了。date和rate變量以及dateLabel標籤,因爲只在__init__()方法中使用,因此咱們沒必要保留他們的引用。另外一方面,咱們確實須要引用到下拉列表以及toLabel,因此咱們在self中保留了變量的引用。

咱們在兩個下拉列表中添加了排列後相同的列表,咱們建立了一個QDoubleSpinBox,這是一個能夠處理浮點型的spinbox。咱們爲它提供了最大值和最小值。spinbox設置取值範圍是一個實用的方法,若是你這麼作了以後,當你設置初始值的時候,若是初始值超過了spinbox的範圍,他會自動增長或減少初始值是初始值達到範圍的邊界。

由於兩個下拉列表開始的時候都會顯示相同的幣種,因此咱們將value初始設置爲1.00,在toLabel裏顯示的結果也是1.00。

grid = QGridLayout()
        grid.addWidget(dateLabel, 0, 0)
        grid.addWidget(self.fromComboBox, 1, 0)
        grid.addWidget(self.fromSpinBox, 1, 1)
        grid.addWidget(self.toComboBox, 2, 0)
        grid.addWidget(self.toLabel, 2, 1)
        self.setLayout(grid)

一個grid layout看上去是給widget佈局的最簡單的方案。當咱們向grid layout添加widget的時候,咱們須要給他一個行列的位置,這是以0做爲基址的。佈局以下圖所示。grid layout還能夠有附加的參數,例子裏面咱們設置了行和列的寬度,在第九章覆蓋了這些話題。

若是咱們運行程序或者看截圖的話,很容易看出第0列要比第1列寬,可是在代碼裏面卻沒有任何附加的說明,這是怎麼發生的呢?佈局管理器很是聰明,他會他會管理空白、文字、以及尺寸政策來使得佈局自動適應環境。這種狀況下,下拉列表水平方向會拉長的列表中最大文字寬度的值。由於下拉列表是地列隊寬度元素,他的寬度就設置爲這一列的最小寬度;第一列的spinBox和它一個狀況。若是咱們運行程序而且向縮小窗口的話,神馬都不會發生,由於窗口已是他的最小寬度。不過咱們可使他變寬,這樣兩列橫向都會拉長。固然你可能更但願某一列的拉伸速度更快,這也是能夠實現的。

沒有一個元素在初始化的時候縱向被拉伸,由於在這個例子裏面是沒有必要地。不過若是咱們增長了窗口的高度,多餘的空白都會跑到dateLabel這裏去,由於他是這個例子裏惟一一個能夠在全部方向上增長大小的部件。

既然咱們建立了widget,得到了數據,進行了佈局,那麼如今是時候設置咱們form的行爲了。

self.connect(self.fromComboBox,
            SIGNAL("currentIndexChanged(int)"), self.updateUi)
    self.connect(self.toComboBox,
            SIGNAL("currentIndexChanged(int)"), self.updateUi)
    self.connect(self.fromSpinBox,
            SIGNAL("valueChanged(double)"), self.updateUi)
    self.setWindowTitle("Currency")

若是用戶更改任意一個下拉列表的當前元素,先關下了列表的comboBox就會發出currentIndexChanged(int)信號,參數是當前最新元素的下標。與之相似,若是用戶經過spinBox更改了value,那麼會發出valueChanged(double)信號。咱們把這幾個信號都鏈接在了一個Python槽上updateUi()。並非必須這麼作,咱們下一節就會看到,不過湊巧在這個例子裏是比較明智的選擇。

在__init__()方法最後,咱們設置了窗口標題。

def updateUi(self):
    to = unicode(self.toComboBox.currentText())
    from_ = unicode(self.fromComboBox.currentText())
    amount = ((self.rates[from_] / self.rates[to]) *
              self.fromSpinBox.value())
    self.toLabel.setText("{0:.2f}".format(amount))

這個方法被調用是爲了迴應下拉表的currentIndexChanged()這個信號,以及spinbox的valueChanged()這個信號。全部信號調用的時候會傳入一個參數。咱們下一節就會看到,咱們能夠忽略掉信號的參數,就像咱們如今作的同樣。

不管哪一個信號被觸發了,咱們會進入相同的處理環節。咱們提取出to和from的幣種,計算to的數值,而且相應的設置toLabel。咱們給予from文本一個名字是from_,由於from是Python的關鍵字。當計算出來的數值過窄的時候,咱們須要避開空白行,來適應頁面;不管在任何狀況下,咱們都更傾向於限制行的寬度去讓用戶在屏幕上更方便的讀取的兩個文件。

def getdata(self): # Idea taken from the Python Cookbook
    self.rates = {}
    try:
        date = "Unknown"
        fh = urllib2.urlopen("http://www.bankofcanada.ca"
                             "/en/markets/csv/exchange_eng.csv")
        for line in fh:
            line = line.rstrip()
            if not line or line.startswith(("#", "Closing ")):
                continue
            fields = line.split(",")
            if line.startswith("Date "):
                date = fields[-1]
            else:
                try:
                    value = float(fields[-1])
                    self.rates[unicode(fields[0])] = value
                except ValueError:
                    pass
        return "Exchange Rates Date: " + date
    except Exception, e:
        return "Failed to download:\n{0}".format(e)

在這個程序中,咱們用這個方法得到數據。開始咱們建立了一個新的屬性self.rates。與c++,java以及其餘類似的語言,Python容許咱們在任何須要的狀況下創造屬性——例如在構造時、初始化時或者在任何方法中。咱們甚至能夠再運行的時候在特殊的實例上添加屬性。

在與網絡鏈接時,有太多出錯誤的可能,例如:網絡可能癱瘓,主機可能掛起,URL可能改變,等等等等,咱們須要讓咱們的這個程序比以前的兩個更加健壯。另外可能遇到的問題是咱們在獲得非法法浮點數如NA(Not Availabel)。咱們有一個內部的try ... except塊,使用這個來捕獲非法數值。因此若是咱們轉換當前幣種失敗的時候,咱們僅僅是忽略掉這個特殊的幣種,而且繼續咱們的程序。

咱們在一個try ... except塊中處理的其餘全部可能出現的突發狀況。若是問題發生,咱們扔出異常,而且將它當作字符串返還給掉重者,__init__()。getdata()方法中返回的字符串會在dataLabel中顯示,一般這個標籤顯示轉換後的利率,不過有錯誤產生的時候,它會顯示錯誤信息。

你可能注意到咱們把URL分紅了兩行,由於它太長了,可是咱們又沒有escape一個新行。這麼作之因此可行是由於這個字符串在圓括號內。若是沒在的時候,咱們就須要escape一個新行或者使用+號(而且依然要escape一個新行)。

咱們初始化data時使用了一個字符串,由於咱們並不知道咱們須要計算的dates的利率。以後咱們使用urllib2.urlopen()方法使咱們獲得了咱們想要的文件的一個句柄。經過這個句柄咱們能夠利用read()方法讀取整個文件,不過在這個例子裏面,咱們更加推薦使用readlines()一行一行讀取文件來節約內存空間。

下面是從exchange_eng.csv文件中獲得的部分數據。有一些列和行被隱藏了,爲了節約空間。

...
#
Date (<m>/<d>/<year>),01/05/2007,...,01/12/2007,01/15/2007
Closing Can/US Exchange Rate,1.1725,...,1.1688,1.1667
U.S. Dollar (Noon),1.1755,...,1.1702,1.1681
Argentina Peso (Floating Rate),0.3797,...,0.3773,0.3767
Australian Dollar,0.9164,...,0.9157,0.9153
...
Vietnamese Dong,0.000073,...,0.000073,0.000073

exchange_eng.csv文件中有幾種不一樣的行格式。註釋及某些空白行從「#」開始,咱們將忽略這些行。交換利率是一個幣種、利率的列表,使用逗號分開。那些利率是對應某種特殊幣種的利率,每行的最後一個是最近的信息。咱們將每行使用逗號分開,而後選取第一個元素做爲幣種,最後一個元素做爲交換利率。也有一行是以」Date「 開頭的,這一個些列的數據是應用於各個列的。當咱們計算這行的時候,咱們選取最後的數據,由於這是咱們須要使用的交換數據。還有一些行開始時」Closing「,咱們無論這些行。

對於每個有交換利率的行,咱們在self.rates字典中插入一項,使用當期幣種做爲key,交換利率做爲value。咱們假設這個文件的編碼方式是7-bit ASCII或者Unicode,若是他不是以上兩種之一,咱們可能會獲得編碼錯誤。若是咱們知道具體編碼,咱們能夠在使用unicode()方法的時候將其做爲第二個參數。

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

信號和槽

任何一個GUI庫都提供了事件處理的一些方法,例如按下鼠標、敲擊鍵盤。例如,有一個上面寫有"Click Me"的按鈕,若是用戶點擊了以後,上面的信息就可使用了。GUI庫能夠告知咱們鼠標點擊按鈕的座標,以及按鈕的母widget,以及關聯的屏幕;它還會告訴咱們Shift,Ctrl,Alt以及NumLock鍵在當時的狀態;以及按鈕精確按下的時間;等等等等。若是用戶經過其餘手段點擊按鈕,相同的信息PyQt也會通知咱們。用戶可能經過使用Tab鍵的連續變化來得到focus,以後按下空格,或者Alt+C等快捷鍵,經過這些方式而不是使用鼠標來訪問咱們的按鈕;不過不管是哪種例子都算按鈕被按下,也會提供一些不一樣的信息。

Qt庫是第一個意識到並非在全部的狀況下,程序員都須要知道這些底層的事件信息:他們並不關心按鈕是如何按下的,他們關心的僅僅是按鈕被按下了,而後他們就會作出適合的處理。因爲這個緣由,Qt以及PyQt提供了兩種交流的機制:一種仍然是於其餘GUI庫類似的底層事件處理方案,另外一種就是Trolltech(Qt的創始人)創造的「信號槽」機制。在第10章和第11章咱們會學習底層的事件處理機制,這一章咱們關注的是它的高級機制,也就是信號槽。

每個QObject——包括全部的PyQt的widget(繼承自QWidget,也是一個QObject)——都提供了信號槽機制。特別的是,他們能夠聲明狀態的轉換,例如當checkbox被選中或者沒有被選中的時候,過着其餘重要的事件發生的時候,例如按鈕按下,全部PyQt的widget提供了一系列提早定義好的信號。

不管何時一個信號發射後,PyQt僅僅是簡單的把它扔掉!爲了讓咱們抓到這些信號,咱們必須將它連接到槽上去。在C++/Qt,槽是一個有特殊語法聲明的方法,不過在PyQt中,任何一個方法均可以是槽,並不須要特殊的語法聲明。

PyQt中大部分的widget也提早預置了一些槽,因此一些時候咱們能夠直接連接預置的信號與預置的槽,並不須要多寫多少代碼就能獲得咱們想要的行爲。PyQt比C++/Qt在這方面更加的多才多藝,由於咱們能夠連接的不只僅是槽,在PyQt中任意能夠調用的對象均可以動態的預置到QObject中。讓咱們看看信號槽在下面這個例子中是怎麼工做的。

不管是QDial仍是QSpinBox都有valueChanged()這個信號,當這個信號發出的時候,他攜帶的是最新時刻的信息。他倆還有setValue()這個槽,他接受一個整數。所以咱們將這兩個widget的這兩個信號和槽相互關聯,當用戶改變其中一個widget的時候,另外一個widget也會作出相應的反應。

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"), spinbox.setValue)
        self.connect(spinbox, SIGNAL("valueChanged(int)"), dial.setValue)
        self.setWindowTitle("Signals and Slots")

兩個widget這樣鏈接以後,若是用戶更改dial後,例如20,dial就會發出valueChanged(20)這個信號,相應的spinbox的setValue()槽就會將20做爲參數接受。不過在此以後,由於spinbox的值改變了,他也會發出valueChanged(20)這個信號,相應的dial的setValue()槽就會將20做爲參數接受。這麼着看起來貌似咱們會陷入一個死循環,不過事實valueChanged()信號並不會發出,由於事實上在這個方法執行以前,他會先檢測目標值於當期值是否真的不一樣,若是不一樣才發出信號。

如今讓咱們看一下連接信號槽的標準語法。咱們假設PyQt模塊提供了from ... import *的語法,s和w都是QObject對象。

s.connect(w, SIGNAL("signalSignature"), functionName)
s.connect(w, SIGNAL("signalSignature"), instance.methodName)
s.connect(w, SIGNAL("signalSignature"),
    instance, SLOT("slotSignature"))

signalSignature是信號的名字,而且帶着參數類型列表,使用逗號隔開。若是是Qt的信號,那麼類型的名字不需是C++的類型,例如int、QString。C++類型的名字也可能帶着const、*、&,不過在信號和槽裏的時候咱們能夠省略掉這些東西。例如,基本上全部的Qt信號中使用QString參數時,參數類型都會是const QString&不過在PyQt中,僅僅使用QString會更加的高效。不過在另外一方面,QListWidget有一個itemActivated(QListWidgetItem*)信號,咱們必須使用這種明確的寫法。

PyQt的信號在發出時能夠發出任意數量、任意類型的參數,咱們稍後會看到。

slotSignature擁有相同於signalSignature的形式。一個槽能夠擁有比於他連接信號更多的參數,不過這樣,多餘的參數會被忽略。對應的信號和槽必許有相同的參數列表,例如,咱們不能把QDial’s valueChanged(int)信號連接到QLineEdit’s setText(QString)槽上去。

在咱們這個例子裏,咱們使用了instance.methodName的語法,不過若是槽確實是Qt的槽而不是一個Python的方法的時候,使用SLOT()語法更加高效:

self.connect(dial, SIGNAL("valueChanged(int)"),
                 spinbox, SLOT("setValue(int)"))
    self.connect(spinbox, SIGNAL("valueChanged(int)"),
                 dial, SLOT("setValue(int)"))

咱們早就看到了一個槽能夠被多個信號鏈接,一個信號鏈接到多個槽也是可能的。雖然有種狀況很罕見,咱們甚至能夠將一個信號鏈接到另外一個信號上:在這種狀況下,當第一個信號發出時,會致使鏈接的信號也發出。

咱們使用QObject.connect()來創建鏈接,這些鏈接能夠被QObject.disconnect()取消。在實際應用中,咱們極少會取消鏈接,由於PyQt在對象被刪除後會自動斷開刪除對象的信號槽。

至今爲止,咱們看到了如何創建信號槽,怎麼寫槽函數——就是普通的方法。咱們知道當有狀態轉換或者某些重要事件發生的時候會發出信號。不過若是咱們想要在本身創建的組件裏發出咱們本身的信號時應該怎麼作呢?經過使用QObject.emit()很容易實現這一點。例如,下面有一個完整的QSpinBox子類,他會發出atzero信號,這須要一個數字作參數:

class ZeroSpinBox(QSpinBox):

    zeros = 0

    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        self.connect(self, SIGNAL("valueChanged(int)"), self.checkzero)


    def checkzero(self):
        if self.value() == 0:
            self.zeros += 1
            self.emit(SIGNAL("atzero"), self.zeros)

咱們將spinbox本身的valueChanged()信號與咱們checkzero()槽進行了鏈接,若是恰好value的值爲0的話,那麼checkzero()槽就會發出atzero信號,他會計算出總共有多少次到達過0。在信號中缺乏圓括號是很是重要的:這會告訴PyQt這是一個「短路」信號。

一個沒有參數的信號(因此沒有圓括號)是一個短路Python信號。當發出這種信號的時候,任何的附加參數均可以經過emit()方法進行傳遞,他們做爲Python對象來傳遞。這會避免在上面進行與C++類型的相互轉換,這就意味着,任何的Python對象均可以當作參數進行傳遞,即便他不能與C++數據類型相互轉換。當一個信號有至少一個參數的時候,這個信號就是一個Qt信號,也是一個非短路python信號。在這種狀況下,PyQt會檢查這些信號,看他是否爲一個Qt信號,若是不是的話就會把他假定爲一個Python信號。不管是哪一種狀況,參數都會被轉換爲C++數據類型。【此段疑似與下文某段重複】

下面是咱們怎麼在form的__init__()方法中進行信號槽的鏈接的:

zerospinbox = ZeroSpinBox()
...
self.connect(zerospinbox, SIGNAL("atzero"), self.announce)

再提一遍,咱們必須不能帶圓括號,由於這是一個短路信號。爲了完整期間,咱們把他鏈接的槽貼在這裏:

def announce(self, zeros):
    print("ZeroSpinBox has been at zero {0} times".format(zeros))

若是咱們在SIGNAL()語法中使用了沒有圓括號的方法,咱們一樣指明瞭一個短路信號。不管是發射短路信號仍是鏈接他們,咱們均可以使用這個語法。兩種用法都已經出如今了例子裏。

【此段內容疑似重複,略過、、、】

如今咱們看另外一個例子,一個小的非GUI自定義類,他是使用繼承QObejct的方式來實現信號槽機制的,因此信號槽機制並不只限於GUI類上。

class TaxRate(QObject):

    def __init__(self):
        super(TaxRate, self).__init__()
        self.__rate = 17.5


    def rate(self):
        return self.__rate


    def setRate(self, rate):
        if rate != self.__rate:
            self.__rate = rate
            self.emit(SIGNAL("rateChanged"), self.__rate)

不管是rate()仍是setRate()均可以被鏈接,由於任何一個Python中能夠被調用的對象均可以做爲一個槽。若是匯率變更,咱們就會更新__rate數據,而後發出rateChanged信號,給予新匯率一個參數。咱們也使用可快速短路語法。若是咱們使用標準語法,那麼惟一的區別可能就是信號會被寫成SIGNAL("rateChanged(float)")。若是咱們將rateChanged信號與setRate()槽創建起鏈接,由於if語句的緣由不會發成死循環。然咱們看看使用中的類。首先咱們定義了一個方法,這個方法將會在匯率變更的時候被調用。

def rateChanged(value):
    print("TaxRate changed to {0:.2f}%".format(value))

如今咱們實驗一下:

vat = TaxRate()
vat.connect(vat, SIGNAL("rateChanged"), rateChanged)
vat.setRate(17.5)    # No change will occur (new rate is the same)
vat.setRate(8.5)     # A change will occur (new rate is different)

這會致使在命令行裏輸出這樣一行文字"TaxRate changed to 8.50%".

在以前的例子裏,咱們將不一樣的信號鏈接在了一個槽上。咱們並不關心誰發出了信號。不過有的時候,咱們想要知道究竟是哪個信號鏈接在了這個槽上,而且根據不一樣的鏈接作出不一樣的反應。在這一節最後一個例子咱們將會研究這個問題。

上圖顯示的Connection程序有5個按鈕和一個標籤,當其中一個按鈕按下的時候,信號槽會更新label的文本。這裏貼上__init__()建立第一個按鈕的代碼:

button1 = QPushButton("One")

其餘按鈕除了變量的名字與文本不用以外,建立方式都相同。

咱們從button1的鏈接開始講起,這是__init__()裏面的connect()調用:

self.connect(button1, SIGNAL("clicked()"), self.one)

這個按鈕咱們使用了一個dedicated方法:

def one(self):
    self.label.setText("You clicked button 'One'")

將按鈕的clicked()信號與一個適當的方法鏈接去相應一個事件是大部分鏈接時的方案。

不過若是大部分處理方案都相同,不一樣之處僅僅是依賴於按下的按鈕呢?在這種狀況下,一般最好把這些按鈕鏈接到相同的槽上。有兩個方法能夠達到這一點。第一是使用partial function,而且使用被按下的按鈕做爲槽的調用參數來作修飾(partial function的做用)。另外一個方案是詢問PyQt看看是哪個按鈕被按下了。

返回本書的65頁,咱們使用Python2.5的functools.partial()方法或者咱們本身實現的簡單partial()方法:

import sys
if sys.version_info[:2] < (2, 5):
    def partial(func, arg):
        def callme():
            return func(arg)
        return callme
else:
    from functools import partial

使用partial(),咱們能夠包裝咱們的槽,而且使用一個按鈕的名字。因此咱們可能會這麼作:

self.connect(button2, SIGNAL("clicked()"),
    partial(self.anyButton, "Two")) # WRONG for PyQt 4.0-4.2

不幸的是,在PyQt 4.3以前的版本,這不會有效果。這個包裝函數式在connect()中建立的,不過當connect()被解釋執行的時候,包裝函數會出界變爲一個垃圾。從PyQt 4.3以後,若是在鏈接時使用functools.partial()包裝函數,那麼這就會被特殊對待。這意味着在鏈接時這樣被建立的方法不會被回收,那麼以前顯示的代碼就會正確執行。

在PyQt 4.0,4.1,4.2這幾個版本,咱們依然可使用partial():咱們在鏈接前建立包裝便可,這樣只要form實例存在,就能確保包裝函數不會越界成爲垃圾。鏈接可能看起來像這樣:

self.button2callback = partial(self.anyButton, "Two")
self.connect(button2, SIGNAL("clicked()"),self.button2callback)

當button2被點擊後,那麼anyButton()方法就會帶着一個「Two」的字符串參數被調用。下面就是該方法的代碼:

def anyButton(self, who):
    self.label.setText("You clicked button '%s'" % who)

咱們能夠講這個槽使用partial的方式應用到全部的按鈕上。事實上,咱們能夠徹底不使用partial()方法,也能夠獲得徹底相同的結果:

self.button3callback = lambda who="Three": self.anyButton(who)
self.connect(button3, SIGNAL("clicked()"),self.button3callback)

咱們在這裏建立了一個lambda方法,參數是按鈕的名字。這與partial()技術是相同的,他調用了相同的anyButton()方法,不一樣之處就是使用了lambda表達式。

不管是button2callback()仍是button3callback()都調用了anyButton()方法;惟一的區別是參數,一個是「Two」另外一個是「Three」。

若是咱們使用PyQt 4.1.1或者更高級的版本,咱們不須要本身保留lambda回調函數的引用。由於PyQt在connection中對待lambda表達式時會作特殊處理。因此,咱們能夠再connect()中直接調用lambda表達式。

self.connect(button3, SIGNAL("clicked()"),
        lambda who="Three": self.anyButton(who))

包裝技術工做的不錯,不過這裏又一個候選方法稍微有些不一樣,不過在某些時候可能頗有用,特別是咱們不想包裝咱們的方法的時候。則是button4和button5使用的另外一種技術。這是他們的鏈接:

self.connect(button4, SIGNAL("clicked()"), self.clicked)
self.connect(button5, SIGNAL("clicked()"), self.clicked)

你可能發現了咱們並無包裝兩個按鈕鏈接的clicked()方法,這樣咱們開始的時候看上去並不能區分究竟是哪一個按鈕按觸發了clicked()信號。然而,看下下面的實現就能清楚的明白咱們想要作什麼了:

def clicked(self):
    button = self.sender()
    if button is None or not isinstance(button, QPushButton):
        return
    self.label.setText("You clicked button '{0}'".format(
                       button.text()))

在一個槽內部,咱們走時能夠調用sender()方法來發現是哪個QObject對象發出的這個信號。當這是一個普通的方法調用這個槽時,這個方法會返回一個None。 儘管咱們知道鏈接這個槽的僅僅是按鈕,咱們依然要當心檢查。咱們使用isinstance()方法,不過咱們可使用hasattr(button, "text")方法代替。若是咱們在這個槽上鍊接全部的按鈕,他們都會正確工做。

有些程序員不喜歡使用sender()方法,由於他們感受這不是面向對象的風格,他們更傾向使用partial function的方法。

事實上確實有其餘的包裝技術。這會使用QSignalMapper類,第九章會展現這個例子。

有些狀況下,一個槽運行的結果可能會發出一個信號,而這個信號可能反過來調用這個槽,不管是直接調用或者間接調用,這會致使這個槽和信號不斷的重複調用,以至陷入一個死循環。這種循環圈子在實際運用中很是少見。兩個因素會減小這種圈子發生的可能性。第一,有些信號只有真正發生改變時纔會發出,例如:若是用戶改變了QSpinBox的值,或者程序經過調用setValue()而改變了值,那麼只有新的數值與剛剛的數值不一樣時,他纔會發出’valueChanged()‘信號。第二,某些信號以後反應用戶操做時纔會發出。例如,QLineEdit的textEdited()信號只有是用戶更改文本時纔會發出,在代碼中調用setText()時是不會發出這個信號的。

若是一個信號槽造成了一個調用循環,一般狀況下,咱們要作的第一件事情是檢查咱們代碼的邏輯是否正確:咱們是否是像咱們想象的那樣進行了處理?若是邏輯正確可是循環鏈還在的話,咱們能夠經過更改發出的信號來打斷循環鏈,例如,將信號發出的手段改成程序中發出(而不是用戶發出)。若是問題依然存在,咱們能夠再某個特定位置用代碼調用QObject.blockSignals(),這個方法全部的QWidget類都有繼承,他傳遞一個布爾值——True,中止對象發出信號;False回覆信號發射。

這完整的覆蓋了咱們的信號槽機制。咱們將在剩下的整本書裏見到各式各樣的信號槽的練習。大部分的GUI庫在不一樣方面上拷貝了這個基址。這是由於信號槽機制很是的實用強大,他使得程序員脫離開那些用戶是如何操做的具體細節,而更加關注程序的邏輯。

總結

在這一章,咱們看到了咱們能夠建立混血的控制檯——GUI程序。咱們固然能夠作的更遠——例如,在一個if塊內導入全部的GUI代碼,並執行他,只要安裝了PyQt,就能夠顯示圖形界面。若是用戶沒有安裝PyQt的話,他就能夠退化爲一個控制檯程序。

咱們也看到了GUI程序不一樣於普通的批處理程序,他又一個一直運行的事件循環,檢查是否有用戶事件發生,例如按下鼠標,槍擊鍵盤,或者系統事件例如timer計時,窗口重繪。程序終止只在請求他這麼作的時候。

計算器程序顯示了一個很是簡單可是很是具備結構特色的對話框__init__()方法。widget被建立,佈局,鏈接,以後又一個或多個方法用於反映用戶的交互。貨幣轉換程序使用了相同的技術,僅僅是有更復雜的用戶界面以及更復雜的行爲處理機制。貨幣轉換程序也顯示了咱們能夠鏈接多個信號去同一個槽。

PyQt的信號槽機制容許咱們在更高的級別去處理用戶的交互。這讓咱們能夠把關注點放在用戶想幹嗎而不是他們是怎麼請求去幹的。全部的PyQt widget經過發射信號的方式來傳達狀態發生了改變,或者是發生了其餘重要事件;而且大部分事件咱們能夠忽略掉某些信號。不過對於那些咱們感興趣的信號,咱們能夠方便的使用QObject.connect()來確保當某個信號發生時咱們調用了想要進行處理的函數。不像C++/Qt,槽生命必須使用特定的語法,在PyQt中任何的callable對象,也就是說任何的方法,均可以做爲一個槽。

咱們也看到了如何將多個信號鏈接到一個槽上,以及如何使用partial function程序或者使用sender()方法來使咱們的槽來適應發出信號的不一樣widget。

咱們也學習了咱們並不必定要生命咱們本身的信號:咱們能夠簡單的使用QObject.emit()並添加任意的參數。

相關文章
相關標籤/搜索