How To implementing multi layer frame or widget in Qt?

Viewed 1353

How can I implement this in Qt, I am using PyQt and Qt Designer but confusing how to use layering like in the following image.

enter image description here

  • I want to first layer in index 0 to show the image or video,
  • then the second layer in index 2 is media control which can hide when no move from mouse,
  • and layer 3 in index 3 when change the volume in example
  • layer 4 in index 4 for notification etc.

is possible to do this in Qt?

2 Answers

This is one of the very few cases for which using a layout is not suggested, as the visible widgets are "floating" and should be possibly moved around.

The solution is to create a "container" widget that has all those controls as children.

Then, some widgets will need some repositioning and resizing (for instance, the control bar should always be on bottom and occupy the whole width), and that can be implemented in the resizeEvent().

Screenshot of the code below

from PyQt5 import QtCore, QtGui, QtWidgets

class ControlBar(QtWidgets.QFrame):
    def __init__(self, parent):
        super().__init__(parent)
        layout = QtWidgets.QVBoxLayout(self)
        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        layout.addWidget(self.slider)
        buttons = QtWidgets.QHBoxLayout()
        layout.addLayout(buttons)
        buttons.addWidget(QtWidgets.QToolButton(text='play'))
        buttons.addWidget(QtWidgets.QToolButton(text='stop'))
        buttons.addStretch()


class VolumeWidget(QtWidgets.QFrame):
    def __init__(self, parent):
        super().__init__(parent)
        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(2, 2, 2, 0)
        layout.setSpacing(1)
        handle = QtWidgets.QFrame()
        handle.setFixedHeight(12)
        handle.setStyleSheet('''
            QFrame {
                border: 1px solid darkGray;
                border-radius: 2px;
                background: #aa646464;
            }
        ''')
        layout.addWidget(handle)
        volumeLayout = QtWidgets.QHBoxLayout()
        layout.addLayout(volumeLayout)
        for i in range(4):
            volumeLayout.addWidget(QtWidgets.QSlider(QtCore.Qt.Vertical))

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.startPos = event.pos()

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            delta = event.pos() - self.startPos
            self.move(self.pos() + delta)


class Notification(QtWidgets.QFrame):
    def __init__(self, parent):
        super().__init__(parent)
        layout = QtWidgets.QHBoxLayout(self)
        self.label = QtWidgets.QLabel('Notification', alignment=QtCore.Qt.AlignCenter)
        layout.addWidget(self.label)


class PlayerWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.video = QtWidgets.QLabel(self)
        self.video.setPixmap(QtGui.QPixmap('movie.png'))
        self.video.setScaledContents(True)
        self.controlBar = ControlBar(self)
        self.notification = Notification(self)
        self.volumeWidget = VolumeWidget(self)
        self.volumeWidget.move(30, 30)

        self.setStyleSheet('''
            VolumeWidget, ControlBar {
                border: 1px outset darkGray;
                border-radius: 4px;
                background: #aad3d3d3;
            }
            VolumeWidget:hover, ControlBar:hover {
                background: #d3d3d3;
            }
            Notification {
                border: 1px outset darkGray;
                border-radius: 4px;
                background: #aa242424;
            }
            Notification QLabel {
                color: white;
            }
        ''')

    def sizeHint(self):
        if self.video.pixmap() and not self.video.pixmap().isNull():
            return self.video.pixmap().size()
        return QtCore.QSize(640, 480)

    def resizeEvent(self, event):
        # set the geometry of the "video"
        videoRect = QtCore.QRect(
            QtCore.QPoint(), 
            self.video.sizeHint().scaled(self.size(), QtCore.Qt.KeepAspectRatio))
        videoRect.moveCenter(self.rect().center())
        self.video.setGeometry(videoRect)

        # control panel
        controlHeight = self.controlBar.sizeHint().height()
        controlRect = QtCore.QRect(0, self.height() - controlHeight, 
            self.width(), controlHeight)
        self.controlBar.setGeometry(controlRect)

        # notification
        notificationWidth = max(self.notification.sizeHint().width(), self.width() * .6)
        notificationRect = QtCore.QRect(
            (self.width() - notificationWidth) * .5, 20, 
            notificationWidth, self.notification.sizeHint().height()
        )
        self.notification.setGeometry(notificationRect)

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.fillRect(self.rect(), QtCore.Qt.black)

Note that in this simple case I only used an image. If you want to play video, you should use the Qt Multimedia module, and in order to correctly have transparency of controls a QGraphicsView with a QGraphicsVideoItem must be used.
In this case, PlayerWidget can directly be a subclass of QGraphicsView.

There is a simple way to implement "layers" - you can add child widgets without any layout and resize/move them on resize event of host widget (using event filter). First layer can be organized with any layout as usual.

from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt

class Layer(QtCore.QObject):
    def __init__(self, host, child, alignment = Qt.AlignLeft, setWidth = False, setHeight = False, parent = None):
        super().__init__(parent)
        self._host = host
        self._child = child
        self._alignment = alignment
        self._setWidth = setWidth
        self._setHeight = setHeight
        child.setParent(host)
        host.installEventFilter(self)

    def eventFilter(self, watched, event):
        if watched != self._host or event.type() != QtCore.QEvent.Resize:
            return False
        hostSize = event.size()
        childSize = self._child.sizeHint()
        alignment = self._alignment
        x = 0
        y = 0
        dWidth = max(0, hostSize.width() - childSize.width())
        dHeight = max(0, hostSize.height() - childSize.height())
        if alignment & Qt.AlignRight:
            x = dWidth
        elif alignment & Qt.AlignHCenter:
            x = dWidth / 2
        if alignment & Qt.AlignVCenter:
            y = dHeight / 2
        elif alignment & Qt.AlignBottom:
            y = dHeight
        width = hostSize.width() if self._setWidth else childSize.width()
        height = hostSize.height() if self._setHeight else childSize.height()
        self._child.setGeometry(x, y, width, height)
        return False

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    widget = QtWidgets.QWidget()
    label1 = QtWidgets.QLabel("right label")
    label2 = QtWidgets.QLabel("bottom label")
    layer1 = Layer(widget, label1, Qt.AlignRight)
    layer2 = Layer(widget, label2, Qt.AlignBottom | Qt.AlignHCenter, True)
    widget.show()
    sys.exit(app.exec_())
Related