Search code examples
pythonpyside6

Python QStyledItemDelegate - How do I properly align the text


This question is almost identical to this question. However, the solution did not work for me and the question is an older version of Qt. I am using a QStyledItemDelegate to color certain cells based on their value. However, after these cells are colored, the text in them shifts far to the upper left. Using Alignment flags helps the issue but they are still far to the left (maybe only a space or two). In the code below, a table is created and the columns next to GOAT are highlighted to demonstrate this. We can see the text is 'more' to the left than the other cells. How do I fix this? translate moves the whole cell, not just the text. Is there a way to just move the text?

import sys

import pandas as pd
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtGui import QPen, QBrush, QColor
from PySide6.QtWidgets import  QStyledItemDelegate


class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._dfDisplay = data
        self._data = data

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

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, col, orientation, role):
        if orientation == Qt.Orientation.Horizontal:
            if role == Qt.ItemDataRole.DisplayRole:
                return str(self._dfDisplay.columns[col])
        return None


class MyDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        super().paint(painter, option, index)
        painter.setPen(QPen(Qt.GlobalColor.black, 3))

        # Highlight our bm and tbm ROE cells
        if index.siblingAtColumn(0).data() in ["GOAT"]:
            if index.column() > 0:
                # Set the fill color
                brush = QBrush(QColor('#90EE90'))
                brush.setStyle(Qt.SolidPattern)
                painter.setBrush(brush)
                # Set the outline color
                painter.setPen(Qt.GlobalColor.white)
                painter.drawRect(option.rect)
                # Draw the text
                painter.setPen(Qt.GlobalColor.black)
                painter.translate(1, 0)
                painter.drawText(option.rect,Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, str(index.data()))


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.table = QtWidgets.QTableView()
        self.delegate = MyDelegate()
        self.table.setItemDelegate(self.delegate)

        header_view = CustomHeaderView(QtCore.Qt.Orientation.Horizontal)
        self.table.setHorizontalHeader(header_view)
        data = pd.DataFrame([["GOAT", "Giraffe", "Potatoe", "Another"],
                             [77, 33, 111111, 233],
                             [50, 70, 89, 100000]
                             ])
        print(data)

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


app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

Solution

  • Your issue is mostly conceptual.

    If you just want to alter the background and foreground color of an item, overriding the paint() function of an item delegate is usually not the correct choice.

    If the appearance has to be consistent from the model part and within all its connected views, then you must properly implement the data() function of the model:

    class TableModel(QtCore.QAbstractTableModel):
        ...
        def data(self, index, role=Qt.DisplayRole):
            if not index.isValid():
                return
            elif role == Qt.DisplayRole:
                value = self._data[index.column()][index.row()]
                return str(value)
            elif (
                role in (Qt.BackgroundRole, Qt.ForegroundRole)
                and index.column() > 0
            ):
                leftValue = self._data[0][index.row()]
                if leftValue == 'GOAT':
                    if role == Qt.BackgroundRole:
                        return QColor('#90EE90')
                    else:
                        return QColor(Qt.white)
    

    If you don't want to alter the behavior of the model and only change the appearance in specific views, then, again, don't try to alter the painting, but just rely on the initStyleOption() of the delegate:

    class MyDelegate(QStyledItemDelegate):
        def initStyleOption(self, opt, index):
            super().initStyleOption(opt, index)
            if index.column() and index.siblingAtColumn(0).data() == 'GOAT':
                opt.backgroundBrush = QBrush(QColor('#90EE90'))
                opt.palette.setColor(QPalette.Text, QColor(Qt.white))
    

    The above approach is not only more accurate, but also safer.

    The paint() function of an item delegate strongly relies on the current QStyle implementation, which internally uses QStyle functions to compute appropriate margins and spacings within each item.

    Each QStyle accesses its internal functions in different ways, most of the times by calling those functions internally, so there is absolutely no certain way to know where the internal objects of an item will be finally shown. Some styles use public functions that can be overridden, but others do that privately, and while there are some "safer" ways to draw item objects that normally work in most circumstances, trying to manually do that drawing is, most of the times, a "leap of faith".

    Considering the above, your attempt in overriding paint() has a lot of issues:

    • if you want to customize background and foreground painting, you shall not call the base implementation;
    • you're setting a pen even before checking if the custom painting has to be done or not;
    • the default brush style is always SolidPattern, so there's no point in redundantly setting it;
    • translating a received painter that is probably be furtherly used for other aspects (eg: other items!) is wrong, since it may translate every further painting that happens after it; the save() and restore() functions should be used instead, before and after applying such important changes to the painting context;

    At the very least, a proper delegate paint() function that alters the default behavior should do the following:

    1. create a clone of the given option;
    2. call initStyleOption() with that option and the given index;
    3. get the current style (either from the option.widget or the QApplication instance);
    4. store the current option's data that needs custom painting and overwrite it with empty/invalid data;
    5. change the basic options that need to be overridden (such as the background);
    6. use the given style to draw the primitive PE_PanelItemViewItem;
    7. use QStyle options to get further aspects of the item (checkbox and/or decoration geometries, available text rect, current item state, etc.) also based on the original option;
    8. use QStyle functions to draw over what's necessary, eventually considering QFontMetrics aspects proper text displaying, including elision;

    I understand that the above may seem a big complication, but that's how Qt allows complex drawing of items in views, and if you need deeply custom behavior you must follow those aspects if you want to provide a look that is as close as possible to the default behavior.

    I strongly suggest you to never underestimate all these aspects: even if you end up never using them, being aware of them is quite mandatory. So, take your time to review the whole documentation about:

    • QStyle;
    • QStyleOption (and all inherited classes);
    • QPainter and related classes such as QPen, QBrush, QColor and QPalette;
    • QAbstractItemView, QAbstractItemModel and the Qt model/view programming guides in general;
    • QAbstractItemDelegate and related classes;
    • QFontMetrics;
    • all geometry based classes that are related to painting in some way, such as QPoint, QLine, QRect, QPolygon (and their floating point counterparts), but also QPainterPath and QRegion;