So I have a PyQt5 application in python that displays a Plotly plot (plotly being an interactive grapher app). Plotly runs on JavaScript, and I want to be able to catch JavaScript events and use them in python. So I wrote the code below, but I'm stuck on a seemingly intractable error/issue: the code stubbornly refuses to correctly register the object self.qt_bridge to the name "qtBridge" in setupWebChannel(), leading to a "ReferenceError: qtBridge is not defined" error later in the html_content.

I've narrowed it down to this line:

self.web_channel.registerObject("qtBridge", self.qt_bridge)

I've tested it extensively, and I'm pretty sure it's not due to one of these reasons:

  1. Problems in the html_content itself: I checked that self.qt_bridge does indeed exist right before html_content gets defined, and except for the "qtBridge undefined" error works perfectly;
  2. Timing issues that could lead to qt_bridge being added to the web_channel too fast or being used in the html_content too fast;
  3. Name conflicts or syntax: checked this like a hundred times already, no error here;
  4. QWebChannel or QWebEngineView not being defined or not being set-up fast enough before doing the registerObject() thing;
  5. Numerous other things I've checked, both in python and in javascript.

At this point I'm pretty much stumped and don't even know what to do to troubleshoot this any further. I have the impression of having tried everything (ofc this is very certainly just an impression :p). I've found a couple of stackoverflow posts that could help in solving this, but idk, either I don't understand those posts, or my mind is just too fried at this point after hours of troubleshooting this to be able to understand them. I'll post their links anyway:

Any help would be greatly appreciated, thanks in advance!

from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineSettings
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QComboBox, QWidget, QSizePolicy, QMainWindow, QPushButton
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QUrl, pyqtSlot
from PyQt5.QtWebChannel import QWebChannel

import sys
import time
import threading
import json
import numpy as np

import plotly.subplots as sp
import plotly.graph_objs as go


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

        self.plotting_mode = plotting_mode

        # Initialize the figure object
        self.fig = go.Figure()
        
        # Setup the webchannel to catch the in-plot javascript events
        self.setupWebChannel()
        self.set_fig_to_webview()


    def setupWebChannel(self):
        # Create webview
        self.webview = QWebEngineView()
        self.webview.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding
        )

        # Create webchannel
        self.web_channel = QWebChannel()
        self.webview.page().setWebChannel(self.web_channel)

        # Create QtBridge to link webview and webchannel
        self.qt_bridge = QtBridge()
        self.qt_bridge.update_it.connect(self.update_plot)

        # Register the QtBridge object with the WebChannel
        self.web_channel.registerObject("qtBridge", self.qt_bridge)

        # Create PyQt layout and add webview widget
        self.widget_layout = QVBoxLayout()
        self.widget_layout.addWidget(self.webview)  # Add the webview to the widget layout


    def set_fig_to_webview(self):
        # Generate a html_content file for a fig, containing various event listeners, and set it to the webview
        fig_json = self.fig.to_json()
        
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>My Plotly Plot</title>
            <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
        </head>
        <body>
            <div id="plotly-container"></div>
            <script>
                
                document.addEventListener("DOMContentLoaded", function() {{
                    var plotlyDiv = document.getElementById('plotly-container');

                    // Parse the Plotly data from the Python variable
                    var plotlyData = {fig_json};

                    // Create the Plotly plot using the extracted data
                    Plotly.newPlot(plotlyDiv, plotlyData);

                    // Delay execution by 500 milliseconds to ensure qtBridge is defined
                    setTimeout(function() {{
                        var x = 0;
                        var y = 0;
                        var message = {{ x: x, y: y }};
                        try {{
                            qtBridge.onPlotClick(JSON.stringify(message))
                        }} catch (error) {{
                            console.error('Error calling qtBridge.onPlotClick():', error);
                        }}
                    }}, 500);

                }});
            </script>
        </body>
        </html>
        """

        self.webview.setHtml(html_content)


    def update_plot(self, eeg_data):
        ### @STACKOVERFLOW: NOT IMPORTANT, JUST ADDS A TRACE TO self.fig ###


# Create a custom QObject to handle communication between JavaScript and Python
class QtBridge(QObject):
    update_it = pyqtSignal()

    @pyqtSlot()
    def onPlotClick(self):
        try:
            print("User clicked in plot!")
            # Your existing code for onPlotClick here
        except Exception as e:
            print("Error in onPlotClick:", str(e))



if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = QMainWindow()
    widget = PlotWidget("Superpose")
    window.setCentralWidget(widget.webview)
    window.show()
    sys.exit(app.exec_())
0

There are 0 best solutions below