pythonqt5qtwebengine

Python QtWebEngine how to disable cacheing


I'm using python3 and Qt5, and recently made the switch from QTextBrowser to QtWebEngine. my project is https://github.com/carlbeech/OpenSongViewer, which is a songbook - songs in a pick list, and the main song text is the web page. I'm constructing html and using setHtml to set content, based on which song is wanted.

All has been working fine, until recently when the web page has started to not have the correct song text displayed. I've got debug statements in and the setHtml is setting the correct text, but its only randomly updating - I temporarily switched back to QTextBrowser and it worked fine - I can only assume that the QTWebEngine is caching and displaying the wrong html?

Can someone give me the correct method to turn off caching?

I've tried putting meta tags for no-cache into the html that is pushed into the QTWebEngine widget, and I've also tried putting in the code self.SongText.page().profile().setHttpCacheType(2) - '2' is the ID for NoCache I believe according to the docs?


Solution

  • It's a little scattered, but there are a bunch of caching settings to take care of. Regarding HttpCache:

    You might end up with something like this (No need to put raw enum values):

    profile = self.web_view.page().profile()
    profile.setHttpCacheType(QWebEngineProfile.NoCache)
    profile.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies)
    
    profile.clearHttpCache() #These are a bit extra, but better to be safe than sorry?
    profile.cookieStore().deleteAllCookies()
    

    While the above is still good practice, your seems to actually come from timing issues. Because the initial caching recommendations didn't work and QTextBrowser works fine while QWebEngine fails for your purposes, my best guess is that QWebEngineView.setHtml() is the root of your issues.

    When setHtml() is used on a QWebEngineView, it is asynchronous. According to the documentation, "The HTML document is loaded immediately, whereas external objects are loaded asynchronously." This means that even though the function is called, the page might not be able to render content, even if refresh() is called. Also note that loadFinished(), the signal called when the load is finished, will trigger with success = false if larger than 2MB.

    Confusion can occur with setHtml() because it is not asynchronous for QTextBrowser.

    Here are a few possible solutions you can try to solve the timing issue:

    First Update to from PyQt5 to PyQt6. Note that support for 5 is ending at the end of May. Qt6 has callbacks and additional techniques that are helpful.

    Second When using a QWebEngineView, you can try to listen for when setHtml() has finished, but it depends on how setHtml() is used. There are conflicting sources online that you can only listen successfully to pages, not the view, but I was able to do so successfully when testing:

    import sys
    from PyQt5.QtWidgets import QApplication, QMainWindow
    from PyQt5.QtWebEngineWidgets import QWebEngineView
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            view = QWebEngineView()
            view.loadFinished.connect(self.no_kludge_finished)
    
            html = """
            <html><body><p>Yay</></body></html>
            """
            view.setHtml(html)
            self.setCentralWidget(view)
    
        def no_kludge_finished(self, ok):
            print("Load finished:", ok)
    
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
    

    They key lies in: view.loadFinished.connect(self.no_kludge_finished) and def no_kludge_finished(self, ok):. In my testing, there was a perfect success rate of the function being called on load.

    Third You can use a URL. As mentioned during the setHtml() explanation, loadFinished() maxes out at 2MB; this is a result of how setHtml sends the actual data– it simply encodes it and ships it as data with text/html. We can do the same.

    I played around to create an example that shows style, JS, and HTML:

    import sys
    import base64
    from PyQt5.QtWidgets import QApplication, QMainWindow
    from PyQt5.QtWebEngineWidgets import QWebEngineView
    from PyQt5.QtCore import QUrl
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            view = QWebEngineView()
            view.loadFinished.connect(self.very_elegant_finish)
    
            html = """
            <!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <title>Look at the stuff it does no problem</title>
                <style>
                    #tog {
                    background-color: red;
                    color: white;
                    border: none;
                    padding: 10px 20px;
                    font-size: 16px;
                    cursor: pointer;
                    border-radius: 5px;
                    transition: background-color 0.3s;
                    }
    
                    #tog.blue {
                    background-color: blue;
                    }
                </style>
                </head>
                <body>
    
                <button id="tog">color toggle</button>
    
                <script>
                    const button = document.getElementById('tog');
                    button.addEventListener('click', () => {
                    button.classList.toggle('blue');
                    });
                </script>
    
                </body>
                </html>
            """
            encoded = base64.b64encode(html.encode("utf-8")).decode("utf-8")
            data_url = QUrl(f"data:text/html;base64,{encoded}")#the setHtml() function does the same in how it sends data
    
            view.setUrl(data_url)# setURL() is a more reliable trigger for loadFinished, which is a benefit over setHTML() on the async side
            self.setCentralWidget(view)
    
        def very_elegant_finish(self, ok):
            print("Load finished:", ok)
    
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
    
    

    view.setUrl() is guaranteed to trigger loadFinished(), so you will know exactly when to execute your next task.