1

I made the PlotlyViewer class shown below to display a Plotly graph, and it works correctly but shows this warning when I close it: Release of profile requested but WebEnginePage still not deleted. Expect troubles !

The warning started happening when I created my own QWebEngineProfile instance in __init__ so I could connect to the downloadRequested signal for showing a save file dialog. I made my PlotlyViewer a parent of the QWebEnginePage but it sounds like it's not getting cleaned up when the parent is closed? I can't understand why.

import os
import tempfile

from plotly.io import to_html
import plotly.graph_objs as go
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets, sip, QtWebEngineWidgets
from PyQt5.QtCore import Qt

class PlotlyViewer(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, fig=None):
        super().__init__()

        # https://stackoverflow.com/a/48142651/3620725
        self.profile = QtWebEngineWidgets.QWebEngineProfile(self)
        self.page = QtWebEngineWidgets.QWebEnginePage(self.profile, self)
        self.setPage(self.page)
        self.profile.downloadRequested.connect(self.on_downloadRequested)

        # https://stackoverflow.com/a/8577226/3620725
        self.temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False)
        self.set_figure(fig)

        self.resize(700, 600)
        self.setWindowTitle("Plotly Viewer")

    def set_figure(self, fig=None):
        self.temp_file.seek(0)

        if fig:
            self.temp_file.write(to_html(fig, config={"responsive": True}))
        else:
            self.temp_file.write("")

        self.temp_file.truncate()
        self.temp_file.seek(0)
        self.load(QtCore.QUrl.fromLocalFile(self.temp_file.name))

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        self.temp_file.close()
        os.unlink(self.temp_file.name)

    def sizeHint(self) -> QtCore.QSize:
        return QtCore.QSize(400, 400)

    # https://stackoverflow.com/questions/55963931/how-to-download-csv-file-with-qwebengineview-and-qurl
    def on_downloadRequested(self, download):
        dialog = QtWidgets.QFileDialog()
        dialog.setDefaultSuffix(".png")
        path, _ = dialog.getSaveFileName(self, "Save File", os.path.join(os.getcwd(), "newplot.png"), "*.png")
        if path:
            download.setPath(path)
            download.accept()

if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    fig = go.Figure()
    fig.add_scatter(
        x=np.random.rand(100),
        y=np.random.rand(100),
        mode="markers",
        marker={
            "size": 30,
            "color": np.random.rand(100),
            "opacity": 0.6,
            "colorscale": "Viridis",
        },
    )

    pv = PlotlyViewer(fig)
    pv.show()
    app.exec_()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
pyjamas
  • 4,608
  • 5
  • 38
  • 70
  • A possible alternative solution might be to make the application the parent of the profile instead of the view/page - the idea being to keep the profile alive for as long as possible, so that the view/page will be deleted first. – ekhumoro Jan 15 '22 at 20:04

2 Answers2

2

The problem is that python's way of working with memory does not comply with the pre-established rules by Qt (that's the bindings problems), that is, Qt wants QWebEnginePage to be removed first but python removes QWebEngineProfile first.

In your case, I don't see the need to create a QWebEnginePage or a QWebEngineProfile different from the one that comes by default, but to obtain the QWebEngineProfile by default:

class PlotlyViewer(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, fig=None):
        super().__init__()
        self.page().profile().downloadRequested.connect(self.on_downloadRequested)

        # https://stackoverflow.com/a/8577226/3620725
        self.temp_file = tempfile.NamedTemporaryFile(
            mode="w", suffix=".html", delete=False
        )
        self.set_figure(fig)

        self.resize(700, 600)
        self.setWindowTitle("Plotly Viewer")
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
0

A custom implementation of the closing event of the QWebEngineView class fix the issue.

I re-adapted (and successfully tested with some other code in PyQt6 with same issue) the answer found in Qt forum.

class PlotlyViewer(QWebEngineView):
    ...

    def closeEvent(self, qclose_event):
        """Overwrite QWidget method"""
        # Sets the accept flag of the event object, indicates that the event receiver wants the event.
        qclose_event.accept()
        # Schedules this object for deletion, QObject
        self.page().deleteLater()

References

cards
  • 3,936
  • 1
  • 7
  • 25