Skip to content Skip to sidebar Skip to footer

Qlabel Correct Positioning For Text Outline

I am trying to create a label with text outline. I just want a simple white text with black outline. I first tried to do it in css like this label.setStyleSheet('color:white; outli

Solution 1:

Just because there is no convenient function or stylesheet property does not mean there is no consistent solution!

There are a number of properties to consider to set the baseline position of the text: the geometry of the QLabel, boundingRect of the text, alignment, indent, font metrics. The outlined text is going to be larger overall than regular text of the same point size, so the sizeHint and minimumSizeHint are reimplemented to account for it. The docs explain how indent is calculated and used with alignment. The text and character geometry, ascent, descent, and bearings are obtained from QFontMetrics. With this information a position for QPainterPath.addText can be determined that will emulate QLabel.

import sys, math
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
        
classOutlinedLabel(QLabel):
    
    def__init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.w = 1 / 25
        self.mode = True
        self.setBrush(Qt.white)
        self.setPen(Qt.black)

    defscaledOutlineMode(self):
        return self.mode

    defsetScaledOutlineMode(self, state):
        self.mode = state

    defoutlineThickness(self):
        return self.w * self.font().pointSize() if self.mode else self.w

    defsetOutlineThickness(self, value):
        self.w = value

    defsetBrush(self, brush):
        ifnotisinstance(brush, QBrush):
            brush = QBrush(brush)
        self.brush = brush

    defsetPen(self, pen):
        ifnotisinstance(pen, QPen):
            pen = QPen(pen)
        pen.setJoinStyle(Qt.RoundJoin)
        self.pen = pen

    defsizeHint(self):
        w = math.ceil(self.outlineThickness() * 2)
        returnsuper().sizeHint() + QSize(w, w)
    
    defminimumSizeHint(self):
        w = math.ceil(self.outlineThickness() * 2)
        returnsuper().minimumSizeHint() + QSize(w, w)
    
    defpaintEvent(self, event):
        w = self.outlineThickness()
        rect = self.rect()
        metrics = QFontMetrics(self.font())
        tr = metrics.boundingRect(self.text()).adjusted(0, 0, w, w)
        if self.indent() == -1:
            if self.frameWidth():
                indent = (metrics.boundingRect('x').width() + w * 2) / 2else:
                indent = w
        else:
            indent = self.indent()

        if self.alignment() & Qt.AlignLeft:
            x = rect.left() + indent - min(metrics.leftBearing(self.text()[0]), 0)
        elif self.alignment() & Qt.AlignRight:
            x = rect.x() + rect.width() - indent - tr.width()
        else:
            x = (rect.width() - tr.width()) / 2if self.alignment() & Qt.AlignTop:
            y = rect.top() + indent + metrics.ascent()
        elif self.alignment() & Qt.AlignBottom:
            y = rect.y() + rect.height() - indent - metrics.descent()
        else:
            y = (rect.height() + metrics.ascent() - metrics.descent()) / 2

        path = QPainterPath()
        path.addText(x, y, self.font(), self.text())
        qp = QPainter(self)
        qp.setRenderHint(QPainter.Antialiasing)

        self.pen.setWidthF(w * 2)
        qp.strokePath(path, self.pen)
        if1 < self.brush.style() < 15:
            qp.fillPath(path, self.palette().window())
        qp.fillPath(path, self.brush)

You can set the OutlinedLabel fill and outline color with setBrush and setPen. The default is white text with a black outline. The outline thickness is based on the point size of the font, the default ratio is 1/25 (i.e. a 25pt font will have a 1px thick outline). Use setOutlineThickness to change it. If you want a fixed outline not based on the point size (e.g. 3px), call setScaledOutlineMode(False) and setOutlineThickness(3).

This class only supports single line, plain text strings with left/right/top/bottom/center alignment. If you want other QLabel features like hyperlinks, word wrap, elided text, etc. those will need to be implemented too. But chances are you wouldn’t use text outline in those cases anyway.

Here is an example to show that it will work for a variety of labels:

enter image description here

classTemplate(QWidget):def__init__(self):
        super().__init__()
        vbox = QVBoxLayout(self)
        label = OutlinedLabel('Lorem ipsum dolor sit amet consectetur adipiscing elit,')
        label.setStyleSheet('font-family: Monaco; font-size: 20pt')
        vbox.addWidget(label)

        label = OutlinedLabel('sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
        label.setStyleSheet('font-family: Helvetica; font-size: 30pt; font-weight: bold')
        vbox.addWidget(label)

        label = OutlinedLabel('Ut enim ad minim veniam,', alignment=Qt.AlignCenter)
        label.setStyleSheet('font-family: Comic Sans MS; font-size: 40pt')
        vbox.addWidget(label)

        label = OutlinedLabel('quis nostrud exercitation ullamco laboris nisi ut')
        label.setStyleSheet('font-family: Arial; font-size: 50pt; font-style: italic')
        vbox.addWidget(label)

        label = OutlinedLabel('aliquip ex ea commodo consequat.')
        label.setStyleSheet('font-family: American Typewriter; font-size: 60pt')
        label.setPen(Qt.red)
        vbox.addWidget(label)

        label = OutlinedLabel('Duis aute irure dolor', alignment=Qt.AlignRight)
        label.setStyleSheet('font-family: Luminari; font-size: 70pt')
        label.setPen(Qt.red); label.setBrush(Qt.black)
        vbox.addWidget(label)

        label = OutlinedLabel('in reprehenderit')
        label.setStyleSheet('font-family: Zapfino; font-size: 80pt')
        label.setBrush(Qt.red)
        vbox.addWidget(label)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = Template()
    window.show()
    sys.exit(app.exec_())

Now Qt actually comes in clutch because you can get so much more out of this than just solid color text and outlines with all the QBrush/QPen options:

enter image description here

classTemplate(QWidget):

    def__init__(self):
        super().__init__()                                          
        vbox = QVBoxLayout(self)
        text = 'Felicitations'
        
        label = OutlinedLabel(text)
        linearGrad = QLinearGradient(0, 1, 0, 0)
        linearGrad.setCoordinateMode(QGradient.ObjectBoundingMode)
        linearGrad.setColorAt(0, QColor('#0fd850'))
        linearGrad.setColorAt(1, QColor('#f9f047'))
        label.setBrush(linearGrad)
        label.setPen(Qt.darkGreen)
        vbox.addWidget(label)

        label = OutlinedLabel(text)
        radialGrad = QRadialGradient(0.3, 0.7, 0.05)
        radialGrad.setCoordinateMode(QGradient.ObjectBoundingMode)
        radialGrad.setSpread(QGradient.ReflectSpread)
        radialGrad.setColorAt(0, QColor('#0250c5'))
        radialGrad.setColorAt(1, QColor('#2575fc'))
        label.setBrush(radialGrad)
        label.setPen(QColor('Navy'))
        vbox.addWidget(label)
        
        label = OutlinedLabel(text)
        linearGrad.setStart(0, 0); linearGrad.setFinalStop(1, 0)
        linearGrad.setColorAt(0, Qt.cyan); linearGrad.setColorAt(1, Qt.magenta)
        label.setPen(QPen(linearGrad, 1)) # pen width is ignored
        vbox.addWidget(label)

        label = OutlinedLabel(text)
        linearGrad.setFinalStop(1, 1)
        for x in [(0, '#231557'), (0.29, '#44107A'), (0.67, '#FF1361'), (1, '#FFF800')]:
            linearGrad.setColorAt(x[0], QColor(x[1]))
        label.setBrush(linearGrad)
        label.setPen(QPen(QBrush(QColor('RoyalBlue'), Qt.Dense4Pattern), 1))
        label.setOutlineThickness(1 / 15)
        vbox.addWidget(label)

        label = OutlinedLabel(text)
        label.setBrush(QBrush(Qt.darkBlue, Qt.BDiagPattern))
        label.setPen(Qt.darkGray)
        vbox.addWidget(label)

        label = OutlinedLabel(text, styleSheet='background-color: black')
        label.setBrush(QPixmap('paint.jpg'))
        label.setPen(QColor('Lavender'))
        vbox.addWidget(label)
        
        self.setStyleSheet('''
        OutlinedLabel {
            font-family: Ubuntu;
            font-size: 60pt;
            font-weight: bold;
        }''')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = Template()
    window.show()
    sys.exit(app.exec_())

Note that I’ve chosen to treat OutlinedLabel like a QGraphicsItem with the setBrush/setPen methods. If you want to use style sheets for the text color fill the path with qp.fillPath(path, self.palette().text())

Another option instead of calling QPainter.strokePath and then QPainter.fillPath is to generate a fillable outline of the text path with QPainterPathStroker, but I’ve noticed it’s slower. I would only use it to adjust the clarity of very small text by setting a larger width to the stroker than the pen. To try it replace the last 5 lines in paintEvent with:

qp.setBrush(self.brush)
self.pen.setWidthF(w)
qp.setPen(self.pen)
stroker = QPainterPathStroker()
stroker.setWidth(w)
qp.drawPath(stroker.createStroke(path).united(path))

Post a Comment for "Qlabel Correct Positioning For Text Outline"