Chapter 2: bookdelegate.cpp to bookdelegate.py

Now that your 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 features specific for handling relational databases, such as a combobox editor for foreign key fields. To begin with, create bookdelegate.py and add the following imports to it:

1
2import copy
3import os
4from pathlib import Path
5
6from PySide6.QtSql import QSqlRelationalDelegate
7from PySide6.QtWidgets import (QItemDelegate, QSpinBox, QStyledItemDelegate,
8    QStyle, QStyleOptionViewItem)

After the necessary import statements, port the constructor code for the BookDelegate class. Both the C++ and Python versions of this code initialize a QSqlRelationalDelegate and QPixmap instance. Here is how they look:

C++ version

1BookDelegate::BookDelegate(QObject *parent)
2    : QSqlRelationalDelegate(parent), star(QPixmap(":images/star.png"))
3{
4}

Python version

1    QStyle, QStyleOptionViewItem)
2from PySide6.QtGui import QMouseEvent, QPixmap, QPalette, QImage
3from PySide6.QtCore import QEvent, QSize, Qt, QUrl
4
5
6class BookDelegate(QSqlRelationalDelegate):
7    """Books delegate to rate the books"""
8

Note

The Python version loads the QPixmap using the absolute path of star.png in the local filesystem.

As the default functionality offered by the QSqlRelationalDelegate is not enough to present the books data, you must reimplement a few functions. For example, painting stars to represent the rating for each book in the table. Here is how the reimplemented code looks like:

C++ version

 1void BookDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
 2                           const QModelIndex &index) const
 3{
 4    if (index.column() != 5) {
 5        QStyleOptionViewItem opt = option;
 6        // Since we draw the grid ourselves:
 7        opt.rect.adjust(0, 0, -1, -1);
 8        QSqlRelationalDelegate::paint(painter, opt, index);
 9    } else {
10        const QAbstractItemModel *model = index.model();
11        QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ?
12            (option.state & QStyle::State_Active) ?
13                        QPalette::Normal :
14                        QPalette::Inactive :
15                        QPalette::Disabled;
16
17        if (option.state & QStyle::State_Selected)
18            painter->fillRect(
19                        option.rect,
20                        option.palette.color(cg, QPalette::Highlight));
21
22        int rating = model->data(index, Qt::DisplayRole).toInt();
23        int width = star.width();
24        int height = star.height();
25        int x = option.rect.x();
26        int y = option.rect.y() + (option.rect.height() / 2) - (height / 2);
27        for (int i = 0; i < rating; ++i) {
28            painter->drawPixmap(x, y, star);
29            x += width;
30        }
31        // Since we draw the grid ourselves:
32        drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1));
33    }
34
35    QPen pen = painter->pen();
36    painter->setPen(option.palette.color(QPalette::Mid));
37    painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
38    painter->drawLine(option.rect.topRight(), option.rect.bottomRight());
39    painter->setPen(pen);
40}
41
42QSize BookDelegate::sizeHint(const QStyleOptionViewItem &option,
43                                 const QModelIndex &index) const
44{
45    if (index.column() == 5)
46        return QSize(5 * star.width(), star.height()) + QSize(1, 1);
47    // Since we draw the grid ourselves:
48    return QSqlRelationalDelegate::sizeHint(option, index) + QSize(1, 1);
49}
50
51bool BookDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
52                               const QStyleOptionViewItem &option,
53                               const QModelIndex &index)
54{
55    if (index.column() != 5)
56        return QSqlRelationalDelegate::editorEvent(event, model, option, index);
57
58    if (event->type() == QEvent::MouseButtonPress) {
59        QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
60        int stars = qBound(0, int(0.7 + qreal(mouseEvent->pos().x()
61            - option.rect.x()) / star.width()), 5);
62        model->setData(index, QVariant(stars));
63        // So that the selection can change:
64        return false;
65    }
66
67    return true;
68}
69
70QWidget *BookDelegate::createEditor(QWidget *parent,
71                                    const QStyleOptionViewItem &option,
72                                    const QModelIndex &index) const
73{
74    if (index.column() != 4)
75        return QSqlRelationalDelegate::createEditor(parent, option, index);
76
77    // For editing the year, return a spinbox with a range from -1000 to 2100.
78    QSpinBox *sb = new QSpinBox(parent);
79    sb->setFrame(false);
80    sb->setMaximum(2100);
81    sb->setMinimum(-1000);
82
83    return sb;
84}

Python version

 1    def __init__(self, parent=None):
 2        QSqlRelationalDelegate.__init__(self, parent)
 3        star_png = Path(__file__).parent / "images" / "star.png"
 4        self.star = QPixmap(star_png)
 5
 6    def paint(self, painter, option, index):
 7        """ Paint the items in the table.
 8
 9            If the item referred to by <index> is a StarRating, we
10            handle the painting ourselves. For the other items, we
11            let the base class handle the painting as usual.
12
13            In a polished application, we'd use a better check than
14            the column number to find out if we needed to paint the
15            stars, but it works for the purposes of this example.
16        """
17        if index.column() != 5:
18            # Since we draw the grid ourselves:
19            opt = copy.copy(option)
20            opt.rect = option.rect.adjusted(0, 0, -1, -1)
21            QSqlRelationalDelegate.paint(self, painter, opt, index)
22        else:
23            model = index.model()
24            if option.state & QStyle.State_Enabled:
25                if option.state & QStyle.State_Active:
26                    color_group = QPalette.Normal
27                else:
28                    color_group = QPalette.Inactive
29            else:
30                color_group = QPalette.Disabled
31
32            if option.state & QStyle.State_Selected:
33                painter.fillRect(option.rect,
34                    option.palette.color(color_group, QPalette.Highlight))
35            rating = model.data(index, Qt.DisplayRole)
36            width = self.star.width()
37            height = self.star.height()
38            x = option.rect.x()
39            y = option.rect.y() + (option.rect.height() / 2) - (height / 2)
40            for i in range(rating):
41                painter.drawPixmap(x, y, self.star)
42                x += width
43
44            # Since we draw the grid ourselves:
45            self.drawFocus(painter, option, option.rect.adjusted(0, 0, -1, -1))
46
47        pen = painter.pen()
48        painter.setPen(option.palette.color(QPalette.Mid))
49        painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight())
50        painter.drawLine(option.rect.topRight(), option.rect.bottomRight())
51        painter.setPen(pen)
52
53    def sizeHint(self, option, index):
54        """ Returns the size needed to display the item in a QSize object. """
55        if index.column() == 5:
56            size_hint = QSize(5 * self.star.width(), self.star.height()) + QSize(1, 1)
57            return size_hint
58        # Since we draw the grid ourselves:
59        return QSqlRelationalDelegate.sizeHint(self, option, index) + QSize(1, 1)
60
61    def editorEvent(self, event, model, option, index):
62        if index.column() != 5:
63            return False
64
65        if event.type() == QEvent.MouseButtonPress:
66            mouse_pos = event.pos()
67            new_stars = int(0.7 + (mouse_pos.x() - option.rect.x()) / self.star.width())
68            stars = max(0, min(new_stars, 5))
69            model.setData(index, stars)
70            # So that the selection can change
71            return False
72
73        return True
74
75    def createEditor(self, parent, option, index):
76        if index.column() != 4:
77            return QSqlRelationalDelegate.createEditor(self, parent, option, index)
78
79        # For editing the year, return a spinbox with a range from -1000 to 2100.
80        spinbox = QSpinBox(parent)
81        spinbox.setFrame(False)
82        spinbox.setMaximum(2100)
83        spinbox.setMinimum(-1000)
84        return spinbox

Now that the delegate is in place, run the following main.py to see how the data is presented:

 1
 2import sys
 3
 4from PySide6.QtCore import Qt
 5from PySide6.QtSql import QSqlQueryModel
 6from PySide6.QtWidgets import QTableView, QApplication
 7
 8import createdb
 9from bookdelegate import BookDelegate
10
11if __name__ == "__main__":
12    app = QApplication()
13    createdb.init_db()
14
15    model = QSqlQueryModel()
16    model.setQuery("select title, author, genre, year, rating from books")
17
18    table = QTableView()
19    table.setModel(model)
20    table.setItemDelegate(BookDelegate())
21    table.resize(800, 600)
22    table.show()
23
24    sys.exit(app.exec())

Here is how the application will look when you run it:

Books table data

The only difference you’ll notice now in comparison to chapter 1 is that the rating column looks different.

Try improving the table even further by adding these features:

  • Title for each column

  • SQL relation for the author_id and genre_id columns

  • Set a title to the window

With these features, this is how your table will look like:

Books table with SQL relation