Search code examples
pythongraphicspyqt6

How to get a boundingRect of a QGraphicsItem based on arbitrary text length


I am subclassing QGraphicsItem in order to create a map marker which draws an icon (based on a constant dictionary of polygon points) and a text label.

The boundingRect method below defines a fixed bounding box rectangle. But since the text label can be of arbitrary length, I need the bounding box to be different for each marker instance (e.g. for selecting and focusing).

How can I get the bounding rectangle of each marker instance, based on the actual pixel width the characters occupy on the screen? Do I need to create a manual estimation based on font size and character count? Or can Qt calculate this automatically?

import sys

from PySide6 import QtCore, QtWidgets
from PySide6.QtWidgets import (
    QApplication, QWidget, QHBoxLayout, QVBoxLayout,
    QGraphicsScene, QGraphicsView, QGraphicsItem
    )
from PySide6.QtGui import QColor, QFont, QPen, QBrush, QPolygon
from PySide6.QtCore import QPointF, Qt, QRectF, QPoint

MARKERS = {
    'peak': {'color': 'limegreen', 'shape': 'star'},
    'tag':  {'color': 'bisque', 'shape': 'x'},
    'pole': {'color': 'coral', 'shape': 'circle'}
    }

SHAPES = {
    'star': [(-1, -1), (0, -5), (1, -1), (5, 4), (0, 1), (-5, 4), (-1, -1)],
    'x': [(0, -2), (3, -5), (5, -3), (2, 0), (5, 3),
          (3, 5), (0, 2), (-3, 5), (-5, 3), (-2, 0), (-5, -3), (-3, -5)]
    }


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('Map')

        self.scene = MapScene()
        self.view = QGraphicsView(self.scene)

        # Main layout
        layout = QHBoxLayout()
        layout.addWidget(self.view)
        self.setLayout(layout)


class MapMarker(QGraphicsItem):
    def __init__(self, marker, label):
        super().__init__()
        self.marker = marker
        self.label = label

        # Build QPolygon dictionary from SHAPES coordinates
        self.shapes = {}
        for k, v in SHAPES.items():
            self.shapes[k] = QPolygon([QPoint(*point)for point in SHAPES[k]])

        # Set marker color
        self.color = QColor(MARKERS[self.marker]['color'])

    def paint(self, painter, option, widget):
        brush = QBrush(Qt.SolidPattern)
        brush.setColor(self.color)
        painter.setPen(QPen(self.color))
        painter.setBrush(brush)

        # Draw marker shape
        shape_name = MARKERS[self.marker]['shape']
        if shape_name == 'circle':
            painter.drawEllipse(-4, -4, 8, 8)
        else:
            painter.drawPolygon(self.shapes[shape_name])

        # Draw marker label
        painter.drawText(10, -2, self.label)

    def boundingRect(self):
        return QRectF(-10, -15, 200, 30)


class MapScene(QGraphicsScene):

    def __init__(self):
        super().__init__()
        markers = [
            (10, 10, 'peak', 'Some peak'),
            (20, 30, 'tag', 'Some tag with a longer name'),
        ]

        # Draw markers
        for x, y, m, b in markers:
            marker = MapMarker(m, b)
            marker.setPos(x, y)
            self.addItem(marker)


if __name__ == '__main__':
    app = QApplication([])
    widget = MainWindow()
    widget.show()

    sys.exit(app.exec())


Solution

  • You can use QFontMetrics or QFontMetricsF (import from QtGui) to get the boundingRect of the text, and unite with the boundingRect of the icon.

    def boundingRect(self):
        shape_name = MARKERS[self.marker]['shape']
        if shape_name == 'circle':
            icon_rect = QRectF(-4, -4, 8, 8)
        else:
            icon_rect = QRectF(self.shapes[shape_name].boundingRect())
            
        text_rect = QFontMetricsF(QFont()).boundingRect(self.label).translated(10, -2)
        
        return icon_rect | text_rect
    

    I added these lines at the end of paint to verify the area of the rect in the photo:

    painter.setPen(QPen(Qt.black, 1, Qt.DashLine))
    painter.setBrush(Qt.NoBrush)
    painter.drawRect(self.boundingRect())
    

    enter image description here