Próbuję utworzyć etykietę z konturem tekstowym. Chcę tylko prosty biały tekst z czarnym konturem. Po raz pierwszy próbowałem to zrobić w CSS tak, jak ten label.setStyleSheet("color:white; outline:2px black;")
Ale zarys nic nie zrobił.

Zrobiłem dużo poszukiwania i znalazłem sposób, aby zrobić to z ścieżką QPainter. Ale problem polega na tym, że tekst jest zawsze odcięty. Wpisz opis obrazu tutaj

Zgodnie z funkcją, tekst ma być uruchomiony z lewej dolnej, ale zaczyna się zbyt niski i w lewo. Wiem, że mogę znaleźć punkt według błędu próbnego, więc nie zostanie odcięty - możesz -20 do wysokości i będzie wystarczająco dobrze dla tego. Ale naprawi to ten konkretny tekst! Nie będzie taki sam dla dowolnej etykiety z inną wielkością lub tekstem lub czcionką.

Umieściłem tutaj minimalny kod

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QWidget, QLabel, QMainWindow


class MainLabel(QLabel):
    def __init__(self, text):
        super(MainLabel, self).__init__(text)

    def paintEvent(self, event):
        qp = QtGui.QPainter()
        qp.begin(self)
        qp.setRenderHint(QtGui.QPainter.Antialiasing)
        font=QtGui.QFont()
        font.setPointSize(70)
        painterPath = QtGui.QPainterPath()
        #how to get the right positioning for addText
        painterPath.addText(0, self.height(), font,self.text())#HERE
        qp.strokePath(painterPath, QtGui.QPen(QtGui.QColor(0,0,0), 6))
        qp.fillPath(painterPath, QtGui.QColor(255,255,255))
        qp.end()


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.centralWidget=QWidget(self)
        self.setCentralWidget(self.centralWidget)
        self.lay = QtWidgets.QVBoxLayout()
        self.centralWidget.setLayout(self.lay)
        self.label = MainLabel("text gets cut off")
        self.label.setStyleSheet("font-size:70pt;color:white; outline:2px black;")
        self.lay.addWidget(self.label)
        self.show()


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec_())

Jest to taka wspólna rzecz, którą widzę dosłownie wszędzie, ale jeszcze nie ma prostej funkcji w Pyqt, nie powodując więcej problemów? Normalny qlabel automatycznie zajmie się pozycjonowaniem, ale jeśli chcesz zarys tekstu, musisz to dać!

Więc pytam, jak znaleźć prawidłowe pozycjonowanie jak normalny qlabel, jeśli jest to jedyny sposób na zarys tekstowy, w przeciwnym razie jeśli jest jakiś inny sposób, w jaki jest lepszy, powiedz mi.

1
WALL-E 10 październik 2020, 09:14

1 odpowiedź

Najlepsza odpowiedź

Tylko dlatego, że nie ma wygodnej funkcji ani właściwości arkusza stylów, nie oznacza, że nie ma spójnego rozwiązania!

Istnieje wiele nieruchomości do rozważenia, aby ustawić pozycję linii bazowej tekstu: geometria qlabel, granica tekstu, wyrównania, tiret, metryki czcionek. Zarysowany tekst będzie większy ogólny niż zwykły tekst o tym samym rozmiarze punktu, więc sizeHint i minimumSizeHint zostaną zwrócone na uwzględnienie jej. Dokumenty wyjaśniają, jak Tiret jest obliczany i używany z wyrównaniem. Tekst i geometria postaci, wynurzenie, zniżanie i łożyska uzyskuje się z QFontmetters. Dzięki tej informacji można określić pozycję dla QPainterPath.addText, która emulacyjna qlabel.

import sys, math
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
        
class OutlinedLabel(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)

    def scaledOutlineMode(self):
        return self.mode

    def setScaledOutlineMode(self, state):
        self.mode = state

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

    def setOutlineThickness(self, value):
        self.w = value

    def setBrush(self, brush):
        if not isinstance(brush, QBrush):
            brush = QBrush(brush)
        self.brush = brush

    def setPen(self, pen):
        if not isinstance(pen, QPen):
            pen = QPen(pen)
        pen.setJoinStyle(Qt.RoundJoin)
        self.pen = pen

    def sizeHint(self):
        w = math.ceil(self.outlineThickness() * 2)
        return super().sizeHint() + QSize(w, w)
    
    def minimumSizeHint(self):
        w = math.ceil(self.outlineThickness() * 2)
        return super().minimumSizeHint() + QSize(w, w)
    
    def paintEvent(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) / 2
            else:
                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()) / 2
            
        if 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)
        if 1 < self.brush.style() < 15:
            qp.fillPath(path, self.palette().window())
        qp.fillPath(path, self.brush)

Możesz ustawić kolor wypełnienia i kontur OutlinedLabel za pomocą setBrush i setPen. Domyślnie jest biały tekst z czarnym konturem. Grubość konspektu opiera się na rozmiarze punktu czcionki, stosunek domyślny jest 1/25 (tj. Czcionka 25pt będzie miała 1px gruby kontur). Użyj setOutlineThickness, aby go zmienić. Jeśli chcesz stały kontur, nie oparty na rozmiarze punktu (np. 3px), zadzwoń setScaledOutlineMode(False) i setOutlineThickness(3).

Ta klasa obsługuje tylko pojedynczą linię, zwykłe struny tekstowe z wyrównaniem lewym / prawym / górnym / dołu / środkowym. Jeśli chcesz, aby inne funkcje qlabel, takie jak hiperłącza, wrap słowo, słowo tekst itp. Te będą musiały zostać wdrożone. Ale tak są szanse, że nie użyjesz konspektu tekstowego w tych przypadkach.

Oto przykład, aby pokazać, że będzie działać dla różnych etykiet:

enter image description here

class Template(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_())

Teraz Qt faktycznie przychodzi w sprzęrzeniu, ponieważ możesz uzyskać o wiele więcej niż tylko jednolity kolorowy tekst i kontury ze wszystkimi opcjami QPRUBS / QPEN:

enter image description here

class Template(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_())

Należy pamiętać, że zdecydowałem się traktować onededLabel jak qgraphicsitem z metodami setBrush / setPen. Jeśli chcesz użyć arkuszy stylów dla koloru tekstu wypełnij ścieżkę za pomocą qp.fillPath(path, self.palette().text())

Inna opcja zamiast dzwonienia QPainter.strokePath, a następnie QPainter.fillPath ma wygenerować wypełniony zarys ścieżki tekstowej z QPainterpathstroker, ale zauważyłem, że jest wolniejszy. Używałbym go, aby dostosować jasność bardzo małego tekstu, ustawiając większą szerokość do Stokera niż pióro. Aby spróbować zastąpić 5 ostatnich linii w paintEvent z:

qp.setBrush(self.brush)
self.pen.setWidthF(w)
qp.setPen(self.pen)
stroker = QPainterPathStroker()
stroker.setWidth(w)
qp.drawPath(stroker.createStroke(path).united(path))
4
alec 10 październik 2020, 08:03