Qt for Python/Porting guide
Qt for Python is an offering that enable developing Qt applications in a pythonic way. I guess, that didn't go well with most you who have been using Python for years! Let's try to rephrase, Qt for Python is a binding for Qt, to enable Python application development using Qt. Ah..that sounds about right! Isn't it? So, the idea behind Qt for Python is probably clear now. Let's see what does it take to port an existing Qt C++ application to Python. Before we start digging deeper into this topic, let's ensure that we have all the prerequisites met. For example, installing either Python2 or Python3, and so on. Wait a minute, isn't this information outlined in the Getting started section of the documentation. In that case, let's dive straight into the topic.
Assuming that you have required environment to develop Python applications using PySide2(Qt for Python), let's get started. But, we need a C++-based Qt application to port. Let's pick a Qt example that has the .ui XML file, defining the application's UI. That way, we avoid the need to write the code to create a UI. Wait a minute, isn't that the reason why Qt was developed in the first place?
Basic differences
Before we get started, let's familiarize ourselves with some of the basic differences between the Qt C++ and Python code:
- C++ being an object-oriented programming language, we reuse code that is already defined in another file, using the "#include" statements at the beginning. Similarly, we use "import" statements in Python to access packages and classes. Here are is the classic Hello World example using the Python bindings for Qt (PySide2):
import sys
from PySide2.QtWidgets import QApplication, QLabel
if __name__ == "__main__":
app = QApplication(sys.argv)
label = QLabel("Hello World")
label.show()
sys.exit(app.exec_())
Notice, that the application code begins with a couple of import statements to include the sys, and QApplication and QLabel classes from PySide2.QtWidgets.
- Similar to a C++ application, the Python application also needs an entry point. Not because a Python application must have one, but because it is a good practice to have one. In the Hello World example code that we looked at earlier, you can see that the entry point is defined by the following line:
if __name__ == "__main__":
#...
- Qt provides classes that are meant to manage the application-specific requirements depending whether the application is a console-only, GUI with QtWidgets, or GUI without QtWidgets. These classes load necessary plugins, such as the GUI libraries required by a GUI application.
- Q_PROPERTY macros are used in Qt to add a public member variable with a getter and setter functions. Python alternative for this is the @property decorator before the getter and setter function definitions.
- Qt offers a unique way of callback mechanism, where a signal is emitted to notify the occurrence of a event, so that slots connected to this signal can react to it. Python alternative for this is to identify
- In C++, we use the keyword, this, to refer to the current object. In a Python context, it is self that offers something similar.
Porting tutorial
To explain this better, let's try to port the existing Qt C++ application to Python. The books SQL example seems ideal for this, as we could avoid writing UI-specific code in Python and use the .ui file, which describes the application's UI.
initDb.h -> createdb.py
To begin with let's try to port the C++ code that creates an sqllite database and tables, and adds data to them. In this case, all C++ code related to this lives in the initdb.h. The code in this header file is divided into these following parts:
- init_db - Creates a db and the necessary tables
- addBooks - Adds book info. to the **books** table.
- addAuthor - Adds author info. to the **authors** table.
- addGenre - Adds genre info. to the **genres** table.
To start with, create the createdb.py and add the following import statements at the beginning:
from PySide2.QtSql import QSqlDatabase, QSqlError, QSqlQuery
from datetime import datetime
These are all the imports we need to get our database in place.
Let's look at at the code for the initDb C++ method and port it to an equivalent function in Python:
C++ | Python |
---|---|
QSqlError initDb()
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (!db.open())
return db.lastError();
QStringList tables = db.tables();
if (tables.contains("books", Qt::CaseInsensitive)
&& tables.contains("authors", Qt::CaseInsensitive))
return QSqlError();
QSqlQuery q;
if (!q.exec(QLatin1String("create table books(id integer primary key, title varchar, author integer, genre integer, year integer, rating integer)")))
return q.lastError();
if (!q.exec(QLatin1String("create table authors(id integer primary key, name varchar, birthdate date)")))
return q.lastError();
if (!q.exec(QLatin1String("create table genres(id integer primary key, name varchar)")))
return q.lastError();
if (!q.prepare(QLatin1String("insert into authors(name, birthdate) values(?, ?)")))
return q.lastError();
QVariant asimovId = addAuthor(q, QLatin1String("Isaac Asimov"), QDate(1920, 2, 1));
QVariant greeneId = addAuthor(q, QLatin1String("Graham Greene"), QDate(1904, 10, 2));
QVariant pratchettId = addAuthor(q, QLatin1String("Terry Pratchett"), QDate(1948, 4, 28));
if (!q.prepare(QLatin1String("insert into genres(name) values(?)")))
return q.lastError();
QVariant sfiction = addGenre(q, QLatin1String("Science Fiction"));
QVariant fiction = addGenre(q, QLatin1String("Fiction"));
QVariant fantasy = addGenre(q, QLatin1String("Fantasy"));
if (!q.prepare(QLatin1String("insert into books(title, year, author, genre, rating) values(?, ?, ?, ?, ?)")))
return q.lastError();
addBook(q, QLatin1String("Foundation"), 1951, asimovId, sfiction, 3);
addBook(q, QLatin1String("Foundation and Empire"), 1952, asimovId, sfiction, 4);
addBook(q, QLatin1String("Second Foundation"), 1953, asimovId, sfiction, 3);
addBook(q, QLatin1String("Foundation's Edge"), 1982, asimovId, sfiction, 3);
addBook(q, QLatin1String("Foundation and Earth"), 1986, asimovId, sfiction, 4);
addBook(q, QLatin1String("Prelude to Foundation"), 1988, asimovId, sfiction, 3);
addBook(q, QLatin1String("Forward the Foundation"), 1993, asimovId, sfiction, 3);
addBook(q, QLatin1String("The Power and the Glory"), 1940, greeneId, fiction, 4);
addBook(q, QLatin1String("The Third Man"), 1950, greeneId, fiction, 5);
addBook(q, QLatin1String("Our Man in Havana"), 1958, greeneId, fiction, 4);
addBook(q, QLatin1String("Guards! Guards!"), 1989, pratchettId, fantasy, 3);
addBook(q, QLatin1String("Night Watch"), 2002, pratchettId, fantasy, 3);
addBook(q, QLatin1String("Going Postal"), 2004, pratchettId, fantasy, 3);
return QSqlError();
}
|
def init_db():
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:")
if not db.open():
return db.lastError()
tables = db.tables()
for table in tables:
if table == "books" and table == "authors":
return QSqlError()
q = QSqlQuery()
if not q.exec_("create table books(id integer primary key, title varchar, author integer, "
"genre integer, year integer, rating integer)"):
return q.lastError()
if not q.exec_("create table authors(id integer primary key, name varchar, birthdate date)"):
return q.lastError()
if not q.exec_("create table genres(id integer primary key, name varchar)"):
return q.lastError()
if not q.prepare("insert into authors(name, birthdate) values(?, ?)"):
return q.lastError()
asimovId = add_author(q, "Isaac Asimov", datetime(1920, 2, 1))
greeneId = add_author(q, "Graham Greene", datetime(1904, 10, 2))
pratchettId = add_author(q, "Terry Pratchett", datetime(1948, 4, 28))
if not q.prepare("insert into genres(name) values(?)"):
return q.lastError()
sfiction = add_genre(q, "Science Fiction")
fiction = add_genre(q, "Fiction")
fantasy = add_genre(q, "Fantasy")
if not q.prepare("insert into books(title, year, author, genre, rating) "
"values(?, ?, ?, ?, ?)"):
return q.lastError()
add_book(q, "Foundation", 1951, asimovId, sfiction, 3)
add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4)
add_book(q, "Second Foundation", 1953, asimovId, sfiction, 3)
add_book(q, "Foundation's Edge", 1982, asimovId, sfiction, 3)
add_book(q, "Foundation and Earth", 1986, asimovId, sfiction, 4)
add_book(q, "Prelude to Foundation", 1988, asimovId, sfiction, 3)
add_book(q, "Forward the Foundation", 1993, asimovId, sfiction, 3)
add_book(q, "The Power and the Glory", 1940, greeneId, fiction, 4)
add_book(q, "The Third Man", 1950, greeneId, fiction, 5)
add_book(q, "Our Man in Havana", 1958, greeneId, fiction, 4)
add_book(q, "Guards! Guards!", 1989, pratchettId, fantasy, 3)
add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3)
add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3)
return QSqlError()
|
Notice that the Python version of the initDb function does not use anything specific to Qt for setting up the database.
- The init_db calls the addAuthor, addGenre, and addBook functions to append data to the tables. Let's try porting these functions to Python.
C++ | Python |
---|---|
void addBook(QSqlQuery &q, const QString &title, int year, const QVariant &authorId,
const QVariant &genreId, int rating)
{
q.addBindValue(title);
q.addBindValue(year);
q.addBindValue(authorId);
q.addBindValue(genreId);
q.addBindValue(rating);
q.exec();
}
QVariant addGenre(QSqlQuery &q, const QString &name)
{
q.addBindValue(name);
q.exec();
return q.lastInsertId();
}
QVariant addAuthor(QSqlQuery &q, const QString &name, const QDate &birthdate)
{
q.addBindValue(name);
q.addBindValue(birthdate);
q.exec();
return q.lastInsertId();
}
|
def add_book(q, title, year, authorId, genreId, rating):
q.addBindValue(title)
q.addBindValue(year)
q.addBindValue(authorId)
q.addBindValue(genreId)
q.addBindValue(rating)
q.exec_()
def add_genre(q, name):
q.addBindValue(name)
q.exec_()
return q.lastInsertId()
def add_author(q, name, birthdate):
q.addBindValue(name)
q.addBindValue(birthdate)
q.exec_()
return q.lastInsertId()
|
bookdelegate.cpp -> bookdelegate.py
Now that the database is in place, try porting the C++ code for the BookDelegate class. The class offers a delegate to present and edit the data in a QTableView. It inherits QSqlRelationalDelegate interface, which offers functionality that is specific to handling relational databases, such as combobox editor for foreign key fields. To begin with, create bookdelegate.py and add the following imports to it:
from PySide2.QtSql import QSqlRelationalDelegate
from PySide2.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate,
QStyle, QStyleOptionViewItem)
from PySide2.QtGui import QMouseEvent, QPixmap, QPalette
from PySide2.QtCore import QEvent, QSize, Qt
Once the necessary imports are added, define the BookDelegate class and port its constructor code to Python. Here is how the C++ and Python version of the code look like:
C++ | Python |
---|---|
BookDelegate::BookDelegate(QObject *parent)
: QSqlRelationalDelegate(parent), star(QPixmap(":images/star.png"))
{
}
|
class BookDelegate(QSqlRelationalDelegate):
"""Books delegate to rate the books"""
def __init__(self, parent=None):
QSqlRelationalDelegate.__init__(self, parent)
self.star = QPixmap(":/images/star.png")
|
As the default functionality offered by QSqlRelationalDelegate is not sufficient to present the books data, you must reimplement a few functions to get the desired results. For example, painting stars to represent the rating for each book in the table. Here is how the reimplemented code look like:
C++ | Python |
---|---|
void BookDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() != 5) {
QStyleOptionViewItem opt = option;
// Since we draw the grid ourselves:
opt.rect.adjust(0, 0, -1, -1);
QSqlRelationalDelegate::paint(painter, opt, index);
} else {
const QAbstractItemModel *model = index.model();
QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ?
(option.state & QStyle::State_Active) ?
QPalette::Normal :
QPalette::Inactive :
QPalette::Disabled;
if (option.state & QStyle::State_Selected)
painter->fillRect(
option.rect,
option.palette.color(cg, QPalette::Highlight));
int rating = model->data(index, Qt::DisplayRole).toInt();
int width = star.width();
int height = star.height();
int x = option.rect.x();
int y = option.rect.y() + (option.rect.height() / 2) - (height / 2);
for (int i = 0; i < rating; ++i) {
painter->drawPixmap(x, y, star);
x += width;
}
// Since we draw the grid ourselves:
drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1));
}
QPen pen = painter->pen();
painter->setPen(option.palette.color(QPalette::Mid));
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
painter->setPen(pen);
}
QSize BookDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == 5)
return QSize(5 * star.width(), star.height()) + QSize(1, 1);
// Since we draw the grid ourselves:
return QSqlRelationalDelegate::sizeHint(option, index) + QSize(1, 1);
}
bool BookDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
if (index.column() != 5)
return QSqlRelationalDelegate::editorEvent(event, model, option, index);
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
int stars = qBound(0, int(0.7 + qreal(mouseEvent->pos().x()
- option.rect.x()) / star.width()), 5);
model->setData(index, QVariant(stars));
// So that the selection can change:
return false;
}
return true;
}
QWidget *BookDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() != 4)
return QSqlRelationalDelegate::createEditor(parent, option, index);
// For editing the year, return a spinbox with a range from -1000 to 2100.
QSpinBox *sb = new QSpinBox(parent);
sb->setFrame(false);
sb->setMaximum(2100);
sb->setMinimum(-1000);
return sb;
}
|
def paint(self, painter, option, index):
""" Paint the items in the table.
If the item referred to by <index> is a StarRating, we
handle the painting ourselves. For the other items, we
let the base class handle the painting as usual.
In a polished application, we'd use a better check than
the column number to find out if we needed to paint the
stars, but it works for the purposes of this example.
"""
if index.column() != 5:
opt = option
# Since we draw the grid ourselves:
opt.rect.adjust(0, 0, -1, -1)
QSqlRelationalDelegate.paint(self, painter, opt, index)
else:
model = index.model()
if option.state & QStyle.State_Enabled:
if option.state & QStyle.State_Active:
color_group = QPalette.Normal
else:
color_group = QPalette.Inactive
else:
color_group = QPalette.Disabled
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect,
option.palette.color(color_group, QPalette.Highlight))
rating = model.data(index, Qt.DisplayRole)
width = self.star.width()
height = self.star.height()
x = option.rect.x()
y = option.rect.y() + (option.rect.height() / 2) - (height / 2)
for i in range(rating):
painter.drawPixmap(x, y, self.star)
x += width
# Since we draw the grid ourselves:
self.drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1))
pen = painter.pen()
painter.setPen(option.palette.color(QPalette.Mid))
painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight())
painter.drawLine(option.rect.topRight(), option.rect.bottomRight())
painter.setPen(pen)
def sizeHint(self, option, index):
""" Returns the size needed to display the item in a QSize object. """
if index.column() == 5:
size_hint = QSize(5 * self.star.width(), self.star.height()) + QSize(1, 1)
return size_hint
# Since we draw the grid ourselves:
return QSqlRelationalDelegate.sizeHint(self, option, index) + QSize(1, 1)
def editorEvent(self, event, model, option, index):
if index.column() != 5:
return False
if event.type() == QEvent.MouseButtonPress:
mouse_pos = event.pos()
new_stars = int(0.7 + (mouse_pos.x() - option.rect.x()) / self.star.width())
stars = max(0, min(new_stars, 5))
model.setData(index, stars)
# So that the selection can change
return False
return True
def createEditor(self, parent, option, index):
if index.column() != 4:
return QSqlRelationalDelegate.createEditor(self, parent, option, index)
# For editing the year, return a spinbox with a range from -1000 to 2100.
spinbox = QSpinBox(parent)
spinbox.setFrame(False)
spinbox.setMaximum(2100)
spinbox.setMinimum(-1000)
return spinbox
|