I’ve spent a lot of the last 10 years working very exclusively in Python. It’s a fantastic language, super quick to iterate, debug and develop for. Its drawback is that it is hard to monetize as the source code cannot be encoded properly and can be slow to run.
One of the main pillars that I’ve been using for my Python code has been the use of Qt, exposed by its Python bindings of PySide and PySide2. These bindings are generated from the C++ bindings automatically with only a few tweaks, it got me thinking, how hard would it be to convert a Python Qt-based tool into a C++ Qt-based tool.
It shouldn’t be too hard… right….
I started off like any developer would, by downloading the latest version of Qt from their website, installing and setting it up. Qt comes with its own IDE software, Qt creator, which can be used for C++, Python, and Qt quick projects right out of the box. After creating a new Qt application windows project setting up the kit to run off the visual studio 2017 compilers, it was surprisingly easy to have a blank Qt Main Window up and running.
With the IDE and toolchain confirmed as working, I started to wonder what Python project I should convert C++, I eventually opted to use the paint on a panel (Revisited) project that I released recently as it was mainly a Qt project but it included things from Models, Advance Widgets, standalone classes and plenty of interaction between all of them.
In the Python project everything was in one single file, the Qt widgets, the model, and the main tool, and whilst having everything in a single file is possible to accomplish in C++ I opted to break it out into about 5 header and source files to make it more manageable.
The first ones I tackled what’s the SplineShape class that is stand-alone doesn’t require any Qt widgets and doesn’t inherit from QObject, and with such simple class, this ended up in a single header file. With the exception of the finalizedShape method, all the methods follow their Python counterparts line for line. The finalizedShape method uses Python list comprehension to speed up generating lists whilst is this wasn’t possible in C++ and a QList was utilized for this.
def finalizeShape(self):
"""
Mark the shape as final so data can be cached out for use later on
"""
xVals = [point.x() for point in self._points]
yVals = [point.y() for point in self._points]
self._minX = min(xVals) - self._width
self._maxX = max(xVals) + self._width
self._minY = min(yVals) - self._width
self._maxY = max(yVals) + self._width
Python finalizeShape
/*!
Mark the shape as final so data can be cached out for use later on
*/
void finalizeShape(){
QList<int> xValues = QList<int>();
QList<int> yValues = QList<int>();
for (auto const& point : _points) {
xValues.append(point.x());
yValues.append(point.y());
}
_minX = *std::min_element(xValues.constBegin(), xValues.constEnd()) - _width;
_maxX = *std::max_element(xValues.constBegin(), xValues.constEnd()) + _width;
_minY = *std::min_element(yValues.constBegin(), yValues.constEnd()) - _width;
_maxY = *std::max_element(yValues.constBegin(), yValues.constEnd()) + _width;
}
C++ finalizeShape
Next up was the PaitningModel class, which similar to the SplineShape class, proved to be very similar with only a few noticeable differences. The first difference is virtual methods for the QAbstractItemModel methods had to match their C++ signatures, whereas the Python is more forgiving in that regard.
The logic being the Python implementation of the erasePoint method used a lot of list comprehension and list slicing to achieve the erasing of the point. This wasn’t practical in the C++ version, so the whole logic was re-written where a nested list of lists of QPoints was created, as we go through each point in the shape, we would check if it was within the erasing threshold if it wasn’t it would be added to the current list, and when a point was detected within the erasing threshold, we incremented the top-level list. After we did a cleanup pass to update or delete the shape and adding more shapes as needed. I feel this implementation is much cleaner than the original Python code, and I would happily backport this logic to Python.
def erasePoint(self, location):
"""
Erase a given point using the location and current width. Will split up lines into differnt
shapes
args:
location (QPoint): The position to erase
"""
newShapeOffset = 0
for layerIdx, shape in enumerate(list(self._shapes)):
idxToRemove = [idx for idx, point in enumerate(shape.points()) if QtCore.QLineF(point, location).length() < self._width]
if len(idxToRemove) == 0:
continue
allPoints = shape.points()
idxToRemove.insert(0, 0)
idxToRemove.append(len(allPoints))
for idx, points in enumerate([allPoints[idxToRemove[idx-1]:idxToRemove[idx]] for idx in xrange(1, len(idxToRemove))]):
if idx == 0:
if len(points) == 0:
self._shapes.remove(shape)
newShapeOffset -= 1
else:
shape.setPoints(points)
else:
if len(points) > 1:
newShape = _SplineShape(shape.colour(), shape.width())
newShape.setPoints(points)
# insert so it doesnt change the order
self._shapes.insert(layerIdx+newShapeOffset, newShape)
self.beginInsertRows(self.createIndex(0,0), 0, 1)
self.shapeAdded.emit(self._currentShape)
self.endInsertRows()
newShapeOffset += 1
Python erasePoint
void PaintingModel::erasePoint(QPoint position){
for(std::size_t layerIdx = 0; layerIdx < _shapes.size(); ++layerIdx){
SplineShape* shape = &_shapes[layerIdx];
// Split the shapes up to remove the points to be removed
QList< QList<QPoint> > newSplinePoints;
newSplinePoints.push_back(QList<QPoint>());
for(std::size_t pointIdx = 0; pointIdx < shape->points().size(); ++pointIdx){
QPoint point = shape->points()[pointIdx];
if (QLineF(point, position).length() < _width){
if (newSplinePoints.back().size() != 0){
newSplinePoints.push_back(QList<QPoint>());
}
}else{
newSplinePoints.back().push_back(point);
}
}
// All parts of this shape have been removed, delete it!
if (newSplinePoints.size() == 1 && newSplinePoints.back().size() == 0){
// check to see if that was the last one, in which case, just clear the model
if(_shapes.size() == 1){
clear();
return;
}
// Remove it from the shapes, remove the row and increment back one
beginRemoveRows(createIndex(0,0), 0, 1);
_shapes.erase(_shapes.begin()+int(layerIdx));
layerIdx --;
endRemoveRows();
continue;
}
// Nothing to be done, its perfect the way it is!
if (newSplinePoints.size() == 1){
continue;
}
for(std::size_t pointIdx = 0; pointIdx < newSplinePoints.size(); ++pointIdx){
if(pointIdx == 0){
shape->setPoints(newSplinePoints[pointIdx]);
}
else{
if (newSplinePoints[pointIdx].size() > 0){
SplineShape newShape = SplineShape(shape->colour(), shape->width());
newShape.setPoints(newSplinePoints[pointIdx]);
_shapes.insert(_shapes.begin() + layerIdx + 1, newShape);
beginInsertRows(createIndex(0,0), 0, 1);
emit shapeAdded(newShape);
endInsertRows();
layerIdx ++;
}
}
}
}
}
C++ erasePoint
Other than those few changes, the rest of the code is the same as its Python source code.
With the Model and SplineShapes done, the next up was the PaintingModelColourSample, and there isn’t much to say about this work, other than it true to its Python counterpart with one small exception. In the Python code, we connect the model’s colourChange signal to the Widgets repaint method. In Python, we can do this as PySide is more lenient about matching method signatures to signal signatures, whereas C++ isn’t, requiring a new method to negotiate the differences.
On to the main event, the PaintingModelView, where the user can create the shapes and they get drawn out into the view. This class follows the same pattern as the PaintingModelColourSample, where it’s extremely true to the Python implementation with just a small addition of a method to negotiate signals.
The PaintingToolEnum class was converted to a real enum and I ended up putting in the header file for the PaintingModelView as that was the lowest level interface that uses it.
Last up is the PaintingTool class to bringing in all the other parts of the project together, which was extremely straightforward and almost identical to the Python version. Having to define the UI elements which will need to be accessed outside the SetupUi method was a little alien, but it makes a lot of sense with a strong typed langue. One issue I did hit which took me a while to debug and work were connecting the thickness QSpinBox’s value changed signal as it wasn’t able to resolve which connection to use.
connect(_thicknessSpinner, QOverload<int>::of(&QSpinBox::valueChanged), this, &PaintingTool::_handleThicknessChange);
I am amazed at how little the code changed from Python to C++ and how easy it was. A lot of the time I lost with the conversion was due to silly issues with using objects rather than pointers and dealing with the limitations of Qt Creator. At one point, it would not compile new code even though it was not reporting any compiler issues and resulted in me chasing my tail until I cleared the cache and rebuilt. I have heard of a Qt plugin for Visual Studio and I’ll be looking to use that in the future.
Ultimately I found this hugely rewarding, not just as a great excuse to use C++, but also that Qt’s similarity provides a perfect bridge between Python and C++ where developers feel more at home and comfortable with it than they would naturally with a different API, and going forward I will be still be using Python for the initial prototyping of code, but porting it into C++ to help refine the bad code design choices that Python allows.
– Geoff
Like!! Great article post.Really thank you! Really Cool.