I've seen many many answers about this question, but still cannot figure it out.
I made two files, one - html with qrc definition + new QWebChannel, and second python file with Object and QWebEngineView.
JS script seems to not working. It doesn't binding channel object to local var.
I've tried many examples from many pages, looking native QT language or C++, I cannot figure it out.
QWebChannel definition and qt object are found.
Maybe something is not ok with PySide6? Please help me figure it out.
My code:
index.html
<head>
<title>Test</title>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript">
let dataSource = null
window.onload = function () {
alert('before') //works
new QWebChannel(qt.webChannelTransport, function (channel) {
alert('inside') //not working
dataSource = channel.objects.backend;
}
);
alert('after') //works
alert(dataSource) //null
}
</script>
</head>
<body>
<p>Hello</p>
</body>
</html>
main.py
import sys, os
from PySide6.QtCore import Signal, QUrl, Slot, QObject
from PySide6.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QWidget
from PySide6.QtWebEngineWidgets import *
from PySide6.QtWebEngineCore import *
from PySide6.QtWebChannel import QWebChannel
class Backend(QObject):
#Slot(result=int)
def getValue(self):
return 1
#Slot(int)
def printValue(self, val):
print(val)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow,self).__init__(*args, **kwargs)
self.browser = QWebEngineView()
backend = Backend()
channel = QWebChannel()
channel.registerObject("backend", backend)
self.browser.page().setWebChannel(channel)
current_dir = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(current_dir, "index.html")
url = QUrl.fromLocalFile(filename)
self.browser.load(url)
self.setCentralWidget(self.browser)
self.resize(1200,900)
self.show()
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())
UPDATE:
I extended view, added dev tool and jquery to check objects - that what I have:
UPDATE 2:
I removed PySide6, installed PyQt5 and used example from here:
How to receive data from python to js using QWebChannel?
Everything works :/
I spend whole day to figure it out. I don't know why it's not working.
Related
Following this post and the mathjax 3 documentation, I am trying to render a simple html with Mathjax content in PyQt5 using a local copy of the mathjax repo.
The main directory contains the notebook from which the following code is executed, a "mathjax" folder containing the content of the repo (especially the es5 folder). Notice that I tried with 2 paths to the js (comment toward the top) :
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl
import os
# <script type="text/javascript" src="file:///Users/mocquin/.../mathjax/es5/tex-chtml-full.js">
pageSource = """
<html><head>
<script type="text/javascript" src="./mathjax/es5/tex-chtml-full.js">
</script></head>
<body>
<p><mathjax style="font-size:2.3em">$$u = \int_{-\infty}^{\infty}(awesome)\cdot du$$</mathjax></p>
</body></html>
"""
app = QApplication(sys.argv)
webView = QWebEngineView()
webView.setHtml(pageSource, baseUrl=QUrl(os.path.abspath('')))
webView.show()
sys.exit(app.exec_())
But this only renders the content without the "latex parsing"
What am I missing so that the mathjax is indeed linked to the html ?
The problem with your example is that you aren't using the correct format for the src path and base url. The src path must be a plain relative path, and the base url should be a backslash-terminated absolute path to the directory containing the mathjax directory.
Assuming the mathjax directory is in the same directory as the python script, the following should work correctly (at least, it does for me):
import sys, os
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
pageSource = """
<html><head>
<script type="text/javascript" async src="mathjax/es5/tex-chtml-full.js"></script>
</head><body>
<p><mathjax style="font-size:2.3em">$$u = \int_{-\infty}^{\infty}(awesome)\cdot du$$</mathjax></p>
</body></html>
"""
baseurl = QUrl.fromLocalFile(os.path.dirname(os.path.abspath(__file__)) + '/')
print(f'BASEURL: {baseurl.toString()!r}') # BASEURL: 'file:///home/tmp/test/'
app = QApplication(sys.argv)
webView = QWebEngineView()
webView.setHtml(pageSource, baseUrl=baseurl)
webView.show()
sys.exit(app.exec_())
I wrote a python test program like this to show openstreetmap:
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView
import sys
def mainPyQt5():
url = 'file:///./index.html'
app = QApplication(sys.argv)
browser = QWebEngineView()
browser.load(QUrl(url))
browser.show()
sys.exit(app.exec_())
mainPyQt5()
index.html fetched by QWebEngineView simply calls openstreetmap:
<title>OSM and Leaflet</title>
<link rel = "stylesheet" href = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css"/>
<div id = "map" style = "width: 900px; height: 580px"></div><script src = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
<script>
// Creating map options
var mapOptions = {
center: [45.641174, 9.114828],
zoom: 10
}
// Creating a map object
var map = new L.map('map', mapOptions);
// Creating a Layer object
var layer = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
// Adding layer to the map
map.addLayer(layer);
</script>
If I fetch index.html with a ordinary browser the map is shown as expected but if I call the simple python program using QWebEngineView no tiles are downloaded from openstreetmap. If I replace openstreetmap with maps.stamen.com everything is fine both with a browser or the python script.
By default QtWebEngine does not set default headers like popular browsers do. In this case the openstreetmap server needs to know the "Accept-Language" to produce the maps since for example the names of the cities will depend on the language to filter non-browser traffic. The solution is to implement a QWebEngineUrlRequestInterceptor that adds that header:
import os.path
import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor
from PyQt5.QtWebEngineWidgets import QWebEngineView
class Interceptor(QWebEngineUrlRequestInterceptor):
def interceptRequest(self, info):
info.setHttpHeader(b"Accept-Language", b"en-US,en;q=0.9,es;q=0.8,de;q=0.7")
def mainPyQt5():
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(CURRENT_DIR, "index.html")
app = QApplication(sys.argv)
browser = QWebEngineView()
interceptor = Interceptor()
browser.page().profile().setUrlRequestInterceptor(interceptor)
browser.load(QUrl.fromLocalFile(filename))
browser.show()
sys.exit(app.exec_())
if __name__ == "__main__":
mainPyQt5()
I'm working on an asset management app for our company. It's a standalone Python3 app using PySide2 and talking to our database backend. One of the views I'm writing is supposed to be a HTML5-style responsive gallery: assets are displayed as thumbnails, on mouse-over they display extra information, and on click they initiate an action (eg opening the asset in the appropriate app).
What's the best way to implement this in PySide2/PyQt5?
Since I'd feel comfortable implementing and styling something like this in HTML5, I'm inclined to do it with QWebEngineView and dynamically generate HTML and CSS in python, then use QWebEngineView.setHtml() to display it.
Is this a good way to do it inside a PySide2 app, that doesn't use HTML otherwise? Are there more Qt-ish ways to achieve a dynamic, responsive, style-able gallery?
If I would use QWebEngineView, how would I intercept the user clicking on one of the HTML elements? I found this question, which sounds like it could be a solution for this: Capture server response with QWebEngineView . Is there a simpler solution?
Qt offers many alternatives for what you want (They are not complete solutions since you do not clearly indicate what you require):
Qt Widgets: PySide2 composite widget Hover Effect,
Qt QML: PySide2/QML populate and animate Gridview model/delegate,
Or QWebEngineView, which focuses my current answer.
The implementation of the mouse-over effect will not be implemented because I am not an expert in frontend, but I will focus on communication between the parties.
To communicate Python information to JS, you can do it with the runJavaScript() method of QWebEnginePage and/or with QWebChannel, and the reverse part with QWebChannel (I don't rule out the idea that QWebEngineUrlRequestInterceptor could be an alternative solution but in this case the previous solutions they are simpler). So in this case I will use QWebChannel.
The idea is to register a QObject that sends the information through signals (in this case JSON), by the side of javascript parsing the JSON and creating dynamic HTML, then before any event such as the click call a slot of the QObject.
Considering the above, the solution is:
├── index.html
├── index.js
└── main.py
import json
from PySide2 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets, QtWebChannel
class GalleryManager(QtCore.QObject):
dataChanged = QtCore.Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._data = []
self._is_loaded = False
#QtCore.Slot(str)
def make_action(self, identifier):
print(identifier)
#QtCore.Slot()
def initialize(self):
self._is_loaded = True
self.send_data()
def send_data(self):
if self._is_loaded:
self.dataChanged.emit(json.dumps(self._data))
#property
def data(self):
return self._data
#data.setter
def data(self, d):
self._data = d
self.send_data()
if __name__ == "__main__":
import os
import sys
# sys.argv.append("--remote-debugging-port=8000")
app = QtWidgets.QApplication(sys.argv)
current_dir = os.path.dirname(os.path.realpath(__file__))
view = QtWebEngineWidgets.QWebEngineView()
channel = QtWebChannel.QWebChannel(view)
gallery_manager = GalleryManager(view)
channel.registerObject("gallery_manager", gallery_manager)
view.page().setWebChannel(channel)
def on_load_finished(ok):
if not ok:
return
data = []
for i, path in enumerate(
(
"https://source.unsplash.com/pWkk7iiCoDM/400x300",
"https://source.unsplash.com/aob0ukAYfuI/400x300",
"https://source.unsplash.com/EUfxH-pze7s/400x300",
"https://source.unsplash.com/M185_qYH8vg/400x300",
"https://source.unsplash.com/sesveuG_rNo/400x300",
"https://source.unsplash.com/AvhMzHwiE_0/400x300",
"https://source.unsplash.com/2gYsZUmockw/400x300",
"https://source.unsplash.com/EMSDtjVHdQ8/400x300",
"https://source.unsplash.com/8mUEy0ABdNE/400x300",
"https://source.unsplash.com/G9Rfc1qccH4/400x300",
"https://source.unsplash.com/aJeH0KcFkuc/400x300",
"https://source.unsplash.com/p2TQ-3Bh3Oo/400x300",
)
):
d = {"url": path, "identifier": "id-{}".format(i)}
data.append(d)
gallery_manager.data = data
view.loadFinished.connect(on_load_finished)
filename = os.path.join(current_dir, "index.html")
view.load(QtCore.QUrl.fromLocalFile(filename))
view.resize(640, 480)
view.show()
sys.exit(app.exec_())
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="index.js"> </script>
</head>
<body>
<div class="container">
<h1 class="font-weight-light text-center text-lg-left mt-4 mb-0">Thumbnail Gallery</h1>
<hr class="mt-2 mb-5">
<div id="container" class="row text-center text-lg-left">
</div>
</div>
</body>
</html>
var gallery_manager = null;
new QWebChannel(qt.webChannelTransport, function (channel) {
gallery_manager = channel.objects.gallery_manager;
gallery_manager.dataChanged.connect(populate_gallery);
gallery_manager.initialize();
});
function populate_gallery(data) {
const container = document.getElementById('container');
// clear
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// parse json
var d = JSON.parse(data);
// fill data
for (const e of d) {
var identifier = e["identifier"];
var url = e["url"];
var div_element = create_div(identifier, url)
container.appendChild(div_element);
}
}
function create_div(identifier, url){
var html = `
<div class="d-block mb-4 h-100">
<img class="img-fluid img-thumbnail" src="${url}" alt="">
</div>
`
var div_element = document.createElement("div");
div_element.className = "col-lg-3 col-md-4 col-6"
div_element.innerHTML = html;
div_element.addEventListener('click', function (event) {
gallery_manager.make_action(identifier);
});
return div_element;
}
Loving the answer from #eyllanesc using QWebChannel! I had not come across this and hadn't dared to dream about having AJAX-style communication between a PySide app and a WebView! Loving it!
Here's a less flexible/elaborate alternative I had come up with meanwhile, using QWebEnginePage.acceptNavigationRequest(). I much prefer the QWebChannel answer, but others might find this option helpfull as well.
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
class MyPage(QWebEnginePage):
def acceptNavigationRequest(self, url, type, isMainFrame):
print(url, type, isMainFrame)
if url.toString().startswith('app://'):
print('intercepted click, do stuff')
return False
return True
def createHtml():
html = """
<html>
<head>
<style>
.item {
position: relative;
}
.animgif {
display: none;
position: absolute;
top: 0;
left: 0;
}
.item:hover .animgif {
display: block;
}
</style>
</head>
<body>
<a href="app://action/click?id=1234">
<div class="item">
<img class="thumb" src="file://server01/path/to/thumbnail.png">
<img class="animgif" src="file://server/path/to/thumbnail.gif">
</div>
</a>
</body>
</html>
"""
return html
if __name__ == '__main__':
import sys
from PySide2 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
page = MyPage()
view = QWebEngineView()
view.setPage(page)
html = createHtml()
baseUrl = "file://server01/"
page.setHtml(html, baseUrl=baseUrl)
view.show()
sys.exit(app.exec_())
The idea is to create html dynamically and use page.setHtml(html) to load it on the view. In this example the createHtml() function is rudimentary, but shows the intention. Subclassing QWebEnginePage allows you to override acceptNavigationRequest(), which allows you to intercept clicks and decide what to do with them. In this example I chose to use and detect a protocol 'app://' and act on in accordingly.
One more note is that in our case, all files/images/ect live on the local file system. To avoid a cross-origin security exception, I had to provide baseUrl in setHtml() and set it to the same file path that the files are hosted on.
html = createHtml()
baseUrl = "file://server01/"
page.setHtml(html, baseUrl=baseUrl)
Python 3.6 PYQT 5.12.1
I am ready to show the style I need by pyqt5 and I knew that the QTextEdit in pyqt5 can display the html code pretty good (I have some experience in web development), so I decided to use html/css to show my style . However , it may have some problem in showing the code in css . What can I do to let it can show the css/javascript ? If it can‘t , can recommend other methods to modify the style?
It can show some style like width = "100" height = "100" when I code it in the html but not css and some can't display like border-radius:50%;. It won't get any effect when I code the style in css . By the way , I've imported CSS code.
The CSS code do nothing in QTextEdit (but it is ok in html)
.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class WINDOW(QMainWindow):
def __init__(self):
super().__init__()
self.init()
def init(self):
w_width,w_height,w_x,w_y = 700,640,700,200
self.set_size_pos(w_width,w_height,w_x,w_y);
self.set_message_textedit()
self.message_textedit.setHtml(self.get_html())
self.show()
def set_size_pos(self,width,height,x,y):
'''
set window's width , height and position
'''
self.resize(width,height)
self.move(x,y)
def set_message_textedit(self):
self.message_textedit = QTextEdit(self)
self.message_textedit.setFont(QFont("Microsoft YaHei",12))
self.message_textedit.resize(680,420)
self.message_textedit.move(10,50)
self.message_textedit.setReadOnly(True)
def get_html(self):
html = ""
with open("./chat-style/chat.html","r",encoding = "utf-8") as f:
html = f.read()
return html
if __name__ == '__main__':
app = QApplication(sys.argv)
test = WINDOW()
sys.exit(app.exec_())
.html
<!doctype html>
<html lange="zh-CN">
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="./chat.css">
<script src = "chat.js"></script>
</head>
<body>
<img class = "tx" src="E:\study\assiataant\picture\icon.jpg">
<div>Welcome ~ Don!</div>
</body>
</html>
.css
.tx
{
border-radius:50%;
width: 30;
height: 30;
}
QTextEdit only supports CSS 2.1 as indicated by the docs:
All CSS 2.1 selector classes are supported except pseudo-class selectors such as :first-child, :visited and :hover.
But border-radius was introduced in CSS3. So you can not use it unfortunately. I recommend you read the following link so that you know the allowed tags.
Another alternative is to use QWebEngineView that supports these tags:
*.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import os
import sys
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
view = QtWebEngineWidgets.QWebEngineView()
file = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"chat-style/chat.html"
)
view.load(QtCore.QUrl.fromLocalFile(file))
self.setCentralWidget(view)
self.resize(640, 480)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
If you do not have QWebEngineView installed, you must install it with the following command:
python -m pip install PyQtWebEngine
I'm trying to open a simple html file (something like this)
<html>
<body>
<h1>hello</h1>
</body>
</html>
which does not have any embedded or external JavaScript, inside QtWebEngine using this code
import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
class Browser(QWebEngineView):
def __init__(self):
self.view = QWebEngineView.__init__(self)
self.setWindowTitle("Loading ...")
self.titleChanged.connect(self.adjustTitle)
def adjustTitle(self):
self.setWindowTitle(self.title())
if __name__ == "__main__":
app = QApplication(sys.argv)
view = Browser()
view.showMaximized()
url = QUrl.fromLocalFile("simple.html")
view.setUrl(url)
sys.exit(app.exec_())
But when i run it the following error appears in the console
js: Uncaught ReferenceError: jstProcess is not defined
What is jstProcess and how can i define it? (The code works fine if I load some remote resource like bing.com)
Your eg works for me
if give a fully qualified file name for simple as
'/home/me/simple.htm' . Or if simple.htm is in same directory
where I am running the python I do
import sys,os
fname = os.getcwd()+ '/simple.html'
url = QUrl.fromLocalFile(fname)
This is with the pyqt that comes with Debian that I am running
under python 2.