3

I am trying to achieve something like this in PySide: https://codepen.io/imprakash/pen/GgNMXO What I want to do is create a child window frameless with a black overlay below.

I didn't succeed to create a child window frameless and the overlay...

This is a base code to replicate the HTML:

from PySide import QtCore, QtGui
import sys

class MainWindow(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.resize(800, 500)

        self.button = QtGui.QPushButton("Click Me")

        self.setLayout(QtGui.QVBoxLayout())
        self.layout().addWidget(self.button)

        # Connections
        self.button.clicked.connect(self.displayOverlay)


    def displayOverlay(self):
        popup = QtGui.QDialog(self)
        popup.setWindowFlags(QtCore.Qt.FramelessWindowHint)
        popup.setLayout(QtGui.QHBoxLayout())
        popup.layout().addWidget(QtGui.QLabel("HI"))
        popup.show()
        print "clicked"

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

If you comment the line with the FramelessWindowHint, the window comes, else nothing happen...

I really hope that someone could help me. Thank you for the time you spent to read my question.

Benoît Valdes
  • 71
  • 1
  • 1
  • 6
  • Its possible to replicate exactly the Codepen example but it will be a bit more complicated than just a couple instructions over a minimal example. Build a QWidget and set it with FramelessWindowHint. Overload the paintEvent to draw the translucent background (hmm..., WA_TranslucentBackground may be required). This widget will completely overlay the parent so also draw the popup box (drawRect in QPainter). Add button in correct position without layout. Overload resize to make sure button is always in the correct position. Overload parent resize to automatically resize popup if open. – armatita May 30 '17 at 16:05
  • Today is no good but If I see you won't get an answer to your problem I'll put up a small example to make my explanation a bit more clearer. Best of luck. – armatita May 30 '17 at 16:05
  • Waw thank for the explanation, I never took a look to Painter so it's obscure for me... I'm gonna try what you said but I will get your example too when you'll post it :) Thank you! – Benoît Valdes May 31 '17 at 07:35
  • The paintEvent is just a way for you to manually control the visuals of your widget. In this particular case you can use it to trick the user into seeing a disabled darkened parent widget and a cute popup. I've added a snippet with my recommendation. Hopefully it will be enough to get you started. – armatita May 31 '17 at 09:32

4 Answers4

15

I'll be using PyQt5 for this explanation. It might have some differences to PySide (which I'm not sure if its still maintained) and PyQt4, but it shouldn't be too hard to convert.

The following example has a parent widget which a few buttons. One of them (the obvious one) calls for the popup. I've prepared the example to deal with the parent resize but have not made any code regarding mouse events of dragging the popup (see mouseMoveEvent and mouseReleaseEvent for that).

So here is the code:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui

class TranslucentWidgetSignals(QtCore.QObject):
    # SIGNALS
    CLOSE = QtCore.pyqtSignal()

class TranslucentWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(TranslucentWidget, self).__init__(parent)

        # make the window frameless
        self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)

        self.fillColor = QtGui.QColor(30, 30, 30, 120)
        self.penColor = QtGui.QColor("#333333")

        self.popup_fillColor = QtGui.QColor(240, 240, 240, 255)
        self.popup_penColor = QtGui.QColor(200, 200, 200, 255)

        self.close_btn = QtWidgets.QPushButton(self)
        self.close_btn.setText("x")
        font = QtGui.QFont()
        font.setPixelSize(18)
        font.setBold(True)
        self.close_btn.setFont(font)
        self.close_btn.setStyleSheet("background-color: rgb(0, 0, 0, 0)")
        self.close_btn.setFixedSize(30, 30)
        self.close_btn.clicked.connect(self._onclose)

        self.SIGNALS = TranslucentWidgetSignals()

    def resizeEvent(self, event):
        s = self.size()
        popup_width = 300
        popup_height = 120
        ow = int(s.width() / 2 - popup_width / 2)
        oh = int(s.height() / 2 - popup_height / 2)
        self.close_btn.move(ow + 265, oh + 5)

    def paintEvent(self, event):
        # This method is, in practice, drawing the contents of
        # your window.

        # get current window size
        s = self.size()
        qp = QtGui.QPainter()
        qp.begin(self)
        qp.setRenderHint(QtGui.QPainter.Antialiasing, True)
        qp.setPen(self.penColor)
        qp.setBrush(self.fillColor)
        qp.drawRect(0, 0, s.width(), s.height())

        # drawpopup
        qp.setPen(self.popup_penColor)
        qp.setBrush(self.popup_fillColor)
        popup_width = 300
        popup_height = 120
        ow = int(s.width()/2-popup_width/2)
        oh = int(s.height()/2-popup_height/2)
        qp.drawRoundedRect(ow, oh, popup_width, popup_height, 5, 5)

        font = QtGui.QFont()
        font.setPixelSize(18)
        font.setBold(True)
        qp.setFont(font)
        qp.setPen(QtGui.QColor(70, 70, 70))
        tolw, tolh = 80, -5
        qp.drawText(ow + int(popup_width/2) - tolw, oh + int(popup_height/2) - tolh, "Yep, I'm a pop up.")

        qp.end()

    def _onclose(self):
        print("Close")
        self.SIGNALS.CLOSE.emit()


class ParentWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ParentWidget, self).__init__(parent)

        self._popup = QtWidgets.QPushButton("Gimme Popup!!!")
        self._popup.setFixedSize(150, 40)
        self._popup.clicked.connect(self._onpopup)

        self._other1 = QtWidgets.QPushButton("A button")
        self._other2 = QtWidgets.QPushButton("A button")
        self._other3 = QtWidgets.QPushButton("A button")
        self._other4 = QtWidgets.QPushButton("A button")

        hbox = QtWidgets.QHBoxLayout()
        hbox.addWidget(self._popup)
        hbox.addWidget(self._other1)
        hbox.addWidget(self._other2)
        hbox.addWidget(self._other3)
        hbox.addWidget(self._other4)
        self.setLayout(hbox)

        self._popframe = None
        self._popflag = False

    def resizeEvent(self, event):
        if self._popflag:
            self._popframe.move(0, 0)
            self._popframe.resize(self.width(), self.height())

    def _onpopup(self):
        self._popframe = TranslucentWidget(self)
        self._popframe.move(0, 0)
        self._popframe.resize(self.width(), self.height())
        self._popframe.SIGNALS.CLOSE.connect(self._closepopup)
        self._popflag = True
        self._popframe.show()

    def _closepopup(self):
        self._popframe.close()
        self._popflag = False


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    main = ParentWidget()
    main.resize(500, 500)
    main.show()
    sys.exit(app.exec_())

Which results in the following:

Parent widget for a PyQt5 experiment Creating a popup in PyQt5

The logic is the following. You create an empty Widget and manually draw the background and popup (paintEvent). You add a button for closing the popup. For this you build a Signal and let the parent widget do the closing. This is important because you need to make the parent widget control some important elements of the popup (such as closing, resizng, etc.). You can add far more complexity but hopefully the example will suffice for starters.

armatita
  • 12,825
  • 8
  • 48
  • 49
4

Thanks to armatita, I succeed to get what I wanted. For now, there are some issues but it works and I get the result that I wanted.

I give you the code to the next who will be looking for the same thing.

from PySide import QtCore, QtGui
import sys

class CtmWidget(QtGui.QWidget):
    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self, parent)

        self.button = QtGui.QPushButton("Close Overlay")
        self.setLayout(QtGui.QHBoxLayout())
        self.layout().addWidget(self.button)

        self.button.clicked.connect(self.hideOverlay)

    def paintEvent(self, event):

        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        path = QtGui.QPainterPath()
        path.addRoundedRect(QtCore.QRectF(self.rect()), 10, 10)
        mask = QtGui.QRegion(path.toFillPolygon().toPolygon())
        pen = QtGui.QPen(QtCore.Qt.white, 1)
        painter.setPen(pen)
        painter.fillPath(path, QtCore.Qt.white)
        painter.drawPath(path)
        painter.end()

    def hideOverlay(self):
        self.parent().hide()



