Qt for Python/Porting guide: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
 
(32 intermediate revisions by 3 users not shown)
Line 1: Line 1:
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.
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 offers binding for Qt C++ API, 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 [[Qt_for_Python/GettingStarted|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
Assuming that you have required environment to develop Python applications using PySide2(Qt for Python), you can start. But, you need a C++-based Qt application to port. Pick a Qt example that has the .ui XML file, defining the application's UI. That way, you 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?
.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?


<!-- <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;"> CODE </span> -->
== Basic differences ==
== Basic differences ==
Before we get started, let's familiarize ourselves with some of the basic differences between the Qt C++ and Python code:
Before you start, familiarize yourself with the following 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):
* C++ being an object-oriented programming language, we reuse code that is already defined in another file, using the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">#include</span> statements at the beginning. Similarly, Python uses <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">import</span> statements to access packages and classes. Here is the classic ''Hello World'' example using the Python bindings for Qt (PySide2):


<syntaxhighlight lang="python" line="line">
<syntaxhighlight lang="python" line="line">
Line 19: Line 19:
</syntaxhighlight>
</syntaxhighlight>


Notice, that the application code begins with a couple of ''import'' statements to include the ''sys'', and ''QApplication'' and ''QLabel'' classes from ''PySide2.QtWidgets''.  
Notice that the application code begins with a couple of <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">import</span> statements to include the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">sys</span>, and ''QApplication'' and ''QLabel'' classes from <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">PySide2.QtWidgets</span>.  


* 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:
* '''Main function:''' 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 that you looked at earlier, the entry point is defined by the following line:<syntaxhighlight lang="python" line="line">
<syntaxhighlight lang="python" line="line">
if __name__ == "__main__":
if __name__ == "__main__":
   #...
   #...
</syntaxhighlight>  '''Note:''' This is just a good practice when writing an application, and in Python is just a formality when executing the current script directly. The variables declared inside the if-statement are considered as global variables.
* '''Qt Application:''' Qt provides classes that are meant to manage the application-specific requirements depending on whether the application is a console-only (QCoreApplication), GUI with QtWidgets (QApplication), or GUI without QtWidgets (QGuiApplication). These classes load necessary plugins, such as the GUI libraries required by an application.
* '''Qt Properties:''' <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">Q_PROPERTY</span> macros are used in Qt to add a public member variable with getter and setter function. Python alternative for this is the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">@property</span> decorator before the getter and setter function definitions.
* '''Qt Signals and Slots:''' Qt offers a unique 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 connect the signal to any function. To understand this better, look at this improved version of the ''Hello World'' example:<syntaxhighlight lang="python">
class MyWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"]
        self.button = QPushButton("Click me!")
        self.text = QLabel("Hello World")
        self.text.setAlignment(Qt.AlignCenter)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.text)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)
        self.button.clicked.connect(self.magic)
    @Slot()
    def magic(self):
        self.text.setText(random.choice(self.hello))
if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    widget = MyWidget()
    widget.resize(800, 600)
    widget.show()
    sys.exit(app.exec_())
</syntaxhighlight>
</syntaxhighlight>
* 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.
Notice that the QPushButton's clicked signal is connected to the ''magic'' function to randomly change the QLabel text. You should always use the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">@Slot</span> decorator just before the function definition if that function is a Slot, this is required to register all the members of your code to the QtMetaObject.
* 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.
* C++ uses the keyword, ''this'', to refer to the current object. In a Python context, it is ''self'' that offers something similar.
* Qt coding style recommends using '''camelCase''' for the variable and function names, whereas the Python style uses '''under_score'''.
* And more importantly, forget about '''{}''', and ''';'''.


