Search code examples
qtpyqt5qlistviewqstyleditemdelegate

Qt displayAlignment not aligning to the right


I am writing an application using PyQt. I am using QListView with QStyledItemDelegate and I need to align some items on the left and some on the right. All of them are aligned on the left by default. However, I still don't get how to align them on the right. I fonud this question How I can align values exported from database with query in PyQt5 table view which looks like something I need, but displayAlignment just doesn't seem to change anything. Here is my code:

import sys

from PyQt5.QtCore import (
    QAbstractListModel,
    QMargins,
    Qt,
    QSize,
)
from PyQt5.QtGui import (
    QPainter,
    QFont,
    QFontMetrics,
)
from PyQt5.QtWidgets import (
    QApplication,
    QListView,
    QMainWindow,
    QVBoxLayout,
    QWidget,
    QStyledItemDelegate,
)

BUBBLE_PADDING = QMargins(15, 5, 15, 5)
TEXT_PADDING = QMargins(25, 15, 25, 15)
FONT_SIZE = 14
FONT_STYLE = "Times"
font = 0

class MessageDelegate(QStyledItemDelegate):
    def initStyleOption(self, option, index):
        super(MessageDelegate, self).initStyleOption(option, index)
        option.displayAlignment = Qt.AlignRight

    def paint(self, painter, option, index):
        global font
        text = index.model().data(index, Qt.DisplayRole)
        option.rect.setSize(self.sizeHint(option, index))
        option.displayAlignment = Qt.AlignRight
        print(int(option.displayAlignment))
        bubblerect = option.rect.marginsRemoved(BUBBLE_PADDING)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)
        painter.setPen(Qt.cyan)
        painter.setBrush(Qt.cyan)
        painter.drawRoundedRect(bubblerect, 10, 10)
        painter.setPen(Qt.black)
        painter.setFont(font)
        painter.drawText(textrect, Qt.TextWordWrap, text)

    def sizeHint(self, option, index):
        global font
        text = index.model().data(index, Qt.DisplayRole)
        metrics = QFontMetrics(font)
        rect = option.rect.marginsRemoved(TEXT_PADDING)
        rect = metrics.boundingRect(rect, Qt.TextWordWrap, text)
        rect = rect.marginsAdded(TEXT_PADDING)
        return rect.size()


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super(MessageModel, self).__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            return self.messages[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, text):
        if text:
            self.messages.append((text))
            self.layoutChanged.emit()


class MainWindow(QMainWindow):
    def __init__(self):
        global font
        super(MainWindow, self).__init__()
        font = QFont(FONT_STYLE, FONT_SIZE)
        self.resize(int(QApplication.primaryScreen().size().width() * 0.3), int(QApplication.primaryScreen().size().height() * 0.5))
        main_layout = QVBoxLayout()
        self.messages = QListView()
        self.messages.setItemDelegate(MessageDelegate())
        self.model = MessageModel()
        self.messages.setModel(self.model)
        self.model.add_message("Hello, world!")
        main_layout.addWidget(self.messages)
        self.w = QWidget()
        self.w.setLayout(main_layout)
        self.setCentralWidget(self.w)


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

And here is the result. As you can see, the message is still aligned on the left despite the fact that the value printed in the paint function is 2 (Qt.AlignRight). (I've assigned Qt.AlignRight 2 times, because the function initStyleOption is never called)


Solution

  • Setting the values of the style option only makes sense if they're used, for example when calling QStyle functions.

    You're overriding paint by painting everything on your own, so any change in the style option is completely useless.
    On the contrary, you should not change options (especially the option.rect) in any function except initStyleOption, but create a new instance of the option (or its objects) based on the one received as argument instead.

    Since you want to align the rect to the right, you need to construct a new rectangle with its X coordinate based on the option rect's right minus the desired width.

        def paint(self, painter, option, index):
            text = index.model().data(index, Qt.DisplayRole)
            size = self.sizeHint(option, index)
            rect = QRect(
                option.rect.right() - size.width(), option.rect.y(), 
                size.width(), size.height())
            bubblerect = rect.marginsRemoved(BUBBLE_PADDING)
            textrect = rect.marginsRemoved(TEXT_PADDING)
            painter.setPen(Qt.cyan)
            painter.setBrush(Qt.cyan)
            painter.drawRoundedRect(bubblerect, 10, 10)
            painter.setPen(Qt.black)
            painter.setFont(font)
            painter.drawText(textrect, Qt.TextWordWrap, text)
    

    Globals should always be avoided whenever it's possible (don't use them if you don't know how to use them, aka: if you know how to use them, you won't use them). If you want to set a font for the delegate, then add an argument for its __init__ with the preferred font, and set it as instance attribute.