当父QFrame调整大小时,PyQt5的 QWebEngineView会闪烁,如何避免?
我有一个PyQt5应用,在一个 QFrame 内只有一个 QWebEngineView。框架起初很小(150×150),按下按钮时我将其调整到更大的尺寸(300×300)。我希望网页内容能够随框架一起缩放。
以下是一个最小可复现的示例:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QFrame
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import Qt
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.resize(600, 500)
self.is_big = False
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignCenter)
self.box = QFrame()
self.box.setStyleSheet("border: 2px solid black;")
self.box.setFixedSize(150, 150)
self.box_layout = QVBoxLayout(self.box)
self.box_layout.setContentsMargins(0, 0, 0, 0)
self.web_widget = QWebEngineView()
self.web_widget.setAttribute(Qt.WA_TranslucentBackground, True)
self.web_widget.setStyleSheet("background: transparent;")
self.web_widget.setHtml("""
<html>
<body style="display:flex; align-items:center; justify-content:center; height:100%; margin:0;">
<h2>Hello World</h2>
</body>
</html>
""")
self.box_layout.addWidget(self.web_widget)
self.button = QPushButton("Toggle Size")
self.button.clicked.connect(self.toggle_size)
layout.addWidget(self.box)
layout.addWidget(self.button)
def toggle_size(self):
if self.is_big:
self.box.setFixedSize(150, 150)
else:
self.box.setFixedSize(300, 300)
self.is_big = not self.is_big
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
问题:
点击按钮时,QWebEngineView 会在左上角短暂闪现,然后才正确地调整大小。
问题:
如何在父框架调整大小时防止 QWebEngineView 闪烁?在PyQt5中是否有更好的方法来平滑缩放内容并同时调整容器大小?
解决方案
这个问题源于底层Chromium引擎应用样式的方式并不理想。大体而言,它先加载HTML,再应用所有样式,这在文档完全加载之前经常会导致页面元素的丑陋重新排布(取决于样式的具体性质)。Qt6使用的最新版Chromium仍然表现出这种行为,因此似乎是一个基本问题,短期内不太可能改变。
到目前为止,我找到的唯一变通方法有点hacky,但似乎相当可靠。(如果有人知道更好的办法,我会很感兴趣地了解)。基本思路是在渲染阶段隐藏 html 元素,然后在短暂延迟后再显示它。因此,在你的示例中,将 toggle_size 的槽函数替换为以下代码:
def toggle_size(self):
def callback():
if self.is_big:
self.box.setFixedSize(150, 150)
else:
self.box.setFixedSize(300, 300)
self.is_big = not self.is_big
self.web_widget.page().runJavaScript(
'document.documentElement.style.display = "block"')
self.web_widget.page().runJavaScript(
'document.documentElement.style.display = "none"',
lambda result: QTimer.singleShot(10, callback))
在Linux上,使用PyQt-5.15.11或 PyQt-6.10.2时对我有效——但我从未在其他平台上测试过,因此见仁见智。
类似的变通方法也可用于应用自定义样式表(以及/或需要在页面完全加载之前操作DOM的脚本)。也许实现这一点的最佳方式是先重写并缓存HTML源代码,使其document-element变成类似如下的结构:
<html style="display: none">
然后使用 QWebEngineScript 通过JavaScript的 DOM操作在取消隐藏document-element之前注入自定义样式表,并通过事件监听在取消隐藏document-element时再次显示它。所以,大致地说,像这样:
script = QWebEngineScript()
script.setName('css')
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)
script.setRunsOnSubFrames(True)
script.setSourceCode(f"""
(function() {{
var css = document.createElement('style')
css.type = 'text/css'
css.id = 'custom-css'
document.head.appendChild(css)
css.innerText = {MY_CUSTOM_CSS!r}
}})()
""")
webview.profile().scripts().insert(script)
script = QWebEngineScript()
script.setName('js')
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setRunsOnSubFrames(False)
script.setSourceCode("""
window.addEventListener('load', function(event) {
document.documentElement.style.display = 'block'
})
""")
webview.profile().scripts().insert(script)