== Porting tutorial ==
== Porting tutorial ==
To explain this better, let's try to port the existing Qt C++ application to Python. [https://code.qt.io/cgit/qt/qtbase.git/tree/examples/sql/books?h=5.12.2 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.
To understand this better, try to port an existing Qt C++ application to Python. [https://code.qt.io/cgit/qt/qtbase.git/tree/examples/sql/books?h=5.12.2 The books SQL example] seems ideal for this, as you could avoid writing UI-specific code in Python and use its <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">.ui</span> file instead.


=== initDb.h -> createdb.py ===
=== 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:
To begin with, 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 <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">initdb.h</span>. The code in this header file is divided into these following parts:


* init_db - Creates a db and the necessary tables
* <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">init_db</span> - Creates a db and the necessary tables
* addBooks - Adds book info. to the **books** table.  
* <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addBooks</span> - Adds book info. to the **books** table.  
* addAuthor - Adds author info. to the **authors** table.
* <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addAuthor</span> - Adds author info. to the **authors** table.
* addGenre - Adds genre info. to the **genres** table.
* <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addGenre</span> - Adds genre info. to the **genres** table.


To start with, create the ''createdb.py'' and add the following import statements at the beginning:
To start with, create the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">createdb.py</span> and add the following import statements at the beginning:
<syntaxhighlight lang="python" line="line">
<syntaxhighlight lang="python" line="line">
from PySide2.QtSql import QSqlDatabase, QSqlError, QSqlQuery
from PySide2.QtSql import QSqlDatabase, QSqlError, QSqlQuery
Line 52: Line 86:
These are all the imports we need to get our database in place.
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:
The <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">init_db</span> function relies on the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addAuthor</span>, <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addGenre</span>, and <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">addBook</span> functions to append data to the tables. Port these functions first.
{|
{|
! style="margin-top: 0px;"| C++
! style="margin-top: 0px;" | C++
! Python
! Python
|-
|-
|<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO;">
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();
}
</syntaxhighlight>
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
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()
 
 
</syntaxhighlight>
|}
 
Now, look at at the code for the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">initDb</span> C++ method and port it to an equivalent function in Python:
{|
! style="margin-top: 0px;" | C++
! Python
|-
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
const auto BOOKS_SQL = QLatin1String(R"(
    create table books(id integer primary key, title varchar, author integer,
                      genre integer, year integer, rating integer)
    )");
 
const auto AUTHORS_SQL =  QLatin1String(R"(
    create table authors(id integer primary key, name varchar, birthdate date)
    )");
 
const auto GENRES_SQL = QLatin1String(R"(
    create table genres(id integer primary key, name varchar)
    )");
 
const auto INSERT_AUTHOR_SQL = QLatin1String(R"(
    insert into authors(name, birthdate) values(?, ?)
    )");
 
const auto INSERT_BOOK_SQL = QLatin1String(R"(
    insert into books(title, year, author, genre, rating)
                      values(?, ?, ?, ?, ?)
    )");
 
const auto INSERT_GENRE_SQL = QLatin1String(R"(
    insert into genres(name) values(?)
    )");
 
QSqlError initDb()
QSqlError initDb()
{
{
Line 72: Line 190:


     QSqlQuery q;
     QSqlQuery q;
     if (!q.exec(QLatin1String("create table books(id integer primary key, title varchar, author integer, genre integer, year integer, rating integer)")))
     if (!q.exec(BOOKS_SQL))
         return q.lastError();
         return q.lastError();
     if (!q.exec(QLatin1String("create table authors(id integer primary key, name varchar, birthdate date)")))
     if (!q.exec(AUTHORS_SQL))
         return q.lastError();
         return q.lastError();
     if (!q.exec(QLatin1String("create table genres(id integer primary key, name varchar)")))
     if (!q.exec(GENRES_SQL))
         return q.lastError();
         return q.lastError();


     if (!q.prepare(QLatin1String("insert into authors(name, birthdate) values(?, ?)")))
     if (!q.prepare(INSERT_AUTHOR_SQL))
         return q.lastError();
         return q.lastError();
     QVariant asimovId = addAuthor(q, QLatin1String("Isaac Asimov"), QDate(1920, 2, 1));
     QVariant asimovId = addAuthor(q, QLatin1String("Isaac Asimov"), QDate(1920, 2, 1));
Line 85: Line 203:
     QVariant pratchettId = addAuthor(q, QLatin1String("Terry Pratchett"), QDate(1948, 4, 28));
     QVariant pratchettId = addAuthor(q, QLatin1String("Terry Pratchett"), QDate(1948, 4, 28));


     if (!q.prepare(QLatin1String("insert into genres(name) values(?)")))
     if (!q.prepare(INSERT_GENRE_SQL))
         return q.lastError();
         return q.lastError();
     QVariant sfiction = addGenre(q, QLatin1String("Science Fiction"));
     QVariant sfiction = addGenre(q, QLatin1String("Science Fiction"));
Line 91: Line 209:
     QVariant fantasy = addGenre(q, QLatin1String("Fantasy"));
     QVariant fantasy = addGenre(q, QLatin1String("Fantasy"));


     if (!q.prepare(QLatin1String("insert into books(title, year, author, genre, rating) values(?, ?, ?, ?, ?)")))
     if (!q.prepare(INSERT_BOOK_SQL))
         return q.lastError();
         return q.lastError();
     addBook(q, QLatin1String("Foundation"), 1951, asimovId, sfiction, 3);
     addBook(q, QLatin1String("Foundation"), 1951, asimovId, sfiction, 3);
Line 110: Line 228:
}
}
</syntaxhighlight>
</syntaxhighlight>
|<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
BOOKS_SQL = """
    create table books(id integer primary key, title varchar, author integer,
                      genre integer, year integer, rating integer)
    """
AUTHORS_SQL = """
    create table authors(id integer primary key, name varchar, birthdate date)
    """
GENRES_SQL = """
    create table genres(id integer primary key, name varchar)
    """
INSERT_AUTHOR_SQL = """
    insert into authors(name, birthdate) values(?, ?)
    """
INSERT_GENRE_SQL = """
    insert into genres(name) values(?)
    """
INSERT_BOOK_SQL = """
    insert into books(title, year, author, genre, rating)
                values(?, ?, ?, ?, ?)
    """
 
def init_db():
def init_db():
    def check(func, *args):
        if not func(*args):
            raise ValueError(func.__self__.lastError())
     db = QSqlDatabase.addDatabase("QSQLITE")
     db = QSqlDatabase.addDatabase("QSQLITE")
     db.setDatabaseName(":memory:")
     db.setDatabaseName(":memory:")


     if not db.open():
     check(db.open)
        return db.lastError()
 
    tables = db.tables()
    for table in tables:
        if table == "books" and table == "authors":
            return QSqlError()


     q = QSqlQuery()
     q = QSqlQuery()
     if not q.exec_("create table books(id integer primary key, title varchar, author integer, "
     check(q.exec_,BOOKS_SQL)
            "genre integer, year integer, rating integer)"):
    check(q.exec_,AUTHORS_SQL)
        return q.lastError()
    check(q.exec_,GENRES_SQL)
    if not q.exec_("create table authors(id integer primary key, name varchar, birthdate date)"):
    check(q.prepare,INSERT_AUTHOR_SQL)
        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))
     asimovId = add_author(q, "Isaac Asimov", datetime(1920, 2, 1))
     greeneId = add_author(q, "Graham Greene", datetime(1904, 10, 2))
     greeneId = add_author(q, "Graham Greene", datetime(1904, 10, 2))
     pratchettId = add_author(q, "Terry Pratchett", datetime(1948, 4, 28))
     pratchettId = add_author(q, "Terry Pratchett", datetime(1948, 4, 28))


     if not q.prepare("insert into genres(name) values(?)"):
     check(q.prepare,INSERT_GENRE_SQL)
        return q.lastError()
     sfiction = add_genre(q, "Science Fiction")
     sfiction = add_genre(q, "Science Fiction")
     fiction = add_genre(q, "Fiction")
     fiction = add_genre(q, "Fiction")
     fantasy = add_genre(q, "Fantasy")
     fantasy = add_genre(q, "Fantasy")


     if not q.prepare("insert into books(title, year, author, genre, rating) "
     check(q.prepare,INSERT_BOOK_SQL)
            "values(?, ?, ?, ?, ?)"):
        return q.lastError()
     add_book(q, "Foundation", 1951, asimovId, sfiction, 3)
     add_book(q, "Foundation", 1951, asimovId, sfiction, 3)
     add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4)
     add_book(q, "Foundation and Empire", 1952, asimovId, sfiction, 4)
