QDockWidget tabify/splitDockWidget weird behavior

2019-07-21 15:03发布

I have an application with MdiChilds which should contain multiple QDockWidgets. I am however having troubles splitting/tabbing up the Widgets so that they produce the desired default layout. I basically want a layout like this:

QDockWidgetTest desired layout

Widget 4 is created last and needs to go next to the tabified widgets 2 & 3. However, inserting it causes itself and another widget to go missing:

QtDockWidgetTest screenshot

Here's the code producing the second screenshot:

In the constructor of the main window (or the mdi childs, doesn't really matter) I do the following:

QDockWidgetTest::QDockWidgetTest(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
    setCentralWidget(0); // only QDockWidgets
    QWidget* testWidget1 = new TestWidget("1", QColor("red"));
    QWidget* testWidget2 = new TestWidget("2", QColor("green"));
    QWidget* testWidget3 = new TestWidget("3", QColor("blue"));
    QWidget* testWidget4 = new TestWidget("4", QColor("yellow"));
    DockWidgetWrapper* testQWidget1 = new DockWidgetWrapper(testWidget1, "Test Widget 1", "TestWidget1");
    DockWidgetWrapper* testQWidget2 = new DockWidgetWrapper(testWidget2, "Test Widget 2", "TestWidget2");
    DockWidgetWrapper* testQWidget3 = new DockWidgetWrapper(testWidget3, "Test Widget 3", "TestWidget3");
    DockWidgetWrapper* testQWidget4 = new DockWidgetWrapper(testWidget4, "Test Widget 4", "TestWidget4");
    addDockWidget(Qt::LeftDockWidgetArea, testQWidget1);
    splitDockWidget(testQWidget1, testQWidget2, Qt::Vertical);
    tabifyDockWidget(testQWidget2, testQWidget3);
    splitDockWidget(testQWidget3, testQWidget4, Qt::Horizontal);
}

where TestWidget is defined as a simple Widget just painting itself in the given color, and the caption in the middle:

#include <QWidget>
#include <QPainter>
#include <QPaintEvent>
#include <QFontMetrics>

class TestWidget: public QWidget
{
private:
    QString m_content;
    QColor m_fillColor;
public:
    TestWidget(QString content, QColor color):
        m_content(content),
        m_fillColor(color)
    {
        m_fillColor.setAlpha(50);
    }
protected:
    void paintEvent(QPaintEvent* e)
    {
        QPainter p(this);
        QFontMetrics fm(p.font());
        QRect g(geometry());
        p.fillRect(g, m_fillColor);
        p.drawText(g.width()/2 - fm.width(m_content), g.height()/2 + fm.height(), m_content);
    }
};

and DockWidgetWrapper is a simple wrapper into QDockWidget for any QWidget:

#include <QDockWidget>
#include <QVBoxLayout>

class DockWidgetWrapper: public QDockWidget
{
public:
    DockWidgetWrapper(QWidget* widget, QString const & windowTitle, QString const & objectName)
    {
        setWindowTitle(windowTitle);
        setFeatures(DockWidgetClosable | DockWidgetMovable | DockWidgetFloatable);
        setWidget(widget);
        setObjectName(objectName);
    }
};

Widgets 3 and 4 are completely gone after the last splitDockWidget call, so calling splitDockWidget on a tabified widget makes both the to-be-inserted and the tabified widget disappear. If I bring up the context menu on a QDockWidget header, and make Widget2 disappear through it, the app looks even more strange:

QtDockWidgetTest after disabling Widget 2

So there is an empty area where widget 3 and 4 are supposed to be (Qt seems to be under the impression they are still shown, according to the context menu!). Only when disabling those two as well, then the empty area below Widget1 disappears.

If I split first and then tabify, it's fine. The problem is that I would like to do the tabbing in a general part of the software, and the splitting afterwards when I load a specialized part of it. So reordering the two is not really an option for me. In the documentation I could only find the following on that:

Note: if first is currently in a tabbed docked area, second will be added as a new tab, not as a neighbor of first. This is because a single tab can contain only one dock widget.

This is definitely not what happens. And in addition, this description leaves something to be desired: In my example, it means that once I have tabbed widgets 2 and 3, there is no way to (programmatically) get widget 4 to be to the right of the two, or is there another way to achieve that?

For me this problem consistently occurs with Qt 4.8 and 5.3. I suppose this might be a bug in Qt? Is it a known bug (my search so far has turned up empty)? Or is there any other reasonable explanation for this or a way to "get back" the two lost widgets?

标签: c++ qt qt5 qt4.8
2条回答
看我几分像从前
2楼-- · 2019-07-21 15:35

I believe your assessment of the bug is correct, so this is a possible workaround.

I find that I can get the results you want by explicitly adding the dock widgets to the form and specifying their dock area, then tabifying. I know you wanted the reverse order, but at least this gets rid of the weird artifacts.

First, make sure dock nesting and tabbing are enabled either through the designer or programmatically like so (you can optionally include animation here):

setDockOptions(DockOption::AllowNestedDocks | DockOption::AllowTabbedDocks);

Then explicitly add each dock. You were only adding one.

addDockWidget(Qt::TopDockWidgetArea, testQWidget1);
addDockWidget(Qt::LeftDockWidgetArea, testQWidget2);
addDockWidget(Qt::LeftDockWidgetArea, testQWidget3);
addDockWidget(Qt::RightDockWidgetArea, testQWidget4);

By setting that last one to:

Qt::RightDockWidgetArea

it appears to position it where you want. Then call:

tabifyDockWidget(testQWidget2, testQWidget3);

You can later programmatically move the docks to new locations if you desire by calling addDockWidget() again. Qt is smart enough to realize you are re-adding an existing one, so it won't duplicate it.

My only problem when running this test code is that the top dock seems to be huge and take up most of the space. I believe that once you add controls to each, they will resize appropriately.

I'm curious to know what your solution ended up being because I am about to embark on some dock related work myself.

Hope that helps.

PS - This is my first SO answer after lurking for many years, so go easy on me =)

