Search code examples
pythonqt-designerpyqt6

How to promote QPdfview in Qt Designer


I am trying to upgrade my PDF viewer on a form from QWebEngineView to QPdfView (PyQt6). I originally promoted QWebEngineView in the QT Designer without an issue but as far as PDF functionality its a bit limited so upgrading to QPdfView. I promoted using QWidget as base:

enter image description here

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QPdfView" name="widget" native="true">
    <property name="geometry">
     <rect>
      <x>109</x>
      <y>69</y>
      <width>291</width>
      <height>371</height>
     </rect>
    </property>
   </widget>
   <widget class="QComboBox" name="comboBox">
    <property name="geometry">
     <rect>
      <x>60</x>
      <y>490</y>
      <width>79</width>
      <height>22</height>
     </rect>
    </property>
   </widget>
   <widget class="QPushButton" name="pushButton">
    <property name="geometry">
     <rect>
      <x>60</x>
      <y>530</y>
      <width>80</width>
      <height>22</height>
     </rect>
    </property>
    <property name="text">
     <string>PushButton</string>
    </property>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>19</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>QPdfView</class>
   <extends>QWidget</extends>
   <header>PyQt6.QtPdfWidgets</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

However, when I try to load it using:

import sys
from PyQt6 import QtWidgets, uic

app = QtWidgets.QApplication(sys.argv)

window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()

I get the following:

Traceback (most recent call last):
  File "/home/serveracct/proj/promos/main.py", line 14, in <module>
    window = MainWindow()
  File "/home/serveracct/proj/promos/main.py", line 10, in __init__
    uic.loadUi("test2.ui", self)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/load_ui.py", line 86, in loadUi
    return DynamicUILoader(package).loadUi(uifile, baseinstance)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/Loader/loader.py", line 62, in loadUi
    return self.parse(filename)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 1014, in parse
    self._handle_widget(ui_file.widget)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 842, in _handle_widget
    self.traverseWidgetTree(el)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 818, in traverseWidgetTree
    handler(self, child)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 280, in createWidget
    self.traverseWidgetTree(elem)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 818, in traverseWidgetTree
    handler(self, child)
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 271, in createWidget
    self.stack.push(self._setupObject(widget_class, parent, elem))
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/uiparser.py", line 233, in _setupObject
    obj = self.factory.createQtObject(class_name, object_name,
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/objcreator.py", line 119, in createQtObject
    return self._cpolicy.instantiate(ctor, object_name, ctor_args,
  File "/home/serveracct/.pyenv/versions/3.10.8/lib/python3.10/site-packages/PyQt6/uic/Loader/qobjectcreator.py", line 145, in instantiate
    return ctor(*ctor_args, **ctor_kwargs)
TypeError: QPdfView(parent: Optional[QWidget]): not enough arguments


I tried promoting QPdfView in Qt Designer and tried to load the window using uic.loadUi()


Solution

  • This is a "bug" (quotes required) caused by the fact that, unlike any other widget, the parent argument of QPdfView is required.

    When child widgets are created by uic, they are always created with their parent in the constructor, using the parent=[parent widget] keyword.

    PyQt classes are a bit peculiar, and their constructors don't behave exactly like normal Python classes.

    Consider the following case:

    class MyClass:
        def __init__(self, parent):
            pass
    

    With the above, doing MyClass(parent=None) will be perfectly valid.

    The QPdfView constructor is the following:

    QPdfView(parent: Optional[QWidget])
    

    This would make us think that using keyworded parent argument will be equally acceptable as using it as a positional one, but, in reality this won't work.

    I suspect that the main problem relies on Qt, and PyQt just "fails" because it correctly follows the wrong signature: the parent shouldn't be mandatory, and that can be demonstrated by the fact that you can perfectly do QPdfView(None).

    There are two possible workarounds: fix the PyQt file that constructs the classes, or use a custom QPdfView subclass as a promoted widget.

    Fix qobjectcreator.py

    The culprit is the qobjectcreator.py file; its path should be the following:

    <python-lib>/site-packages/PyQt6/uic/Loader/.

    Then go to the instantiate() function (around line 136) and insert the following before the return:

            if ctor.__name__ == 'QPdfView' and 'parent' in ctor_kwargs:
                ctor_args = (ctor_kwargs.pop('parent'), )
    

    Use a promoted subclass

    This is probably a better choice, as it doesn't require changing PyQt files and ensures compatibility for future versions.

    Just create a QPdfView subclass like this:

    class MyPdfView(QPdfView):
        def __init__(self, parent=None):
            super().__init__(parent)
    

    Then use MyPdfView instead of QPdfView as class name for the promoted widget, using the file name as header.

    You can even include that class in your main script, just ensure that you properly use the if __name__ == '__main__': block to create the QApplication.