Line 160: Line 288:
     add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3)
     add_book(q, "Night Watch", 2002, pratchettId, fantasy, 3)
     add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3)
     add_book(q, "Going Postal", 2004, pratchettId, fantasy, 3)
</syntaxhighlight>
|}
Notice that the python version uses the  '''check''' function to execute the SQL statements instead of the '''if...else''' block like in the C++ version. Although both are valid approaches, the earlier one produces code that looks cleaner and shorter.
=== bookdelegate.cpp -> bookdelegate.py ===
Now that the database is in place, port the C++ code for the BookDelegate class. This 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 <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">bookdelegate.py</span> and add the following imports to it:
<syntaxhighlight lang="python">
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
</syntaxhighlight>
Once the necessary imports are added, define the BookDelegate class and port its constructor code to Python. Here is how the C++ and Python versions look like:
{|
! style="margin-top: 0px;" | C++
! Python
|-
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
BookDelegate::BookDelegate(QObject *parent)
    : QSqlRelationalDelegate(parent), star(QPixmap(":images/star.png"))
{
}
</syntaxhighlight>
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
class BookDelegate(QSqlRelationalDelegate):
    """Books delegate to rate the books"""


     return QSqlError()
     def __init__(self, parent=None):
        QSqlRelationalDelegate.__init__(self, parent)
        self.star = QPixmap(":/images/star.png")
</syntaxhighlight>
</syntaxhighlight>
|}
|}


Notice that the Python version of the ''initDb'' function does not use anything specific to Qt for setting up the database.
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 looks like:
 
{|
! style="margin-top: 0px;" | C++
! Python
|-
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
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;


* The ''init_db'' calls the ''addAuthor'', ''addGenre'', and ''addBook'' functions to append data to the tables. Let's try porting these functions to Python.
        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;
}
</syntaxhighlight>
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
    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
</syntaxhighlight>
|}
 
=== bookwindow.cpp -> bookwindow.py ===
 
After the bookdelegate, port the C++ code for the BookWindow class. It offers a QMainWindow, containing a QTableView to present the books data, and a ''Details'' section with a set of input fields to edit the selected row in the table. To begin with, create the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">bookwindow.py</span> and add the following imports to it:
 
<syntaxhighlight lang="python">
from PySide2.QtWidgets import (QAction, QAbstractItemView, qApp, QDataWidgetMapper,
    QHeaderView, QMainWindow, QMessageBox)
from PySide2.QtGui import QKeySequence
from PySide2.QtSql import QSqlRelation, QSqlRelationalTableModel, QSqlTableModel
from PySide2.QtCore import QAbstractItemModel, QObject, QSize, Qt
import createdb
from ui_bookwindow import Ui_BookWindow
from bookdelegate import BookDelegate
</syntaxhighlight>
 
Notice that one of the statements imports the BookDelegate we ported earlier and the Ui_BookWindow. The <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">ui_bookwindow</span> is the Python Code generated by the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">pysde2-uic</span> tool based on the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">bookwindow.ui</span> file that exists. To generate this file, run the following command on the prompt:
 
<syntaxhighlight lang="Bash shell scripts">
pyside2-uic bookwindow.ui > ui_bookwindow.py
</syntaxhighlight>
 
Try porting the remaining code now. Here is how the C++ and Python versions of the BookWindow constructor code looks like:
 
