How to implement a responsive gallery view with PySide2 - python

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)

Related

Python, PySide6; JS not receiving data from QWebChannel

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.

QT QWebEngineView transparency for transparent html elements? [duplicate]

This question already has answers here:
Transparent Background in QWebEnginePage
(2 answers)
Closed 1 year ago.
I am trying to display some html in PyQT5 with QWebEngine. The problem is that the background (body) of the html is set to 'transparent', but QT won't display it as transparent.
I have been able to make the window frameless, and have been able to make the white part around the html that QT adds transparent, but no where on the internet can I find a way to have QT correctly render the transparent body background as actually transparent (instead of white).
If anyone can help, I would really appreciate it!
import os
import sys
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets
from PyQt5.QtWebChannel import QWebChannel
my_html = """<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
background-color: transparent;
}
#square {
width: 200px;
height: 200px;
background-color: red;
position: absolute;
top: 100px;
left: 100px;
}
</style>
</head>
<body>
<div id="square"></div>
</body>
</html>
"""
class Browser(QtWebEngineWidgets.QWebEngineView):
def __init__(self, html):
super().__init__()
self.url = QtCore.QUrl.fromLocalFile(os.getcwd() + os.path.sep)
self.page().setHtml(html, baseUrl=self.url)
class Window(QtWidgets.QMainWindow):
def __init__(self, html):
super().__init__()
self.html = html
self.init_widgets()
self.init_layout()
self.setFixedSize(400, 400)
# these make the border QT adds transparent, and removes the title bar
# but doesn't affect the body background of the html which is set to transparent
self.setStyleSheet("background: transparent; border: transparent;")
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setAutoFillBackground(True) # don't know what this does, as far as I know, nothing
def init_widgets(self):
self.browser = Browser(self.html)
def init_layout(self):
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.browser)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
def start():
app = QtWidgets.QApplication(sys.argv)
window = Window(my_html)
window.show()
app.exec_()
if __name__ == '__main__':
start()
Picture for the result of the code above
I found answer in old question for Qt and C/C++:
Transparent Background in QWebEnginePage
page has default background white and you have to change it
page = self.page()
page.setBackgroundColor(QtCore.Qt.transparent)
page.setHtml(html, baseUrl=self.url)

how I use the QTextEdit in pyqt5 to show all style of the html (including the style of css)

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

Google Fonts (ttf) being ignored in QtWebEngine when using #font face

