2

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.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
fredrik
  • 9,631
  • 16
  • 72
  • 132
  • the QAbstractTableModel class should not know the proxy, just the purpose of the proxy is not to modify the base model but to use the proxy as an intermediary. I think you do not understand the use of the proxy. – eyllanesc Feb 01 '18 at 13:03
  • You could explain what is your goal in using the proxy and your table. I think you have a design problem instead of an access problem. – eyllanesc Feb 01 '18 at 13:06
  • My goal is to be able to select multiple cells, enter a new value and have that value get populated to all the selected cells. I was unable to achieve that without having the view (to detect the "multiselection") and the proxymodel (to map cells appropriately, in case they were sorted/filtered). @eyllanesc I realize that the data model should not know about the view or proxy, hence my question. – fredrik Feb 01 '18 at 13:59
  • that task you do not have to do in the code of the model. How do you enter the data? Do you have a form?, you have to create a function where you get the selected cells and use the function setData(), you should not overwrite the setData() method. You have a design problem, if you want us to help you, you must provide a [mcve] of your background problem. I stress, you should not modify the setData method to perform that task. – eyllanesc Feb 01 '18 at 14:08
  • @eyllanesc ok, I updated the code. If you run that, you can select multiple cells, enter a new value and hit enter. The value is populated onto each cell which you had selected, and the underlying `table_data` list is updated. Sorting and filtering is maintained. This is the behavior I need. But I don't want to pass the view and the proxy model to my table model... – fredrik Feb 01 '18 at 15:12

1 Answers1

1

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)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Wow, I wish I could give you more than just one upvote. One last question, in the `on_data_changed` method, how can I access the model's `table_data`? I wish to read it so I can pass it on to the rest of my program once editing of the table is done. – fredrik Feb 01 '18 at 17:35
  • In general you should not directly access table_data, but if you want to do it you should execute the following: `model.sourceModel().table_data` – eyllanesc Feb 01 '18 at 17:37
  • I figured it out. I renamed `Widget.model` to self.model and was able to access it from within `on_data_changed` by calling `self.model.table_data`. Thanks again so much! – fredrik Feb 01 '18 at 17:37
  • Ah, `model.sourceModel().table_data` is better. What do you mean with "should not directly access"? Imagine e.g. a "submit" button below this table, so that the table's contents can be submitted into some other function. This is what I want to do. – fredrik Feb 01 '18 at 17:39
  • The appropriate thing is to create a method of the model that returns a copy because if you directly modify the table_data the model will not realize it. – eyllanesc Feb 01 '18 at 17:41
  • Okay, you mean, basically creating method: `def getTableData(self): return self.table_data` in the model class? – fredrik Feb 01 '18 at 17:43
  • Something similar to this, remember that in Python when you return a list by a function and modify that list the original list is also modified, for more information read the following: https://stackoverflow.com/questions/2612802/how-to-clone-or-copy-a-list – eyllanesc Feb 01 '18 at 17:49
  • I noticed an issue. #1. Run your code. #2. Click the second column "2" to sort it. #3. Click "five" and drag down to "two" and release (selecting the column's values). #4 Enter a new value and hit enter. Only one of the column's values was edited. Do you see this on your end too? https://imgur.com/a/KcHFU – fredrik Feb 08 '18 at 10:13
  • @fredrik okay, I understand the problem, I think I already have a solution to correct that problem, in a few hours I will update my answer. – eyllanesc Feb 08 '18 at 10:16
  • what was the solution you were thinking of? – fredrik Feb 09 '18 at 08:27
  • I had forgotten, when I leave a meeting I will publish the answer, apologize. – eyllanesc Feb 09 '18 at 09:06
  • @fredrik I already update my answer, that behavior only occurs in PySide2, not in PyQt5. PySide2 has several similar problems with proxy classes – eyllanesc Feb 12 '18 at 07:36