1

I have subclassed QAbstractTableModel (using PyQt4), including the setData method. My data is held in a nested list, with column headers ("col_headers") and row headers ("row_headers) stored in lists. Row and col headers are not unique in case that is relevant.

Here's the init:

def __init__(self):
    self.arraydata = [[]]
    self.col_headers = [] # Columns
    self.row_headers = [] # Rows

I have implemented setData as follows (with some debugging print statements):

def setData(self, index, value, role=QtCore.Qt.EditRole):
    row = index.row()
    column = index.column()
    print('row {} col {}'.format(row, col) # This gives the expected values
    print('data before: {}'.format(self.arraydata) # The before state
    self.arraydata[row][column] = value
    print('data after: {}'.format(self.arraydata) # The after state = the problem
    self.dataChanged.emit(index, index)

This usually works when I first start running it, and the table populates as expected, one cell at a time.

However, as more setData calls come through, instead of only updating the particular item, it often (but not always) updates the value for the same column for most or all of the other rows. This problem is made worse (or triggered if it hasn't already happened) if my view of the model (a QTableView) is scrolled or resized, or the window is resized.

So my question is, how do I setData on my model so that only one item in the model is changed? And is a list of lists the correct way for me to be holding the data? Finally, could the problem be caused by too many emits in quick succession to the view, with the subsequent callbacks "under the hood" by the view to the model messing with the indexing?

Full (simplified) code:

from PyQt4 import QtCore, QtGui

ROWS = 0
COLS = 0

class MyTableModel(QtCore.QAbstractTableModel):

    def updateData(self, row_header, col_header, value):
        ix_to_chg = []
        for i, row_h in enumerate(self.row_headers):
            for j, col_h in enumerate(self.col_headers):
                if row_h == row_header and col_h == col_header:
                    ix_to_chg.append((i, j))
        for i, j in ix_to_chg:
            index=self.index(i, j)
            self.setData(index=index, value=value, role=QtCore.Qt.EditRole)

    def insertRows(self, position, count, new_row_headers, parent=QtCore.QModelIndex()):
        self.beginInsertRows(QtCore.QModelIndex(), position, position + count -1)
        empty_row = ['' for x in range(self.columnCount())]
        for i in range(count):
            self.arraydata.insert(position, empty_row)
            new_row_header = new_row_headers[::-1][i]
            self.row_headers.insert(position, new_row_header)
        self.endInsertRows()

    def insertColumns(self, position, count, new_col_headers, parent=QtCore.QModelIndex()):
        self.beginInsertColumns(QtCore.QModelIndex(), position, position + count -1)
        empty_cell = ''
        for i in range(count):
            self.arraydata[0].insert(position+i, empty_cell)
            new_col_header = new_col_headers[i]
            self.col_headers.insert(position + i, new_col_header)
        self.endInsertColumns()

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        row = index.row()
        column = index.column()
        print('row {} col {}'.format(row, column)) # This gives the expected values
        print('data before: {}'.format(self.arraydata)) # The before state
        self.arraydata[row][column] = value
        print('data after: {}'.format(self.arraydata)) # The after state = the problem
        self.dataChanged.emit(index, index)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            i = index.row()
            j = index.column()
            return '{}'.format(self.arraydata[i][j])
        else:
            return None

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                if self.columnCount() == 0:
                    return []
                else:
                    return self.col_headers[section]
            else:
                if self.rowCount() == 0:
                    return []
                else:
                    return self.row_headers[section]

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.arraydata)-1

    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.arraydata[0])

    def setupModel(self, numRows, numCols):
        for row_num in range(numRows):
            for col_num in range(numCols):
                if self.rowCount(self) < numRows:
                    for i in range(numRows - self.rowCount(self)):
                        self.arraydata.append([])
                if self.columnCount(self) < numCols:
                    for i in range(numCols - self.columnCount(self)):
                        for j in range(self.rowCount(self)):
                            item = '_'.join(['row', str(j), 'col', str(i)])
                            self.arraydata[j].append(item)

    def __init__(self, col_headers=None, row_headers=None, rows=ROWS, cols=COLS):
        QtCore.QAbstractTableModel.__init__(self)
        self.arraydata = [[]]
        col_headers = ['emptyColHeader' for i in range(cols)] if col_headers is None else col_headers
        row_headers = ['emptyRowHeader' for i in range(rows)] if row_headers is None else row_headers
        self.col_headers = col_headers
        self.row_headers = row_headers

EDIT:

Solved it (with thanks to List of lists changes reflected across sublists unexpectedly).

In insertRows() I was adding the same list in a loop to each row - they were all referring to the same list. The new code for that function:

def insertRows(self, position, count, new_row_headers, parent=QtCore.QModelIndex()):
    self.beginInsertRows(QtCore.QModelIndex(), position, position + count -1)
    empty_row = ['' for x in range(self.columnCount())]
    for i in range(count):
        self.arraydata.insert(position, empty_row.copy())
        new_row_header = new_row_headers[::-1][i]
        self.row_headers.insert(position, new_row_header)
    self.endInsertRows()
Community
  • 1
  • 1
Ben
  • 1,759
  • 1
  • 19
  • 23
  • it would be helpful if this was a runnable example that demonstrates the problem. – segFaultCoder Oct 27 '16 at 12:30
  • Agreed. Unfortunately the working code parses live data via an API. To build test cases that simulate the API is a non-trivial project. Fortunately I've solved it myself now (see my edit), a dumb error on my part that had nothing to do with pyqt and everything to do with the way lists work in Python. – Ben Oct 27 '16 at 12:43

0 Answers0