Chapter 3: Port bookdwindow.cpp to 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:

 1from __future__ import annotations
 2
 3from PySide6.QtGui import QAction
 4from PySide6.QtWidgets import (QAbstractItemView, QDataWidgetMapper,
 5    QHeaderView, QMainWindow, QMessageBox)
 6from PySide6.QtGui import QKeySequence
 7from PySide6.QtSql import (QSqlRelation, QSqlRelationalTableModel, QSqlTableModel,
 8    QSqlError)
 9from PySide6.QtCore import QAbstractItemModel, QObject, QSize, Qt, Slot
10import createdb
11from ui_bookwindow import Ui_BookWindow
12from bookdelegate import BookDelegate
13
14

Note

The imports include the BookDelegate you ported earlier and the Ui_BookWindow. The pyside-uic tool generates the ui_bookwindow Python code based on the bookwindow.ui XML file.

To generate this Python code, run the following command on the prompt:

pyside6-uic bookwindow.ui -o ui_bookwindow.py

Try porting the remaining code now. To begin with, here is how both the versions of the constructor code looks:

C++ version

 1    // Initialize the database:
 2    QSqlError err = initDb();
 3    if (err.type() != QSqlError::NoError) {
 4        showError(err);
 5        return;
 6    }
 7
 8    // Create the data model:
 9    model = new QSqlRelationalTableModel(ui.bookTable);