class Overlay(QtGui.QWidget):
    def __init__(self, parent, widget):
        QtGui.QWidget.__init__(self, parent)
        palette = QtGui.QPalette(self.palette())
        palette.setColor(palette.Background, QtCore.Qt.transparent)
        self.setPalette(palette)

        self.widget = widget
        self.widget.setParent(self)


    def paintEvent(self, event):
        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.fillRect(event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127)))
        painter.end()

    def resizeEvent(self, event):
        position_x = (self.frameGeometry().width()-self.widget.frameGeometry().width())/2
        position_y = (self.frameGeometry().height()-self.widget.frameGeometry().height())/2

        self.widget.move(position_x, position_y)
        event.accept()

class MainWindow(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.resize(800, 500)

        self.button = QtGui.QPushButton("Click Me")

        self.setLayout(QtGui.QVBoxLayout())
        self.layout().addWidget(self.button)
        self.popup = Overlay(self, CtmWidget())
        self.popup.hide()

        # Connections
        self.button.clicked.connect(self.displayOverlay)

    def displayOverlay(self):
        self.popup.show()
        print "clicked"

    def resizeEvent(self, event):
        self.popup.resize(event.size())
        event.accept()

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

Once again thank you both of you(ymmx and armatita) to spend time on my issue.

Benoît Valdes
  • 71
  • 1
  • 1
  • 6
0

did you try replacing popup.show() by popup.exec_()? and remove self as a parameter of the Qdialog? I change QDialog to QmessageBox to be able to quit the subwindow but it still work with the QDialog.

popup =  QMessageBox()
popup.setWindowFlags( Qt.FramelessWindowHint)
popup.setLayout( QHBoxLayout())
popup.layout().addWidget( QLabel("HI"))
popup.exec_()

update

class Popup(QDialog  ):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(  Qt.CustomizeWindowHint)
        self.setLayout( QHBoxLayout())
        Button_close = QPushButton('close')
        self.layout().addWidget( QLabel("HI"))
        self.layout().addWidget( Button_close)
        Button_close.clicked.connect( self.close )
        self.exec_()
        print("clicked") 

    def mousePressEvent(self, event):
        self.oldPos = event.globalPos()

    def mouseMoveEvent(self, event):
        delta = QPoint (event.globalPos() - self.oldPos)
        #print(delta)
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.oldPos = event.globalPos()



class MainWindow( QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.resize(800, 500)

        self.button =  QPushButton("Click Me")

        self.setLayout( QVBoxLayout())
        self.layout().addWidget(self.button)

        # Connections
        self.button.clicked.connect(self.displayOverlay)


    def displayOverlay(self):
        Popup( )


if __name__ == "__main__":
    app =  QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
ymmx
  • 4,769
  • 5
  • 32
  • 64
  • Nice start, the issue is that I can't delete it, move it (with code, to follow the main window) or something else once created... – Benoît Valdes May 30 '17 at 15:08
  • you can't drag the window or close it because you use this property: popup.`setWindowFlags( Qt.FramelessWindowHint)` – ymmx May 31 '17 at 06:29
  • If you want drag or other property you have to specify them all. – ymmx May 31 '17 at 06:30
  • to drag the popup, i think you'll have to make a new class and add mousepressevent and mousemoveevent as suggest here https://stackoverflow.com/questions/37718329/pyqt5-draggable-frameless-window – ymmx May 31 '17 at 07:14
  • I know that I can't drag it because there is no title bar to drag but I specified in the comment that I tried by code so it should work with code to do a simple popup.move(where I want) – Benoît Valdes May 31 '17 at 07:37
-1

The example below displays a panel with a couple of widgets with buttons. Once a button is clicked, a semi-transparent blocking layer with an overlay is shown on top of the corresponding parent widget. The overlay is self-contained, reacts to the changes of the parent's size, and renders a simple text in the middle.

It's an updated and polished implementation inspired by @armatita's solution. The code has been tested using qtpy abstract layer with PySide2 installed:

import sys
from typing import Tuple

from qtpy.QtCore import QEvent, QRect, Qt
from qtpy.QtGui import QColor, QPainter, QPalette
from qtpy.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class TextOverlayWidget(QWidget):
    def __init__(
            self,
            parent: QWidget,
            text: str,
            overlay_width: int = 300,
            overlay_height: int = 150,
    ):
        super().__init__(parent)

        self._text = text

        self._overlay_width = overlay_width
        self._overlay_height = overlay_height

        self._TRANSPARENT_COLOR = QColor(0, 0, 0, 0)
        self._WINDOW_BACKGROUND_COLOR = QColor(25, 25, 25, 125)
        self._OVERLAY_BACKGROUND_COLOR = self.palette().color(QPalette.Base)

        parent.installEventFilter(self)

        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground)

        self._add_close_button()

        self._resize_to_parent()

    def _add_close_button(self):
        self._close_button = QPushButton(self)
        self._close_button.setText("x")
        self._close_button.setFixedSize(30, 30)

        font = self._close_button.font()
        font.setPixelSize(15)
        self._close_button.setFont(font)

        self._close_button.clicked.connect(lambda: self.close())

    def eventFilter(self, obj, event) -> bool:
        if event.type() == QEvent.Resize:
            self._resize_to_parent()

        return super().eventFilter(obj, event)

    def _resize_to_parent(self):
        self.move(0, 0)
        self.resize(self.parent().width(), self.parent().height())

        overlay_corner_width, overlay_corner_height = self._get_overlay_corner()
        self._close_button.move(
            overlay_corner_width + self._overlay_width - self._close_button.width(),
            overlay_corner_height,
        )

    def _get_window_size(self) -> Tuple[int, int]:
        size = self.size()
        return size.width(), size.height()

    def _get_overlay_corner(self) -> Tuple[int, int]:
        width, height = self._get_window_size()
        overlay_corner_width = int(width / 2 - self._overlay_width / 2)
        overlay_corner_height = int(height / 2 - self._overlay_height / 2)
        return overlay_corner_width, overlay_corner_height

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)

        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setPen(self._TRANSPARENT_COLOR)
        painter.setBrush(self._WINDOW_BACKGROUND_COLOR)
        width, height = self._get_window_size()
        painter.drawRect(0, 0, width, height)

        painter.setPen(self._TRANSPARENT_COLOR)
        painter.setBrush(self._OVERLAY_BACKGROUND_COLOR)
        rounding_radius = 5
        overlay_rectangle = QRect(
            *self._get_overlay_corner(), self._overlay_width, self._overlay_height
        )
        painter.drawRoundedRect(overlay_rectangle, rounding_radius, rounding_radius)

        font = self.font()
        font.setPixelSize(20)
        painter.setFont(font)
        painter.setPen(QColor(0, 0, 0))
        painter.drawText(overlay_rectangle, Qt.AlignCenter, self._text)

        painter.end()


class MainWindow(QWidget):
    def __init__(self, parent: QWidget = None):
        super().__init__(parent)

        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        layout.addWidget(self._create_button_widget("Hello!", 200, False))
        layout.addWidget(self._create_button_widget("Hola!", 200, False))
        layout.addWidget(
            self._create_button_widget("Hi in the whole window!", 400, True)
        )
        self.setLayout(layout)

    def _create_button_widget(
            self, text: str, overlay_width: int, show_on_full_window: bool
    ):
        widget = QWidget()
        widget.setMinimumWidth(300)
        widget.setMinimumHeight(500)

        button = QPushButton(f"Say '{text}'")
        overlay_parent = self if show_on_full_window else widget
        button.clicked.connect(
            lambda: TextOverlayWidget(overlay_parent, text, overlay_width).show()
        )

        layout = QHBoxLayout()
        layout.addWidget(button)
        widget.setLayout(layout)

        return widget


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = MainWindow()
    main.show()
    sys.exit(app.exec_())

Sample text overlay

The example above is available in the GitHub repository: https://github.com/machur/qt-extra-widgets/blob/main/examples/text_overlay_widget_example.py