Search code examples
pythonqtpyqt5pyside2qt-quick

Can FastBlur blur everything behind it?


I have this code, qml

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.0
ApplicationWindow {
    property var theme: String("#ffffff")
    property var focusColor: String('transparent')
    id: applicationWindow
    visible: false                                      
    width: 600
    height:600
    Image {
        id: image_bug
        anchors.fill: parent
        source: "im.png"                                
    }   
    Rectangle {
    width: 100; height: 600
    color: "green"
   Text {
    id: helloText
    text: "Hello world!"
    anchors.verticalCenter: parent.verticalCenter
    anchors.horizontalCenter: parent.horizontalCenter
    font.pointSize: 10; font.bold: true
}
    MouseArea {
        anchors.fill: parent
        onClicked: { effectSource.width = 1200; effectSource.height = 1200;}
    }
}
    ShaderEffectSource {
        id: effectSource
        sourceItem: image_bug
        anchors.centerIn: image_bug
        width: 300 
        height: 300 
        sourceRect: Qt.rect(x,y, width, height)
    }
    FastBlur{
        id: blur
        anchors.fill: effectSource
        source: effectSource
        radius: 100
    }
}

PyQT5 or Pyside2

import sys
import os                                                            # +++
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QVBoxLayout, QLabel
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtCore import (QCoreApplication, QMetaObject, QObject, QPoint,
    QRect, QSize, QUrl, Qt)
    '''
    from PySide2.QtCore import Qt, QUrl
    from PySide2.QtWidgets import QApplication, QWidget, QMainWindow, QVBoxLayout, QLabel
    from PySide2.QtQml import QQmlApplicationEngine
    from PySide2.QtCore import (QCoreApplication, QMetaObject, QObject, QPoint,
        QRect, QSize, QUrl, Qt)
    '''
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
class GUI_MainWindow(QMainWindow):
    def __init__(self, widget, parent=None):
        QMainWindow.__init__(self, parent)
        self.setWindowTitle('GUI_MainWindow')
        self.resize(600, 600)
        self.widget = widget
        centralWidget = QWidget()
        self.setCentralWidget(centralWidget)    
        self.widget_test_2 = QLabel("<h1>Hello World !</h1>", alignment=Qt.AlignCenter)
        self.widget_test_2.setObjectName(u"widget_test_2")
        self.widget_test_2.setGeometry(QRect(180, 40, 151, 181))
        self.widget_test_2.raise_()
        layout = QVBoxLayout(centralWidget)
        layout.addWidget(self.widget_test_2)
        layout.addWidget(self.widget, stretch=1)#blur box
if __name__ == "__main__":
    myApp = QApplication(sys.argv)
    file = os.path.join(DIR_PATH, "qml_window.qml")                     
    url = QUrl.fromLocalFile(file)                                      
    engine = QQmlApplicationEngine()
    context = engine.rootContext()
    context.setContextProperty("main", engine)
    engine.load(url)
    if not engine.rootObjects():
        sys.exit(-1)
    widget = QWidget.createWindowContainer(engine.rootObjects()[0])
    window = GUI_MainWindow(widget)
    window.show()

    sys.exit(myApp.exec_())     

and I want ShaderEffectSource to blur everything behind it,

even widgets created by PyQt5 or PySide2. While moving or staying in place

Everything that is behind the widget should be blurry.

I already tried to use the QGraphicsBlurEffect effect for this, but this did not give me the desired results. I hope FastBlur can do it. if there are any other options then let me know

Can i do it?

image


