简体   繁体   中英

How to get QLabel font information for a given rectangle?

I am trying to get font size of QLabel that the text fill entire QLabel's rectangle.I try to use QFontMetrics to get, but QFontMetrics can't get font size by gived rectangle?

The example:
The GUI may stuck while resize the window.

class Label(QLabel):
    def __init__(self):
        super().__init__()
        self.resize(400, 300)
        font = self.calculate_font()
        self.setFont(font)
        self.setText('PASS')

    def calculate_font(self):
        for i in range(400):
            fm = QFontMetrics( QFont('Helvetica', i) )
            fmSize = fm.boundingRect(self.geometry(), Qt.AlignCenter, 'PASS').size()
            print(fm.boundingRect(self.geometry(), Qt.AlignCenter, 'PASS'), self.size())
            #need font height equal label height
            if fmSize.height() > self.size().height():
                return QFont('Helvetica', i)

    def resizeEvent(self, event):
        font = self.calculate_font()
        self.setFont(font)

app = QApplication([])
demo = Label()
demo.show()
app.exec()

Don't do it

There are dozens of reasons for which what you want to achieve is simply wrong.
The most important one is that dealing with text drawing and its size is not an easy task; also, Qt uses the label contents to tell the window layout about the size it could have, the size it wants to have and, most importantly, the minimum size it should have; all this is very important to the GUI, as it will be taken into account in order to correctly resize all other elements of your interface.
Finally, all those factors are based on the text of the label and its formatting, which might depend on the text contents and the fact that the text might be "rich text" (including multiple font size and weight).

If you really are conscious about the concepts explained above, here are 4 possible implementations. All of them partially support rich text (font weight, text color, etc, but not real text alignment).

标签测试示例

Resizing font label

While subclassing a QLabel might seem the simplest approach, it actually isn't, as it is a widget much more complex than it seems (as I wrote before, dealing with text widgets is not an easy task)

The most important downside of this method is that it is very slow, since it has to carefully compute the font size at each resize. While some improvement might be achieved with a better implementation, I wouldn't suggest this method anyway.

class ResizeFontLabel(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.paintFont = self.font()

    def updateFont(self):
        doc = QTextDocument()
        if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
            doc.setHtml(self.text())
        else:
            doc.setPlainText(self.text())
        frame = doc.rootFrame().frameFormat()
        frame.setMargin(0)
        doc.rootFrame().setFrameFormat(frame)
        doc.setDefaultFont(self.paintFont)

        width = self.width()
        height = self.height()

        if doc.size().width() > width or doc.size().height() > height:
            while doc.size().width() > width or doc.size().height() > height:
                self.paintFont.setPointSizeF(self.paintFont.pointSizeF() - .1)
                doc.setDefaultFont(self.paintFont)
        elif doc.size().width() < width and doc.size().height() < height:
            while doc.size().width() < width and doc.size().height() < height:
                self.paintFont.setPointSizeF(self.paintFont.pointSizeF() + .1)
                doc.setDefaultFont(self.paintFont)

    def resizeEvent(self, event):
        self.updateFont()

    def paintEvent(self, event):
        doc = QTextDocument()
        if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
            doc.setHtml(self.text())
        else:
            doc.setPlainText(self.text())
        frame = doc.rootFrame().frameFormat()
        frame.setMargin(0)
        doc.rootFrame().setFrameFormat(frame)
        doc.setDefaultFont(self.paintFont)
        qp = QPainter(self)
        doc.drawContents(qp, QRectF(self.rect()))

QLabels internally use a QTextDocument for both painting and sizing. The reason of the setMargin is due to the fact that a QTextDocument has some margin by default, and that's not used in labels.

Notes:

  • be careful about the different or/and in the updateFont() method. Their logic is very important
  • this method is slow
  • it doesn't support text alignment (at least with this basic implementation)

Painting based font label

This method is a simplification of the one above. It doesn't compute the font size, but just paints the contents scaled to the size.

class PaintLabel(QLabel):
    def paintEvent(self, event):
        doc = QTextDocument()
        if self.textFormat() == Qt.RichText or self.textFormat() == Qt.AutoText and Qt.mightBeRichText(self.text()):
            doc.setHtml(self.text())
        else:
            doc.setPlainText(self.text())
        frame = doc.rootFrame().frameFormat()
        frame.setMargin(0)
        doc.rootFrame().setFrameFormat(frame)
        scale = min(self.width() / doc.size().width(), self.height() / doc.size().height())
        qp = QPainter(self)
        qp.scale(scale, scale)
        doc.drawContents(qp, QRectF(self.rect()))

Notes:

  • it's faster than the first method
  • it doesn't support alignment
  • since the scaling doesn't take into account the font size, the text will not be as big as it could be (that's due to the fact that a QTextDocument can have multiple "text blocks", and computing them at each paintEvent would make this really complex and slow

Graphics view label

This is a completely different approach, as it uses the Graphics View Framework . The trick is to use a single QGraphicsTextItem in the scene, and let the view take care about the resizing/alignment.

class GraphicsLabel(QGraphicsView):
    def __init__(self, text=''):
        super().__init__()
        # graphics view usually have a background and a frame around them,
        # let's remove both of them
        self.setFrameShape(0)
        self.setStyleSheet('background: transparent;')
        # as most QAbstractScrollAreas, views have a default minimum size hint
        # that makes them "bigger" whenever they're shown; let's ignore that
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        scene = QGraphicsScene(self)
        self.setScene(scene)
        self.textItem = scene.addText('')
        self.setText(text)

    def minimumSizeHint(self):
        # this is related to the minimum size hint specified above
        font = self.font()
        font.setPointSize(1)
        return QFontMetrics(font).boundingRect(self.textItem.toPlainText()).size()

    def setText(self, text):
        font = self.font()
        font.setPointSize(1)
        self.setMinimumSize(QFontMetrics(font).boundingRect(text).size())
        if Qt.mightBeRichText(text):
            self.textItem.setHtml(text)
        else:
            self.textItem.setPlainText(text)
        doc = self.textItem.document()
        frame = self.textItem.document().rootFrame().frameFormat()
        if frame.margin():
            frame.setMargin(0)
            doc.rootFrame().setFrameFormat(frame)
            self.textItem.setDocument(doc)

    def text(self):
        # provide a basic interface similar to a QLabel
        return self.textItem.toPlainText()

    def resizeEvent(self, event):
        # the base class implementation is always required for QAbstractScrollArea
        # descendants; then we resize its contents to fit its size.
        super().resizeEvent(event)
        self.fitInView(self.textItem, Qt.KeepAspectRatio)

Notes:

  • it does support alignment
  • there's no "optimal" minimum size (the readability of the text is based on the font, and we can't do anything about that)
  • there's no "optimal" size hint, so the widget will be resized without any "complaints" about its text contents: if the label has very long text, that text will just be very, very small.
  • basic QWidget styling (through QWidget.setStyleSheet ) is not supported