10    model->setEditStrategy(QSqlTableModel::OnManualSubmit);
11    model->setTable("books");
12
13    // Remember the indexes of the columns:
14    authorIdx = model->fieldIndex("author");
15    genreIdx = model->fieldIndex("genre");
16
17    // Set the relations to the other database tables:
18    model->setRelation(authorIdx, QSqlRelation("authors", "id", "name"));
19    model->setRelation(genreIdx, QSqlRelation("genres", "id", "name"));
20
21    // Set the localized header captions:
22    model->setHeaderData(authorIdx, Qt::Horizontal, tr("Author Name"));
23    model->setHeaderData(genreIdx, Qt::Horizontal, tr("Genre"));
24    model->setHeaderData(model->fieldIndex("title"),
25                         Qt::Horizontal, tr("Title"));
26    model->setHeaderData(model->fieldIndex("year"), Qt::Horizontal, tr("Year"));
27    model->setHeaderData(model->fieldIndex("rating"),
28                         Qt::Horizontal, tr("Rating"));
29
30    // Populate the model:
31    if (!model->select()) {
32        showError(model->lastError());
33        return;
34    }
35
36    // Set the model and hide the ID column:
37    ui.bookTable->setModel(model);
38    ui.bookTable->setItemDelegate(new BookDelegate(ui.bookTable));
39    ui.bookTable->setColumnHidden(model->fieldIndex("id"), true);
40    ui.bookTable->setSelectionMode(QAbstractItemView::SingleSelection);
41
42    // Initialize the Author combo box:
43    ui.authorEdit->setModel(model->relationModel(authorIdx));
44    ui.authorEdit->setModelColumn(
45                model->relationModel(authorIdx)->fieldIndex("name"));
46
47    ui.genreEdit->setModel(model->relationModel(genreIdx));
48    ui.genreEdit->setModelColumn(
49                model->relationModel(genreIdx)->fieldIndex("name"));
50
51    // Lock and prohibit resizing of the width of the rating column:
52    ui.bookTable->horizontalHeader()->setSectionResizeMode(
53                model->fieldIndex("rating"),
54                QHeaderView::ResizeToContents);
55
56    QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
57    mapper->setModel(model);
58    mapper->setItemDelegate(new BookDelegate(this));
59    mapper->addMapping(ui.titleEdit, model->fieldIndex("title"));
60    mapper->addMapping(ui.yearEdit, model->fieldIndex("year"));
61    mapper->addMapping(ui.authorEdit, authorIdx);
62    mapper->addMapping(ui.genreEdit, genreIdx);
63    mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating"));
64
65    connect(ui.bookTable->selectionModel(),
66            &QItemSelectionModel::currentRowChanged,
67            mapper,
68            &QDataWidgetMapper::setCurrentModelIndex
69            );
70
71    ui.bookTable->setCurrentIndex(model->index(0, 0));
72    createMenuBar();
73}
74
75void BookWindow::showError(const QSqlError &err)
76{
77    QMessageBox::critical(this, "Unable to initialize Database",
78                "Error initializing database: " + err.text());
79}
80
81void BookWindow::createMenuBar()
82{
83    QAction *quitAction = new QAction(tr("&Quit"), this);

Python version

 1
 2class BookWindow(QMainWindow, Ui_BookWindow):
 3    # """A window to show the books available"""
 4
 5    def __init__(self):
 6        QMainWindow.__init__(self)
 7        self.setupUi(self)
 8
 9        #Initialize db
10        createdb.init_db()
11
12        model = QSqlRelationalTableModel(self.bookTable)
13        model.setEditStrategy(QSqlTableModel.OnManualSubmit)
14        model.setTable("books")
15
16        # Remember the indexes of the columns:
17        author_idx = model.fieldIndex("author")
18        genre_idx = model.fieldIndex("genre")
19
20        # Set the relations to the other database tables:
21        model.setRelation(author_idx, QSqlRelation("authors", "id", "name"))
22        model.setRelation(genre_idx, QSqlRelation("genres", "id", "name"))
23
24        # Set the localized header captions:
25        model.setHeaderData(author_idx, Qt.Horizontal, self.tr("Author Name"))
26        model.setHeaderData(genre_idx, Qt.Horizontal, self.tr("Genre"))
27        model.setHeaderData(model.fieldIndex("title"), Qt.Horizontal, self.tr("Title"))
28        model.setHeaderData(model.fieldIndex("year"), Qt.Horizontal, self.tr("Year"))
29        model.setHeaderData(model.fieldIndex("rating"), Qt.Horizontal, self.tr("Rating"))
30
31        if not model.select():
32            print(model.lastError())
33
34        # Set the model and hide the ID column:
35        self.bookTable.setModel(model)
36        self.bookTable.setItemDelegate(BookDelegate(self.bookTable))
37        self.bookTable.setColumnHidden(model.fieldIndex("id"), True)
38        self.bookTable.setSelectionMode(QAbstractItemView.SingleSelection)
39
40        # Initialize the Author combo box:
41        self.authorEdit.setModel(model.relationModel(author_idx))
42        self.authorEdit.setModelColumn(model.relationModel(author_idx).fieldIndex("name"))
43
44        self.genreEdit.setModel(model.relationModel(genre_idx))
45        self.genreEdit.setModelColumn(model.relationModel(genre_idx).fieldIndex("name"))
46
47        # Lock and prohibit resizing of the width of the rating column:
48        self.bookTable.horizontalHeader().setSectionResizeMode(model.fieldIndex("rating"),
49            QHeaderView.ResizeToContents)
50
51        mapper = QDataWidgetMapper(self)
52        mapper.setModel(model)
53        mapper.setItemDelegate(BookDelegate(self))
54        mapper.addMapping(self.titleEdit, model.fieldIndex("title"))
55        mapper.addMapping(self.yearEdit, model.fieldIndex("year"))
56        mapper.addMapping(self.authorEdit, author_idx)
57        mapper.addMapping(self.genreEdit, genre_idx)
58        mapper.addMapping(self.ratingEdit, model.fieldIndex("rating"))
59
60        selection_model = self.bookTable.selectionModel()
61        selection_model.currentRowChanged.connect(mapper.setCurrentModelIndex)
62
63        self.bookTable.setCurrentIndex(model.index(0, 0))
64        self.create_menubar()

Note

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 like:

C++ version

 1    mapper->setItemDelegate(new BookDelegate(this));
 2    mapper->addMapping(ui.titleEdit, model->fieldIndex("title"));
 3    mapper->addMapping(ui.yearEdit, model->fieldIndex("year"));
 4    mapper->addMapping(ui.authorEdit, authorIdx);
 5    mapper->addMapping(ui.genreEdit, genreIdx);
 6    mapper->addMapping(ui.ratingEdit, model->fieldIndex("rating"));
 7
 8    connect(ui.bookTable->selectionModel(),
 9            &QItemSelectionModel::currentRowChanged,
10            mapper,
11            &QDataWidgetMapper::setCurrentModelIndex
12            );
13
14    ui.bookTable->setCurrentIndex(model->index(0, 0));
15    createMenuBar();
16}
17
18void BookWindow::showError(const QSqlError &err)
19{
20    QMessageBox::critical(this, "Unable to initialize Database",
21                "Error initializing database: " + err.text());
22}
23
24void BookWindow::createMenuBar()
25{
26    QAction *quitAction = new QAction(tr("&Quit"), this);
27    QAction *aboutAction = new QAction(tr("&About"), this);
28    QAction *aboutQtAction = new QAction(tr("&About Qt"), this);
29
30    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
31    fileMenu->addAction(quitAction);
32
33    QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
34    helpMenu->addAction(aboutAction);
35    helpMenu->addAction(aboutQtAction);
36
37    connect(quitAction, &QAction::triggered, this, &BookWindow::close);
38    connect(aboutAction, &QAction::triggered, this, &BookWindow::about);
39    connect(aboutQtAction, &QAction::triggered, qApp, &QApplication::aboutQt);
40}
41
42void BookWindow::about()
43{
44    QMessageBox::about(this, tr("About Books"),
45            tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
46               "with a model/view framework."));
47}

Python version

 1
 2    def showError(err):
 3        QMessageBox.critical(self, "Unable to initialize Database",
 4                    "Error initializing database: " + err.text())
 5
 6    def create_menubar(self):
 7        file_menu = self.menuBar().addMenu(self.tr("&File"))
 8        quit_action = file_menu.addAction(self.tr("&Quit"))
 9        quit_action.triggered.connect(qApp.quit)
10
11        help_menu = self.menuBar().addMenu(self.tr("&Help"))
12        about_action = help_menu.addAction(self.tr("&About"))
13        about_action.setShortcut(QKeySequence.HelpContents)
14        about_action.triggered.connect(self.about)
15        aboutQt_action = help_menu.addAction("&About Qt")
16        aboutQt_action.triggered.connect(qApp.aboutQt)
17
18    def about(self):
19        QMessageBox.about(self, self.tr("About Books"),
20            self.tr("<p>The <b>Books</b> example shows how to use Qt SQL classes "
21                "with a model/view framework."))

Now that all the necessary pieces are in place, try to put them together in main.py.

 1from __future__ import annotations
 2
 3import sys
 4from PySide6.QtWidgets import QApplication
 5from bookwindow import BookWindow
 6import rc_books
 7
 8if __name__ == "__main__":
 9    app = QApplication([])
10
11    window = BookWindow()
12    window.resize(800, 600)
13    window.show()
14
15    sys.exit(app.exec())

Try running this to see if you get the following output:

BookWindow with a QTableView and a few input fields

Now, if you look back at chapter2, you’ll notice that the bookdelegate.py loads the star.png from the filesytem. Instead, you could add it to a qrc file, and load from it. The later approach is rececommended if your application is targeted for different platforms, as most of the popular platforms employ stricter file access policy these days.

To add the star.png to a .qrc, create a file called books.qrc and the following XML content to it:

1<!DOCTYPE RCC><RCC version="1.0">
2<qresource>
3  <file>images/star.png</file>
4</qresource>
5</RCC>

This is a simple XML file defining a list all resources that your application needs. In this case, it is the star.png image only.

Now, run the pyside6-rcc tool on the books.qrc file to generate rc_books.py.

pyside6-rcc books.qrc -o rc_books.py

Once you have the Python script generated, make the following changes to bookdelegate.py and main.py:

--- /data/snapshot-pyside-6.8/tqtc-pyside-setup/build/testenv-tqtc_6.8/build/pyside6/doc/base/tutorials/portingguide/chapter2/bookdelegate.py
+++ /data/snapshot-pyside-6.8/tqtc-pyside-setup/build/testenv-tqtc_6.8/build/pyside6/doc/base/tutorials/portingguide/chapter3/bookdelegate.py
@@ -2,24 +2,19 @@
 # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 from __future__ import annotations
 
-import copy
-import os
-from pathlib import Path
-
+import copy, os
 from PySide6.QtSql import QSqlRelationalDelegate
 from PySide6.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate,
     QStyle, QStyleOptionViewItem)
 from PySide6.QtGui import QMouseEvent, QPixmap, QPalette, QImage
 from PySide6.QtCore import QEvent, QSize, Qt, QUrl
 
-
 class BookDelegate(QSqlRelationalDelegate):
     """Books delegate to rate the books"""
 
-    def __init__(self, parent=None):
+    def __init__(self, star_png, parent=None):
         QSqlRelationalDelegate.__init__(self, parent)
-        star_png = Path(__file__).parent / "images" / "star.png"
-        self.star = QPixmap(star_png)
+        self.star = QPixmap(":/images/star.png")
 
     def paint(self, painter, option, index):
         """ Paint the items in the table.
--- /data/snapshot-pyside-6.8/tqtc-pyside-setup/build/testenv-tqtc_6.8/build/pyside6/doc/base/tutorials/portingguide/chapter3/main-old.py
+++ /data/snapshot-pyside-6.8/tqtc-pyside-setup/build/testenv-tqtc_6.8/build/pyside6/doc/base/tutorials/portingguide/chapter3/main.py
@@ -5,6 +5,7 @@
 import sys
 from PySide6.QtWidgets import QApplication
 from bookwindow import BookWindow
+import rc_books
 
 if __name__ == "__main__":
     app = QApplication([])

Although there will be no noticeable difference in the UI after these changes, using a .qrc is a better approach.

Now that you have successfully ported the SQL Books example, you know how easy it is. Try porting another C++ application.