'PyQt5 Stacking QWidgets as layers to decorate QGraphicsView

I am creating an application that reads the input of a webcam and updates the following QGraphicsView with QPixMaps. Additionally the QGraphicsView has a rubberband that is used to capture screenshots.

import logging
import os.path
import random
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QRect, QSize, QPoint, Qt
from PyQt5.QtGui import QMouseEvent, QPixmap, QImage, QRegion
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QRubberBand, QVBoxLayout, QLabel
import metaX.stylesheets as stylesheets

logger = logging.getLogger(__name__)


# TODO: maybe change the name of the module to video_output

def save_image(pixmap, photo_file_path):
    try:
        pixmap.save(photo_file_path)
    except OSError as e:
        logger.error(e)


class VideoContainerView(QGraphicsView):
    snapshot_taken = pyqtSignal(str)

    def __init__(self, mutex, wait_condition, config):
        super().__init__()
        self.mutex = mutex
        self.wait_condition = wait_condition
        self.config = config
        self.scene = self.set_scene()
        self.rubber_band = self.set_rubber_band()
        self.setMouseTracking(True)
        self._drawable = True
        self.set_ui()

    def set_scene(self):
        scene = QGraphicsScene()
        self.setScene(scene)
        return scene

    def set_rubber_band(self):
        rubber_band = QRubberBand(QRubberBand.Rectangle, self)
        # rubber_band.show()
        return rubber_band

    def set_ui(self):
        self.setStyleSheet("background-color: transparent;")


    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        self.draw_rubber_band(event)

    def mousePressEvent(self, event: QMouseEvent) -> None:
        self.take_snapshot()

    def draw_rubber_band(self, event):
        self.rubber_band.setGeometry(
            QRect(QPoint(event.x() - self.config.crop_area / 2,
                         event.y() - self.config.crop_area / 2),
                  QSize(self.config.crop_area, self.config.crop_area)).normalized())

    def take_snapshot(self):
        if self.rubber_band.isVisible():
            self.rubber_band.hide()
            selected_area = self.rubber_band.geometry()
            pixmap = self.grab(selected_area)
            photo_file_path = f'{self.config.captures_path()}/{random.randint(1, 10000000)}.png'
            photo_file_path = os.path.abspath((photo_file_path))
            save_image(pixmap, photo_file_path)
            self.rubber_band.show()
            self.snapshot_taken.emit(photo_file_path)

    @pyqtSlot(QImage)
    def update_frame(self, current_frame):
        if self._drawable:
            self.mutex.lock()
            try:
                current_frame = current_frame
                pixmap = QPixmap.fromImage(current_frame)
                self.scene.clear()
                self.resetTransform()
                self.scene.addPixmap(pixmap)
                self.scene.update()
            finally:
                self.mutex.unlock()
                self.wait_condition.wakeAll()

    @pyqtSlot()
    def toggle_rubberband(self):
        if self.rubber_band.isVisible():
            self.rubber_band.hide()
        else:
            self.rubber_band.show()

    def reset_scene(self):
        print('reset_scene')
        self.scene.clear()
        self.scene.addPixmap(QPixmap())
        self.scene.update()

    @pyqtSlot()
    def set_drawable(self, drawable):
        self._drawable = drawable
        print(f'The VideoContainerView is drawable:{self._drawable}')

Now I would like to decorate the UI using custom shapes drawn on Figma.

Using this image for example: https://imgur.com/a/SeqNuyE, I would like the QGraphicsScene to be shown "under" the inner rounded rectange, i.e. the center of the rounded rectangle should align with the center of the shown QPixmap and only the part of the QPixmap that fits into the size of the inner rectangle should be shown, i.e. the inner rounded rectangle should act something like a mask.

I am not familiar with manually drawing mechanisms so any code snippet would be highly appreciated!



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source