Solution

  • I'm updating this answer as the question has been partially cleared in the comments, but I will leave the original answer at the end, as it might still be useful.

    Besides that, the concept at the base remains the same: a graphics effect is applied to an object, and it modifies that object look, not how underlying objects appear. If you want to apply that effect to multiple objects, they have to be children of a common parent, and the effect has to be set for that parent, but everything that is below that parent (and it's not its child) will only be partially affected by the result of the effect.
    Imagine a blur effect as a filter applied to a real life photograph that is partially transparent: while the image in the photograph is blurred, what you can see behind it will not be blurred.

    Subclass the graphics effect

    QGraphicsEffects don't provide the ability to limit the extent of their processing, as they usually modify the whole "bounding rect" of the object they are set for.
    In order to achieve that, subclassing is necessary and the draw() method has to be overridden, as it is the one responsible for the actual drawing.

    I'm going to presume that the whole interface is going to be affected by the effect in some way: even if some objects are "outside" the rectangle of the effect, they are still part of the same parent, so this is what we're going to do:

    • create a main widget that acts as container for the full interface
    • add a main layout for the main interface (the one normally shown)
    • create a sub widget that contains the main interface, set a layout for it and add anything you need to that layout
    • set the subclassed graphics effect to the sub widget
    • create a widget for the menu, that has the main widget as parent, so it will not be part of the main layout; it will have its own layout with its buttons, labels, etc.
    • add a system that changes the graphics effect according to the geometry of the menu, and whenever that changes, the effect will be applied to that geometry only

    a very very cool menu!

    class BlurEffect(QtWidgets.QGraphicsBlurEffect):
        effectRect = None
    
        def setEffectRect(self, rect):
            self.effectRect = rect
            self.update()
    
        def draw(self, qp):
            if self.effectRect is None or self.effectRect.isNull():
                # no valid effect rect to be used, use the default implementation
                super().draw(qp)
            else:
                qp.save()
                # clip the drawing so that it's restricted to the effectRect
                qp.setClipRect(self.effectRect)
                # call the default implementation, which will draw the effect
                super().draw(qp)
                # get the full region that should be painted
                fullRegion = QtGui.QRegion(qp.viewport())
                # and subtract the effect rectangle
                fullRegion -= QtGui.QRegion(self.effectRect)
                qp.setClipRegion(fullRegion)
                # draw the *source*, which has no effect applied
                self.drawSource(qp)
                qp.restore()
    
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
    
            background = QtGui.QPixmap('background.png')
    
            # apply a background to this widget, note that this only serves for the
            # graphics effect to know what's outside the boundaries
            p = self.palette()
            p.setBrush(p.Window, QtGui.QBrush(background))
            self.setPalette(p)
    
            self.resize(background.size())
    
            # this layout is only for the child "sub" widget
            mainLayout = QtWidgets.QVBoxLayout(self)
            mainLayout.setContentsMargins(0, 0, 0, 0)
    
            # the "sub" widget, that contains the main interface
            self.subWidget = QtWidgets.QWidget()
            mainLayout.addWidget(self.subWidget)
            # set the background for the subwidget; note that we can't use setPalette()
            # because palette and fonts are inherited by children; using ".QWidget"
            # we ensure that the background is only applied to the subwidget
            self.subWidget.setStyleSheet('''
                .QWidget {
                    background-image: url(background.png);
                }
            ''')
    
            # some random widgets
            subLayout = QtWidgets.QGridLayout(self.subWidget)
            for row in range(3):
                for col in range(3):
                    btn = QtWidgets.QPushButton()
                    subLayout.addWidget(btn, row, col)
    
            btn.setText('Open menu')
            btn.setFocus()
            btn.clicked.connect(self.openMenu)
    
            # create an instance of our effect subclass, and apply it to the subwidget
            self.effect = BlurEffect()
            self.subWidget.setGraphicsEffect(self.effect)
            self.effect.setEnabled(False)
            self.effect.setBlurRadius(10)
    
            # create the menu container, that *HAS* to have this main widget as parent
            self.topMenu = QtWidgets.QWidget(self)
            self.topMenu.setVisible(False)
            self.topMenu.setFixedWidth(200)
            # move the menu outside the window left margin
            self.topMenu.move(-self.topMenu.width(), 0)
    
            menuLayout = QtWidgets.QVBoxLayout(self.topMenu)
            menuLayout.addSpacing(20)
            for b in range(4):
                btn = QtWidgets.QPushButton('Button {}'.format(b + 1))
                menuLayout.addWidget(btn)
    
            menuLayout.addSpacing(10)
    
            closeButton = QtWidgets.QPushButton('Close menu')
            menuLayout.addWidget(closeButton)
            closeButton.clicked.connect(self.closeMenu)
            # a stretch to ensure that the items are always aligned on top
            menuLayout.addStretch(1)
    
            # an animation that will move the menu laterally
            self.menuAnimation = QtCore.QVariantAnimation()
            self.menuAnimation.setDuration(500)
            self.menuAnimation.setEasingCurve(QtCore.QEasingCurve.OutQuart)
            self.menuAnimation.setStartValue(-self.topMenu.width())
            self.menuAnimation.setEndValue(0)
            self.menuAnimation.valueChanged.connect(self.resizeMenu)
            self.menuAnimation.finished.connect(self.animationFinished)
    
            # a simple transparent widget that is used to hide the menu when
            # clicking outside it; the event filter is to capture click events
            # it may receive
            self.clickGrabber = QtWidgets.QWidget(self)
            self.clickGrabber.installEventFilter(self)
            self.clickGrabber.setVisible(False)
    
        def resizeMenu(self, value):
            # move the menu and set its geometry to the effect
            self.topMenu.move(value, 0)
            self.effect.setEffectRect(self.topMenu.geometry())
    
        def openMenu(self):
            if self.topMenu.x() >= 0:
                # the menu is already visible
                return
            # ensure that the menu starts hidden (that is, with its right border
            # aligned to the left of the main widget)
            self.topMenu.move(-self.topMenu.width(), 0)
            self.topMenu.setVisible(True)
            self.topMenu.setFocus()
    
            # enable the effect, set the forward direction for the animation, and
            # start it; it's important to set the effect rectangle here too, otherwise
            # some flickering might show at the beginning
            self.effect.setEffectRect(self.topMenu.geometry())
            self.effect.setEnabled(True)
            self.menuAnimation.setDirection(QtCore.QVariantAnimation.Forward)
            self.menuAnimation.start()
    
            # "show" the grabber (it's invisible, but it's there) and resize it
            # to cover the whole window area
            self.clickGrabber.setGeometry(self.rect())
            self.clickGrabber.setVisible(True)
            # ensure that it is stacked under the menu and above everything else
            self.clickGrabber.stackUnder(self.topMenu)
    
        def closeMenu(self):
            # in case that the menu has changed its size, set again the "start" value
            # to its negative width, then set the animation direction to backwards
            # and start it
            self.menuAnimation.setStartValue(-self.topMenu.width())
            self.menuAnimation.setDirection(QtCore.QVariantAnimation.Backward)
            self.menuAnimation.start()
            # hide the click grabber
            self.clickGrabber.setVisible(False)
    
        def animationFinished(self):
            # if the animation has ended and the direction was backwards it means that
            # the menu has been closed, hide it and disable the effect
            if self.menuAnimation.direction() == QtCore.QVariantAnimation.Backward:
                self.topMenu.hide()
                self.effect.setEnabled(False)
    
        def focusNextPrevChild(self, next):
            if self.topMenu.isVisible():
                # a small hack to prevent tab giving focus to widgets when the
                # menu is visible
                return False
            return super().focusNextPrevChild(next)
    
        def eventFilter(self, source, event):
            if source == self.clickGrabber and event.type() == QtCore.QEvent.MouseButtonPress:
                # the grabber has been clicked, close the menu
                self.closeMenu()
            return super().eventFilter(source, event)
    
        def resizeEvent(self, event):
            super().resizeEvent(event)
            # always set the menu height to that of the window
            self.topMenu.setFixedHeight(self.height())
            # resize the grabber to the window rectangle, even if it's invisible
            self.clickGrabber.setGeometry(self.rect())
            if self.topMenu.isVisible():
                # resize the effect rectangle
                self.effect.setEffectRect(self.topMenu.geometry())
    

    Previous answer

    Since you want to apply the effect to the underlying objects, I believe that the solution is to use a "container" to embed them, and then apply the blur effect to that. The same concept would be applied with QGraphicsBlurWidget too.

    ApplicationWindow {
        property var theme: String("#ffffff")
        property var focusColor: String('transparent')
        id: applicationWindow
        visible: false                                      
        width: 600
        height:600
    
        Rectangle {
            id: container
            anchors.fill: parent
    
            Image {
                id: image_bug
                anchors.fill: parent
                source: "im.png"                                
            }
    
            Rectangle {
                width: 100; height: 600
                color: "green"
                Text {
                    id: helloText
                    text: "Hello world!"
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.horizontalCenter: parent.horizontalCenter
                    font.pointSize: 10; font.bold: true
                }
                MouseArea {
                    anchors.fill: parent
                    onClicked: { effectSource.width = 1200; effectSource.height = 1200;}
                }
            }
        }
    
        ShaderEffectSource {
            id: effectSource
            sourceItem: container
            anchors.centerIn: image_bug
            width: 300 
            height: 300 
            sourceRect: Qt.rect(x,y, width, height)
        }
    
        FastBlur{
            id: blur
            anchors.fill: effectSource
            source: effectSource
            radius: 100
        }
    }
    

    resulting full blur effect