Centered text in QGraphicsView
-
So, I thought I've already solved it but no... Something so simple as just having centered text in QGraphicsView is seriously testing my sanity.
What I want:
Have text in QGraphicsView IN THE CENTER. That's all. The text should be wrappable and kept in center of the QGraphicsView even on resize of the window. User can load the image, in which case the text becomes hidden. User can remove the image, and then the scene returns to its original state.Now, I've tried endless options to make this but something was always off.
I apologize for a bit long MRE: I really cannot pinpoint the issue to some more specific location. In MRE, I can load dummy image and remove it, texts appears/disappears, but it's location seems pretty much random to me due to some Qt intrinsics.
I am on PySide6.
import sys from PySide6.QtWidgets import ( QApplication, QGraphicsView, QGraphicsScene, QGraphicsTextItem, QGraphicsPixmapItem, QVBoxLayout, QWidget, QPushButton, ) from PySide6.QtGui import ( QPixmap, QColor, QBrush, QFont, QTransform, QTextCursor, QTextBlockFormat, ) from PySide6.QtCore import Qt, QRectF class ImageViewer(QGraphicsView): SCALE_FACTOR = 1.25 def __init__(self, text="", parent=None): super().__init__(parent) self._zoom = 0 self._pinned = False self._empty = True self._placeholder_str = text self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setBackgroundBrush(QBrush(QColor(0, 0, 0))) self.setFrameShape(QGraphicsView.Shape.NoFrame) self.setCacheMode(QGraphicsView.CacheModeFlag.CacheNone) self._create_empty_scene() self.interaction_allowed = False def _create_empty_scene(self): self._scene = QGraphicsScene(self) self.setScene(self._scene) self.setSceneRect(0, 0, self.viewport().width(), self.viewport().height()) self._scene.setSceneRect( 0, 0, self.viewport().width(), self.viewport().height() ) self._photo = QGraphicsPixmapItem() self._photo.setShapeMode(QGraphicsPixmapItem.ShapeMode.BoundingRectShape) self._scene.addItem(self._photo) self._text = QGraphicsTextItem(self._placeholder_str) self._text.setVisible(True) self._text.setDefaultTextColor(QColor(255, 255, 255)) self._scene.addItem(self._text) font = QFont() font.setPointSize(18) self._text.setFont(font) self._center_text() def _center_text(self): width = max(0, self.viewport().width() - 20) height = max(0, self.viewport().height() - 20) self._scene.setSceneRect(QRectF(0, 0, width, height)) doc = self._text.document() doc.setTextWidth(-1) natural_width = doc.idealWidth() text_width = min(natural_width, width) doc.setTextWidth(text_width) cursor = QTextCursor(doc) block_format = QTextBlockFormat() block_format.setAlignment(Qt.AlignCenter) cursor.select(QTextCursor.Document) cursor.mergeBlockFormat(block_format) text_rect = self._text.boundingRect() scene_rect = self._scene.sceneRect() center_x = scene_rect.center().x() - text_rect.width() / 2 center_y = scene_rect.center().y() - text_rect.height() / 2 self._text.setPos(center_x, center_y) def setPixmap(self, pixmap=None): if pixmap and not pixmap.isNull(): self._empty = False self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) self._text.setVisible(False) self.setBackgroundBrush(Qt.NoBrush) prev_rect = self._photo.pixmap().rect() new_rect = pixmap.rect() prev_transform = self.transform() self._photo.setPixmap(pixmap) if not prev_rect.isNull() and not new_rect.isNull(): prev_size = prev_rect.size() new_size = new_rect.size() scale_x = new_size.width() / prev_size.width() scale_y = new_size.height() / prev_size.height() self.setTransform(prev_transform) self.scale(1 / scale_x, 1 / scale_y) self.interaction_allowed = True else: self._empty = True self.interaction_allowed = False self._create_empty_scene() self.setTransform(QTransform()) self.setZoomPinned(False) self.setDragMode(QGraphicsView.DragMode.NoDrag) self.setBackgroundBrush(QBrush(QColor(0, 0, 0))) self.resetView(ImageViewer.SCALE_FACTOR**self._zoom) def resetView(self, scale=1): rect = QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self.setSceneRect(rect) if (scale := max(1, scale)) == 1: self._zoom = 0 if self.hasPhoto(): unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = ( min( viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height(), ) * scale ) self.scale(factor, factor) if not self.zoomPinned(): self.centerOn(self._photo) def zoom(self, step): zoom = max(0, self._zoom + (step := int(step))) if zoom != self._zoom: self._zoom = zoom if self._zoom > 0: if step > 0: factor = ImageViewer.SCALE_FACTOR**step else: factor = 1 / ImageViewer.SCALE_FACTOR ** abs(step) self.scale(factor, factor) else: self.resetView() def wheelEvent(self, event): if self.interaction_allowed: delta = event.angleDelta().y() self.zoom(delta and delta // abs(delta)) def resizeEvent(self, event): super().resizeEvent(event) self.resetView() self._center_text() def toggleDragMode(self): if self.dragMode() == QGraphicsView.DragMode.ScrollHandDrag: self.setDragMode(QGraphicsView.DragMode.NoDrag) elif not self._photo.pixmap().isNull(): self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) def hasPhoto(self): return not self._empty def zoomPinned(self): return self._pinned def setZoomPinned(self, enable): self._pinned = bool(enable) class MainWindow(QWidget): def __init__(self): super().__init__() self.viewer = ImageViewer( "Placeholder Text Text Text Text Text Text Text Text Text Text" ) self.viewer.setMinimumSize(800, 600) self.load_button = QPushButton("Load Image") self.load_button.clicked.connect(self.load_image) self.unload_button = QPushButton("Remove Image") self.unload_button.clicked.connect(self.remove_image) layout = QVBoxLayout() layout.addWidget(self.viewer) layout.addWidget(self.load_button) layout.addWidget(self.unload_button) self.setLayout(layout) self.setWindowTitle("Qt Image Viewer") def load_image(self): pixmap = QPixmap(10000, 5000) pixmap.fill(QColor(255, 0, 0)) self.viewer.setPixmap(pixmap) def remove_image(self): self.viewer.setPixmap(None) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())
-
Hi,
You are missing
self.setSceneRect(0, 0, self.viewport().width(), self.viewport().height())
before calling
self._center_text()
inresizeEvent
. -
Hi,
You are missing
self.setSceneRect(0, 0, self.viewport().width(), self.viewport().height())
before calling
self._center_text()
inresizeEvent
.