When a widget is docked, I would like it to change direction and have minimal size relative to how the dock expands.
That is,
- left/right dock => top-to-bottom direction with minimum width
- top/bottom dock => left-to-right direction with minimum height
The trouble is, whenever the direction is changed, the dock takes on a seemingly arbitrary width or height. I cannot find a way to resize/force the dock widget to a particular size when docked. I have tried countless variations of overriding the sizeHint
, minimumSizeHint
, calling adjustSize
, and fiddling with the sizePolicy
.
How can I ensure the initial dock size?
My basic application looks like:
The application shows primary and secondary information along with corresponding sets of controls. A tab widget containing the primary and secondary content is set as the Central Widget. A QStackedWidget housing the controls in respective dashboards lives in a dock. When the tab changes, the corresponding dashboard is shown. The code for this is given below in basic application code.
The difficulty lies in that changing the direction of the dashboard upsets the size of the dock.
To adjust the dashboard direction, I can think of two reasonable solutions:
- via the
resizeEvent
or - via the
dockLocationChanged
signal
Adjusting direction via resizeEvent
This seems, to me, the preferable option. It allows the user the most flexibility. If they dislike the direction of a dock, dragging it past a particular limit will allow them to change the dock's direction. Here I check for whether it is wider than tall.
class MyDock(QtWidgets.QDockWidget):
def __init__(self):
super(MyDock, self).__init__()
def resizeEvent(self, event):
size = event.size()
is_wide = size.width() > size.height()
container_object = self.widget().currentWidget()
if is_wide:
container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
else:
container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)
The complete code for this is given below in resize approach.
Change direction on dockLocationChange
As the resize event happens all the time, another approach might be to change the direction only when the dock location changes. To do this, connect a function to the dockLocationChanged
signal and adjust the direction depending on the dock.
class MyDock(QtWidgets.QDockWidget):
def __init__(self):
super(MyDock, self).__init__()
self.dockLocationChanged.connect(self.dock_location_changed)
def dock_location_changed(self, area):
top = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
bottom = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea
container_object = self.widget().currentWidget()
if area in [top, bottom]:
container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
else:
container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)
Basic application code
The program consists of 5 separate classes.
For
MyWindow
,PrimaryDashboard
, andSecondaryDashboard
the reason for separation should be clear enough.
For
MyDock
andDockContainer
the separation is to facilitate overriding sizeHint
, setDirection
, or other methods.
import qtpy
from qtpy import QtWidgets, QtGui, QtCore
import sys
class PrimaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(PrimaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Primary dashboard')
self.ok = QtWidgets.QPushButton('OK')
self.cancel = QtWidgets.QPushButton('Cancel')
def init_layout(self):
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(self.label)
self.layout.addWidget(self.ok)
self.layout.addWidget(self.cancel)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class SecondaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(SecondaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Secondary dashboard')
self.descr1 = QtWidgets.QLabel('Thing 1')
self.check1 = QtWidgets.QCheckBox()
self.descr2 = QtWidgets.QLabel('Thing 2')
self.check2 = QtWidgets.QCheckBox()
def init_layout(self):
self.layout = QtWidgets.QVBoxLayout()
self.grid = QtWidgets.QGridLayout()
self.grid.addWidget(self.descr1, 0, 0)
self.grid.addWidget(self.check1, 0, 1)
self.grid.addWidget(self.descr2, 1, 0)
self.grid.addWidget(self.check2, 1, 1)
self.layout.addWidget(self.label)
self.layout.addLayout(self.grid)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class DockContainer(QtWidgets.QStackedWidget):
def __init__(self):
super(DockContainer, self).__init__()
class MyDock(QtWidgets.QDockWidget):
def __init__(self):
super(MyDock, self).__init__()
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent=parent)
self.resize(600, 400)
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.tab_widget = QtWidgets.QTabWidget()
self.tab1 = QtWidgets.QLabel('Primary content')
self.tab2 = QtWidgets.QLabel('Secondary content')
self.tab_widget.addTab(self.tab1, 'Primary')
self.tab_widget.addTab(self.tab2, 'Secondary')
self.tab_widget.currentChanged.connect(self.tab_selected)
self.primary_dashboard = PrimaryDashboard()
self.secondary_dashboard = SecondaryDashboard()
self.dashboard = DockContainer()
self.dashboard.addWidget(self.primary_dashboard)
self.dashboard.addWidget(self.secondary_dashboard)
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.dock = MyDock()
self.dock.setWidget(self.dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
def init_layout(self):
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.tab_widget)
self.main_widget = QtWidgets.QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
def tab_selected(self):
tab_index = self.tab_widget.currentIndex()
if self.tab_widget.tabText(tab_index) == 'Secondary':
self.dashboard.setCurrentWidget(self.secondary_dashboard)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
else: # Primary
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Resize approach
This code is identical to the basic application code yet with resizeEvent
overridden in the dock widget.
import qtpy
from qtpy import QtWidgets, QtGui, QtCore
import sys
class PrimaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(PrimaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Primary dashboard')
self.ok = QtWidgets.QPushButton('OK')
self.cancel = QtWidgets.QPushButton('Cancel')
def init_layout(self):
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(self.label)
self.layout.addWidget(self.ok)
self.layout.addWidget(self.cancel)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class SecondaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(SecondaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Secondary dashboard')
self.descr1 = QtWidgets.QLabel('Thing 1')
self.check1 = QtWidgets.QCheckBox()
self.descr2 = QtWidgets.QLabel('Thing 2')
self.check2 = QtWidgets.QCheckBox()
def init_layout(self):
self.layout = QtWidgets.QVBoxLayout()
self.grid = QtWidgets.QGridLayout()
self.grid.addWidget(self.descr1, 0, 0)
self.grid.addWidget(self.check1, 0, 1)
self.grid.addWidget(self.descr2, 1, 0)
self.grid.addWidget(self.check2, 1, 1)
self.layout.addWidget(self.label)
self.layout.addLayout(self.grid)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class DockContainer(QtWidgets.QStackedWidget):
def __init__(self):
super(DockContainer, self).__init__()
class MyDock(QtWidgets.QDockWidget):
def __init__(self):
super(MyDock, self).__init__()
def resizeEvent(self, event):
size = event.size()
is_wide = size.width() > size.height()
container_object = self.widget().currentWidget()
if is_wide:
container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
else:
container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent=parent)
self.resize(600, 400)
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.tab_widget = QtWidgets.QTabWidget()
self.tab1 = QtWidgets.QLabel('Primary content')
self.tab2 = QtWidgets.QLabel('Secondary content')
self.tab_widget.addTab(self.tab1, 'Primary')
self.tab_widget.addTab(self.tab2, 'Secondary')
self.tab_widget.currentChanged.connect(self.tab_selected)
self.primary_dashboard = PrimaryDashboard()
self.secondary_dashboard = SecondaryDashboard()
self.dashboard = DockContainer()
self.dashboard.addWidget(self.primary_dashboard)
self.dashboard.addWidget(self.secondary_dashboard)
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.dock = MyDock()
self.dock.setWidget(self.dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
def init_layout(self):
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.tab_widget)
self.main_widget = QtWidgets.QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
def tab_selected(self):
tab_index = self.tab_widget.currentIndex()
if self.tab_widget.tabText(tab_index) == 'Secondary':
self.dashboard.setCurrentWidget(self.secondary_dashboard)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
else: # Primary
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
dockLocationChanged
approach
This code is identical to the basic application code yet with the dockLocationChanged
signal connected to a method which adjusts the direction based on the current dock location.
import qtpy
from qtpy import QtWidgets, QtGui, QtCore
import sys
class PrimaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(PrimaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Primary dashboard')
self.ok = QtWidgets.QPushButton('OK')
self.cancel = QtWidgets.QPushButton('Cancel')
def init_layout(self):
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(self.label)
self.layout.addWidget(self.ok)
self.layout.addWidget(self.cancel)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class SecondaryDashboard(QtWidgets.QWidget):
def __init__(self):
super(SecondaryDashboard, self).__init__()
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.label = QtWidgets.QLabel('Secondary dashboard')
self.descr1 = QtWidgets.QLabel('Thing 1')
self.check1 = QtWidgets.QCheckBox()
self.descr2 = QtWidgets.QLabel('Thing 2')
self.check2 = QtWidgets.QCheckBox()
def init_layout(self):
self.layout = QtWidgets.QVBoxLayout()
self.grid = QtWidgets.QGridLayout()
self.grid.addWidget(self.descr1, 0, 0)
self.grid.addWidget(self.check1, 0, 1)
self.grid.addWidget(self.descr2, 1, 0)
self.grid.addWidget(self.check2, 1, 1)
self.layout.addWidget(self.label)
self.layout.addLayout(self.grid)
self.setLayout(self.layout)
def setDirection(self, direction):
self.layout.setDirection(direction)
class DockContainer(QtWidgets.QStackedWidget):
def __init__(self):
super(DockContainer, self).__init__()
class MyDock(QtWidgets.QDockWidget):
def __init__(self):
super(MyDock, self).__init__()
self.dockLocationChanged.connect(self.dock_location_changed)
def dock_location_changed(self, area):
top = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
bottom = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea
# left = QtCore.Qt.DockWidgetArea.LeftDockWidgetArea
# right = QtCore.Qt.DockWidgetArea.RightDockWidgetArea
container_object = self.widget().currentWidget()
if area in [top, bottom]:
container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
else:
container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)
class MyWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent=parent)
self.resize(600, 400)
self.init_widgets()
self.init_layout()
def init_widgets(self):
self.tab_widget = QtWidgets.QTabWidget()
self.tab1 = QtWidgets.QLabel('Primary content')
self.tab2 = QtWidgets.QLabel('Secondary content')
self.tab_widget.addTab(self.tab1, 'Primary')
self.tab_widget.addTab(self.tab2, 'Secondary')
self.tab_widget.currentChanged.connect(self.tab_selected)
self.primary_dashboard = PrimaryDashboard()
self.secondary_dashboard = SecondaryDashboard()
self.dashboard = DockContainer()
self.dashboard.addWidget(self.primary_dashboard)
self.dashboard.addWidget(self.secondary_dashboard)
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.dock = MyDock()
self.dock.setWidget(self.dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
def init_layout(self):
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.tab_widget)
self.main_widget = QtWidgets.QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
def tab_selected(self):
tab_index = self.tab_widget.currentIndex()
if self.tab_widget.tabText(tab_index) == 'Secondary':
self.dashboard.setCurrentWidget(self.secondary_dashboard)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
else: # Primary
self.dashboard.setCurrentWidget(self.primary_dashboard)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Think of an application as a set of Matryoshka dolls. An inner doll's size dictates that of the subsequent outer ones. Clearly, an inner doll cannot be bigger than the one that contains it! QWidgets are modeled similarly.
The documentation for QWidget.sizeHint() goes on to say,
Altogether, a widget's sizing comes from the inside out, based on the layout.
If you were to implement a
resizeEvent
1 for each of the objects in the base application code, you would see the following sequence of sizings,PrimaryDashboard
resizeEventDockContainer
resizeEventMyDock
resizeEventThis is the nesting we expect. The
PrimaryDashboard
is consulted first for its size, then theDockContainer
, and then theMyDock
. Technically speaking, it's widgets all the way down. However, thePrimaryDashboard
contains buttons and labels which ought to be smaller than the width/height of the MainWindow in the majority of circumstances. The first doll in the sequence to significantly affect dock sizing is thePrimaryDashboard
.Examining the
resizeEvent
, usingevent.size()
, we can see that a reasonable minimum for a horizontal dashboard is a height of120
pixels whereas a vertical orientation has a reasonable minimum width of146
. ThesizeHint()
can then be set to return theminimumSizeHint()
and have the minimum return the(146, 120)
for each of the dashboards2. In effect, this tells the application to prefer a minimum size of(146, 120)
while still allowing for resizing in general.Granted, using fixed sizing can be dangerous, as absolutes are unforgiving and not flexible by definition. However, the content likely has a natural minimum size3. We can simply
setMinimumSize()
on the entire application to not allow resizing smaller than ourminimumSizeHint()
.To change the direction of the dock widget content, we can use the
dockLocationChanged
signal. We can also make the code a little neater than how it was presented in the question. Rather than connect the signal within the dock widget, we can connect it at the instance level withinMyWindow
. In fact, there is no need to defineMyDock
at all. A plainQDockWidget
will suffice.Docks with a minimal initial size
1. How one might implement such a
resizeEvent
to see who is resizing:2. A natural question is, "Why not simply set the
sizeHint()
to return the minimum size instead of callingminimumSizeHint()
? The best response I have is, "It don't work that way." Setting only thesizeHint()
doesn't resize the dock to the minimum as you might expect.The
sizeHint()
andminimumSizeHint()
methods are virtual functions. My guess is that Qt has other functionality, which we're not privy to, that references these methods independently, requiring us to define things this way.3. If the content is a map, for example, it's unlikely the user would ever want the map to be
10px x 10px
. Furthermore, it's a reasonable assumption that the user won't be working with a screen resolution less than600 x 400
unless they're on mobile. But if you're developing for mobile using PySide or PyQt5, there are some important questions you should be asking yourself, such as "Why?".