PySide: Creating a Currency Converter

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!

pyqt_currency

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

pyside_currency

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

Download the Source

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary