0

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?

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

9 Comments

Hi Unfortunately this doesn't appear to have made any difference? I've set the above in the 'py' file that has been created by QT5 designer (from the UI file), and also in the code where I use "self.SongText.setHtml(SongLyricsDisplay)" (SongText is the QtWebEngine field and SongLyricsDisplay is the HTML that has been generated by the program.
e.g. # original code: self.SongText = QtWidgets.QTextBrowser(self.splitter) self.SongText = QtWebEngineWidgets.QWebEngineView(self.splitter) profile = self.SongText.page().profile() profile.setHttpCacheType(QtWebEngineWidgets.QWebEngineProfile.NoCache) profile.setPersistentCookiesPolicy(QtWebEngineWidgets.QWebEngineProfile.NoPersistentCookies) profile.clearHttpCache() profile.cookieStore().deleteAllCookies() Is there somewhere the the filesystem I can check for cached files - if so, how do I locate that?
Ah - located it : C:/Users/<user>/AppData/Local/python/cache/QtWebEngine/Default Manually deleted items in that - I'll give it some testing to see what happens...
Ok - this is ultra-cludgy - but this seems to work 99% of the time... in a loop (1-10), set the html of the webengine widget, and then do a sleep for 0.1 seconds, then call the refresh() on the webengine widget... bottom line, hit the refresh 10 times each time you want to update the display.... (I did say it was kludgy!) For info, I added a QTextBrowser widget on the screen, and updated the text of that at the same time as the qtwebengine widget - the TextBrowser works flawlessly - this definately implies that its a caching issue... :-(
@CarlBeech From what you just told, I actually think it is not a caching issue. Your issue is one of timing using functions like setHtml(), which can act asynchronously depending on context. If you call it on QWebEngineView, for example, it's async, so refresh() could be called on something that hasn't actually been updated yet, hence the kludge. I'm going to play around with some stuff to make sure I'm right about this whole thing and how I can update my answer for you, and then hopefully the updated answer fixes the root cause of your troubles.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.