Skillversitas

The Graphics View Framework

The graphics view framework is Qt's architecture for the 2D scene that is larger than the viewport: the QGraphicsScene holds the items (the shapes, the images, the text, the custom-drawn objects) in a scene with its own coordinate system, the QGraphicsView is the viewport that shows a window onto the scene (the scroll, the zoom, the pan), and the items are the QGraphicsItem (the shape, the image, the text, the custom). The separation is the point: the scene is the model (the items, the coordinates, the interaction), the view is the presentation (the viewport, the transform, the rendering of the visible region), and the same scene can be shown by several views (the editor and the minimap). This chapter covers the scene and the view, the built-in items (the shape, the image, the text), the custom item (the QGraphicsItem subclass with the paint, the boundingRect), the scene's coordinates and the view's transform (the zoom, the pan), and the interaction (the item's mousePressEvent, the drag).

The Scene and the View

from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView,
                             QApplication)
from PyQt5.QtGui import (QGraphicsRectItem, QGraphicsEllipseItem,
                         QPen, QColor, QGraphicsPixmapItem)
from PyQt5.QtCore import QRectF, QPointF
import sys

# the scene: the MODEL - the items, the coordinates
scene = QGraphicsScene()
scene.setSceneRect(QRectF(0, 0, 2000, 2000))
# the sceneRect: the scene's extent (the scrollable area)
# - the scene is INFINITELY large by default; the sceneRect
# bounds it (the view's scroll range is the sceneRect)

# the view: the PRESENTATION - the viewport onto the scene
view = QGraphicsView(scene)
view.setRenderHint(QPainter.Antialiasing)
view.resize(600, 400)
# the view shows a WINDOW onto the scene; the scroll bars
# appear when the sceneRect exceeds the viewport

# the built-in items: the shape, the ellipse, the pixmap
rect = QGraphicsRectItem(QRectF(100, 100, 200, 150))
rect.setPen(QPen(QColor(200, 0, 0), 3))
rect.setBrush(QColor(255, 200, 200))
scene.addItem(rect)

ellipse = QGraphicsEllipseItem(QRectF(400, 100, 150, 150))
scene.addItem(ellipse)

# the SAME scene, a second view (the separation): the
# minimap, the preview, the split view - the scene is the
# data, the view is the window, and the two views show the
# same items (the editor + the overview)
view2 = QGraphicsView(scene)
view2.show()
view.show()

The QGraphicsScene is the model: the items, the scene's coordinate system, and the setSceneRect (the scene's extent, the scrollable area - the scene is infinitely large by default, and the sceneRect bounds it, which is the view's scroll range). The QGraphicsView is the presentation: the viewport that shows a window onto the scene, with the scroll (the sceneRect larger than the viewport), the zoom (the next section's transform), and the render hints (the Antialiasing). The built-in items are the QGraphicsRectItem, the QGraphicsEllipseItem, the QGraphicsPixmapItem (the image), the QGraphicsTextItem (the text), and the QGraphicsPathItem (the path) - the setPen/setBrush for the style, the addItem to the scene. The separation is the point: the scene is the data, the view is the window, and the same scene can be shown by several views (the editor and the minimap, the split view) - the QGraphicsView(scene) is the attachment, and the two views render the same items from the same scene. The graphics view is the framework for the 2D content that is larger than the screen, and the scene/view split is the architecture that makes the zoom and the pan and the multiple views possible.

The Custom Item

from PyQt5.QtGui import (QGraphicsItem, QPainter, QPen, QColor)
from PyQt5.QtCore import QRectF, QPointF
import math

# the custom item: the QGraphicsItem subclass - the paint
# (the draw), the boundingRect (the item's extent), and the
# optional mouse / the drag
class HandItem(QGraphicsItem):
    def __init__(self, angle=0.0):
        super().__init__()
        self.angle = angle
        self.setFlag(QGraphicsItem.ItemIsMovable)   # the
        # draggable (the scene's drag, the built-in)

    def boundingRect(self):
        # the item's extent (the scene coordinates) - the
        # clip for the paint, the hit-test area
        return QRectF(-90, -90, 180, 180)

    def paint(self, painter, option, widget=None):
        # the draw: the painter is on the item, the item's
        # coordinates (the 0,0 is the item's position in the
        # scene) - the SAME discipline as the paintEvent:
        # the pure function of the state (the angle)
        painter.setPen(QPen(QColor(0, 0, 0), 3))
        painter.drawLine(QPointF(0, 0),
            QPointF(80 * math.cos(math.radians(self.angle)),
                    -80 * math.sin(math.radians(self.angle))))

    def setAngle(self, a):
        self.angle = a
        self.update()     # the item's repaint (the region
                          # the boundingRect)

# the item in the scene: the scene coordinates (the item's
# position is the setPos / the scene's placeItem)
# scene.addItem(HandItem()); item.setPos(1000, 1000)
# the item's coordinate is the scene's; the item's 0,0 is
# its position, and the paint is relative to it

