fredrik
fredrik

Reputation: 10281

Access view and proxy model from within QAbstractTableModel class?

Using QTableView and QAbstractTableModel, I wish to be able to select multiple cells in a table and make an edit affect all those selected cells. This is how I currently are doing it, which involves passing view (QTableView) and proxy_model (QSortFilterProxyModel) into the class, so that I can access them (in order to acquire the appropriate rows and columns):

import sys
from pprint import pprint

try:
    from PySide2 import QtWidgets, QtCore
except ImportError:
    from PyQt5 import QtWidgets, QtCore


class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self,
                view,
                proxy_model,
                table_data,
                parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)

        self.table_data = table_data
        self.view = view
        self.proxy_model = proxy_model

    def rowCount(self, parent):
        return len(self.table_data)

    def columnCount(self, parent):
        return len(self.table_data[0])

    def flags(self, index):
        # Original, inherited flags:
        original_flags = super(MyTableModel, self).flags(index)

        return original_flags | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            column = index.column()
            item = index.internalPointer()

            if item is not None:
                print(item)
            value = self.table_data[row][column]

            return value

        return None

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()

            selection_model = self.view.selectionModel()
            selected_indexes = selection_model.selectedIndexes()

            for selected_index in selected_indexes:

                mapped_index = self.proxy_model.mapToSource(selected_index)
                selected_row = mapped_index.row()
                selected_column = mapped_index.column()

                self.table_data[selected_row][selected_column] = value
                pprint(self.table_data)

                self.dataChanged.emit(index, selected_index)  # from, to

            return True

        return False


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    table_data = [['one', 'two', 'three'], ['four', 'five', 'six']]

    view = QtWidgets.QTableView()
    proxy_model = QtCore.QSortFilterProxyModel()
    model = MyTableModel(view=view, proxy_model=proxy_model, table_data=table_data)

    proxy_model.setSourceModel(model)
    proxy_model.setDynamicSortFilter(True)

    view.setModel(proxy_model)
    view.setSortingEnabled(True)  # requires proxy model
    view.sortByColumn(0, QtCore.Qt.AscendingOrder)
    view.horizontalHeader().setStretchLastSection(True)
    view.horizontalHeader().setSectionsMovable(True)

    view.show()
    app.exec_()

I suspect that I don't have to pass view and proxy_model into the class, and that I can access these objects in some other way. Is this possible, and if so - how?


I know my example is Python-specific, but my question is really a binding-agnostic question and so I'm also tagging my question with qt.

Upvotes: 2

Views: 1093

Answers (1)

eyllanesc
eyllanesc

Reputation: 243973

The base model should not know the view or the proxy, so you should have something similar to the following:

class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, table_data, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.table_data = table_data

    def rowCount(self, parent):
        return len(self.table_data)

    def columnCount(self, parent):
        return len(self.table_data[0])

    def flags(self, index):
        original_flags = super(MyTableModel, self).flags(index)
        return original_flags | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            column = index.column()
            item = index.internalPointer()
            if item is not None:
                print(item)
            value = self.table_data[row][column]
            return value

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            self.table_data[row][column] = value
            self.dataChanged.emit(index, index)
            return True
        return QtCore.QAbstractTableModel.setData(self, index, value, role)

The proxy has methods to pass the changes to the original model so in general it is not necessary to access the base model to make the changes but the same proxy, to have an order I implemented a widget and I used the dataChanged() method, this could cause an infinite loop so we must block other dataChanged() signals for it we use blockSignals().

class Widget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        QtWidgets.QWidget.__init__(self, *args, **kwargs)
        self.view = QtWidgets.QTableView()
        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().addWidget(self.view)

        table_data = [['one', 'two', 'three'], ['four', 'five', 'six']]
        proxy_model = QtCore.QSortFilterProxyModel()
        model = MyTableModel(table_data=table_data)
        proxy_model.setSourceModel(model)
        proxy_model.setDynamicSortFilter(True)
        self.view.setModel(proxy_model)
        proxy_model.dataChanged.connect(self.on_data_changed)

        self.view.setSortingEnabled(True)  # requires proxy model
        self.view.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.view.horizontalHeader().setStretchLastSection(True)
        self.view.horizontalHeader().setSectionsMovable(True)

    def on_data_changed(self, _from, _to):
        model = _from.model() # proxy model
        model.blockSignals(True)
        for index in self.view.selectionModel().selectedIndexes():
            model.setData(index, _from.data())
        model.blockSignals(False)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

Update:

It seems that PySide2 has a bug because it does not return the selected elements but an empty list so it does not enter the for loop, before we create that list for it we use the selectionChanged signal of the selectionModel(), this does not happen in PyQt5.:

class Widget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        [...]
        self.view.horizontalHeader().setSectionsMovable(True)

        self.view.selectionModel().selectionChanged.connect(self.on_selectionChanged)
        self.currentSelected = []

    def on_selectionChanged(self, selected, deselected):
        for ix in selected.indexes():
           if ix not in self.currentSelected:
            self.currentSelected.append(ix)
        for ix in deselected.indexes():
            if ix in self.currentSelected:
                self.currentSelected.remove(ix)

    def on_data_changed(self, _from, _to):
        model = _from.model() # proxy model
        model.blockSignals(True)
        pindexes = [QtCore.QPersistentModelIndex(ix) for ix in self.currentSelected]
        for index in pindexes:
            model.setData(index, _from.data())
        model.blockSignals(False)

Upvotes: 1

Related Questions