Graphics view QLabel widget

This method is very similar to the one above, but instead of creating a simple "text item", it adds an actual QLabel to the graphics scene.

class GraphicsLabelWidget(QGraphicsView):
    def __init__(self, text=''):
        super().__init__()
        self.setFrameShape(0)
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        self.setStyleSheet('background: transparent;')
        scene = QGraphicsScene(self)
        self.setScene(scene)
        self.label = QLabel(text)
        self.labelItem = scene.addWidget(self.label)
        self.label.setStyleSheet(self.styleSheet())
        self.setText(text)

    def minimumSizeHint(self):
        font = self.font()
        font.setPointSize(1)
        doc = QTextDocument()
        if Qt.mightBeRichText(self.label.text()):
            doc.setHtml(self.label.text())
        else:
            doc.setPlainText(self.label.text())
        return QFontMetrics(font).boundingRect(self.label.text()).size()

    def setText(self, text):
        font = self.font()
        font.setPointSize(1)
        self.setMinimumSize(QFontMetrics(font).boundingRect(text).size())
        self.label.setText(text)

    def text(self):
        return self.label.toPlainText()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.fitInView(self.labelItem, Qt.KeepAspectRatio)

Notes:

  • slightly better than the basic addText graphics implementation; at the same time, it's also slightly slower
  • better support for stylesheets (but they should be applied to the actual child label)

Test example:

Provided just for commodity (add the classes above to see it in action).

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout(self)

        testText = 'PASS <b>bold</b><br/><i>new italic line</i><br/>{}'

        resizeLabel = ResizeFontLabel(testText.format('resize mode'))
        layout.addWidget(resizeLabel)
        resizeLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)

        paintLabel = PaintLabel(testText.format('paint mode'))
        layout.addWidget(paintLabel)
        paintLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)

        graphicsLabel = GraphicsLabel(testText.format('graphicsview mode'))
        layout.addWidget(graphicsLabel)
        graphicsLabel.setAlignment(Qt.AlignRight|Qt.AlignBottom)

        graphicsLabelWidget = GraphicsLabelWidget(testText.format('graphicsview mode'))
        layout.addWidget(graphicsLabelWidget)
        graphicsLabelWidget.setAlignment(Qt.AlignRight|Qt.AlignBottom)
        graphicsLabelWidget.label.setStyleSheet('QLabel {background: green;}')


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    app.exec()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM