I have an application where fixed-size child widgets need to be added programatically to a dock widget at run time based on user input. I want to add these widgets to a dock on the Qt::RightDockArea, from top to bottom until it runs out of space, then create a new column and repeat (essentially just the reverse of the flow layout example here, which I call a fluidGridLayout)
I can get the dock widget to resize itself properly using an event filter, but the resized dock's geometry doesn't change, and some of the widgets are drawn outside of the main window. Interestingly, resizing the main window, or floating and unfloating the dock cause it to 'pop' back into the right place (I haven't been able to find a way to replicate this programatically however)
I can't use any of the built-in QT layouts because with the widgets in my real program, they end up also getting drawn off screen.
Is there some way that I can get the dock to update it's top left coordinate to the proper position once it has been resized?
I think this may be of general interest as getting intuitive layout management behavior for dock widgets in QT is possibly the hardest thing known to man.
The code to replicate this is example given below.
Below I give the code to replicate the GUI from the screenshots.
main.cpp:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "QFluidGridLayout.h"
#include "QDockResizeEventFilter.h"
#include <QDockWidget>
#include <QGroupBox>
#include <QPushButton>
#include <QWidget>
#include <QDial>
class QTestWidget : public QGroupBox
{
public:
QTestWidget() : QGroupBox()
{
setFixedSize(50,50);
setStyleSheet("background-color: red;");
QDial* dial = new QDial;
dial->setFixedSize(40,40);
QLayout* testLayout = new QVBoxLayout;
testLayout->addWidget(dial);
//testLayout->setSizeConstraint(QLayout::SetMaximumSize);
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
setLayout(testLayout);
}
QSize sizeHint()
{
return minimumSize();
}
QDial* dial;
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QDockWidget* rightDock = new QDockWidget();
QDockWidget* bottomDock = new QDockWidget();
QGroupBox* central = new QGroupBox();
QGroupBox* widgetHolder = new QGroupBox();
QGroupBox* placeHolder = new QGroupBox();
placeHolder->setStyleSheet("background-color: green;");
placeHolder->setMinimumHeight(50);
widgetHolder->setStyleSheet("background-color: blue;");
widgetHolder->setMinimumWidth(50);
widgetHolder->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
widgetHolder->setLayout(new QFluidGridLayout);
widgetHolder->layout()->addWidget(new QTestWidget);
QPushButton* addWidgetButton = new QPushButton("Add another widget");
connect(addWidgetButton, &QPushButton::pressed, [=]()
{
widgetHolder->layout()->addWidget(new QTestWidget);
});
central->setLayout(new QVBoxLayout());
central->layout()->addWidget(addWidgetButton);
rightDock->setWidget(widgetHolder);
rightDock->installEventFilter(new QDockResizeEventFilter(widgetHolder,dynamic_cast<QFluidGridLayout*>(widgetHolder->layout())));
bottomDock->setWidget(placeHolder);
this->addDockWidget(Qt::RightDockWidgetArea, rightDock);
this->addDockWidget(Qt::BottomDockWidgetArea, bottomDock);
this->setCentralWidget(central);
central->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
this->setMinimumSize(500,500);
}
};
QFluidGirdLayout.h
#ifndef QFluidGridLayout_h__
#define QFluidGridLayout_h__
#include <QLayout>
#include <QGridLayout>
#include <QRect>
#include <QStyle>
#include <QWidgetItem>
class QFluidGridLayout : public QLayout
{
public:
enum Direction { LeftToRight, TopToBottom};
QFluidGridLayout(QWidget *parent = 0)
: QLayout(parent)
{
setContentsMargins(8,8,8,8);
setSizeConstraint(QLayout::SetMinAndMaxSize);
}
~QFluidGridLayout()
{
QLayoutItem *item;
while ((item = takeAt(0)))
delete item;
}
void addItem(QLayoutItem *item)
{
itemList.append(item);
}
Qt::Orientations expandingDirections() const
{
return 0;
}
bool hasHeightForWidth() const
{
return false;
}
int heightForWidth(int width) const
{
int height = doLayout(QRect(0, 0, width, 0), true, true);
return height;
}
bool hasWidthForHeight() const
{
return true;
}
int widthForHeight(int height) const
{
int width = doLayout(QRect(0, 0, 0, height), true, false);
return width;
}
int count() const
{
return itemList.size();
}
QLayoutItem *itemAt(int index) const
{
return itemList.value(index);
}
QSize minimumSize() const
{
QSize size;
QLayoutItem *item;
foreach (item, itemList)
size = size.expandedTo(item->minimumSize());
size += QSize(2*margin(), 2*margin());
return size;
}
void setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
doLayout(rect);
}
QSize sizeHint() const
{
return minimumSize();
}
QLayoutItem *takeAt(int index)
{
if (index >= 0 && index < itemList.size())
return itemList.takeAt(index);
else
return 0;
}
private:
int doLayout(const QRect &rect, bool testOnly = false, bool width = false) const
{
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom);
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
int lineWidth = 0;
QLayoutItem* item;
foreach(item,itemList)
{
QWidget* widget = item->widget();
if (y + item->sizeHint().height() > effectiveRect.bottom() && lineWidth > 0)
{
y = effectiveRect.y();
x += lineWidth + right;
lineWidth = 0;
}
if (!testOnly)
{
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
}
y += item->sizeHint().height() + top;
lineHeight = qMax(lineHeight, item->sizeHint().height());
lineWidth = qMax(lineWidth, item->sizeHint().width());
}
if (width)
{
return y + lineHeight - rect.y() + bottom;
}
else
{
return x + lineWidth - rect.x() + right;
}
}
QList<QLayoutItem *> itemList;
Direction dir;
};
#endif // QFluidGridLayout_h__
QDockResizeEventFilter.h
#ifndef QDockResizeEventFilter_h__
#define QDockResizeEventFilter_h__
#include <QObject>
#include <QLayout>
#include <QEvent>
#include <QDockWidget>
#include <QResizeEvent>
#include "QFluidGridLayout.h"
class QDockResizeEventFilter : public QObject
{
public:
QDockResizeEventFilter(QWidget* dockChild, QFluidGridLayout* layout, QObject* parent = 0)
: QObject(parent), m_dockChild(dockChild), m_layout(layout)
{
}
protected:
bool eventFilter(QObject *p_obj, QEvent *p_event)
{
if (p_event->type() == QEvent::Resize)
{
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(p_event);
QMainWindow* mainWindow = static_cast<QMainWindow*>(p_obj->parent());
QDockWidget* dock = static_cast<QDockWidget*>(p_obj);
// determine resize direction
if (resizeEvent->oldSize().height() != resizeEvent->size().height())
{
// vertical expansion
QSize fixedSize(m_layout->widthForHeight(m_dockChild->size().height()), m_dockChild->size().height());
if (dock->size().width() != fixedSize.width())
{
m_dockChild->resize(fixedSize);
m_dockChild->setFixedWidth(fixedSize.width());
dock->setFixedWidth(fixedSize.width());
mainWindow->repaint();
//dock->setGeometry(mainWindow->rect().right()-fixedSize.width(),dock->geometry().y(),fixedSize.width(), fixedSize.height());
}
}
if (resizeEvent->oldSize().width() != resizeEvent->size().width())
{
// horizontal expansion
m_dockChild->resize(m_layout->sizeHint().width(), m_dockChild->height());
}
}
return false;
}
private:
QWidget* m_dockChild;
QFluidGridLayout* m_layout;
};
#endif // QDockResizeEventFilter_h__
The problem is, nothing in the code above actually causes the QMainWindowLayout to recalculate itself. That function is buried within the QMainWindowLayout private class, but can be stimulated by adding and removing a dummy QDockWidget, which causes the layout to invalidate and recalcualte the dock widget positions
QDockWidget* dummy = new QDockWidget;
mainWindow->addDockWidget(Qt::TopDockWidgetArea, dummy);
mainWindow->removeDockWidget(dummy);
The only problem with this is that if you dig into the QT source code, you'll see that adding a dock widget causes the dock separator to be released, which causes unintuitive and choppy behavior as the user tries to resize the dock, and the mouse unexpectedly 'lets go'.
void QMainWindowLayout::addDockWidget(Qt::DockWidgetArea area,
QDockWidget *dockwidget,
Qt::Orientation orientation)
{
addChildWidget(dockwidget);
// If we are currently moving a separator, then we need to abort the move, since each
// time we move the mouse layoutState is replaced by savedState modified by the move.
if (!movingSeparator.isEmpty())
endSeparatorMove(movingSeparatorPos);
layoutState.dockAreaLayout.addDockWidget(toDockPos(area), dockwidget, orientation);
emit dockwidget->dockLocationChanged(area);
invalidate();
}
That can be corrected by moving the cursor back onto the separator and simulating a mouse press, basically undoing the endSeparatorMove
callafter the docks have been repositioned. It's important to post the event, rather than send it, so thatit occurs after the resize event. The code for doing so looks like:
QPoint mousePos = mainWindow->mapFromGlobal(QCursor::pos());
mousePos.setY(dock->rect().bottom()+2);
QCursor::setPos(mainWindow->mapToGlobal(mousePos));
QMouseEvent* grabSeparatorEvent =
new QMouseEvent(QMouseEvent::MouseButtonPress,mousePos,Qt::LeftButton,Qt::LeftButton,Qt::NoModifier);
qApp->postEvent(mainWindow, grabSeparatorEvent);
Where 2 is a magic number that accounts for the group box border.
Put that all together, and here is the event filter than gives the desired behavior:
#ifndef QDockResizeEventFilter_h__
#define QDockResizeEventFilter_h__
#include <QObject>
#include <QLayout>
#include <QEvent>
#include <QDockWidget>
#include <QResizeEvent>
#include <QCoreApplication>
#include <QMouseEvent>
#include "QFluidGridLayout.h"
class QDockResizeEventFilter : public QObject
{
public:
friend QMainWindow;
friend QLayoutPrivate;
QDockResizeEventFilter(QWidget* dockChild, QFluidGridLayout* layout, QObject* parent = 0)
: QObject(parent), m_dockChild(dockChild), m_layout(layout)
{
}
protected:
bool eventFilter(QObject *p_obj, QEvent *p_event)
{
if (p_event->type() == QEvent::Resize)
{
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(p_event);
QMainWindow* mainWindow = dynamic_cast<QMainWindow*>(p_obj->parent());
QDockWidget* dock = static_cast<QDockWidget*>(p_obj);
// determine resize direction
if (resizeEvent->oldSize().height() != resizeEvent->size().height())
{
// vertical expansion
QSize fixedSize(m_layout->widthForHeight(m_dockChild->size().height()), m_dockChild->size().height());
if (dock->size().width() != fixedSize.width())
{
m_dockChild->setFixedWidth(fixedSize.width());
dock->setFixedWidth(fixedSize.width());
// cause mainWindow dock layout recalculation
QDockWidget* dummy = new QDockWidget;
mainWindow->addDockWidget(Qt::TopDockWidgetArea, dummy);
mainWindow->removeDockWidget(dummy);
// adding dock widgets causes the separator move event to end
// restart it by synthesizing a mouse press event
QPoint mousePos = mainWindow->mapFromGlobal(QCursor::pos());
mousePos.setY(dock->rect().bottom()+2);
QCursor::setPos(mainWindow->mapToGlobal(mousePos));
QMouseEvent* grabSeparatorEvent = new QMouseEvent(QMouseEvent::MouseButtonPress,mousePos,Qt::LeftButton,Qt::LeftButton,Qt::NoModifier);
qApp->postEvent(mainWindow, grabSeparatorEvent);
}
}
if (resizeEvent->oldSize().width() != resizeEvent->size().width())
{
// horizontal expansion
// ...
}
}
return false;
}
private:
QWidget* m_dockChild;
QFluidGridLayout* m_layout;
};
#endif // QDockResizeEventFilter_h__
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With