{|
! style="vertical-align: top;text-align: center;" | C++
! Python
|-
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO;text-align: top;">
BookWindow::BookWindow()
{
    ui.setupUi(this);
 
    if (!QSqlDatabase::drivers().contains("QSQLITE"))
        QMessageBox::critical(
                    this,
                    "Unable to load database",
                    "This demo needs the SQLITE driver"
                    );
 
    // Initialize the database:
    QSqlError err = initDb();
    if (err.type() != QSqlError::NoError) {
        showError(err);
        return;
    }
 
    // Create the data model:
    model = new QSqlRelationalTableModel(ui.bookTable);
    model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    model->setTable("books");
 
    // Remember the indexes of the columns:
    authorIdx = model->fieldIndex("author");
    genreIdx = model->fieldIndex("genre");
 
    // Set the relations to the other database tables:
    model->setRelation(authorIdx, QSqlRelation("authors", "id", "name"));
    model->setRelation(genreIdx, QSqlRelation("genres", "id", "name"));
 
    // Set the localized header captions:
    model->setHeaderData(authorIdx, Qt::Horizontal, tr("Author Name"));
    model->setHeaderData(genreIdx, Qt::Horizontal, tr("Genre"));
    model->setHeaderData(model->fieldIndex("title"),
                        Qt::Horizontal, tr("Title"));
    model->setHeaderData(model->fieldIndex("year"), Qt::Horizontal, tr("Year"));
    model->setHeaderData(model->fieldIndex("rating"),
                        Qt::Horizontal, tr("Rating"));
 
    // Populate the model:
    if (!model->select()) {
        showError(model->lastError());
        return;
    }
 
    // Set the model and hide the ID column:
    ui.bookTable->setModel(model);
    ui.bookTable->setItemDelegate(new BookDelegate(ui.bookTable));
    ui.bookTable->setColumnHidden(model->fieldIndex("id"), true);
    ui.bookTable->setSelectionMode(QAbstractItemView::SingleSelection);
 
    // Initialize the Author combo box:
    ui.authorEdit->setModel(model->relationModel(authorIdx));
    ui.authorEdit->setModelColumn(
                model->relationModel(authorIdx)->fieldIndex("name"));
 
    ui.genreEdit->setModel(model->relationModel(genreIdx));
    ui.genreEdit->setModelColumn(
                model->relationModel(genreIdx)->fieldIndex("name"));
 
    // Lock and prohibit resizing of the width of the rating column:
    ui.bookTable->horizontalHeader()->setSectionResizeMode(
                model->fieldIndex("rating"),
                QHeaderView::ResizeToContents);
 
    QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
    mapper->setModel(model);
    mapper->setItemDelegate(new BookDelegate(this));
    mapper->addMapping(ui.titleEdit, model->fieldIndex("title"));
    mapper->addMapping(ui.yearEdit, model->fieldIndex("year"));
    mapper->addMapping(ui.authorEdit, authorIdx);
    mapper->addMapping(ui.genreEdit, genreIdx);
    mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating"));
 
    connect(ui.bookTable->selectionModel(),
            &QItemSelectionModel::currentRowChanged,
            mapper,
            &QDataWidgetMapper::setCurrentModelIndex
            );
 
    ui.bookTable->setCurrentIndex(model->index(0, 0));
    createMenuBar();
}
</syntaxhighlight>
| style="vertical-align: top;text-align: left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO;">
class BookWindow(QMainWindow, Ui_BookWindow):
    """A window to show the books available"""
 
    def __init__(self):
        QMainWindow.__init__(self)
        self.setupUi(self)
 
        #check for SQL errors
        err = createdb.init_db()
        if not err.type() == QSqlError.NoError:
            showError(err)
            return
 
        model = QSqlRelationalTableModel(self.bookTable)
        model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        model.setTable("books")
 
        # Remember the indexes of the columns:
        author_idx = model.fieldIndex("author")
        genre_idx = model.fieldIndex("genre")
 
        # Set the relations to the other database tables:
        model.setRelation(author_idx, QSqlRelation("authors", "id", "name"))
        model.setRelation(genre_idx, QSqlRelation("genres", "id", "name"))
 
        # Set the localized header captions:
        model.setHeaderData(author_idx, Qt.Horizontal, self.tr("Author Name"))
        model.setHeaderData(genre_idx, Qt.Horizontal, self.tr("Genre"))
        model.setHeaderData(model.fieldIndex("title"), Qt.Horizontal, self.tr("Title"))
        model.setHeaderData(model.fieldIndex("year"), Qt.Horizontal, self.tr("Year"))
        model.setHeaderData(model.fieldIndex("rating"), Qt.Horizontal, self.tr("Rating"))
 
        if not model.select():
            print(model.lastError())
 
        # Set the model and hide the ID column:
        self.bookTable.setModel(model)
        self.bookTable.setItemDelegate(BookDelegate(self.bookTable))
        self.bookTable.setColumnHidden(model.fieldIndex("id"), True)
        self.bookTable.setSelectionMode(QAbstractItemView.SingleSelection)
 
        # Initialize the Author combo box:
        self.authorEdit.setModel(model.relationModel(author_idx))
        self.authorEdit.setModelColumn(model.relationModel(author_idx).fieldIndex("name"))
 
        self.genreEdit.setModel(model.relationModel(genre_idx))
        self.genreEdit.setModelColumn(model.relationModel(genre_idx).fieldIndex("name"))
 
        # Lock and prohibit resizing of the width of the rating column:
        self.bookTable.horizontalHeader().setSectionResizeMode(model.fieldIndex("rating"),
            QHeaderView.ResizeToContents)
 
        mapper = QDataWidgetMapper(self)
        mapper.setModel(model)
        mapper.setItemDelegate(BookDelegate(self))
        mapper.addMapping(self.titleEdit, model.fieldIndex("title"))
        mapper.addMapping(self.yearEdit, model.fieldIndex("year"))
        mapper.addMapping(self.authorEdit, author_idx)
        mapper.addMapping(self.genreEdit, genre_idx)
        mapper.addMapping(self.ratingEdit, model.fieldIndex("rating"))
 
        selection_model = self.bookTable.selectionModel()
        selection_model.currentRowChanged.connect(mapper.setCurrentModelIndex)
 
        self.bookTable.setCurrentIndex(model.index(0, 0))
        self.create_menubar()
</syntaxhighlight>
|}
 
Notice that the Python version of the ''BookWindow'' class definition inherits from both QMainWindow and Ui_BookWindow, which is defined in the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">ui_bookwindow.py</span> file that you generated earlier.
 
Here is how the rest of the code looks:


{|
{|
! style="margin-top: 0px;"| C++
! style="margin-top: 0px;" | C++
! Python
! Python
|-
|-
|<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
void addBook(QSqlQuery &q, const QString &title, int year, const QVariant &authorId,
void BookWindow::showError(const QSqlError &err)
            const QVariant &genreId, int rating)
{
{
     q.addBindValue(title);
     QMessageBox::critical(this, "Unable to initialize Database",
    q.addBindValue(year);
                "Error initializing database: " + err.text());
    q.addBindValue(authorId);
    q.addBindValue(genreId);
    q.addBindValue(rating);
    q.exec();
}
}


QVariant addGenre(QSqlQuery &q, const QString &name)
void BookWindow::createMenuBar()
{
{
     q.addBindValue(name);
     QAction *quitAction = new QAction(tr("&Quit"), this);
     q.exec();
    QAction *aboutAction = new QAction(tr("&About"), this);
     return q.lastInsertId();
    QAction *aboutQtAction = new QAction(tr("&About Qt"), this);
 
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(quitAction);
 
    QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
    helpMenu->addAction(aboutAction);
    helpMenu->addAction(aboutQtAction);
 
    connect(quitAction, &QAction::triggered, this, &BookWindow::close);
     connect(aboutAction, &QAction::triggered, this, &BookWindow::about);
     connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt);
}
}


QVariant addAuthor(QSqlQuery &q, const QString &name, const QDate &birthdate)
void BookWindow::about()
{
{
     q.addBindValue(name);
     QMessageBox::about(this, tr("About Books"),
    q.addBindValue(birthdate);
            tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
    q.exec();
              "with a model/view framework."));
    return q.lastInsertId();
}
}
</syntaxhighlight>
</syntaxhighlight>
|<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO">
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO;text-align: top">
def add_book(q, title, year, authorId, genreId, rating):
    def showError(err):
    q.addBindValue(title)
        QMessageBox.critical(self, "Unable to initialize Database",
    q.addBindValue(year)
                    "Error initializing database: " + err.text())
    q.addBindValue(authorId)
 
    q.addBindValue(genreId)
    def create_menubar(self):
     q.addBindValue(rating)
        file_menu = self.menuBar().addMenu(self.tr("&File"))
    q.exec_()
        quit_action = file_menu.addAction(self.tr("&Quit"))
        quit_action.triggered.connect(qApp.quit)
 
        help_menu = self.menuBar().addMenu(self.tr("&Help"))
        about_action = help_menu.addAction(self.tr("&About"))
        about_action.setShortcut(QKeySequence.HelpContents)
        about_action.triggered.connect(self.about)
        aboutQt_action = help_menu.addAction("&About Qt")
        aboutQt_action.triggered.connect(qApp.aboutQt)
 
     def about(self):
        QMessageBox.about(self, self.tr("About Books"),
            self.tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
                "with a model/view framework."))
</syntaxhighlight>
|}
 
=== main.cpp -> main.py ===
 
Now that you have ported all the necessary pieces to Python, try to put them together in <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">main.py</span>, which is the entry point for the application. Here is how the C++ and Python variants of the ''main'' looks like:
 
{|
! style="margin-top: 0px;" | C++
! Python
|-
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="c++" style="font-size: 12px;font-family: DROID SANS MONO">
#include "bookwindow.h"


#include <QtWidgets>


def add_genre(q, name):
int main(int argc, char * argv[])
     q.addBindValue(name)
{
    q.exec_()
     Q_INIT_RESOURCE(books);
     return q.lastInsertId()
 
     QApplication app(argc, argv);


    BookWindow win;
    win.show();


def add_author(q, name, birthdate):
    return app.exec();
    q.addBindValue(name)
}
    q.addBindValue(birthdate)
</syntaxhighlight>
    q.exec_()
| style="vertical-align:top;text-align:left;" |<syntaxhighlight lang="python" style="font-size: 12px;font-family: DROID SANS MONO;text-align: top">
     return q.lastInsertId()
import sys
from PySide2.QtWidgets import QApplication
from bookwindow import BookWindow
import rc_books
 
if __name__ == "__main__":
     app = QApplication([])


    window = BookWindow()
    window.resize(800, 600)
    window.show()


    sys.exit(app.exec_())
</syntaxhighlight>
</syntaxhighlight>
|}
|}


=== bookdelegate.cpp -> bookdelegate.py ===
Notice that the Python version imports ''BookWindow'' that you ported earlier and ''rc_books'', which is generated by the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">pyside2-rcc</span> tool based on the <span style="font-family: monospace;background-color: #cecfd5;display: inline; border-radius: 4px;padding: 3px;">books.qrc</span>. To generate this Python file, run the following command on the prompt:
 
<syntaxhighlight lang="Bash shell scripts">
pyside2-rcc -o rc_books.py books.qrc
</syntaxhighlight>
 
