semicondoctor
semicondoctor

Reputation: 20

QGraphicsDropShadowEffect makes QWebEngineView un-responsive

Can anyone help my understand this issue. When i put a QWebEngineView inside a layout in a widget that has a QGraphicsDropShadowEffect applied, the QWebEngineView becomes un-responsive until window is resized or similar window action (minimize etc.)?

Heres a small example

from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QApplication, QFrame, QGraphicsDropShadowEffect


class WebEdit(QWebEngineView):
    html = """
    <!DOCTYPE html>
        <html lang="en">
        <style>
            div {
                font-family: "Tahoma", monospace;
                white-space: nowrap;
            }
        </style>
        <body>
            <div contenteditable="true" id="myEditor" style="padding: 10px;"></div>
        </body>
    </html>
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMaximumHeight(55)
        self.setHtml(self.html)


class ApplicationWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Boilerplate")
        self.resize(800, 600)

        self.central_widget = QWidget(self)
        self.central_widget.setLayout(QVBoxLayout())
        self.setCentralWidget(self.central_widget)

        self.frame = QFrame(self)
        self.frame.setLayout(QVBoxLayout())
        self.frame.setStyleSheet(f"background: yellow;")

        shadowEffect = QGraphicsDropShadowEffect(self.frame)
        shadowEffect.setBlurRadius(10)
        shadowEffect.setXOffset(0)
        shadowEffect.setYOffset(0)

        self.frame.setGraphicsEffect(shadowEffect)

        self.edit_in_frame = WebEdit(self)  # this one is not responsive until window is resized

        self.edit = WebEdit(self)  # this one is always responsive

        self.central_widget.layout().addWidget(self.frame)
        self.frame.layout().addWidget(self.edit_in_frame)

        self.central_widget.layout().addWidget(self.edit)

        self.resize(400, 400)


if __name__ == "__main__":
    app = QApplication([])
    view = ApplicationWindow()
    view.show()
    app.exec()

I have tried different parents for the widgets, in and out of layouts and so on but i need this webview to live inside this frame with the effect applied.

Im gueesing its something with updating the graphics rendering but it's beyond me. Thanks in advance!

Upvotes: 0

Views: 102

Answers (1)

musicamante
musicamante

Reputation: 48424

This is probably caused by the fact that QGraphicsEffect actually act as some form of "painting proxy", and QWebEngineView internally uses a QOpenGLWidget to show the page contents.

A graphics effect uses the render() function to draw the contents of the widget before applying the effect, but QWebEngineView doesn't draw anything on its own and therefore the internal QOpenGLWidget doesn't get updated.

One possibility could be to force an update on first show:

class WebEdit(QWebEngineView):
    firstShown = False

    ...

    def showEvent(self, event):
        super().showEvent(event)
        if not self.firstShown:
            self.firstShown = True
            QTimer.singleShot(100, self.update)

Note that the relatively high timeout is required due to the complex way drawing is achieved through the open gl widget and the graphics effect. You can try to decrease it, but there is no guarantee that it will work in any case: in fact, it's not impossible that under certain conditions it would still be insufficient.

Another possibility would be to force an update whenever the QOpenGLWidget receives a Paint event:

class WebEdit(QWebEngineView):
    def __init__(self, parent=None):
        ...
        self.findChild(QOpenGLWidget).installEventFilter(self)

    def eventFilter(self, obj, event):
        if event.type() == event.Paint:
            self.update()
        return super().eventFilter(obj, event)

IMPORTANT: I don't know much about QOpenGLWidget, and I cannot exclude that this would cause some form of delayed infinite recursion. I'd suggest you to add a simple debugging print after self.update(): it will be obviously called more than once, but it should stop at some point. If anybody with sufficient knowledge about QOpenGLWidget mechanics can add further information, please comment accordingly.

Assuming that no infinite recursion actually happens, you could add another single shot QTimer that eventually removes the filter. A 1-2 seconds interval would probably suffice.

It's also important to note that further calls to setHtml() will have similar problems, so mixing the above strategies may be necessary.

The following code should work fine under any circumstance.

class WebEdit(QWebEngineView):
    ...
    def __init__(self, parent=None):
        super().__init__(parent)
        self.filterTimer = QTimer(self, singleShot=True, 
            interval=1000, timeout=self._removeFilter)
        self.loadStarted.connect(self._installFilter)

        self.setMaximumHeight(55)
        self.setHtml(self.html)

    def _installFilter(self):
        self.findChild(QOpenGLWidget).installEventFilter(self)

    def _removeFilter(self):
        self.findChild(QOpenGLWidget).removeEventFilter(self)

    def showEvent(self, event):
        super().showEvent(event)
        self.filterTimer.start()

    def eventFilter(self, obj, event):
        if event.type() == event.Paint:
            self.update()
        return super().eventFilter(obj, event)

Note that this is obviously required only for graphics effect. You may add further checks (such as checking for ParentChange events and possible graphics effects in the parent tree) in order to avoid unnecessary calls.

Also note that the drop shadow effect is the only QGraphicsEffect that can be used with QWebEngineView, as the documentation clearly states that OpenGL based widgets do not support graphics effects; in this case it "works" just because the effect is applied on the parent and is drawn around (outside of) the widget.

Upvotes: 1

Related Questions