I am currently reading through Mark Summerfield’s book on PyQt, Rapid GUI Programming with Python and Qt and thought it would be fun to take some of the example applications in it and convert them to PySide. So I’ll be creating a series of articles where I’ll show the original PyQt examples from the book and then convert them to PySide and probably add something of my own to the code. The book doesn’t really get in Qt GUI coding until chapter 4 where the author creates a fun little currency converter. Come along and enjoy the fun!
Here’s the mostly original code:
import sys import urllib2 from PyQt4.QtCore import SIGNAL from PyQt4.QtGui import QComboBox, QDialog, QDoubleSpinBox, QLabel from PyQt4.QtGui import QApplication, QGridLayout ######################################################################## class CurrencyDlg(QDialog): """""" #---------------------------------------------------------------------- def __init__(self, parent=None): """Constructor""" super(CurrencyDlg, 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") # layout the controls 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) # set up the event handlers 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 getdata(self): """ """ self.rates = {} url = "http://www.bankofcanada.ca/en/markets/csv/exchange_eng.csv" try: date = None fh = urllib2.urlopen(url) 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 #---------------------------------------------------------------------- def updateUi(self): """ Update the user interface """ to = unicode(self.toComboBox.currentText()) frum = unicode(self.fromComboBox.currentText()) amount = ( self.rates[frum] / self.rates[to] ) amount *= self.fromSpinBox.value() self.toLabel.setText("%0.2f" % amount) #---------------------------------------------------------------------- if __name__ == "__main__": app = QApplication(sys.argv) form = CurrencyDlg() form.show() app.exec_()
If you really want to know how this code works, I recommend getting the aforementioned book or by studying the code. It is Python after all.
Porting to PySide
Now we need to “port” this to PySide, the LGPL version of the QT bindings for Python. Fortunately, in this example it is beyond easy. All you have to do is change the following imports
from PyQt4.QtCore import SIGNAL from PyQt4.QtGui import QComboBox, QDialog, QDoubleSpinBox, QLabel from PyQt4.QtGui import QApplication, QGridLayout
to
from PySide.QtCore import SIGNAL from PySide.QtGui import QComboBox, QDialog, QDoubleSpinBox from PySide.QtGui import QApplication, QGridLayout, QLabel
If you do that and save it, you’ll be done porting and the code will now work in PySide land. However, I think we need to go a bit further and make this code a little smarter. You’ll notice in the original that every time you run it, the application will go online and download the exchange rate data. That seems to be a little bit of overkill, so let’s add a check to see if it’s already downloaded and less than a day old. Only if it’s older than a day will we want to download a new copy. And instead of using urllib2, let’s use the new-fangled requests library instead.
Here’s the new code:
import datetime import os import PySide import requests import sys from PySide.QtCore import SIGNAL from PySide.QtGui import QComboBox, QDialog, QDoubleSpinBox from PySide.QtGui import QApplication, QGridLayout, QLabel ######################################################################## class CurrencyDlg(QDialog): """""" #---------------------------------------------------------------------- def __init__(self, parent=None): """Constructor""" super(CurrencyDlg, 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") # layout the controls 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) # set up the event handlers 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 - by PySide %s" % PySide.__version__) #---------------------------------------------------------------------- def downloadFile(self, rate_file): """ Download the file """ url = "http://www.bankofcanada.ca/en/markets/csv/exchange_eng.csv" r = requests.get(url) try: with open(rate_file, "wb") as f_handler: f_handler.write(r.content) except IOError: print "ERROR: Unable to download file to %s" % rate_file #---------------------------------------------------------------------- def getdata(self): """ """ base_path = os.path.dirname(os.path.abspath(__file__)) rate_file = os.path.join(base_path, "exchange_eng.csv") today = datetime.datetime.today() self.rates = {} if not os.path.exists(rate_file): self.downloadFile(rate_file) else: # get last modified date: ts = os.path.getmtime(rate_file) last_modified = datetime.datetime.fromtimestamp(ts) if today.day != last_modified.day: self.downloadFile(rate_file) try: date = None with open(rate_file) as fh: for line in fh: result = self.processLine(line) if result != None: date = result return "Exchange Rates Date: " + date except Exception, e: return "Failed to download: \n%s" % e #---------------------------------------------------------------------- def processLine(self, line): """ Processes each line and updates the "rates" dictionary """ line = line.rstrip() if not line or line.startswith(("#", "Closing ")): return fields = line.split(",") if line.startswith("Date "): date = fields[-1] return date else: try: value = float(fields[-1]) self.rates[unicode(fields[0])] = value except ValueError: pass return None #---------------------------------------------------------------------- def updateUi(self): """ Update the user interface """ to = unicode(self.toComboBox.currentText()) frum = unicode(self.fromComboBox.currentText()) amount = ( self.rates[frum] / self.rates[to] ) amount *= self.fromSpinBox.value() self.toLabel.setText("%0.2f" % amount) #---------------------------------------------------------------------- if __name__ == "__main__": app = QApplication(sys.argv) form = CurrencyDlg() form.show() app.exec_()
Okay, let’s spend a little time looking at the code. We won’t explain everything, but we’ll go over what was changed. First off, we imported some more modules: datetime, os, requests and sys. We also imported PySide itself so we could easily add its version information to the dialog’s title bar. Next we added a couple of new methods: downloadFile and processLine. But first we need to look at what was changed in getdata. Here we grab the path that the script is running in and use that to create a fully qualified path to the rates file. Then we check if it already exists. If it doesn’t exist, we call the downloadFile method which uses the requests library to download the file. We’re also using Python’s with context manager construct which will automatically close the file when we’re done writing to it.
If the file DOES exist, then we fall to the else part of the statement in which we check if the file’s last modified date is older than today’s date. If it is, then we download the file and overwrite the original. Then we move on to processing the file. To do the processing, we move most of the code into a separate method called processLine. The primary reason for doing so is that if we don’t, we end up with a very complicated nested structure that can be hard to follow. We also add a check to see if the date has been returned from processLine and if it has, we set the date variable. The rest of the code stayed the same.
Wrapping Up
At this point, you should know how to create a very simple PySide application that can actually do something useful. Not only that, but you also have a very general idea of how to port your PyQt code to PySide. You have also been exposed to two different ways of downloading files from off the web. I hope this has opened your eyes to the power of Python and the fun of programming.
Note: The code in this article was tested on Windows 7 Professional with Python 2.6.6, PySide 1.2.2 and PyQt4
Related Links
- Python 101: How to Download a File
- The Official PySide website
- The Official PyQt website
- The requests package
- UPDATE (04/10/2013) – I noticed someone else decided to do this same tutorial as a video tutorial
Download the Source
my biggest complaint with PySide is that they don’t have a uic module, which makes it a pain to use .ui files from designer.
Instead of
self.connect(self.fromComboBox, SIGNAL(“currentIndexChanged(int)”),
self.updateUi)
… you can define the event handlers in a more Pythonic way:
self.fromComboBox.currentIndexChanged.connect(self.updateUi)
That’s good to know. I didn’t really like the other way much. Thanks!
Hello Mike,
I’ve re-subscribed to your blog after deciding to finally settle on Python as my implementation language, but it’s interesting that you’re working on the same book I’ll do. 🙂
I’m not sure if wxWidgets-3.0 will be released ever, so I gave up on wx(python) and decided to use Qt, but still not sure whether PyQt or PySide (we’ll do open-source project, so license is not the issue).
In any case, nice to be here again, this time learning from you about Python GUI programming using Qt. 😉
i need help can somebody good with python get to me really quickly on my e-mail please… lowkeyjaz@live.co.uk its really really important as im a beginner on python
This is helpful, wish I had seen it before, as I’m in the process of translating all the examples from his book into PySide (with new-style signals and slots). You can find it here: https://github.com/EricThomson/PySideSummer