Display data with different colunm sizes in a TableModel?

Heads up! You've already completed this tutorial.

ushu | 2021-04-28 20:15:55 UTC | #1

Hi, first thank for this book and clear examples, I learnt a lot!

I try to code a window to load data from a *txt file and preview it into a TableView (after to select, using combobox, some row and column as headers containing data on which to perform analyses).

How to display into the example of TableView from your TableModel you provide p.295 a table that has some row and column of different sizes / number of elements?

What I naively tried: 1- calculate the max number of columns in the file, then send to columnCount this max number for every column 2- and add a test if element is void, and return a string with a space or another char

But it doesn't work, and I don't understand how this Tablemodel works to succeed. I think solution is in indexing row and column but still don't understand how this class does that?

python
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (QApplication, QWidget, QFileDialog, QTextEdit, QPushButton, QLabel, QVBoxLayout)
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QDir

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
        self.maxcolumn = max([len(i) for i in self._data])
    def data(self, index, role):
        if role == Qt.TextAlignmentRole:
            return Qt.AlignCenter
        if role == Qt.DisplayRole:
            if not self._data[index.row()][index.column()]:
                print("sad!")
                return " "
            else:
                value = self._data[index.row()][index.column()]
                if isinstance(value, float):
                    return "%.2f" % value
            return value
    def rowCount(self, index):        
        return len(self._data)
    def columnCount(self, index):
        return self.maxcolumn



class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.data1 = []

        self.button = QPushButton('Upload data')
        self.button.clicked.connect(self.get_text_file)

        self.table = QtWidgets.QTableView()
        layout = QVBoxLayout()
        layout.addWidget(self.table)
        layout.addWidget(self.button)
        self.setLayout(layout)

    def get_text_file(self):
        dialog = QFileDialog()
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setFilter(QDir.Files)
        if dialog.exec_():
            file_name = dialog.selectedFiles()
            if file_name[0].endswith('.txt'):
                with open(file_name[0], 'r') as f:
                    self.data1 = f.readlines()
                    for x in range(len(self.data1)) :
                           a = self.data1[x]
                           b = a.split()   
                           self.data1[x] = b       
                    self.model = TableModel(self.data1)
                    self.table.setModel(self.model)
                    f.close()
            else:
                pass


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

ushu | 2021-04-29 10:14:56 UTC | #2

ok, I solved my problem by creating a dataview, adding empty string to row with less column than maxcolumn, abnd send this dataview to TableModel.

Here is the code above with modifications

python
    import sys, copy
    from PyQt5 import QtCore, QtGui, QtWidgets
    from PyQt5.QtWidgets import (QApplication, QWidget, QFileDialog, QTextEdit, QPushButton, QLabel, QVBoxLayout)
    from PyQt5.QtCore import Qt
    from PyQt5.QtCore import QDir

    class TableModel(QtCore.QAbstractTableModel):
        def __init__(self, data):
            super().__init__()
            self._data = data
        def data(self, index, role):
            if role == Qt.TextAlignmentRole:
                return Qt.AlignCenter
            if role == Qt.DisplayRole:
                return self._data[index.row()][index.column()]
        def rowCount(self, index):        
            return len(self._data)
        def columnCount(self, index):
            return len(self._data[0])



    class MainWindow(QWidget):
        def __init__(self):
            super().__init__()
            self.data1 = []
            self.dataview = []

            self.button = QPushButton('Upload data')
            self.button.clicked.connect(self.get_text_file)

            self.table = QtWidgets.QTableView()
            layout = QVBoxLayout()
            layout.addWidget(self.table)
            layout.addWidget(self.button)
            self.setLayout(layout)

        def get_text_file(self):
            dialog = QFileDialog()
            dialog.setFileMode(QFileDialog.AnyFile)
            dialog.setFilter(QDir.Files)
            if dialog.exec_():
                file_name = dialog.selectedFiles()
                if file_name[0].endswith('.txt'):
                    with open(file_name[0], 'r') as f:
                        self.data1 = f.readlines()
                        for x in range(len(self.data1)) :
                               a = self.data1[x]
                               b = a.split()   
                               self.data1[x] = b
                        self.maxcolumn = max([len(i) for i in self.data1])  # compute maximum number of column
                        self.dataview = copy.deepcopy(self.data1)  # create a drtaview for TableModel
                        for i,x in enumerate(self.dataview) :
                            index=len(x)
                            while index < self.maxcolumn:
                                x.append(' ') # add a empty/space string to row with less column than maxcolumn
                                index+=1
                            self.dataview[i] = x

                        self.model = TableModel(self.dataview)  # send self.dataview to Tablemodel rather than data with different number of columns
                        self.table.setModel(self.model)
                        f.close()
                else:
                    pass


    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

