1
votes

this is the code I use to fill a table drawn in QT Designer. Designed to be universal for any table, it works fine, but... When I try to show a datasat containing 18 columns and ~12000 rows, it just freezes for 30 seconds or more. So, what I am doing wrong and is there way to speed up, keeping the code still suitable for any table?

That's my code:

...blablabla...

self.connect(self, SIGNAL("set"), self.real_set)

...blablabla...

def set_table(self, table, data):
    self.emit(SIGNAL('set'), table, data)

def real_set(self, table, data):
    """
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Assuming data is list of dict and table is a QTableWidget.

    Get first key and get len of contents
    """
    for key in data:
        rows = len(data[key])
        table.setRowCount(rows)
        break

    """
    Forbid resizing(speeds up)
    """
    table.horizontalHeader().setResizeMode(QHeaderView.Fixed)
    table.verticalHeader().setResizeMode(QHeaderView.Fixed)
    table.horizontalHeader().setStretchLastSection(False)
    table.verticalHeader().setStretchLastSection(False)

    """
    Set number of columns too
    """
    table.setColumnCount(len(data))
    table.setHorizontalHeaderLabels(sorted(data.keys()))

    """
    Now fill data
    """
    for n, key in enumerate(sorted(data.keys())):
        for m, item in enumerate(data[key]):
            newitem = QTableWidgetItem(item)
            table.setItem(m, n, newitem)
2
Do you mean to say QTableWidget in the title? Because you seem to be using QTableWidgetItem in the code... Also, a more complete code would be good, because now there is no idea what the types of table or item are, for example. - hyde
You can't add all 12000 items at once, because that blocks the event loop for too long time. The technique of the pagination solution given in that one answer is way to go, but instead of pagination by user actions, you could just have a timer with interval 0, and add some suitable number of items (universal solution is to time it at runtime, and stop adding items for that timeout when you have spent, say, 100 ms adding them. and then continue when the timer timeout fires next time). That way Qt event loop can keep running, and GUI works. - hyde
@pmus. An 18x12000 table is quite small. It shouldn't take anywhere near 30 seconds to fill that. In my own tests, a 20x15000 table only takes 3-4 seconds to fill. You need to provide a complete example that shows the performance problem, so other people can try to reproduce it. - ekhumoro
@hyde. My tests used a randomized data-set, and the results include a sortByColumn after the table has been populated. There must be some other code that the OP is not showing that is the cause of the slow down. - ekhumoro
@pmus. You can get much better performance if you use a QTableView with a custom model which pulls the values directly from your data structure. The main bottleneck is creating all those instances of QTableWidgetItem. In my tests, a 20x15000 table only takes a fraction of a second to fill when using a custom model. - ekhumoro

2 Answers

6
votes

Here a test script which compares a few ways of populating a table.

The custom model is much faster, because it does not have to create all the items up front - but note that it is a very basic implementation, so does not implement sorting, editing, etc. (See Model/View Programming for more details).

from random import shuffle
from PyQt4 import QtCore, QtGui

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data, parent=None):
        super(TableModel, self).__init__(parent)
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._data[0]) if self.rowCount() else 0

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            if 0 <= row < self.rowCount():
                column = index.column()
                if 0 <= column < self.columnCount():
                    return self._data[row][column]

class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.table = QtGui.QTableView(self)
        self.tablewidget = QtGui.QTableWidget(self)
        self.tablewidget.setSortingEnabled(True)
        self.button1 = QtGui.QPushButton('Custom Model', self)
        self.button1.clicked.connect(
            lambda: self.populateTable('custom'))
        self.button2 = QtGui.QPushButton('StandardItem Model', self)
        self.button2.clicked.connect(
            lambda: self.populateTable('standard'))
        self.button3 = QtGui.QPushButton('TableWidget', self)
        self.button3.clicked.connect(
            lambda: self.populateTable('widget'))
        self.spinbox = QtGui.QSpinBox(self)
        self.spinbox.setRange(15000, 1000000)
        self.spinbox.setSingleStep(10000)
        layout = QtGui.QGridLayout(self)
        layout.addWidget(self.table, 0, 0, 1, 4)
        layout.addWidget(self.tablewidget, 1, 0, 1, 4)
        layout.addWidget(self.button1, 2, 0)
        layout.addWidget(self.button2, 2, 1)
        layout.addWidget(self.button3, 2, 2)
        layout.addWidget(self.spinbox, 2, 3)
        self._data = []

    def populateTable(self, mode):
        if mode == 'widget':
            self.tablewidget.clear()
            self.tablewidget.setRowCount(self.spinbox.value())
            self.tablewidget.setColumnCount(20)
        else:
            model = self.table.model()
            if model is not None:
                self.table.setModel(None)
                model.deleteLater()
        if len(self._data) != self.spinbox.value():
            del self._data[:]
            rows = list(range(self.spinbox.value()))
            shuffle(rows)
            for row in rows:
                items = []
                for column in range(20):
                    items.append('(%d, %d)' % (row, column))
                self._data.append(items)
        timer = QtCore.QElapsedTimer()
        timer.start()
        if mode == 'widget':
            self.tablewidget.setSortingEnabled(False)
            for row, items in enumerate(self._data):
                for column, text in enumerate(items):
                    item = QtGui.QTableWidgetItem(text)
                    self.tablewidget.setItem(row, column, item)
            self.tablewidget.sortByColumn(0, QtCore.Qt.AscendingOrder)
        else:
            self.table.setSortingEnabled(False)
            if mode == 'custom':
                model = TableModel(self._data, self.table)
            elif mode == 'standard':
                model = QtGui.QStandardItemModel(self.table)
                for row in self._data:
                    items = []
                    for column in row:
                        items.append(QtGui.QStandardItem(column))
                    model.appendRow(items)
            self.table.setModel(model)
            self.table.setSortingEnabled(True)
            self.table.sortByColumn(0, QtCore.Qt.AscendingOrder)
        print('%s: %.3g seconds' % (mode, timer.elapsed() / 1000))

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 50, 1200, 800)
    window.show()
    sys.exit(app.exec_())
4
votes

In GUI applications one comes across a situation where there is a need to display a lot of items in a tabular or list format (for example displaying large number of rows in a table). One way to increase the GUI responsiveness is to load a few items when the screen is displayed and defer loading of rest of the items based on user action. Qt provides a solution to address this requirement of loading the data on demand.

You can find the implementation of this technique called pagination in this link