Search code examples
pythonimage-gallerypyside2

How to implement a responsive gallery view with PySide2


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?


Solution

  • Qt offers many alternatives for what you want (They are not complete solutions since you do not clearly indicate what you require):


    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;
    }