Шаг 175.
Библиотека PyQt5. Работа с базами данных. Выполнение SQL-запросов и получение их результатов. Модели, связанные с данными. Использование связанных делегатов

    На этом шаге мы рассмотрим использование связанных делегатов.

    К сожалению, наше новое приложение имеет один существеннейший недостаток - как только мы решим добавить новую запись или даже исправить уже существующую, то столкнемся с тем, что все сделанные нами изменения не сохраняются. Почему?

    Дело в том, что модель QSqlRelationalTableModel не "знает", как перевести введенное нами название категории в ее идентификатор, который и хранится в поле category таблицы good. Она лишь выполняет попытку занести строковое название категории в поле целочисленного типа, что вполне ожидаемо вызывает ошибку, и запись в таблице не сохраняется.

    Исправить такое положение дел нам позволит особый делегат, называемый связанным (о делегатах рассказывалось в 156 и 157 шагах). Он способен выполнить поиск в первичной таблице нужной записи, извлечь ее идентификатор и сохранить его в поле вторичной таблицы. А, кроме того, он представляет все доступные для занесения в поле значения, взятые из первичной таблицы, в виде раскрывающегося списка - очень удобно!

    Функциональность связанного делегата реализует класс QSqlRelationalDelegate. Иерархия наследования:

  QObject - QAbstractItemDelegate - QItemDelegate - QSqlRelationalDelegate

    Использовать связанный делегат очень просто - нужно лишь создать его экземпляр, передав конструктору класса ссылку на компонент-представление (в нашем случае - таблицу), и вызвать у представления метод setItemDelegate(), setItemDelegateForColumn() или setItemDelegateForRow(), указав в нем только что созданный делегат.

    Здесь надо признаться, что в коде PyQt5 присутствует ошибка, приводящая к тому, что, если пользователь исправит значение в столбце, для которого был задан делегат QSqlRelationalDelegate, и перейдет на другую строку компонента-представления, то вместо исправленного значения будет выведен его идентификатор (как будто мы вообще не использовали упомянутый делегат). Такая ошибка проявляется не во всех случаях, но довольно часто.

    Чтобы обезопасить себя от этой ошибки, следует выполнить два действия:

  1. Задать для таблицы режим редактирования данных OnManualSubmit.
  2. Привязать к сигналу dataChanged модели обработчик, выполняющий собственно сохранение данных в базе вызовом метода submit() или submitAll() этой же модели. Проще всего задать один из этих методов, которые, как мы помним, являются слотами, в качестве обработчика сигнала прямо в выражении его привязки.

    Исходя из этого, доделаем наше складское приложение, дав пользователю возможность выбирать категории товаров из списка. Для этого нам потребуется лишь вставить в код пример из предыдущего шага три новых выражения (они выделены комментариями):

from PyQt5 import QtCore, QtWidgets, QtSql
import sys
def addRecord():
    # Вставляем пустую запись, в которую пользователь сможет
    # ввести нужные данные
    stm.insertRow(stm.rowCount())
def delRecord():
    # Удаляем запись из модели
    stm.removeRow(tv.currentIndex().row())
    # Выполняем повторное считывание данных в модель,
    # чтобы убрать пустую "мусорную" запись
    stm.select()
    
app = QtWidgets.QApplication(sys.argv)
window = QtWidgets.QWidget()
window.setWindowTitle("QSqlRelationalTableModel")
# Устанавливаем соединение с базой данных
con = QtSql.QSqlDatabase.addDatabase('QSQLITE')
con.setDatabaseName('c:\\temp\\data.sqlite')
con.open()
# Создаем модель
stm = QtSql.QSqlRelationalTableModel(parent = window)

# ===== 1-я вставка =====
stm.setEditStrategy(QtSql.QSqlTableModel.OnManualSubmit)
# ===== Конец 1-й вставки =====

stm.setTable('good')
stm.setSort(1, QtCore.Qt.AscendingOrder)
# Задаем для поля категории связь с таблицей списка категорий
stm.setRelation(3, QtSql.QSqlRelation('category', 'id', 'catname'))
stm.select()
stm.setHeaderData(1, QtCore.Qt.Horizontal, 'Название')
stm.setHeaderData(2, QtCore.Qt.Horizontal, 'Кол-во')
stm.setHeaderData(3, QtCore.Qt.Horizontal, 'Категория')

# ===== 2-я вставка =====
stm.dataChanged.connect(stm.submitAll)
# ===== Конец 2-й вставки =====

vbox = QtWidgets.QVBoxLayout()
tv = QtWidgets.QTableView()
tv.setModel(stm)

# ===== 3-я вставка =====
tv.setItemDelegateForColumn(3, QtSql.QSqlRelationalDelegate(tv))
# ===== Конец 3-й вставки =====

tv.hideColumn(0)
tv.setColumnWidth(1, 150)
tv.setColumnWidth(2, 60)
tv.setColumnWidth(3, 150)
vbox.addWidget(tv)
btnAdd = QtWidgets.QPushButton("&Добавить запись")
btnAdd.clicked.connect(addRecord)
vbox.addWidget(btnAdd)
btnDel = QtWidgets.QPushButton("&Удалить запись")
btnDel.clicked.connect(delRecord)
vbox.addWidget(btnDel)
window.setLayout(vbox)
window.resize(400, 250)
window.show()
sys.exit(app.exec_())
Архив с файлом и базой данных можно взять здесь.

    Результат работы приложения изображен на рисунке 1.


Рис.1. Окончательный вариант складского приложения, использующего связанный делегат

    Со следующего шага мы начнем рассматривать работу с графикой.




Предыдущий шаг Содержание Следующий шаг