I have a table that looks like this
I am looking to set bold underlines on certain rows based on the value in the first cell in that row's values such as revenue avg 3, and tangible book ... I am stumped as where to start.
I am looking to implement a QItemDelegate to underline a whole row based on the value in the leftmost cell so 'revenue avg 3' would result in the whole line getting underlined.
My code:
import pandas as pd
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPen, QBrush
from PySide6.QtWidgets import QStyle
class TickerTableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, *args):
super(TickerTableModel, self).__init__(parent, *args)
self._filters = {}
self._sortBy = []
self._sortDirection = []
self._dfSource = pd.DataFrame()
self._dfDisplay = pd.DataFrame()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dfDisplay.shape[0]
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dfDisplay.shape[1]
def data(self, index, role):
if index.isValid() and role == QtCore.Qt.ItemDataRole.DisplayRole:
return self._dfDisplay.values[index.row()][index.column()]
return None
def headerData(self, col, orientation=QtCore.Qt.Orientation.Horizontal, role=QtCore.Qt.ItemDataRole.DisplayRole):
if orientation == QtCore.Qt.Orientation.Horizontal and role == QtCore.Qt.ItemDataRole.DisplayRole:
return str(self._dfDisplay.columns[col])
return None
def setupModel(self, data):
self._dfSource = data
self._sortBy = []
self._sortDirection = []
self.setFilters({})
def setFilters(self, filters):
self.modelAboutToBeReset.emit()
self._filters = filters
self.updateDisplay()
self.modelReset.emit()
def updateDisplay(self):
dfDisplay = self._dfSource.copy()
# Filtering
cond = pd.Series(True, index=dfDisplay.index)
for column, value in self._filters.items():
cond = cond & \
(dfDisplay[column].str.lower().str.find(str(value).lower()) >= 0)
dfDisplay = dfDisplay[cond]
# Sorting
if len(self._sortBy) != 0:
dfDisplay.sort_values(by=self._sortBy,
ascending=self._sortDirection,
inplace=True)
# Updating
self._dfDisplay = dfDisplay
class TickerTableView(QtWidgets.QTableView):
def setupUi(self, TickerTableWidget):
self.verticalLayout = QtWidgets.QVBoxLayout(TickerTableWidget)
self.tableView = QtWidgets.QTableView(TickerTableWidget)
self.tableView.setSortingEnabled(True)
self.tableView.setAlternatingRowColors(True)
self.verticalLayout.addWidget(self.tableView)
QtCore.QMetaObject.connectSlotsByName(TickerTableWidget)
class TickerTableWidget(QtWidgets.QWidget):
def __init__(self, data, parent=None):
super(TickerTableWidget, self).__init__(parent)
self.ui = TickerTableView()
self.ui.setupUi(self)
self.tableModel = TickerTableModel()
self.tableModel.setupModel(data)
self.ui.tableView.setModel(self.tableModel)
# Set delegate here
self.delegate = TickerTableDelegate(self.ui)
self.ui.setItemDelegate(self.delegate)
class TickerTableDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None, *args):
QtWidgets.QItemDelegate.__init__(self, parent, *args)
def paint(self, painter, option, index):
painter.save()
# set text color
painter.setPen(QPen(Qt.black))
text = index.data(Qt.DisplayRole)
if text:
painter.setPen(QPen(Qt.red))
painter.drawText(option.rect, Qt.AlignCenter, text)
painter.restore()
My main.py might be something like:
if __name__ == '__main__':
app = QApplication(sys.argv)
df = pd.DataFrame({'revenue avg 3' : [1,5,10], 'profit': [5, 4, 5], 'income': [3,5,6]})
window = TickerTableWidget(df)
window.show()
sys.exit(app.exec())
I believe the process is get the row in the item delegate, test the value in column 0 and then set some property (which) to underline the whole row (maybe underline each cell in that row). I am currently stuck on the Item Delegate doing nothing no matter what I try.
You have two issues in your code.
The most important is that you're trying to mimic the behavior of uic generated files, and you're also doing it in the wrong way. Those files are intended to be left unmodified their classes should not ever be subclassed (unless you really, really know what you're doing).
The culprit is this line:
self.ui.setItemDelegate(self.delegate)
You're setting the delegate on the ui
object, but that object is never used within the UI: the classes generated by uic are only used to set up the UI, they are just common interfaces that also happen to be classes in order to allow multiple inheritance. They are not intended to be used for direct inheritance, they should only be used either for composition or with multiple inheritance.
The table you're actually using is the tableView
attribute of self.ui
, so that line should be the following:
self.ui.tableView.setItemDelegate(self.delegate)
^^^^^^^^^
Unless you're actually basing your program on a UI file created in Designer, there's absolutely no point in creating such "form classes". Just inherit your main class(es) from a QWidget subclass and build the UI around it.
In any case, trying to make those classes inherit from Qt widget classes is pointless as it is wrong.
Then, since you only need to draw over the default painting, you don't need to do everything on your own. Just call the default paint()
implementation and then draw the line.
In your latest modification the model doesn't have any named reference for the first column as in your original post, so I'm just providing an example based on the current data: I'm going to assume that we only want to draw a line whenever the first column has 5 as its value:
class TickerTableDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
if index.column() > 0:
reference = index.siblingAtColumn(0)
else:
reference = index
if reference.data() == 5:
painter.drawLine(
option.rect.bottomLeft(), option.rect.bottomRight())
Note that the parent argument you used for the delegate constructor is also invalid. The common convention is to set it to the view that will actually use it, and, considering the above note, using self.ui
is wrong; it should be created like this:
self.delegate = TickerTableDelegate(self.ui.tableView)
Finally, it's important to be aware about pros and cons of providing sorting or filtering within a model. While supporting sorting in a model is completely valid, it should be done only whenever the implementation actually provides benefits (most importantly, performance-wise). Filtering is a completely different matter: unless done with extreme awareness about model and view interactions, trying to achieve it in a basic QAbstractItemModel is usually a perfect recipe for headaches. In both cases, you should at least consider using a QSortFilterProxyModel set on top of your basic one.
In a case like yours, it would just be a matter of doing the following:
class TickerTableWidget(QtWidgets.QWidget):
def __init__(self, data, parent=None):
super(TickerTableWidget, self).__init__(parent)
self.ui = TickerTableView()
self.ui.setupUi(self)
self.tableModel = TickerTableModel()
self.tableModel.setupModel(data)
self.proxyModel = QSortFilterProxyModel()
self.proxyModel.setSourceModel(self.tableModel)
self.ui.tableView.setModel(self.proxyModel)
The above will automatically provide proper sorting when clicking the headers, and setting a filter would just be a matter of calling the related functions (such as setFilterRegExp()
) or eventually implementing filterAcceptsRow()
.