一个25行的弹出警告

 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
import sys
import time
from PyQt4.QtCore import *
from PyQt4.QtGui import *

app = QApplication(sys.argv)

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

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

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

每一个PyQt图形程序必须有一个QApplication对象,因为它能识别一些命令行的参数,接受sys.argv作为 参数。

PyQt中任何部件都能用作顶级窗口,比如一个按钮或一个标签。当部件如此使用时,PyQt自动给它一个标题栏。 一旦窗口设置好后,就可以调用show方法,这个时候窗口没有显示。show方法仅仅是将一个画图事件加入 QApplication对象的事件队列。

app.exec_开始QApplication对象事件循环。第一个事件是画图事件,因此标签窗口弹出。一分钟后超时 事件触发,app.quit被调用。这个方法执行图形程序的结束清理工作,关闭窗口,释放资源。

图形程序的事件循环,伪代码如下:

1
2
3
4
5
6
while True:
    event = getNextEvent()
    if event:
        if event == Terminate:
            break
        processEvent(event)

一个30行的表达式求值程序

 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
32
33
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 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")

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

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

所有PyQt的部件,都继承自QWidget,并且都是新风格的类。默认地,当一个部件被关闭时,它仅仅是被隐藏了。 当一个窗体隐藏了,如果PyQt检查到程序没有可见窗体,而且进一步交互也不可能,PyQt会执行程序的结束清理工作。

对象所有权

  • 所有PyQt类继承自QObject,包括所有的部件。没有父亲的部件是一个顶级窗口,孩子部件被包含在父亲部件 里面。父亲部件对孩子部件拥有所有权。
  • PyQt使用父子所有权模型来保证当一个父亲部件被销毁,所有它的孩子部件也被自动销毁。
  • 为避免内存泄漏,除了顶级窗口,我们应该保证所有部件都有父亲。
  • 布局管理器自动将部件重新绑定到正确的父亲部件上。

PyQt提供3种布局管理器:垂直布局,水平布局,网格布局。布局可以嵌套。 每一个部件通过发射信号声明状态改变。

一个70行的汇率转换器

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import sys
import urllib2
from PyQt4.QtCore import *
from PyQt4.QtGui import *

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

    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" % amount)

    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%s" % e

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

信号和槽

每一个QObject支持信号和槽机制。所有的PyQt部件都有一组预定义的信号。不管什么时候一个信号发射,PyQt 默认简单地将其丢掉。必须将信号连接到槽来捕捉信号。在PyQt中,槽是任何可调用的对象。大多数部件也有预定义 好的槽。

connect的语法,s通常是self,w是部件:

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

signalSignature是信号的名字,并带一个逗号隔开的参数列表。如果是Qt信号,则参数类型必须是C++类型。 当书写信号的C++参数类型时,可以丢弃const&,但是必须保留*

PyQt信号发射时被定义,它们可以有任意数量,任意类型的参数。

slotSignature和signalSignature有着一样的形式。一个槽的参数可能比信号少。相应的信号和槽的参数必须类型相同。 如果是Qt槽而不是Python方法时,使用SLOT语法效率更高。

1
2
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发射自定义的信号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)

一个没有参数(没有括号)的信号是一个短路Python信号。当这种信号被发射,任何数据都可以当作额外的参数传给 emit方法,这些参数被当作Python对象传递。 至少有一个参数的信号是Qt信号或非短路Python信号,其参数都将转换为C++数据类型。

PyQt的信号和槽机制并不局限于GUI类,任何QObject的子类都可以使用信号和槽。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PyQt4.QtCore import *

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)

def rateChanged(value):
    print 'TaxRate changed to %.2f%%' % value

vat = TaxRate()
vat.connect(vat, SIGNAL('rateChanged'), rateChanged)
vat.setRate(17.5)
vat.setRate(8.5)

多个信号连接到同一个槽时,如何确定谁调用了槽。

1
2
3
4
self.connect(button2, SIGNAL("clicked()"),
             partial(self.anyButton, "Two")) # WRONG for PyQt 4.0-4.2
self.connect(button3, SIGNAL("clicked()"),
             lambda who="Three": self.anyButton(who)) # WRONG before 4.1.1

在PyQt4.3之前,在connect中创建的函数,在connect返回时被垃圾回收。因此需要保存一个引用到该函数。 在PyQt4.1.1之前,在connect中创建的lambda也会被垃圾回收,同样需要保存一个引用。

1
2
3
4
5
self.button2callback = partial(self.anyButton, "Two")
self.connect(button2, SIGNAL("clicked()"), self.button2callback)

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

另外一种方法是使用sender告诉我们是哪个对象。

1
2
3
4
5
6
7
self.connect(button4, SIGNAL("clicked()"), self.clicked)
self.connect(button5, SIGNAL("clicked()"), self.clicked)
def clicked(self):
    button = self.sender()
    if button is None or not isinstance(button, QPushButton):
        return
    self.label.setText("You clicked button '%s'" % button.text())