We Ported a Qt App from C++ to Python. Here's What Happened.
Earlier this year, The Qt Company announced that Python would be officially supported in Qt via the Qt For Python (formerly PySide2) Python bindings and tooling. In June 2018, the first technical preview was offered, built against the Qt 5.11 release.
I have some experience with Python, including using it with the PyQt Python bindings, so I thought I would take a closer look at how Qt for Python is coming along. In this blog post I'll share some of my thoughts and experiences with porting a real Qt application from C++ to Python.
What Was Done
I decided to try porting a spreadsheet example program. This is a moderately complex (about 1000 lines of C++ source code) widget-based desktop application that originally came from the book C++ GUI Programming with Qt 4, that I had ported to Qt 5 some time ago.
I installed the initial tech review release of Qt for Python on an Ubuntu Linux 18.04 desktop (laptop) system. I then started porting the C++ application to Python by pasting in the C++ code for each class and converting it to Python. I started with the main program, then continued with the smaller and simpler classes, and finally the larger classes including the one for the main window. I used some of the example programs that came with Qt for Python as a reference for how to implement various features in Python.
Source Code
The entire ported source code is too long to list here, but I will show some highlights to give a flavor for how the C++ and Python versions compare, and to mention some specific features of note. Even if you don't "speak" Python, you should be able to follow the code.
Here is the main program from the C++ version, in the file main.cpp, which is pretty standard Qt boilerplate code:
#include ˂QApplication˃
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mainWin;
mainWin.show();
return app.exec();
}
And here is the equivalent in Python, starting from the top of the source file:
#!/usr/bin/env python3
from PySide2 import QtCore, QtGui, QtWidgets
import spreadsheet_rc from ui_gotocelldialog
import Ui_GoToCellDialog from ui_sortdialog
import Ui_SortDialog
The first line allows the file to be directly executed under Linux using the Python interpreter (you need to give the file execute permission).
The next line imports the resource data from the file spreadsheet_rc.py, that was generated by running the Qt for Python resource compiler (pyside2-rcc) on the resource file spreadsheet.qrc, which was directly taken from the C++ version.
The next two "from" lines import the code generated from the UI forms that was generated by the Qt for Python UI compiler (pyside2-uic) from the two ui files for the forms used by the application, gotocelldialog.ui and sortdialog.ui. These were also used unchanged from the C++ version. For convenience, I manually created a make file to run the appropriate commands to generate the resource and UI forms.
By convention, the Python main function is at the end of the file, and contains these lines of code:
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit(app.exec_())
You can see that the above is very similar to the C++ version.
Moving on to porting the classes used in the application, here is a slightly abbreviated version of the header and implementation files for the sort dialog in the C++ version. It is pretty straightforward, with a class derived from QDialog and from the UI compiler generated dialog class, and then implementing two public methods: the constructor and one to set the column range. This was the C++:
sortdialog.h:
class SortDialog : public QDialog, public Ui::SortDialog
{
Q_OBJECT
public:
SortDialog(QWidget *parent = 0);
void setColumnRange(QChar first, QChar last);
};
sortdialog.cpp:
SortDialog::SortDialog(QWidget *parent)
: QDialog(parent)
{
setupUi(this);
secondaryGroupBox-˃hide();
tertiaryGroupBox-˃hide();
layout()-˃setSizeConstraint(QLayout::SetFixedSize);
setColumnRange('A', 'Z');
}
void SortDialog::setColumnRange(QChar first, QChar last)
{
primaryColumnCombo-˃clear();
secondaryColumnCombo-˃clear();
tertiaryColumnCombo-˃clear();
secondaryColumnCombo-˃addItem(tr("None"));
tertiaryColumnCombo-˃addItem(tr("None"));
primaryColumnCombo-˃setMinimumSize(
secondaryColumnCombo-˃sizeHint());
QChar ch = first;
while (ch ˂= last) {
primaryColumnCombo-˃addItem(QString(ch));
secondaryColumnCombo-˃addItem(QString(ch));
tertiaryColumnCombo-˃addItem(QString(ch));
ch = ch.unicode() + 1;
}
}
Here is the corresponding Python code, which is all implemented in the same source file:
class SortDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent)
self.ui = Ui_SortDialog()
self.ui.setupUi(self)
self.ui.secondaryGroupBox.hide()
self.ui.tertiaryGroupBox.hide()
self.layout().setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.setColumnRange('A', 'Z')
def setColumnRange(self, first, last):
self.ui.primaryColumnCombo.clear()
self.ui.secondaryColumnCombo.clear()
self.ui.tertiaryColumnCombo.clear()
self.ui.secondaryColumnCombo.addItem(self.tr("None"))
self.ui.tertiaryColumnCombo.addItem(self.tr("None"))
self.ui.primaryColumnCombo.setMinimumSize(self.ui.secondaryColumnCombo.sizeHint())
ch = first
while ch ˂= last:
self.ui.primaryColumnCombo.addItem(ch)
self.ui.secondaryColumnCombo.addItem(ch)
self.ui.tertiaryColumnCombo.addItem(ch)
ch = chr(ord(ch) + 1)
You can see pretty much a one to one correspondence between the lines of code in C++ and Python. Some of the significant differences in Python include:
- the constructor is called __init__ rather than the class name
- the instance of the class is called "self" rather that "this" and needs to be qualified when used
- variables do not need to be explicitly declared or their types defined
- no semicolons are required at the end of each line (though they are permitted in Python)
- the dot (".") operator is used in Python where a pointer dereference ("->") would be used in C++
- constants and enumerated values like QLayout::SetFixedSize become QtWidgets.QLayout.SetFixedSize, with the Qt module explicitly listed.
As another example, here is the C++ code for the constructor for the MainWindow class:
MainWindow::MainWindow()
{
spreadsheet = new Spreadsheet;
setCentralWidget(spreadsheet);
createActions();
createMenus();
createContextMenu();
createToolBars();
createStatusBar();
readSettings();
findDialog = 0;
setWindowIcon(QIcon(":/images/icon.png"));
setCurrentFile("");
}
Here is the corresponding Python code, which is similar:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.spreadsheet = Spreadsheet()
self.setCentralWidget(self.spreadsheet)
self.recentFiles = []
self.createActions()
self.createMenus()
self.createContextMenu()
self.createToolBars()
self.createStatusBar()
self.readSettings()
self.findDialog = 0
self.setWindowIcon(QtGui.QIcon(":/images/icon.png"))
self.setCurrentFile("")
Signals and slots work much the same as they do in C++. The syntax for connecting signals to slots is a little different, but conceptually the same as when using the "new style" connect in C++. Here are some examples of connect calls in Python from the code for the find dialog:
self.lineEdit.textChanged.connect(self.enableFindButton)
self.findButton.clicked.connect(self.findClicked)
self.closeButton.clicked.connect(self.close)
This class has two signals it defines. In the C++ code in finddialog.h they were defined like this:
signals:
void findNext(const QString &str, Qt::CaseSensitivity cs);
void findPrevious(const QString &str, Qt::CaseSensitivity cs);
And the signals were emitted in the dialog using this code:
if (backwardCheckBox-˃isChecked()) {
emit findPrevious(text, cs);
} else {
emit findNext(text, cs);
}
In the code for the main window, these signals were connected to slots in the Spreadsheet class like this:
connect(findDialog, &FindDialog::findNext, spreadsheet, &Spreadsheet::findNext);
connect(findDialog, &FindDialog::findPrevious, spreadsheet, &Spreadsheet::findPrevious);
In Python, the signals were defined in the FindDialog class like so:
class FindDialog(QtWidgets.QDialog):
findNext = QtCore.Signal(str, QtCore.Qt.CaseSensitivity)
findPrevious = QtCore.Signal(str, QtCore.Qt.CaseSensitivity)
The code that emits the signals looks like this:
if self.backwardCheckBox.isChecked():
self.findPrevious.emit(text, cs)
else:
self.findNext.emit(text, cs)
And the Python code that connects the signals to slots is:
self.findDialog.findNext.connect(self.spreadsheet.findNext)
self.findDialog.findPrevious.connect(self.spreadsheet.findPrevious)
I spent a few hours on the Python port, and at the end of that time most of the application was functional. Here are some screen shots of the application running, which appeared identical to the C++ version:
There are still a few issues I haven't resolved related to file saving and loading, for example. I'm sure these could be completed after spending a little more time.
Observations
Over the course of the port I observed some issues that may be of interest to others developing with Qt for Python. There are a few small differences between C++ and Python APIs that are subtle and can cause you some grief until you know about them.
Some methods and functions that change the contents of a QString argument were modified to receive a Python string (str) and to return the value of the modified string. This includes the methods fixup() and validate() for the classes QAbstractSpinBox, QDateTimeEdit, QDoubleSpinBox, QSpinBox, and QValidator.
Some QFileDialog methods that in C++ modify a string and return a value, now return two values as a tuple. This includes the QFileDialog methods getOpenFileName, getOpenFileNames, and getSaveFileName.
Some Qt method names conflict with native Python functions. The Qt names have had an underscore appended to resolve this conflict. This includes the QTextStream methods bin_(), hex_(), oct_() and QDialog exec_().
There is no support for QVariants! But you don't really need it because Python variables can store different types at run time. Any Qt function expecting or returning a QVariant can receive or return any Python object (and "None" is used for an invalid QVariant).
You will need to learn the Python way of doing some things, most notably:
- string manipulation
- containers
- loops and iterators
If you have used Python for writing simple scripts, you probably haven't used its object-oriented features. As with Qt in C++, you will need to understand the basics of how Python supports classes, methods, instance variables, etc. There are many good books and tutorials on Python that cover this.
Python has a standard programming style which most programmers follow. There is an automated tool called "flake8" (formerly "pep8") that will check your code for conformance against the standard and also point out possible errors or poor practices. I recommend regularly running it on your code and fixing any warnings.
When converting from C++ to Python, make sure you don't overlook defining any inline methods that might have been defined in the C++ class header file.
Being an interpreter, you should expect to get errors at run time. Some will be found at program startup, but others will only occur when the offending lines or code are executed. For this reason you need to test your Python code well. A good set of unit tests will help catch these earlier (and later regressions). The good news is that errors are usually very obvious from the error messages (something you can't always say about C++ compiler error messages!).
You can easily add debug output to your code, but the Python interpreter also has helpful options for tracing and a debugger that you might want to explore. Qt also provides some useful functions, such as QtCore.QObject.dumpObjectTree() and QtCore.QObject.dumpObjectInfo().
Likes and Dislikes
Based on my brief experience with Qt for Python, these were some of the things I liked:
- The APIs are almost identical to C++, so experienced Qt users can quickly get up to speed and leverage their Qt knowledge.
- You can continue to use Qt Designer .ui files and resources.
- If you are porting a C++ application, you can mostly one for one port much of the code.
- Python in general is a well-designed, powerful, and readable language.
- Run time errors are usually straightforward.
- A developer can be very productive due to not needing a compile/build cycle.
A few things that I disliked or saw as challenges were the following:
- Deploying your application may be more complex due to the need to provide all Python source files as well as Python, Qt, and Qt for Python. There are tools like cx_Freeze that allow you to convert a Python application into a binary executable.
- Qt Creator supports editing Python code but doesn't (yet?) support Qt for Python projects. You might want to use a dedicated Python IDE such as PyCharm, although arguably an IDE is less important for Python as there is no compilation stage.
- You still need to use make or similar build system to run the Qt resource compiler and UI compiler. While you can load ui files at run time using QUiLoader (like you can from C++) it's generally not recommended for performance, security, and ease of deployment reasons. Possibly in the future qmake or Qt Creator will handle this.
- Despite being an interpreter, Python programs can still crash! Python's previously mentioned tracing features and debugger can help identify when and where this happens.
- As compared to C++, with Python you are farther away from the machine. Some issues, like memory management, are mostly outside of your control, and if you need to solve a memory or resource leak, it will be even more difficult than with a compiled language.
- The Qt APIs for Python are not yet as well documented as Qt for C++. There are not a lot of good example programs, and it is not yet clear what some of the best programming practices are. (For example, should I split all classes into separate source files?) I expect this to improve over time. Converting some or all the Qt examples to Python would be an admirable project for some people to undertake.
Conclusions
My earlier experience with using Qt and Python was with the PyQt bindings. Qt for Python is similar and quite compatible with PyQt. In fact, I took a number of PyQt example applications that we use for Qt training and was able to quickly port them to Qt for Python. It was mostly a matter of changing imports from "PyQt5" to "PySide2", and in some cases a few tweaks such as handling the differences in method names and how signals and slots are handled.
As I write this, Qt for Python is in a tech preview status, built against Qt 5.11.1. It is not clear if there will be more tooling coming, like better Qt Creator support. But so far it looks quite complete and stable.
Our experience at ICS with Python and PyQt is that large complex (typically desktop) applications can be developed with Python. Users can't tell that it was implemented in Python and not C++. Performance is typically not an issue, and experienced Qt C++ developers can quickly make the transition to Python.
Qt for Python offers the possibility to expand the developer base of Qt. Historically it has been hard to find good C++ programmers, and even harder to find experienced Qt developers. Python is widely being used by young people on the Raspberry Pi, for example, so Qt For Python offers the potential for a new generation of Qt developers.
Want more Qt-related technical content? Click here.
References
- http://blog.qt.io/blog/2018/05/04/hello-qt-for-python/
- http://blog.qt.io/blog/2018/06/13/qt-python-5-11-released/
- http://blog.qt.io/blog/2018/07/17/qt-python-available-pypi/
- http://code.qt.io/cgit/pyside/pyside-setup.git/
- https://doc-snapshots.qt.io/qtforpython/
- https://wiki.qt.io/Qt_for_Python
- https://www.riverbankcomputing.com/software/pyqt/intro
- https://www.jetbrains.com/pycharm/