=== Run the application ===
 
The final step is to run the application to check if it looks like this:


Now that the database is in place, try porting the C++ code for the BookDelegate class. The class offers a delegate to present the data in a QTableView. It inherits QSqlRelationalDelegate interface, which offers functionality that is specific to SQL database interaction.
[[File:Books.png]]

Latest revision as of 14:26, 21 June 2019

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 offers binding for Qt C++ API, 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), you can start. But, you need a C++-based Qt application to port. Pick a Qt example that has the .ui XML file, defining the application's UI. That way, you 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 you start, familiarize yourself with the following 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, Python uses import statements to access packages and classes. Here 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.

  • Main function: 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 that you looked at earlier, the entry point is defined by the following line:
    if __name__ == "__main__":
       #...
    
    Note: This is just a good practice when writing an application, and in Python is just a formality when executing the current script directly. The variables declared inside the if-statement are considered as global variables.
  • Qt Application: Qt provides classes that are meant to manage the application-specific requirements depending on whether the application is a console-only (QCoreApplication), GUI with QtWidgets (QApplication), or GUI without QtWidgets (QGuiApplication). These classes load necessary plugins, such as the GUI libraries required by an application.
  • Qt Properties: Q_PROPERTY macros are used in Qt to add a public member variable with getter and setter function. Python alternative for this is the @property decorator before the getter and setter function definitions.
  • Qt Signals and Slots: Qt offers a unique 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 connect the signal to any function. To understand this better, look at this improved version of the Hello World example:
    class MyWidget(QWidget):
        def __init__(self):
            super().__init__()
    
            self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"]
    
            self.button = QPushButton("Click me!")
            self.text = QLabel("Hello World")
            self.text.setAlignment(Qt.AlignCenter)
    
            self.layout = QVBoxLayout()
            self.layout.addWidget(self.text)
            self.layout.addWidget(self.button)
            self.setLayout(self.layout)
    
            self.button.clicked.connect(self.magic)
    
        @Slot()
        def magic(self):
            self.text.setText(random.choice(self.hello))
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication([])
    
        widget = MyWidget()
        widget.resize(800, 600)
        widget.show()
    
        sys.exit(app.exec_())
    

Notice that the QPushButton's clicked signal is connected to the magic function to randomly change the QLabel text. You should always use the @Slot decorator just before the function definition if that function is a Slot, this is required to register all the members of your code to the QtMetaObject.

  • C++ uses the keyword, this, to refer to the current object. In a Python context, it is self that offers something similar.
  • Qt coding style recommends using camelCase for the variable and function names, whereas the Python style uses under_score.
  • And more importantly, forget about {}, and ;.

Porting tutorial

To understand this better, try to port an existing Qt C++ application to Python. The books SQL example seems ideal for this, as you could avoid writing UI-specific code in Python and use its .ui file instead.

initDb.h -> createdb.py

To begin with, 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.

The init_db function relies on the addAuthor, addGenre, and addBook functions to append data to the tables. Port these functions first.

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()

Now, look at at the code for the initDb C++ method and port it to an equivalent function in Python:

C++ Python
const auto BOOKS_SQL = QLatin1String(R"(
    create table books(id integer primary key, title varchar, author integer,
                       genre integer, year integer, rating integer)
    )");

const auto AUTHORS_SQL =  QLatin1String(R"(
    create table authors(id integer primary key, name varchar, birthdate date)
    )");

const auto GENRES_SQL = QLatin1String(R"(
    create table genres(id integer primary key, name varchar)
    )");

const auto INSERT_AUTHOR_SQL = QLatin1String(R"(
    insert into authors(name, birthdate) values(?, ?)
    )");

const auto INSERT_BOOK_SQL = QLatin1String(R"(
    insert into books(title, year, author, genre, rating)
                      values(?, ?, ?, ?, ?)
    )");

const auto INSERT_GENRE_SQL = QLatin1String(R"(
    insert into genres(name) values(?)
    )");

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(BOOKS_SQL))
        return q.lastError();
    if (!q.exec(AUTHORS_SQL))
        return q.lastError();
    if (!q.exec(GENRES_SQL))
        return q.lastError();

    if (!q.prepare(INSERT_AUTHOR_SQL))
        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(INSERT_GENRE_SQL))
        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(INSERT_BOOK_SQL))
        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();
}
BOOKS_SQL = """
    create table books(id integer primary key, title varchar, author integer,
                       genre integer, year integer, rating integer)
    """
AUTHORS_SQL = """
    create table authors(id integer primary key, name varchar, birthdate date)
    """
GENRES_SQL = """
    create table genres(id integer primary key, name varchar)
    """
INSERT_AUTHOR_SQL = """
    insert into authors(name, birthdate) values(?, ?)
    """
INSERT_GENRE_SQL = """
    insert into genres(name) values(?)
    """
INSERT_BOOK_SQL = """
    insert into books(title, year, author, genre, rating)
                values(?, ?, ?, ?, ?)
    """

def init_db():
    def check(func, *args):
        if not func(*args):
            raise ValueError(func.__self__.lastError())
    db = QSqlDatabase.addDatabase("QSQLITE")
    db.setDatabaseName(":memory:")

    check(db.open)

    q = QSqlQuery()
    check(q.exec_,BOOKS_SQL)
    check(q.exec_,AUTHORS_SQL)
    check(q.exec_,GENRES_SQL)
    check(q.prepare,INSERT_AUTHOR_SQL)

    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))

    check(q.prepare,INSERT_GENRE_SQL)
    sfiction = add_genre(q, "Science Fiction")
    fiction = add_genre(q, "Fiction")
    fantasy = add_genre(q, "Fantasy")

    check(q.prepare,INSERT_BOOK_SQL)
    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)