The custom item is the QGraphicsItem subclass with the three methods: the boundingRect (the item's extent in the scene coordinates - the clip for the paint and the hit-test area), the paint (the draw - the QPainter on the item, the item's coordinates with the 0,0 at the item's position, the same discipline as the paintEvent: the pure function of the state), and the flags (the ItemIsMovable for the built-in drag, the ItemIsSelectable for the selection). The update is the item's repaint (the boundingRect region), and the setPos is the item's position in the scene. The item's coordinate is the scene's - the item's 0,0 is its position, and the paint is relative to it, which is the separation that makes the item movable and the scene the model: the item's paint does not know where it is in the scene, and the scene places it. The interaction is the item's mouse events (the mousePressEvent, the mouseMoveEvent, the mouseReleaseEvent - the item's own drag, the custom), and the built-in ItemIsMovable is the scene's drag for the simple case. The custom item is where the graphics view's built-in items end and the app's own 2D objects begin, and the boundingRect/paint/update are the pattern for every one of them.

The View's Transform: The Zoom and the Pan

from PyQt5.QtGui import QGraphicsView
from PyQt5.QtCore import Qt

# the view's TRANSFORM: the scene -> viewport mapping
# (the zoom is the scale, the pan is the scroll)

# the zoom: the scale (the wheel event, the built-in):
class ZoomView(QGraphicsView):
    def wheelEvent(self, event):
        # the zoom factor per wheel notch
        factor = 1.15 if event.angleDelta().y() > 0 else 1/1.15
        self.scale(factor, factor)
        # the scale is the view's transform: the scene is
        # unchanged, the view's window onto it is scaled
        # (the ZOOM) - the scene's coordinates are the model,
        # the view's transform is the presentation

    # the fit: the view the scene to the viewport
    def fit(self):
        self.fitInView(self.scene().sceneRect(),
                       Qt.KeepAspectRatio)
        # the fitInView: the transform that shows the whole
        # sceneRect in the viewport (the overview, the
        # "the zoom to fit")

# the pan: the SCROLL (the built-in, the scroll bars) or
# the DRAG (the custom, the scrollHandDrag / the
# ScrollHandDrag mode):
# view.setDragMode(QGraphicsView.ScrollHandDrag)
# the ScrollHandDrag: the drag pans the view (the hand) -
# the pan without the scroll bars (the canvas's drag-pan)

# the view's coordinate map: the mapToScene / the
# mapFromScene (the viewport -> scene, the scene ->
# viewport) - the click's position in the SCENE (the item
# hit-test, the place the new item)
# scene_pos = view.mapToScene(event.pos())
# viewport_pos = view.mapFromScene(scene_pos)

# the transform is the PRESENTATION: the scene (the model)
# is unchanged by the zoom / the pan; the view's transform
# is the window's mapping, and the multiple views of the
# same scene each have their own transform (the editor
# zoomed, the minimap fitted) - the separation holds

The view's transform is the scene-to-viewport mapping, and it is the presentation - the scene (the model) is unchanged by the zoom or the pan, and the view's transform is the window's mapping. The zoom is the scale (the wheelEvent that scales the view by the factor per notch - the scene is unchanged, the view's window onto it is scaled), and the fitInView is the transform that shows the whole sceneRect in the viewport (the overview, the zoom-to-fit). The pan is the scroll (the built-in, the scroll bars when the sceneRect exceeds the viewport) or the drag (the setDragMode(ScrollHandDrag) - the drag pans the view, the canvas's drag-pan without the scroll bars). The coordinate map is the mapToScene/mapFromScene (the viewport to the scene, the scene to the viewport) - the click's position in the scene (the item's hit-test, the place the new item) is the mapToScene(event.pos()). The transform is the presentation, and the multiple views of the same scene each have their own transform (the editor zoomed, the minimap fitted) - the scene/view separation holds through the zoom and the pan, which is the point of the framework: the content is the scene, the window is the view, and the mapping is the transform the view owns.

The Working Rules

  • The scene is the model, the view is the presentation, the QGraphicsView(scene) is the attachment. The scene holds the items and the coordinates; the view is the window onto it; the same scene can back several views (the editor, the minimap) - the content is the scene's, the window is the view's.
  • The setSceneRect bounds the scene (the scroll range). The scene is infinitely large by default; the sceneRect is the extent, and the view's scroll appears when it exceeds the viewport - the bound the layout needs.
  • The custom item is the boundingRect + the paint + the update. The boundingRect is the extent (the clip, the hit-test), the paint is the pure function of the state (the item's coordinates, the 0,0 the position), the update is the repaint - the pattern the paintEvent chapter's discipline applies to the item.
  • The zoom is the view's scale, the pan is the scroll or the ScrollHandDrag. The transform is the presentation - the scene is unchanged, the view's mapping is the zoom/pan, and the fitInView is the zoom-to-fit. The mapToScene/mapFromScene is the coordinate map the interaction uses.
  • The item's flags are the interaction. The ItemIsMovable is the built-in drag, the ItemIsSelectable is the selection, and the item's mouse events (mousePressEvent, the mouseMoveEvent) are the custom interaction - the flags for the simple, the events for the app-specific.

Summary

The graphics view framework is the 2D scene larger than the viewport: the QGraphicsScene is the model (the items, the scene coordinates, the setSceneRect the extent), the QGraphicsView is the presentation (the viewport, the transform, the render of the visible region), and the QGraphicsView(scene) is the attachment - the same scene can back several views (the editor, the minimap). The built-in items are the QGraphicsRectItem, the QGraphicsEllipseItem, the QGraphicsPixmapItem, the QGraphicsTextItem (the setPen/setBrush the style), and the custom item is the QGraphicsItem subclass with the boundingRect (the extent, the clip, the hit-test), the paint (the pure function of the state, the item's coordinates), and the update (the repaint). The view's transform is the presentation: the scale is the zoom (the scene unchanged, the view's mapping scaled), the fitInView is the zoom-to-fit, the scroll or the ScrollHandDrag is the pan, and the mapToScene/mapFromScene is the coordinate map the interaction uses. The item's flags (ItemIsMovable, the ItemIsSelectable) are the built-in interaction, and the item's mouse events are the custom. The separation is the point: the content is the scene, the window is the view, the mapping is the transform the view owns, and the multiple views of the same scene each have their own window onto the same content.