пятница, 14 октября 2011 г.

Очистка значений внешних ключей в модели

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

Сначала рассмотрим более сложную задачу - очистка поля внешнего ключа модели из пользовательского интерфейса. Для редактирования внешних ключей я использую QComboBox, связанный посредством QDataWidgetMapper и QSqlRelationalDelegate c QSqlRelationalTableModel. К сожалению, родной комбобокс не умеет очищать значение, поэтому придется его научить. Пусть он очищается по нажатию кнопки Delete. Очевидно, что комбобокс очистится, если установить его текущий индекс в -1. Напишем виджет:

class ResettableComboBox(QComboBox):

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Delete:
            self.setCurrentIndex(-1)
        else:
            QComboBox.keyPressEvent(self, event)

и в дальнейшем будем использовать именно его.

К сожалению, это работает совсем не так, как ожидалось. Комбобокс действительно благополучно очищается, но при потере фокуса в модель пустое значение не попадает, восстанавливается предыдущее. В чем же дело?

Расследование показало, что дело в делегате и великом классе QVariant.

Здесь необходимо небольшое лирическое отступление, посвященное некоторым особенностям QVariant. Как известно, это контейнер, способный содержать значения разных типов, в том числе и пустое значение (NULL). Так вот великим изобретением разработчиков Qt являются типизированные пустые значения. Без осознания данного факта решить задачу не получилось бы. Проиллюстрирую:

>>> from PyQt4.QtCore import QVariant
>>> QVariant().isNull()
True
>>> QVariant().isValid()
False
>>> QVariant(QVariant.Int).isNull()
True
>>> QVariant(QVariant.Int).isValid()
True
>>> QVariant(QVariant.String) == QVariant(QVariant.Int)
False
Как видно из примера, конструктор без параметров возвращает невалидное значение, а конструкторы с указанием типа - валидные NULL, причем не равные друг другу. Никогда бы не подумал, что отсутствие значение может иметь тип :)

Вернемся к нашим баранам. QSqlRelationalDelegate поступает просто: по текущему индексу комбобокса достает ключ связанной модели и вставляет полученное значение в поле основной модели. По индексу строки -1 в связанной модели нет значения и метод data возвращает невалидный QVariant(). Основная модель в свою очередь не принимает невалидное значение, ей подавай валидный NULL (QVariant(Type)). Вот и получается, что модель сохраняет старое значение.

Победить ситуацию можно, переписав метод QSqlRelationalDelegatе.setModelData. Нужно просто проверить: если из связанной модели пришло невалидное значение, просто подменить его пустым значением соответствующего типа:

class NullRelationalDelegate(QSqlRelationalDelegate):

    def setModelData(self, widget, model, index):
        if not index.isValid():
            return

        childModel = None
        if isinstance(model, QSqlRelationalTableModel):
            childModel = model.relationModel(index.column())
        if not childModel or not isinstance(widget, QComboBox):
            QItemDelegate.setModelData(self, widget, model, index)
            return

        currentItem = widget.currentIndex()
        childColIndex = childModel.fieldIndex(model.relation(index.column()).displayColumn())
        childEditIndex = childModel.fieldIndex(model.relation(index.column()).indexColumn())

        displayData = childModel.data(childModel.index(currentItem, childColIndex), Qt.DisplayRole)
        displayData = displayData if displayData.isValid() else QVariant(QVariant.String)

        editData = childModel.data(childModel.index(currentItem, childEditIndex), Qt.EditRole)
        editData = editData if editData.isValid() else QVariant(QVariant.Int)

        model.setData(index, displayData, Qt.DisplayRole)
        model.setData(index, editData, Qt.EditRole)

Из кода поместить пустое значение в поле внешнего ключа модели (как и в любое другое) еще проще. Методу setData нужно передавать не QVariant(), а QVariant(Type) соответствующего типа.

Комментариев нет:

Отправить комментарий