rapid gui programming with python and qt

I purchased Rapid GUI Programming back around 2008 because I was developing Qt4-based tools on an Open SuSE Linux notebook I’d built using a Gateway computer, and I was investigating an alternative way to write ostensible Qt4 tools. The SuSE system pretty much worked, which was surprising back in the day, if you can remember how hard it was to make any Linux distribution work on contemporary hardware, especially a notebook computer. But the program came to an end and I moved on to other projects.

I found this book again when I recently started to clean up a section of the house I’d not been through in some time. It was stacked in with other books from that period of time. I remembered RGP as being of very good to excellent quality. I’d gone into it because I was already writing Qt4 applications using C++ and the Qt4 SDK. While development and testing was pretty fast, I wanted some way to prototype even faster, and if the prototype was good enough, then to just ship the prototype out into the field for others to use until I could finish the “real” application. It soon reached a point where I just shipped the Python/Qt4 version and let it go at that.

I wondered how well the book had aged since it’s initial publication back in 2007. I went looking to see if the author had updated the book at all since then, but it appears he hadn’t. I picked one of the examples in the book and tried to see if I could migrate it from Python 2 and PyQt4 to Python 3 and PyQT5. To make it even more interesting I decided to do all the development work on my Raspberry Pi 4 with Raspbian Buster. Python 3.7.3 and PyQt5 5.11.3 are installed. Things have progressed sufficiently from both Python and Qt to make the problem of upgrading one of the books examples an interesting problem.

I turned to Chapter 4, page 121, and the Currency Converter. I migrated the application from Python 2 and PyQt4 to Python 3 and PyQt5 on Raspbian Buster. The Currency section noted the original was 70 lines long. If you subtract the comments, white lines, and print statements in my example, the line count drops down to 64 lines. Whatever. Chasing lines of code is so … 1970s.

This is my final migration.

#!/usr/bin/env python3

import sys
import math
from urllib.request import urlopen
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets 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.fromSpinBox = QDoubleSpinBox()
        self.fromSpinBox.setRange(0.01, 10000000.00)
        self.toComboBox = QComboBox()
        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.setWindowTitle("Currency/Bank of Canada")

    def updateUi(self):
        to = self.toComboBox.currentText()
        from_ = self.fromComboBox.currentText()
        amount = (self.rates[from_] / self.rates[to]) * self.fromSpinBox.value()
        self.toLabel.setText("%0.2f" % amount)

    def getdata(self):
        self.rates = {}
        date = "Unknown"
        csvurl = "https://www.bankofcanada.ca/valet/observations/group/FX_RATES_DAILY/csv"

            csvbytedata = urlopen(csvurl).read()
            csvlist = csvbytedata.decode("utf=8").replace('\ufeff','').replace('"','').split("\n")
            csvlist[:] = [item for item in csvlist if item]
            # print("\n".join(csvlist))
            headers = csvlist[csvlist.index("OBSERVATIONS")+1].split(",")[1:]
            exchangeRates = csvlist[-1].split(",")
            date = exchangeRates[0]
            labelIndex = csvlist.index("SERIES") + 2

            for i in range(len(headers)):
                    if exchangeRates[i+1]:
                        self.rates[csvlist[labelIndex].split(",")[-1].split(" to ")[0]] = float(exchangeRates[i+1])
                        print("Skipping %s" % csvlist[labelIndex])
                    labelIndex += 1
                except ValueError:
            return "Exchange Rates for " + date

        except Exception as e:
            return "Failed to download:\n%s" % e

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

The first big change is lines 33 to 35. Here we simply use the PyQt5 style signals and slots instead of the PyQt4 style. Everything still works the same.

The second really big change is lines 46 to 71. That entire function, getdata(), was re-written for two reasons:

  1. Python 3
  2. A major change in how data is queried from the Bank of Canada

It’s obvious that changes will occur in the migration from Python 2 to Python 3, but the change in how data is read, and the form it comes in, required a major re-think and re-write of the original code. The Bank of Canada has provided a section on their website titled Valet API (https://www.bankofcanada.ca/valet/docs) that details their RESTful API for querying for certain types of information, and the way the response payload is formatted. For this example I stayed with CSV formatting, and set up the query URL (see line 48) to load the data required by the application.

Unlike the original code, I chose (or was forced due to changes between Python 2 and Python 3) to read in the entire document as byte data and then convert it into a list of strings. Thus, line 51 does the reading, and line 52 does the conversion; first, into Unicode, then a pair of filters to remove a Unicode character and all the double quotes that come across as well. Finally, line 53 uses a Python list comprehension to remove all empty strings, in place. In place means that a second copy of the list isn’t created, but the original list is modified. After all of that, here’s what the data looks like:

Daily exchange rates
Daily average exchange rates - published once each business day by 16:30 ET. All Bank of Canada exchange rates are indicative rates only.
FXAUDCAD,AUD/CAD,Australian dollar to Canadian dollar daily exchange rate
FXBRLCAD,BRL/CAD,Brazilian real to Canadian dollar daily exchange rate
FXCNYCAD,CNY/CAD,Chinese renminbi to Canadian dollar daily exchange rate
FXGBPCAD,GBP/CAD,UK pound sterling to Canadian dollar daily exchange rate
FXUSDCAD,USD/CAD,US dollar to Canadian dollar daily exchange rate
FXVNDCAD,VND/CAD,Vietnamese dong to Canadian dollar daily exchange rate

This is what the data looks like with everything superfluous removed. The highlighted lines, 7 and 16, are our “selectors”, which we will use to locate specific data within the list. Line 55 of the code searched for “OBSERVATIONS” to then pull out what would be spreadsheet headers, so we can use them later in the code. Line 58 searches for “SERIES” so that we know the start of the verbose spreadsheet column header definitions. It turns out that the spreadsheet headers are in the same order as the verbose header definitions.

The exchange rates fill the bulk of the file, starting two lines below observations. A full load of data creates a list with over 800 lines, of which only a small fraction are shown here for illustrative purposes. But looking at a full print of the list (via the commented print statement in code line 54) will produce an 800+ line text file. Inspection of the text file shows three years of observations, starting back on the first business day of January 2017. The observations are all in chronological order, and we only care about the day the file was generated, so we can reference the last line in the list with the day’s current observations in code line 56.

With everything set up, we then do the work of creating a dictionary of the full names as keys of each associated exchange rate value. The keys will then be used to populate the GUI pull-downs.

We’re not out of the woods just yet. It turns out that there are gaps (blanks) in the row data corresponding to each numeric exchange rate. Thus, there’s the test on code line 62 to make sure we have a non-blank string to convert to a float. If the string is blank, we skip and move to the next value. I wasn’t paying attention to this the first time I looked at the textual data because the first few years in the collection had all the rates filled. But for year 2020 (and probably earlier) there are three missing exchange rates: Malaysian, Thai, and Vietnamese. I don’t know why they’re missing, but they are. Those two print statements inside the if block will print out if the value was used or skipped. If I were writing more robustly I would check to see if the value is a float, rather than depend upon the inner try/exception block.

When we finally run everything, we get the following:

Which looks pretty much like the screenshot on page 122 of RGP.

I tackled this because I found the challenge of migrating the original code from 2008 to 2020 to be fun and interesting (yes, I said fun). I got to exercise a bit of sleuthing, especially figuring out pulling the exchange rates from the Bank of Canada’s Valet REST API, and then using that to set up the application. The was really the only major effort. Everything else was straightforward, and not all the difficult to migrate from PyQt4 to PyQt5.

But I do have a concern with Qt in general. The owners of Qt are now beginning to talk of restrictions in the use of Qt in open source projects. I don’t have all of the facts, so I’ll refrain from any comments for now. But I don know that the effects of the COVID-19 pandemic are having a highly negative business effect all over, including the Qt Company, owners of the Qt Framework. I truly hope everything works out for the best for the Qt Company so that we can continue to use the Qt Framework and the many tools built on top of Qt, such as PyQt.