Создайте QDockWidget, размер которого изменяется в соответствии с его содержимым

У меня есть приложение, в котором дочерние виджеты фиксированного размера необходимо программно добавлять в виджет док-станции во время выполнения на основе ввода данных пользователем. Я хочу добавить эти виджеты в док на Qt :: RightDockArea, сверху вниз, пока в нем не закончится место, затем создать новый столбец и повторить (по сути, как раз наоборот, как показано в примере макета потока здесь, который я называю fluidGridLayout)

Я могу заставить виджет док-станции изменять свой размер должным образом с помощью фильтра событий, но геометрия док-станции с измененным размером не меняется, а некоторые виджеты отображаются за пределами главного окна. Интересно, что изменение размера главного окна или перемещение и снятие дока заставляет его «всплывать» обратно в нужное место (однако я не смог найти способ воспроизвести это программно)

Я не могу использовать какие-либо встроенные макеты QT, потому что с виджетами в моей реальной программе они также оказываются за пределами экрана.

Есть ли способ заставить док-станцию ​​обновить ее верхнюю левую координату до правильного положения после изменения размера?

Я думаю, что это может представлять общий интерес, поскольку получение интуитивно понятного поведения управления компоновкой для док-виджетов в QT, возможно, является самой сложной задачей, известной человеку.

ВИЗУАЛЬНЫЙ ПРИМЕР:

Код для воспроизведения этого примера приведен ниже.

  1. Добавьте в программу 4 виджета с помощью кнопки

Шаг 1

  1. Измените размер зеленой нижней док-станции, пока не будут отображаться только два виджета. Обратите внимание, что 3 оставшихся виджета окрашиваются за пределы главного окна, однако док-станция имеет правильный размер, о чем свидетельствует тот факт, что вы больше не видите кнопку закрытия.

Шаг 2

  1. Отстыкуйте синий виджет док-станции. Обратите внимание на то, что он защелкнется до нужного размера.

Шаг 3

  1. Повторно закрепите синюю док-станцию ​​в правой области док-станции. Обратите внимание, что теперь он ведет себя правильно.

Шаг 4

  1. Теперь измените размер зеленой док-станции до минимального размера. Обратите внимание, что док теперь находится ПОСРЕДНИК ГИП. WTf, как такое возможно ??

Шаг 5

КОД

Ниже я привожу код для репликации графического интерфейса со снимков экрана.

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__

person Nicolas Holthaus    schedule 09.10.2014    source источник


Ответы (1)


Проблема в том, что ничто в приведенном выше коде на самом деле не заставляет QMainWindowLayout пересчитывать себя. Эта функция похоронена в частном классе QMainWindowLayout, но ее можно стимулировать путем добавления и удаления фиктивного QDockWidget, что приводит к тому, что макет недействителен и пересчитывает позиции виджета док-станции.

QDockWidget* dummy = new QDockWidget;
mainWindow->addDockWidget(Qt::TopDockWidgetArea, dummy);
mainWindow->removeDockWidget(dummy);

Единственная проблема с этим заключается в том, что если вы покопаетесь в исходном коде QT, вы увидите, что добавление виджета док-станции вызывает освобождение разделителя док-станции, что вызывает неинтуитивное и прерывистое поведение, когда пользователь пытается изменить размер док-станции, и мышь неожиданно «отпускает».

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();
}

Это можно исправить, переместив курсор обратно на разделитель и имитируя нажатие мыши, в основном отменяя вызов endSeparatorMove после того, как доки были перемещены. Важно публиковать событие, а не отправлять его, чтобы оно произошло после события изменения размера. Код для этого выглядит так:

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);

Где 2 - это магическое число, определяющее границу рамки группы.

Сложите все это вместе, и вот фильтр событий, который дает желаемое поведение:

Исправленный фильтр событий

#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__
person Nicolas Holthaus    schedule 10.10.2014