查看更多
3楼-- · 2019-07-21 15:40

I had the same issue in pyqt
(so this answer provides python code).

As said in the documentation:

... because a single tab can contain only one dock widget.

I believe that the split command needs a truly standalone Dockwidget, because it seems to replace this Widget with a layout that is then filled with the single Dockwidget and the new one.


Quick and Dirty:

The approach based on that, is to remove all tabified QDockwidgets except for one:

other_tabs = myMainWindow.tabifiedDockWidgets( baseDockWidget )
for tab in other_tabs:
    myMainWindow.addDockWidget( Qt.BottomDockWidgetArea, tab ) # any other area
myMainWindow.splitDockWidget( baseDockWidget, newDockWidget, Qt.Vertical )
for tab in other_tabs:
    myMainWindow.tabifyDockWidget( baseDockWidget, tab )

Complete Explaination and Solution:

When testing around with dockWidgetArea(), I realised that a Dock maintains it's old position even when "floating" to a new one, example:

someDock = QDockWidget( myMainWindow )
someDock.setFloating( True )
print( myMainWindow.dockWidgetArea( someDock ) ) # NoDockWidgetArea

myMainWindow.addDockWidget( Qt.TopDockWidgetArea, someDock )
print( myMainWindow.dockWidgetArea( someDock ) ) # TopDockWidgetArea (obviously)

someDock.setFloating( True )
print( myMainWindow.dockWidgetArea( someDock ) ) # (still) TopDockWidgetArea

myMainWindow.addDockWidget( Qt.LeftDockWidgetArea, someDock )
print( myMainWindow.dockWidgetArea( someDock ) ) # LeftDockWidgetArea (obviously)

... so, we need to assign it to a different section, to remove (overwrite) the old assignment. setFloating() is not enough and so is removeDockWidget() as well.

Based on that reassigning to an other location and bringing it back did the trick (see above), but you will lose the order of the tabs and/or the currently highlighted one if you are in the middle.


To get the currently visible Tab I added a Visibilty-attribute to my DockWidgets:
(No, the basic QWidget:isVisible() will be true even when you have tabified docks and this one is not the selected one)

class dockWidget(QDockWidget):
    def __init__(self, *args, **kwargs):
        QDockWidget.__init__(self, *args, **kwargs)
        self._tabVisible
        self.visibilityChanged.connect(self._setTabVisible)

    def isTabVisible(self):
        return self._tabVisible

    def _setTabVisible(self, bool):
        self._tabVisible = bool

This will change the boolean based on the visibiltyChanged-Signal.


The Current visible Tab is important to get following tab, in case you have the dockwidget also tabified:

def _splitWindowByOrientation(self, dockWidget, orientation):
    #the dockWidget should get splited from the rest of the tabified docks
    #first: get all other tabs (they are ordered as displayed)
    other_tabs = self.tabifiedDockWidgets(dockWidget) 
    dockWidget.hide() # hide the target tab
    qApp.processEvents() # process eventloop

    newtoplevel = None # one of the other tabs will be visible now
    for tab in other_tabs:
        if tab.isTabVisible(): # find the visible one
            newtoplevel = tab
            break

    # to maintain the same order, we use the first tab from the list as base
    for tab in other_tabs[1:] + [dockWidget]: # hide and remove all others
        tab.hide() 
        # you may could skip hide and show, because there will be 
        # no visual effect until the eventloop is processed again
        self.addDockWidget(Qt.BottomDockWidgetArea, tab)

    # do the split now, with a truely single tab (using first tab as base)
    self.splitDockWidget(other_tabs[0], dockWidget, Qt.Vertical)
    dockWidget.show()

    # now add all old tabs back in, in the rigth order
    for tab in other_tabs[1:]:
        self.tabifyDockWidget(other_tabs[0], tab)
        tab.show()
    # set focus on the real follower
    newtoplevel.raise_()

you can split the windows by size by adding a this around the split-command to e.g. split 50/50:

 height = dockWidget.rect().height() # or .width() with Qt.Horizontal
 self.splitDockWidget(other_tabs[0], dockWidget, Qt.Vertical)
 dockWidget.show()
 self.resizeDocks((other_tabs[0], dockWidget), (height/2, height/2), Qt.Vertical)


Hope this helps!
Feel free to edit the Answer and add a C++ Translation to the code snippets ;)

查看更多
登录 后发表回答