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