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

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
andgenre_id
columnsSet a title to the window
With these features, this is how your table will look like:
