第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对话框的特性。
货币转换工具是一个试用的小工具。可是因为兑换汇率时常变换,咱们不能简单的像前几章同样使用一个静态的字典来存储这些信息。一般,加拿大银行都会在网上提供这些银行汇率,而且这个文件的格式咱们能够很是容易的读取更改。这些汇率与最新的数据可能有几天的时差,可是这些信息对于有国际合同须要估算预付款的时候是足够使用了。
这个程序要想使用首先下载这些汇率数据,而后他才会建立用户界面。 一般咱们从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()并添加任意的参数。