martin | 2021-04-29 10:27:10 UTC | #3

Hi @ushu welcome to the forum! Nice work on finding the solution -- what you've done is fine. In case it's helpful, I'll explain a bit more what's happening.

The table model is designed around the assumption that a table of data has a consistent number of columns and rows -- that is, every row has the same number of columns (and vice versa). When you specify the rowCount and columnCount these are taken to apply to all columns and rows respectively.

If you have a data table where some rows are shorter than others, Qt will still request data for those missing columns, by passing an index into the data method. If you take that index and try do a lookup into your Python lists, it will fail with an index error because it is out of bounds.

In your code below, I think you're trying to check the existence of a column using if not self._data[index.row()][index.column()]: -- however, that is still attempting to index using the values. The not only comes into effect after the value is returned from the list, and that will fail.

python
    def data(self, index, role):
        if role == Qt.DisplayRole:
            if not self._data[index.row()][index.column()]:
                print("sad!")
                return " "
            else:
                value = self._data[index.row()][index.column()]
                if isinstance(value, float):
                    return "%.2f" % value

Indexing beyond the end of a list throws an IndexError

python
>>> a = [1,2,3]
>>> a[4]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

So what we can do instead, is catch that IndexError and then return an empty string, e.g.

python
    def data(self, index, role):
        if role == Qt.DisplayRole:
            try:
                value = self._data[index.row()][index.column()]

            except IndexError:
                return "" # invalid index, return empty string

            # Value was found, return it formatted.
            if isinstance(value, float):
                return "%.2f" % value

This should give the same result you have now, but without needing to modify the loaded data. You can also use this trick to highlight cells in the table a certain way, e.g. shade missing cells grey.


ushu | 2021-04-29 10:53:50 UTC | #4

Thank you for your answer and explanations, more elegant method indeed.

I would love to find a more develop part about Table widget in your book (for a next version or a small additional DLC? ;) ): how to change background color of a row or a column or a header, how to manipulate row/column headers, if possible, how to allow user editing values within a Table widget, linking values from a row to a combobox .. and so on.


martin | 2021-04-29 12:36:43 UTC | #5

Here's a small example of setting header styles on a tableview based off the examples on the site. You just need to implement a headerData method which responds to calls with index (int) orientation (horizontal or vertical, for top and left headers respectively) and the role, which work the same as in data.

There is a gotcha though -- you cannot set the background using Qt.BackgroundRole on all platforms. On Windows it has no effect. You can get around this by using a cross-platform style, although this means your applications won't look native -- in the example below I do this using app.setStyle("fusion")

Changing the text colour and formatting of the numbers, etc. all works as expected.

python
import sys

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt


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

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            return self._data[index.row()][index.column()]

    def headerData(self, index, orientation, role):
        if orientation == Qt.Horizontal:
            if role == Qt.ForegroundRole and index == 1:
                return QtGui.QColor("red")

            if role == Qt.BackgroundRole and index == 0:
                return QtGui.QColor("red")

            if role == Qt.DisplayRole:
                return "%.2f" % index

        if orientation == Qt.Vertical:
            if role == Qt.DisplayRole:
                return index

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()

        data = [
            [4, 9, 2],
            [1, 0, 0],
            [3, 5, 0],
            [3, 3, 2],
            [7, 8, 9],
        ]

        self.model = TableModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


app = QtWidgets.QApplication(sys.argv)
app.setStyle("fusion")  # without this the BackgroundRole will not work in headers
window = MainWindow()
window.show()
app.exec_()

The screenshots below show how it looks in Windows and on Windows with Fusion style.

headerdatawindows|485x353

headerdatafusion|498x360


ushu | 2021-04-30 06:19:44 UTC | #6

Thank you again for your nice explanations and this pedagogic code. I learnt so much in few days about pyQt (after spending 2 weeks on tkinter that finished in a dead end for me). Best


Over 10,000 developers have bought Create GUI Applications with Python & Qt!

To support developers in [[ countryRegion ]] I give a [[ localizedDiscount[couponCode] ]]% discount with the code [[ couponCode ]] — Enjoy!

For [[ activeDiscount.description ]] I'm giving a [[ activeDiscount.discount ]]% discount with the code [[ couponCode ]] — Enjoy!

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak