pyqt5qdockwidget

PyQt5 using custom QDockWidget from Designer ui file


I'm trying to build an app with extensive use of QDockwidgets. However, I've found that I cannot use QDockwidgets made in Qt Designer.

Here's my customized dockwidget.ui as displayed in Designer:

enter image description here

Here's the QMainWindow form main2.ui with promoted widget (called DockWidget with instance name dw)

enter image description here

Here's what the finished app looks like. The dockwidget is completely empty. Are there any tricks to getting a QDockWidget form (ui file) from Designer to display properly in PyQt5?

enter image description here

main2.py

import sys
from PyQt5 import uic, QtWidgets
from PyQt5.QtWidgets import QApplication

class Main(QtWidgets.QMainWindow, uic.loadUiType('main2.ui')[0]):
    def __init__(self, parent=None):
        super().__init__()
        self.setupUi(self)


app = QApplication(sys.argv)
main = Main(None)

main.show()
sys.exit(app.exec_())

dockwidget.py

from PyQt5 import uic
from PyQt5.QtWidgets import QDockWidget

# ~ form_class = uic.loadUiType('dockwidget.ui')[0]

class DockWidget(QDockWidget):
    def __init__(self, parent=None):

        super().__init__()
        uic.loadUi('dockwidget.ui', self)

main2.ui

<?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>390</width>
    <height>233</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <property name="maximumSize">
    <size>
     <width>0</width>
     <height>0</height>
    </size>
   </property>
   <layout class="QGridLayout" name="gridLayout"/>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>390</width>
     <height>26</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
  <widget class="DockWidget" name="dw">
   <attribute name="dockWidgetArea">
    <number>4</number>
   </attribute>
   <widget class="QWidget" name="dockWidgetContents"/>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>DockWidget</class>
   <extends>QDockWidget</extends>
   <header>dockwidget.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

dockwidget.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>DockWidget</class>
 <widget class="QDockWidget" name="DockWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>328</width>
    <height>303</height>
   </rect>
  </property>
  <property name="features">
   <set>QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetMovable</set>
  </property>
  <property name="allowedAreas">
   <set>Qt::BottomDockWidgetArea|Qt::TopDockWidgetArea</set>
  </property>
  <property name="windowTitle">
   <string>My Custom Dock Widget</string>
  </property>
  <widget class="QWidget" name="dockWidgetContents">
   <layout class="QGridLayout" name="gridLayout_5">
    <property name="leftMargin">
     <number>0</number>
    </property>
    <property name="topMargin">
     <number>0</number>
    </property>
    <property name="rightMargin">
     <number>0</number>
    </property>
    <property name="bottomMargin">
     <number>0</number>
    </property>
    <item row="2" column="0">
     <widget class="QRadioButton" name="radioButton_2">
      <property name="text">
       <string>RadioButton</string>
      </property>
     </widget>
    </item>
    <item row="1" column="0">
     <widget class="QRadioButton" name="radioButton">
      <property name="text">
       <string>RadioButton</string>
      </property>
     </widget>
    </item>
    <item row="0" column="0">
     <widget class="QPushButton" name="pushButton">
      <property name="text">
       <string>PushButton</string>
      </property>
     </widget>
    </item>
    <item row="3" column="0">
     <widget class="QRadioButton" name="radioButton_3">
      <property name="text">
       <string>RadioButton</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

Solution

  • Similarly to scroll areas, Designer creates a "contents" widget for new QDockWidgets. That widget is actually set using setWidget() when the main window UI is loaded, and it cannot be removed from Designer.

    You already have a contents widget in your custom QDockWidget, but when it's added by the main ui, it's "overwritten" by it.

    Consider that promoted classes that act as containers should only have their contents set in the class as long as the promoted widget does not change/override them, otherwise the previously set contents will be removed. For instance, if you create a custom widget with its layout and buttons, and use it as a promoted class in Designer, you cannot set another layout and add other children to that widget from there.

    The only way to avoid the issue and actually create a customizable QDockWidget that already has basic contents set and can be added to a QMainWindow in Designer would be to create a Designer plugin. That's not easy, the documentation is complex and, especially for python, extremely poor (with support even missing for some classes).

    There are various possibilities here, though:

    1. the simplest: do not add the dock widget to the main window in Designer, but only by explicitly calling addDockWidget() in your code;
    2. create a promoted class for the dock widget contents (a basic QWidget/form), and promote the dockWidgetContents of the dock widget that is added to the main window ui;
    3. as long as you are completely sure that you will never call again setWidget() on the dock widget, and you really need the dock widget in the main window of Designer, override setWidget() in a similar way:
    class DockWidget(QDockWidget):
        def __init__(self, parent=None):
            super().__init__()
            uic.loadUi('dockwidget.ui', self)
    
        def setWidget(self, widget):
            if self.widget() is None:
                super().setWidget(widget)
    

    with the above code, the setWidget() call made by uic/setupUi of the main window will be ignored, as at that point the dock widget already has a widget set by its own loadUi.