How to prevent crash with QDockWidget and custom titleBar?

50 views Asked by At

I have extended QDockWidget with the aim to add the following features:

  • Use a custom titleBar to float and close QDockWidgets (this has many additional actions in my full project).
  • The titleBar shows the title of the QDockWidget, unless it is tabbed and the tab is showing the title.

The minimal example below achieves essentially what I want but there are some reaming problems. All problems are related to the behavior when the user is floating two docks and combines them outside of the main window. This leads to the creation of a QWidget that acts as the new parent for both docks. Any further docking of this docks causes the application to become unstable and eventually crash.

  1. Main issue: When two docks are combined outside the main window and then both floated using the custom float button, an empty window remains. I assume that this is the QWidget which still acts as parent of the docks. When combining the docks in a third window or dragging them back into the main window the whole application crashes with Exit code -1073741819, -1073740791, -1073740771, -1073740940, and possibly others. In some cases the docks just disappear or can only be docked to the original external window. In my full application it also happens that the dock is stuck to the mouse and cannot be docked anywhere. Is there a way to make this robust, maybe a workaround that explicitly removes the QWidget and resets the parent back to the mainWindow?

The following are related but do not cause the application to crash. Let me know if they deserve their own question.

  1. The update of titleBars is not always triggered reliably, for example when dragging directly from main window into external window. Is there a signal I could use to catch that change?

  2. Even though the floating QWidget is a child of MainWindow, tabifiedDockWidgets always returns 0 for tabified docks outside of the main window.

  3. How do I remove the close button of the QWidget that contains both docks? As the custom close button handles closing, there is no need for it. I have tried the following without success:

    dock.parent().setWindowFlag(QtCore.Qt.WindowType.WindowCloseButtonHint, False)

Minimal Example:

import sys
from PyQt6.QtWidgets import QApplication,QMainWindow,QDockWidget,QWidget,QVBoxLayout,QPushButton,QToolBar,QLabel
from PyQt6.QtCore import Qt,QObject,pyqtSignal
from PyQt6.QtGui import QAction
from threading import Timer, current_thread

class MainWindow(QMainWindow):
        
    updateTitleBarSignal = pyqtSignal()
    
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 100, 400, 400)        
        self.updateTitleBarSignal.connect(self.updateTitleBar)                
        self.setDockOptions(QMainWindow.DockOption.AllowTabbedDocks | QMainWindow.DockOption.AllowNestedDocks
                            | QMainWindow.DockOption.GroupedDragging |QMainWindow.DockOption.AnimatedDocks)
                       
        self.td = BetterDockWidget(self,'topdock')
        self.ld = BetterDockWidget(self,'leftdock')
        self.rd = BetterDockWidget(self,'rightdock')
        self.rd2 = BetterDockWidget(self,'rightdock2')
                
        self.addDockWidget(Qt.DockWidgetArea.TopDockWidgetArea,self.td)
        self.splitDockWidget(self.td,self.ld,Qt.Orientation.Vertical)
        self.splitDockWidget(self.ld,self.rd,Qt.Orientation.Horizontal)   
        self.tabifyDockWidget(self.rd,self.rd2)        
        
        self.updateTitleBarButton = QPushButton('updateTitleBars')
        self.updateTitleBarButton.clicked.connect(self.updateTitleBarDelayed)
        self.setCentralWidget(self.updateTitleBarButton)
        
        self.updateTitleBarDelayed()

    def updateTitleBarDelayed(self):
        Timer(.5, self.updateTitleBarSignal.emit).start()

    def updateTitleBar(self):
        for d in [self.td,self.ld,self.rd,self.rd2]:
            d.updateTitleBar()        

class BetterDockWidget(QDockWidget): 
    
    def __init__(self, mainWindow, title):
        super().__init__(title)
        self.mainWindow = mainWindow
        self.title = title
        
        # in the real application all docks should update if one dock changes. for debugging only update the current titleBar (see below) might be sufficient
        self.topLevelChanged.connect(self.mainWindow.updateTitleBar) 
        self.dockLocationChanged.connect(self.mainWindow.updateTitleBar)
        
        # if I only update the current titleBar, the error still occurs but slightly less frequently. changes of the tabified state of other docks are not handled as intended
        # self.topLevelChanged.connect(self.updateTitleBar)
        # self.dockLocationChanged.connect(self.updateTitleBar) 
        
        self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetFloatable) # | QDockWidget.DockWidgetFeature.DockWidgetClosable
        
        lay = QVBoxLayout()
        self.titleBar = QToolBar()
        self.titleBar.setStyleSheet('background:lightgreen')      
        self.titleLabel = QLabel(title)
        self.titleBar.addWidget(self.titleLabel)
        self.floatAction = QAction('float')
        self.floatAction.triggered.connect(lambda : self.setFloating(not self.isFloating()))
        self.titleBar.addAction(self.floatAction)
        self.closeAction = QAction('close')
        self.closeAction.triggered.connect(self.deleteLater)
        self.titleBar.addAction(self.closeAction)
        
        lay.addWidget(self.titleBar)
        self.statusLabel = QLabel()
        lay.addWidget(self.statusLabel)
        self.mainWidget = QWidget()
        self.mainWidget.setLayout(lay)        
        self.setWidget(self.mainWidget)
        self.setTitleBarWidget(self.titleBar)

    def updateTitleBar(self):
        if self.isFloating():
            self.titleLabel.setText(self.title)
        else:
            self.titleLabel.setText(self.title if not isinstance(self.parent(), QMainWindow) or len(self.parent().tabifiedDockWidgets(self)) == 0 else '')
        # show dock status for debugging            
        self.statusLabel.setText(f"floating: {self.isFloating()}\nparent: {self.parent()}\nthread: {current_thread()}\ntabified: {len(self.mainWindow.tabifiedDockWidgets(self))}")
            
if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec())
0

There are 0 answers