Notice that the python version uses the check function to execute the SQL statements instead of the if...else block like in the C++ version. Although both are valid approaches, the earlier one produces code that looks cleaner and shorter.

bookdelegate.cpp -> bookdelegate.py

Now that the database is in place, port the C++ code for the BookDelegate class. This 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 versions 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 looks 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

bookwindow.cpp -> bookwindow.py

After the bookdelegate, port the C++ code for the BookWindow class. It offers a QMainWindow, containing a QTableView to present the books data, and a Details section with a set of input fields to edit the selected row in the table. To begin with, create the bookwindow.py and add the following imports to it:

from PySide2.QtWidgets import (QAction, QAbstractItemView, qApp, QDataWidgetMapper,
    QHeaderView, QMainWindow, QMessageBox)
from PySide2.QtGui import QKeySequence
from PySide2.QtSql import QSqlRelation, QSqlRelationalTableModel, QSqlTableModel
from PySide2.QtCore import QAbstractItemModel, QObject, QSize, Qt
import createdb
from ui_bookwindow import Ui_BookWindow
from bookdelegate import BookDelegate

Notice that one of the statements imports the BookDelegate we ported earlier and the Ui_BookWindow. The ui_bookwindow is the Python Code generated by the pysde2-uic tool based on the bookwindow.ui file that exists. To generate this file, run the following command on the prompt:

pyside2-uic bookwindow.ui > ui_bookwindow.py

Try porting the remaining code now. Here is how the C++ and Python versions of the BookWindow constructor code looks like:

C++ Python
BookWindow::BookWindow()
{
    ui.setupUi(this);

    if (!QSqlDatabase::drivers().contains("QSQLITE"))
        QMessageBox::critical(
                    this,
                    "Unable to load database",
                    "This demo needs the SQLITE driver"
                    );

    // Initialize the database:
    QSqlError err = initDb();
    if (err.type() != QSqlError::NoError) {
        showError(err);
        return;
    }

    // Create the data model:
    model = new QSqlRelationalTableModel(ui.bookTable);
    model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    model->setTable("books");

    // Remember the indexes of the columns:
    authorIdx = model->fieldIndex("author");
    genreIdx = model->fieldIndex("genre");

    // Set the relations to the other database tables:
    model->setRelation(authorIdx, QSqlRelation("authors", "id", "name"));
    model->setRelation(genreIdx, QSqlRelation("genres", "id", "name"));

    // Set the localized header captions:
    model->setHeaderData(authorIdx, Qt::Horizontal, tr("Author Name"));
    model->setHeaderData(genreIdx, Qt::Horizontal, tr("Genre"));
    model->setHeaderData(model->fieldIndex("title"),
                         Qt::Horizontal, tr("Title"));
    model->setHeaderData(model->fieldIndex("year"), Qt::Horizontal, tr("Year"));
    model->setHeaderData(model->fieldIndex("rating"),
                         Qt::Horizontal, tr("Rating"));

    // Populate the model:
    if (!model->select()) {
        showError(model->lastError());
        return;
    }

    // Set the model and hide the ID column:
    ui.bookTable->setModel(model);
    ui.bookTable->setItemDelegate(new BookDelegate(ui.bookTable));
    ui.bookTable->setColumnHidden(model->fieldIndex("id"), true);
    ui.bookTable->setSelectionMode(QAbstractItemView::SingleSelection);

    // Initialize the Author combo box:
    ui.authorEdit->setModel(model->relationModel(authorIdx));
    ui.authorEdit->setModelColumn(
                model->relationModel(authorIdx)->fieldIndex("name"));

    ui.genreEdit->setModel(model->relationModel(genreIdx));
    ui.genreEdit->setModelColumn(
                model->relationModel(genreIdx)->fieldIndex("name"));

    // Lock and prohibit resizing of the width of the rating column:
    ui.bookTable->horizontalHeader()->setSectionResizeMode(
                model->fieldIndex("rating"),
                QHeaderView::ResizeToContents);

    QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
    mapper->setModel(model);
    mapper->setItemDelegate(new BookDelegate(this));
    mapper->addMapping(ui.titleEdit, model->fieldIndex("title"));
    mapper->addMapping(ui.yearEdit, model->fieldIndex("year"));
    mapper->addMapping(ui.authorEdit, authorIdx);
    mapper->addMapping(ui.genreEdit, genreIdx);
    mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating"));

    connect(ui.bookTable->selectionModel(),
            &QItemSelectionModel::currentRowChanged,
            mapper,
            &QDataWidgetMapper::setCurrentModelIndex
            );

    ui.bookTable->setCurrentIndex(model->index(0, 0));
    createMenuBar();
}
class BookWindow(QMainWindow, Ui_BookWindow):
    """A window to show the books available"""

    def __init__(self):
        QMainWindow.__init__(self)
        self.setupUi(self)

        #check for SQL errors 
        err = createdb.init_db()
        if not err.type() == QSqlError.NoError:
            showError(err)
            return

        model = QSqlRelationalTableModel(self.bookTable)
        model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        model.setTable("books")

        # Remember the indexes of the columns:
        author_idx = model.fieldIndex("author")
        genre_idx = model.fieldIndex("genre")

        # Set the relations to the other database tables:
        model.setRelation(author_idx, QSqlRelation("authors", "id", "name"))
        model.setRelation(genre_idx, QSqlRelation("genres", "id", "name"))

        # Set the localized header captions:
        model.setHeaderData(author_idx, Qt.Horizontal, self.tr("Author Name"))
        model.setHeaderData(genre_idx, Qt.Horizontal, self.tr("Genre"))
        model.setHeaderData(model.fieldIndex("title"), Qt.Horizontal, self.tr("Title"))
        model.setHeaderData(model.fieldIndex("year"), Qt.Horizontal, self.tr("Year"))
        model.setHeaderData(model.fieldIndex("rating"), Qt.Horizontal, self.tr("Rating"))

        if not model.select():
            print(model.lastError())

        # Set the model and hide the ID column:
        self.bookTable.setModel(model)
        self.bookTable.setItemDelegate(BookDelegate(self.bookTable))
        self.bookTable.setColumnHidden(model.fieldIndex("id"), True)
        self.bookTable.setSelectionMode(QAbstractItemView.SingleSelection)

        # Initialize the Author combo box:
        self.authorEdit.setModel(model.relationModel(author_idx))
        self.authorEdit.setModelColumn(model.relationModel(author_idx).fieldIndex("name"))

        self.genreEdit.setModel(model.relationModel(genre_idx))
        self.genreEdit.setModelColumn(model.relationModel(genre_idx).fieldIndex("name"))

        # Lock and prohibit resizing of the width of the rating column:
        self.bookTable.horizontalHeader().setSectionResizeMode(model.fieldIndex("rating"),
            QHeaderView.ResizeToContents)

        mapper = QDataWidgetMapper(self)
        mapper.setModel(model)
        mapper.setItemDelegate(BookDelegate(self))
        mapper.addMapping(self.titleEdit, model.fieldIndex("title"))
        mapper.addMapping(self.yearEdit, model.fieldIndex("year"))
        mapper.addMapping(self.authorEdit, author_idx)
        mapper.addMapping(self.genreEdit, genre_idx)
        mapper.addMapping(self.ratingEdit, model.fieldIndex("rating"))

        selection_model = self.bookTable.selectionModel()
        selection_model.currentRowChanged.connect(mapper.setCurrentModelIndex)

        self.bookTable.setCurrentIndex(model.index(0, 0))
        self.create_menubar()