I am trying to load a google font to my pyqt5 QtWebEngine app.
The app loads a local html file with css stying. I used font face to load the ttf file as below:
#font-face {
font-family: "Work Sans";
src: url("C:\Users\Godwin\TIAT\fonts\WorkSans-Regular.ttf") format('truetype');
}
body {
font-family: "Work Sans";
background-color: #eef0f2;
}
the font seem to be ignored when loading the html file.
Can someone please assist.
Editstrong text
Here is my full html
#font-face {
font-family: Work Sans;
src: url("Work_Sans/WorkSans-Regular.ttf")
}
div {
font-family: Work Sans;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" type="text/css" href="style.css"/>
<title>Document</title>
</head>
<style>
</style>
<body>
<div>
Hello World
</div>
</body>
</html>
These works on chrome, firefox and chrome but if i use qtwebengine like this
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
if __name__ == "__main__":
import sys
sys.argv.append("--disable-web-security")
app = QtWidgets.QApplication(sys.argv)
wnd = QtWidgets.QWidget()
genVLayout = QtWidgets.QVBoxLayout(wnd)
verticalLayout_7 = QtWidgets.QVBoxLayout()
webEngineViewGen = QtWebEngineWidgets.QWebEngineView(wnd)
webEngineViewGen.setUrl(QtCore.QUrl("about:blank"))
fh = open('main.html','r')
html = fh.read()
webEngineViewGen.setHtml(html)
verticalLayout_7.addWidget(webEngineViewGen)
genVLayout.addLayout(verticalLayout_7)
wnd.show()
sys.exit(app.exec_())
it font does not work
If you are using setHtml() then as indicated by the docs the external resources will be relative to the url that you pass as second parameters:
void QWebEngineView::setHtml(const QString &html, const QUrl &baseUrl = QUrl())
[...]
External objects, such as stylesheets or images referenced in the HTML document, are located relative to baseUrl.
[...]
So in your case the solution is:
import os
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
if __name__ == "__main__":
import sys
sys.argv.append("--disable-web-security")
app = QtWidgets.QApplication(sys.argv)
wnd = QtWidgets.QWidget()
genVLayout = QtWidgets.QVBoxLayout(wnd)
verticalLayout_7 = QtWidgets.QVBoxLayout()
webEngineViewGen = QtWebEngineWidgets.QWebEngineView(wnd)
webEngineViewGen.setUrl(QtCore.QUrl("about:blank"))
with open('main.html','r') as fh:
html = fh.read()
current_dir = os.path.dirname(os.path.abspath(__file__))
url = QtCore.QUrl.fromLocalFile(os.path.join(current_dir, "main.html"))
webEngineViewGen.setHtml(html, url)
verticalLayout_7.addWidget(webEngineViewGen)
genVLayout.addLayout(verticalLayout_7)
wnd.show()
sys.exit(app.exec_())
Or simply use the load() method:
import os
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
if __name__ == "__main__":
import sys
sys.argv.append("--disable-web-security")
app = QtWidgets.QApplication(sys.argv)
wnd = QtWidgets.QWidget()
genVLayout = QtWidgets.QVBoxLayout(wnd)
verticalLayout_7 = QtWidgets.QVBoxLayout()
webEngineViewGen = QtWebEngineWidgets.QWebEngineView(wnd)
webEngineViewGen.setUrl(QtCore.QUrl("about:blank"))
current_dir = os.path.dirname(os.path.abspath(__file__))
url = QtCore.QUrl.fromLocalFile(os.path.join(current_dir, "main.html"))
webEngineViewGen.load(url)
verticalLayout_7.addWidget(webEngineViewGen)
genVLayout.addLayout(verticalLayout_7)
wnd.show()
sys.exit(app.exec_())

wxPython WebView example

I am writing a small reporting app using wxPython (wxAUI). I want to render my data as HTML, to be displayed in a WebView 'widget'. I am looking for a sample 'hello world' snippet that will show how to display/render an HTML string in a WebView widget - but have been unable to find a single example - and the WebView widget does not seem to be well documented.
Could someone please provide a link to such an example or (better still), post a short snippet here that shows how to use the WebView widget to render an HTML string?
# sample html string to display in WebView widget
html_string = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Hello World!</title>
<script type="text/javascript" src="jquery.js"></script>
<style type="text/css" src="main.css"></style>
</head>
<body>
<span id="foo">The quick brown fox jumped over the lazy dog</span>
<script type="text/javascript">
$(document.ready(function(){
$("span#foo").click(function(){ alert('I was clicked!'); });
});
</script>
</body>
</html>
"""
This is a simple example that works for me.
Make sure you are running the latest version of wxpython. (wxpython 2.9)
import wx
import wx.html2
class MyBrowser(wx.Dialog):
def __init__(self, *args, **kwds):
wx.Dialog.__init__(self, *args, **kwds)
sizer = wx.BoxSizer(wx.VERTICAL)
self.browser = wx.html2.WebView.New(self)
sizer.Add(self.browser, 1, wx.EXPAND, 10)
self.SetSizer(sizer)
self.SetSize((700, 700))
if __name__ == '__main__':
app = wx.App()
dialog = MyBrowser(None, -1)
dialog.browser.LoadURL("http://www.google.com")
dialog.Show()
app.MainLoop()
I posted to this thread after reading the first two entries, and in my post I said something like:
There is an answer here, but it doesn't answer the question. The question was: How do I display an HTML file in a string in a browser
window? The only answer opens the browser
window, but gets data from a url and doesn't use the string contents.
But then I researched the answer further, I took the postings here and came up with the actual answer to the original question, which was: How do I display from a string?:
If you copy the html string assignment into the code sample, but replace the line:
dialog.browser.LoadURL("http://www.google.com")
with:
dialog.browser.SetPage(html_string,"")
Everything should work as desired (displaying html page from a string (instead of url))
Share and Enjoy!

Categories

Resources