如何应用 BlurEffect(QtWidgets.QGraphicsBlurEffect) 两次?

How do I apply BlurEffect(QtWidgets.QGraphicsBlurEffect) twice?

我有这个代码

import sys                                                         # +++
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 PyQt5 import QtCore, QtGui, QtWidgets


'''
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)
from PySide2 import QtCore, QtGui, QtWidgets
'''



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)
            print('bao')
        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())



if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    w = Window()
    w.resize(640, 570)  
    w.show()
    sys.exit(app.exec_())

我需要能够一次对多个小部件应用模糊效果

问题是当我添加

    self.effect2 = BlurEffect()
    self.subWidget.setGraphicsEffect(self.effect2)
    self.effect2.setEnabled(False)
    self.effect2.setBlurRadius(10)

之后

    self.effect = BlurEffect()
    self.subWidget.setGraphicsEffect(self.effect)
    self.effect.setEnabled(False)
    self.effect.setBlurRadius(10)

我收到这个错误

Traceback (most recent call last): File "C:\Users\user\Desktop\test\widgets\menu.py", line 157, in openMenu self.effect.setEffectRect(self.topMenu.geometry()) File "C:\Users\user\Desktop\test\widgets\menu.py", line 15, in setEffectRect self.update() RuntimeError: wrapped C/C++ object of type BlurEffect has been deleted

有人知道如何解决这个问题吗?

photoshop

你不能。一次只能在一个小部件上应用一种效果(之后,不能对它的任何 children 或 parents 应用任何效果),至少对于 QWidgets。

来自QWidget.setGraphicsEffect()

If there already is an effect installed on this widget, QWidget will delete the existing effect before installing the new effect.

发生的事情是,一旦您在 subWidget 上应用 self.effect2self.effect 就会从中移除并实际删除。在 PyQt 术语中,这意味着 python object 仍然存在,但它的 C++ 对应物不存在。

更新

看来你还是不明白 QGraphicsEffect 的工作原理。 效果 NOT 应用于您看到的背景模糊的小部件。它应用于底层小部件(subWidget,在本例中),并且仅应用于使用小部件的几何形状指定的矩形。您甚至可以将 effectRect 设置为您想要的任何矩形,即使除了 subWidget.

之外没有任何其他小部件

如果您需要将效果应用于多个矩形,那么您应该使用 setClipRegion 并使用复合 QRegion。
假设您将始终使用 QWidgets 作为效果的参考,并且效果将 始终 应用于占据整个 window 区域的小部件,您可以使用"watch list" 个需要跟踪的小部件,并在其几何形状发生变化时更新效果。

class BlurEffect(QtWidgets.QGraphicsBlurEffect):
    shouldEnable = True
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.watched = []

    def watchWidget(self, widget):
        widget.installEventFilter(self)
        self.watched.append(widget)

    def unwatchWidget(self, widget):
        if widget in self.watched:
            self.watched.remove(widget)
            self.update()

    def setEnabled(self, enabled):
        # in case you want to manually disable the effect, keep track of
        # the selected behavior
        self.shouldEnable = enabled
        super().setEnabled(enabled)

    def draw(self, qp):
        rects = []
        for widget in self.watched:
            if widget.isVisible():
                rect = widget.rect()
                if rect.isNull():
                    continue
                # map the widget geometry to the window
                rect.translate(
                    widget.mapTo(widget.window(), QtCore.QPoint()))
                rects.append(rect)
            if not self.isEnabled() and self.shouldEnable:
                super().setEnabled(True)
        if not rects:
            # no valid rect to be used, disable the effect if we should
            if not self.shouldEnable:
                super().setEnabled(False)
            # otherwise, keep drawing the source with the effect applied
            # to the whole area of the widget
            else:
                self.drawSource(qp)
        else:
            qp.save()
            # create a region that includes all rects
            rectRegion = QtGui.QRegion()
            for rect in rects:
               rectRegion |= QtGui.QRegion(rect) 
            # clip the effect painting to the region
            qp.setClipRegion(rectRegion)
            # 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 used before
            fullRegion -= rectRegion
            qp.setClipRegion(fullRegion)
            # draw the *source*, which has no effect applied
            self.drawSource(qp)
            qp.restore()

    def eventFilter(self, source, event):
        # update the effect whenever a widget changes its geometry or
        # becomes visible
        if event.type() in (QtCore.QEvent.Resize, QtCore.QEvent.Move, 
            QtCore.QEvent.Show) and source.isVisible():
                super().setEnabled(True)
                self.update()
        # if a widget is going to be deleted, remove it from the list
        # of watched list; this is **VERY** important
        elif event.type() == QtCore.QEvent.DeferredDelete:
            self.unwatchWidget(source)
        return super().eventFilter(source, event)

重要提示:

  • 您必须对要查看效果的任何小部件使用 watchWidget,包括 topMenu;同样,这并不意味着效果应用于那些小部件,而是它们的几何形状用于此;
  • 显然,没有 setEffectRect 了;
  • 使用此实现,如果所有监视的小部件都被隐藏或它们的几何图形为空,效果将自动禁用,这意味着您不再需要调用 self.effect.setEnabled()
  • 即使在这种情况下(看不到小部件可见),您仍然可以通过显式调用 setEnabled(True);
  • 整个区域 启用效果

最后,我强烈建议您仔细研究这段代码(和之前的代码)以及有关 QGraphicsEffect and QPainter (including the clipping 部分和所有相关页面的文档),并创建一些 simple 自己测试和示例,以更好地理解它们的工作原理,然后再尝试实现您想要实现的目标。