Notice that the Python version of the BookWindow class definition inherits from both QMainWindow and Ui_BookWindow, which is defined in the ui_bookwindow.py file that you generated earlier.

Here is how the rest of the code looks:

C++ Python
void BookWindow::showError(const QSqlError &err)
{
    QMessageBox::critical(this, "Unable to initialize Database",
                "Error initializing database: " + err.text());
}

void BookWindow::createMenuBar()
{
    QAction *quitAction = new QAction(tr("&Quit"), this);
    QAction *aboutAction = new QAction(tr("&About"), this);
    QAction *aboutQtAction = new QAction(tr("&About Qt"), this);

    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(quitAction);

    QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
    helpMenu->addAction(aboutAction);
    helpMenu->addAction(aboutQtAction);

    connect(quitAction, &QAction::triggered, this, &BookWindow::close);
    connect(aboutAction, &QAction::triggered, this, &BookWindow::about);
    connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt);
}

void BookWindow::about()
{
    QMessageBox::about(this, tr("About Books"),
            tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
               "with a model/view framework."));
}
    def showError(err):
        QMessageBox.critical(self, "Unable to initialize Database",
                    "Error initializing database: " + err.text())

    def create_menubar(self):
        file_menu = self.menuBar().addMenu(self.tr("&File"))
        quit_action = file_menu.addAction(self.tr("&Quit"))
        quit_action.triggered.connect(qApp.quit)

        help_menu = self.menuBar().addMenu(self.tr("&Help"))
        about_action = help_menu.addAction(self.tr("&About"))
        about_action.setShortcut(QKeySequence.HelpContents)
        about_action.triggered.connect(self.about)
        aboutQt_action = help_menu.addAction("&About Qt")
        aboutQt_action.triggered.connect(qApp.aboutQt)

    def about(self):
        QMessageBox.about(self, self.tr("About Books"),
            self.tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
                "with a model/view framework."))

main.cpp -> main.py

Now that you have ported all the necessary pieces to Python, try to put them together in main.py, which is the entry point for the application. Here is how the C++ and Python variants of the main looks like:

C++ Python
#include "bookwindow.h"

#include <QtWidgets>

int main(int argc, char * argv[])
{
    Q_INIT_RESOURCE(books);

    QApplication app(argc, argv);

    BookWindow win;
    win.show();

    return app.exec();
}
import sys
from PySide2.QtWidgets import QApplication
from bookwindow import BookWindow
import rc_books

if __name__ == "__main__":
    app = QApplication([])

    window = BookWindow()
    window.resize(800, 600)
    window.show()

    sys.exit(app.exec_())

Notice that the Python version imports BookWindow that you ported earlier and rc_books, which is generated by the pyside2-rcc tool based on the books.qrc. To generate this Python file, run the following command on the prompt:

pyside2-rcc -o rc_books.py books.qrc

Run the application

The final step is to run the application to check if it looks like this:

Books.png