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:
- 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;
- 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;
- Name conflicts or syntax: checked this like a hundred times already, no error here;
- QWebChannel or QWebEngineView not being defined or not being set-up fast enough before doing the registerObject() thing;
- 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:
- QWebEnginePage interactive with javascript not working?
- PyQt5 returns None value when loading Web page content
- Returned value from python is not available at javascript
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_())