15

I am super new to Qt programming. I am trying to make a simple table that can have rows added by clicking a button. I can implement the table fine but can't seem to get the updated data to show on the table. I believe my problem stems from the fact that I can't seem to properly call any sort of "change data" method using the button. I've tried several different solutions online all of which have lead to 4 year old, dead-end posts. What I have so far is the basic structure, I just can't figure out how to make the table update with new data.

This is the basic view

I have set up with some test data.

In the final implementation, the table will start empty and I would like to append rows and have them displayed in the table view.

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        # create table
        self.get_table_data()
        self.table = self.createTable()

        # layout
        self.layout = QVBoxLayout()

        self.testButton = QPushButton("test")
        self.connect(self.testButton, SIGNAL("released()"), self.test)        

        self.layout.addWidget(self.testButton)
        self.layout.addWidget(self.table)
        self.setLayout(self.layout)

    def get_table_data(self):
        self.tabledata = [[1234567890,2,3,4,5],
                          [6,7,8,9,10],
                          [11,12,13,14,15],
                          [16,17,18,19,20]]

    def createTable(self):
        # create the view
        tv = QTableView()

        # set the table model
        header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']
        tablemodel = MyTableModel(self.tabledata, header, self)
        tv.setModel(tablemodel)

        # set the minimum size
        tv.setMinimumSize(400, 300)

        # hide grid
        tv.setShowGrid(False)

        # hide vertical header
        vh = tv.verticalHeader()
        vh.setVisible(False)

        # set horizontal header properties
        hh = tv.horizontalHeader()
        hh.setStretchLastSection(True)

        # set column width to fit contents
        tv.resizeColumnsToContents()

        # set row height
        tv.resizeRowsToContents()

        # enable sorting
        tv.setSortingEnabled(False)

        return tv

    def test(self):
        self.tabledata.append([1,1,1,1,1])
        self.emit(SIGNAL('dataChanged()'))
        print 'success'

class MyTableModel(QAbstractTableModel):
    def __init__(self, datain, headerdata, parent=None):
        """
        Args:
            datain: a list of lists\n
            headerdata: a list of strings
        """
        QAbstractTableModel.__init__(self, parent)
        self.arraydata = datain
        self.headerdata = headerdata

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

    def columnCount(self, parent):
        if len(self.arraydata) > 0: 
            return len(self.arraydata[0]) 
        return 0

    def data(self, index, role):
        if not index.isValid():
            return QVariant()
        elif role != Qt.DisplayRole:
            return QVariant()
        return QVariant(self.arraydata[index.row()][index.column()])

    def setData(self, index, value, role):
        pass         # not sure what to put here

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant(self.headerdata[col])
        return QVariant()

    def sort(self, Ncol, order):
        """
        Sort table by given column number.
        """
        self.emit(SIGNAL("layoutAboutToBeChanged()"))
        self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))       
        if order == Qt.DescendingOrder:
            self.arraydata.reverse()
        self.emit(SIGNAL("layoutChanged()"))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
user3439556
  • 153
  • 1
  • 1
  • 5

3 Answers3

14

When the underlying data of the model changes, the model should emit either layoutChanged or layoutAboutToBeChanged, so that view updates properly (there's also dataChanged, if you want to update a specific range of cells).

So you just need something like this:

    def test(self):
        self.tabledata.append([1,1,1,1,1])
        self.table.model().layoutChanged.emit()
        print 'success'
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Wouldn't setData() be needed as well in the model? I've looked through all the documentation and I keep seeing references to needing to have either setData() or insertRows(), etc. Problem is I'm not 100% sure how to implement the setData() method or more importantly how to call it. - EDIT: Well I just tried your edit and it worked perfectly without the setData() method so I guess it isn't needed after all! I really appreciate the help! – user3439556 Apr 01 '14 at 20:34
  • Usually you need to implement setData() when you want an editable grid, in order to update the correct value in your model. For example: `def setData(self, index, value, role = Qt.EditRole): if role == Qt.EditRole: setattr(self.arraydata[index.row()], self.columns[index.column()], value) self.dataChanged.emit(index, index, ()) return True else: return False` – TheGerm Jan 14 '15 at 21:24
  • Technically this wouldn't work, because `self.tabledata` is just a `list` and is not really connected to the QTableView. – NL23codes Apr 15 '20 at 23:05
2

QAbstractTableModel have two special methods for that ( beginInsertRows() and endInsertRows()).

You can add api-point in your custom model. For example:

    def insertGuest(self, guest):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self.guestsTableData.append(guest)
        self.endInsertRows()
Andrey Topoleov
  • 1,591
  • 15
  • 20
  • 1
    I think this is the better solution since a layoutChanged signal likely causes a redraw or at least reevaluation of the entire view. While begin/endInstertRows is a more subtle way to notify the visuals that a new row has been added and its location. If the row added is not in the current view, then it can be ignored (or just scrollbars updated/redrawn) – brookbot Mar 10 '22 at 06:00
1

I've made your table reference a class variable instead of an instance variable, so you could edit the data for the table from virtually anywhere in your code.

# First access the data of the table
self.tv_model = self.tv.model()

Secondly, I use the sort of pandas-dataframe-editing type approach. Lets say your data that you want to add is stored in a variable on its own:

# These can be whatever, but for consistency, 
# I used the data in the OP's example

new_values = [1, 1, 1, 1, 1]

There are different ways the next step can be approached, depending on whether the data is being added to the table, or updating existing values. Adding the data as a new row would be as follows.

# The headers should also be a class variable, 
# but I left it as the OP had it

header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']

# There are multiple ways of establishing what the row reference should be,
# this is just one example how to add a new row

new_row = len(self.tv_model.dataFrame.index)

for i, col in enumerate(header):
    self.tv_model.dataFrame.loc[new_row, col] = new_values[i]

Since self.tv_model is a reference to the actual data of the table, emitting the following signal will update the data, or 'commit' it to the model, so to speak.

self.tv_model.layoutChanged.emit()
NL23codes
  • 1,181
  • 1
  • 14
  • 31