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?
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
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.
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
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.
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
>>> 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.
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.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__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.
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
PyQt6 Crash Course — a new tutorial in your Inbox every day
Beginner-focused crash course explaining the basics with hands-on examples.