Issue
I am attempting to use a QPdfView inside a simple PyQt6 app. I have two samples, one works and one does not. The first example displays the PDF correctly but is not very useful as it loads in a basic view that is not part of a layout. When I try to place the view inside a layout, the PDF renders as a gray screen only. I am looking for assistance in how to get the PDF to display properly inside a layout so I can incorporate this into a larger application.
Sample that works:
import os
import sys
from PyQt6 import QtWidgets
from PyQt6.QtPdf import QPdfDocument
from PyQt6.QtPdfWidgets import QPdfView
app = QtWidgets.QApplication(sys.argv)
file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.pdf"))
document = QPdfDocument(None)
document.load(file_path)
view = QPdfView(None)
view.setPageMode(QPdfView.PageMode.MultiPage)
view.setDocument(document)
view.show()
app.exec()
Sample that does not work:
import os
from PyQt6.QtPdf import QPdfDocument
from PyQt6.QtPdfWidgets import QPdfView
from PyQt6.QtWidgets import *
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.pdf"))
document = QPdfDocument(None)
document.load(file_path)
view = QPdfView(None)
view.setPageMode(QPdfView.PageMode.MultiPage)
view.setDocument(document)
layout = QVBoxLayout()
layout.addWidget(view)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.show()
app = QApplication([])
window = MainWindow()
app.exec()
I am new to PyQT and am guessing there is something wrong with the layout and and how the QPdfView is inheriting layout parameters, but I am at a loss. Can anyone explain how to get the second code sample to render the PDF? I really do not want to go the WebView route to display the PDF and was hoping the QPdfView and QDocument objects would work.
Solution
tl;dr
Make the document a persistent object, either by using a proper parent in the constructor (instead of None
), or by making it an instance attribute (self.document
).
Object persistence and Qt parent/child relationships
In your second case, the document
is a local variable, and when __init__
is completed it gets garbage collected.
The viewer does not take ownership of the document when calling setDocument()
, meaning that if document
does not have some persistence, it will be destroyed.
Now, for various reasons, similar Qt document/view situations do not take ownership, but a code like the above would have worked without making the "document" persistent (for instance, QTextEdit/QTextDocument or QGraphicsView/QGraphicsScene), but simpler cases like QPdfView are not able to keep a reference.
Still, it's good practice to ensure that a persistence always exists and not always taking it for granted whenever the documentation doesn't specifically point out ownership aspects. The Qt documentation is normally quite thorough about this, unfortunately it seems that the new classes in Qt6 haven't received that kind of attention yet.
When a Qt object takes ownership of another one, it remains persistent for the lifespan of that parent: when it gets destroyed, the child will be destroyed along with it, unless it's transferred to another parent. If the ownership is not automatic, it is achieved by calling setParent()
on the target.
Now, in your case, there are two possible solutions:
- make the object persistent in python, by making it an instance attribute:
self.document = QPdfDocument(None)
; - set the a valid parent for the document, which could be the view (after it's created,
QPdfDocument(view)
) or even the window (QPdfDocument(self)
);
Note that, for some reason, Qt6 makes the parent argument of both QPdfView and QPdfDocument as mandatory (contrary to most Qt objects and widgets). I am not sure about that reason, and it may also be a bug.
Also, like most non widget Qt classes, the parent of QPdfDocument does not need to be a QWidget, but can be another QObject subclass (like, for instance, the QApplication), as shown in the documentation of its constructor.
The difference between these two approaches is small, but important.
For example, you may think that also other objects created as local variables should be destroyed as well (view
, layout
or widget
), but this doesn't happen because they are quite related to the MainWindow
instance:
setCentralWidget(widget)
makes the QMainWindow take ownership of the central widget;widget.setLayout(layout)
makes the widget take ownership of the layout;layout.addWidget(view)
makes the layout (and, therefore, the parent widget) take ownership of the view;
If the Qt window gets destroyed, all those objects will be destroyed as well (due to the parent/child relation above).
Note that I specifically wrote "Qt window", not the window
reference to its instance: a Qt object can be destroyed while its python reference may still exist. This may happen if the WA_DeleteOnClose
attribute is set and the window is closed, or by explicitly calling deleteLater()
on it.
Remember that PyQt and PySide are just python bindings: any python reference is a wrapper around a C++ created object, and both of them can have different lifespans.
In that case, you may still have a python reference to the window, but the Qt object will be inaccessible (PyQt will throw an exception if you try to access its Qt functions). At the same time, if the document was created with no parent and was made an instance attribute, that document will still exist and will be completely functional, even if the Qt window that the instance referred to isn't.
The opposite is also true: you can remove any python reference to a Qt object, but if that object is owned by another one that still exists, then it will not be destroyed. For instance, you can do del widget
at the end of __init__
, and it will still work as expected: while the reference count of widget
reaches zero (by using del
for that unique reference), the Qt object still exists, since it's owned by another one which is still alive.
These are important aspects to remember, as it's common to "forget" objects created without parent that have no ownership set, which may result in important memory leaks and even fatal crash (for instance, a QTimer that tries to call a function of a Qt object that has been destroyed in the meantime).
Why the other example works
In your first code all this didn't happen, because the code execution stops at QApplication.exec()
and doesn't go on until that returns: that function starts the application event loop, and nothing after that in that code block will be executed until the application is quit. So, in that case, all the variables remain alive.
This is even more clear if all that code is moved to a function:
def showDoc():
app = QtWidgets.QApplication(sys.argv)
file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.pdf"))
document = QPdfDocument(None)
document.load(file_path)
view = QPdfView(None)
view.setPageMode(QPdfView.PageMode.MultiPage)
view.setDocument(document)
view.show()
app.exec() # <- code blocks here until the application is quit
print('All above variables will be garbage collected after this line')
if __name__ == '__main__':
showDoc()
print('those variables do not exist anymore, along with their objects')
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.