如何使用异步代码(例如 QWebEnginePage.load() 或 QWebEnginePage.printToPdf())保持在 QRunnable 中
How to stay in QRunnable with async code (e.g. QWebEnginePage.load() or QWebEnginePage.printToPdf())
我的演示程序无法正常工作:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtPrintSupport import *
from PyQt5.QtWebEngineWidgets import *
class Runnable(QRunnable):
def __init__(self, window, mode):
super(Runnable, self).__init__()
self.window = window
self.mode = mode
self.html = '<!DOCTYPE HTML><html><body><p>test</p></body></html>'
self.report_filename = 'report.pdf'
def run(self):
if self.mode == 'sync':
# this works ok
printer = QPrinter()
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setPaperSize(QPrinter.A4)
printer.setOutputFileName(self.report_filename)
doc = QTextDocument()
doc.setHtml(self.html)
#doc.setPageSize(printer.pageRect().size())
doc.print(printer)
print('pdf file created')
elif self.mode == 'async':
# this doesn't work
self.page = QWebEnginePage()
self.page.loadFinished.connect(self.on_load_finished)
loadFinished_works_for_setHtml = False
if loadFinished_works_for_setHtml:
# async func, but no loadFinished signal in docs, too bad
self.page.setHtml(self.html)
else:
# silly, all because no loadFinished signal for setHtml()
with open('report.html', 'w', encoding='utf-8') as f:
f.write(self.html)
url = QUrl('file:report.html')
print('url.isLocalFile():', url.isLocalFile())
self.page.load(url)
def on_load_finished(self, ok):
# 1. problem: This method never executes, why?
# I tried with QWebEngineView() too, but without luck.
# 2. problem: If this method somehow executes, it will run in main thread,
# but self.page.printToPdf() can be slow, so I want this to also
# run in runnable thread or some other thread, but not in main thread
print('load finished, ok: ', ok)
#self.page.pdfPrintingFinished.connect(self.on_pdf_printing_finished)
#page_layout = QPageLayout(QPageSize(QPageSize.A4), QPageLayout.Portrait, QMarginsF(20, 20, 20, 20), QPageLayout.Millimeter, minMargins = QMarginsF(0, 0, 0, 0))
#self.page.printToPdf(self.report_filename, page_layout)
#def on_pdf_printing_finished(self, file_path, ok):
# print('printToPdf finished', file_path, ok)
# # send signal to main thread or open pdf file with subprocess.Popen() or sth.
# # 1a. problem: I want this (for example opening pdf file) to also run in
# # runnable thread or some other thread, but not in main thread
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.print_sync_button = QPushButton('Make PDF (sync)', self)
self.print_async_button = QPushButton('Make PDF (async)', self)
self.print_sync_button.clicked.connect(lambda : self.handle_print('sync'))
self.print_async_button.clicked.connect(lambda : self.handle_print('async'))
layout = QHBoxLayout(self)
layout.addWidget(self.print_sync_button)
layout.addWidget(self.print_async_button)
def handle_print(self, mode='sync'):
worker = Runnable(self, mode)
QThreadPool.globalInstance().start(worker)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
我在代码的注释 (1., 1a., 2.) 中描述了我的问题。
我的第一个问题是 self.page.load(url) 从不发送 loadFinished 信号。不知道为什么?
第二个问题更笼统:如何在 QRunnable 运行() 方法中 运行 异步代码(如果可能的话)?例如,我想用 QWebEnginePage(或 QWebEngineView.page())生成 pdf 报告,如 QWebEnginePage.printToPdf(),但在那种情况下,我应该为 QWebEnginePage.load() 和 pdfPrintingFinished 信号使用 loadFinished 信号对于 QWebEnginePage.printToPdf()。这些信号将连接到 QRunnable 线程中不再 运行 的方法。他们将在主线程中 运行 减慢 gui(这两种方法可能很慢,更不用说我想在线程中打开 Adobe Reader 生成的 pdf 文档)。
如何在线程(QRunnable 或其他一些)中完成所有代码 运行 而不是返回主线程?
类似的问题是here,但似乎没有进一步讨论就悬而未决。
基于错误的假设,您的问题及其代码中存在各种问题。
- 虽然 QWebEnginePage 在技术上不是一个小部件,但它的行为是这样的,所以,就像 any UI 元素一样,它不能被创建,也不能从外部线程访问(包括使用 QRunnable);
- 给定的 QUrl 对此无效:网络“浏览器”始终使用绝对路径,包括在加载本地文件时。虽然
file:report.html
是本地 Qt 文件访问可接受的 QUrl,但从浏览器的角度来看,它 不是 有效地址:如果您尝试在您的文件中写入该路径Web 浏览器,它不会工作,即使该文件位于浏览器可执行文件的路径中。
您可以使用 QDir 或 QFileInfo 来构建绝对路径:
QUrl.fromLocalFile(QDir.current().absoluteFilePath('report.html'))
QFileInfo('report.html').absoluteFilePath()
loadFinished
信号对 setHtml
不起作用,因为该函数不会“加载”任何内容:它只是设置页面内容。 Loading,就网络浏览器而言,意味着both设置内容和完成其[下载]加载;
- 打印必须在同一台打印机上排队,因为不可能同时访问一台打印机;由于pdf打印机显然是抽象打印机,属于网页对象;
所以,解决方案是不使用QRunnable,而是正确管理打印。
然后,您可以为每个需要打印的文档创建一个 QWebEnginePage,或者使用一个唯一的 QWebEnginePage 并在每次加载 和 打印时排队。
无论哪种情况,所提供的 QUrl 都必须具有正确的 绝对 路径。
显然是有区别的。
第二个选项需要的资源非常有限,但是完全排队显然意味着这个过程是顺序的,所以可能会花费很多时间。
第一个选项需要更多的系统资源(想象它为每个文档打开一个选项卡,并且每个 Web 呈现都非常 CPU/RAM 要求很高,即使它不显示),但具有打印速度更快的好处:自加载和打印是异步的,你实际上可以在很短的时间间隔内处理几十页。
一个可能且更安全的解决方案是创建一个系统来限制并发页数和打印进程,并在每次打印完成后立即将剩余的排队。
最后,QTextDocument 和 QPrinter 都是线程安全的,所以在 QRunnable 中使用它们是没有问题的,而且如果你正确地创建一个正在运行的 QRunnable,它们是不是同步的为每次打印执行(这是 QRunnable 的主要目的之一:能够 运行 它不止一次)。唯一的区别是 QTextDocument 有一个 limited HTML support.
我的演示程序无法正常工作:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtPrintSupport import *
from PyQt5.QtWebEngineWidgets import *
class Runnable(QRunnable):
def __init__(self, window, mode):
super(Runnable, self).__init__()
self.window = window
self.mode = mode
self.html = '<!DOCTYPE HTML><html><body><p>test</p></body></html>'
self.report_filename = 'report.pdf'
def run(self):
if self.mode == 'sync':
# this works ok
printer = QPrinter()
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setPaperSize(QPrinter.A4)
printer.setOutputFileName(self.report_filename)
doc = QTextDocument()
doc.setHtml(self.html)
#doc.setPageSize(printer.pageRect().size())
doc.print(printer)
print('pdf file created')
elif self.mode == 'async':
# this doesn't work
self.page = QWebEnginePage()
self.page.loadFinished.connect(self.on_load_finished)
loadFinished_works_for_setHtml = False
if loadFinished_works_for_setHtml:
# async func, but no loadFinished signal in docs, too bad
self.page.setHtml(self.html)
else:
# silly, all because no loadFinished signal for setHtml()
with open('report.html', 'w', encoding='utf-8') as f:
f.write(self.html)
url = QUrl('file:report.html')
print('url.isLocalFile():', url.isLocalFile())
self.page.load(url)
def on_load_finished(self, ok):
# 1. problem: This method never executes, why?
# I tried with QWebEngineView() too, but without luck.
# 2. problem: If this method somehow executes, it will run in main thread,
# but self.page.printToPdf() can be slow, so I want this to also
# run in runnable thread or some other thread, but not in main thread
print('load finished, ok: ', ok)
#self.page.pdfPrintingFinished.connect(self.on_pdf_printing_finished)
#page_layout = QPageLayout(QPageSize(QPageSize.A4), QPageLayout.Portrait, QMarginsF(20, 20, 20, 20), QPageLayout.Millimeter, minMargins = QMarginsF(0, 0, 0, 0))
#self.page.printToPdf(self.report_filename, page_layout)
#def on_pdf_printing_finished(self, file_path, ok):
# print('printToPdf finished', file_path, ok)
# # send signal to main thread or open pdf file with subprocess.Popen() or sth.
# # 1a. problem: I want this (for example opening pdf file) to also run in
# # runnable thread or some other thread, but not in main thread
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.print_sync_button = QPushButton('Make PDF (sync)', self)
self.print_async_button = QPushButton('Make PDF (async)', self)
self.print_sync_button.clicked.connect(lambda : self.handle_print('sync'))
self.print_async_button.clicked.connect(lambda : self.handle_print('async'))
layout = QHBoxLayout(self)
layout.addWidget(self.print_sync_button)
layout.addWidget(self.print_async_button)
def handle_print(self, mode='sync'):
worker = Runnable(self, mode)
QThreadPool.globalInstance().start(worker)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
我在代码的注释 (1., 1a., 2.) 中描述了我的问题。
我的第一个问题是 self.page.load(url) 从不发送 loadFinished 信号。不知道为什么?
第二个问题更笼统:如何在 QRunnable 运行() 方法中 运行 异步代码(如果可能的话)?例如,我想用 QWebEnginePage(或 QWebEngineView.page())生成 pdf 报告,如 QWebEnginePage.printToPdf(),但在那种情况下,我应该为 QWebEnginePage.load() 和 pdfPrintingFinished 信号使用 loadFinished 信号对于 QWebEnginePage.printToPdf()。这些信号将连接到 QRunnable 线程中不再 运行 的方法。他们将在主线程中 运行 减慢 gui(这两种方法可能很慢,更不用说我想在线程中打开 Adobe Reader 生成的 pdf 文档)。
如何在线程(QRunnable 或其他一些)中完成所有代码 运行 而不是返回主线程?
类似的问题是here,但似乎没有进一步讨论就悬而未决。
基于错误的假设,您的问题及其代码中存在各种问题。
- 虽然 QWebEnginePage 在技术上不是一个小部件,但它的行为是这样的,所以,就像 any UI 元素一样,它不能被创建,也不能从外部线程访问(包括使用 QRunnable);
- 给定的 QUrl 对此无效:网络“浏览器”始终使用绝对路径,包括在加载本地文件时。虽然
file:report.html
是本地 Qt 文件访问可接受的 QUrl,但从浏览器的角度来看,它 不是 有效地址:如果您尝试在您的文件中写入该路径Web 浏览器,它不会工作,即使该文件位于浏览器可执行文件的路径中。
您可以使用 QDir 或 QFileInfo 来构建绝对路径:QUrl.fromLocalFile(QDir.current().absoluteFilePath('report.html'))
QFileInfo('report.html').absoluteFilePath()
loadFinished
信号对setHtml
不起作用,因为该函数不会“加载”任何内容:它只是设置页面内容。 Loading,就网络浏览器而言,意味着both设置内容和完成其[下载]加载;- 打印必须在同一台打印机上排队,因为不可能同时访问一台打印机;由于pdf打印机显然是抽象打印机,属于网页对象;
所以,解决方案是不使用QRunnable,而是正确管理打印。
然后,您可以为每个需要打印的文档创建一个 QWebEnginePage,或者使用一个唯一的 QWebEnginePage 并在每次加载 和 打印时排队。
无论哪种情况,所提供的 QUrl 都必须具有正确的 绝对 路径。
显然是有区别的。
第二个选项需要的资源非常有限,但是完全排队显然意味着这个过程是顺序的,所以可能会花费很多时间。
第一个选项需要更多的系统资源(想象它为每个文档打开一个选项卡,并且每个 Web 呈现都非常 CPU/RAM 要求很高,即使它不显示),但具有打印速度更快的好处:自加载和打印是异步的,你实际上可以在很短的时间间隔内处理几十页。
一个可能且更安全的解决方案是创建一个系统来限制并发页数和打印进程,并在每次打印完成后立即将剩余的排队。
最后,QTextDocument 和 QPrinter 都是线程安全的,所以在 QRunnable 中使用它们是没有问题的,而且如果你正确地创建一个正在运行的 QRunnable,它们是不是同步的为每次打印执行(这是 QRunnable 的主要目的之一:能够 运行 它不止一次)。唯一的区别是 QTextDocument 有一个 limited HTML support.