在 Qt 中制作可折叠的 GroupBoxes - 什么决定了折叠的大小?
Making collapsible GroupBoxes in Qt - what determines the collapsed size?
我正在尝试使用 Collapse/Expand 按钮制作 GroupBox,希望在 Qt 中具有明显的功能。我已经使用绘制按钮对 QGroupBox 进行了子类化。单击时,我的代码对 GroupBox 的所有子项调用 setVisible(false)
。此外,它遍历 GroupBox 中的所有 QLayout,并将它们的 contentMargins 设置为零。然而,一些 GroupBoxes 在折叠状态下仍然比其他的大,我不知道是什么原因造成的。
这是我到目前为止的想法(我知道可能的 nullptr
问题):
void CollapsibleGroupBox::onVisibilityChanged()
{
CollapseExpandButton::State s;
s = m_clExpButton->state();
QLayout *master = this->layout();
QList<QObject *> children = this->children();
switch (s) {
case CollapseExpandButton::State::COLLAPSED:
for (QObject *o : children) {
QWidget *w = qobject_cast<QWidget *>(o);
if (w != nullptr) {
if (w != m_clExpButton)
w->setVisible(false);
continue;
}
if (o == master) {
m_layoutMargins.clear();
collapseLayout(master);
}
}
break;
case CollapseExpandButton::State::EXPANDED:
for (QObject *o : children) {
QWidget *w = qobject_cast<QWidget *>(o);
if (w != nullptr) {
w->setVisible(true);
continue;
}
if (o == master)
expandLayout(master);
}
break;
}
}
void CollapsibleGroupBox::collapseLayout(QLayout *layout)
{
for (QObject *o : layout->children()) {
QLayout *l= qobject_cast<QLayout *>(o);
if (l == nullptr)
continue;
collapseLayout(l);
}
if (m_layoutMargins.contains(layout))
return;
QMargins m = layout->contentsMargins();
m_layoutMargins[layout] = m;
layout->setContentsMargins(0, 0, 0, 0);
}
void CollapsibleGroupBox::expandLayout(QLayout *layout)
{
for (QObject *o : layout->children()) {
QLayout *l = qobject_cast<QLayout *>(o);
if (l == nullptr)
continue;
if (m_layoutMargins.contains(l))
expandLayout(l);
}
if (m_layoutMargins.contains(layout)) {
QMargins m = m_layoutMargins[layout];
layout->setContentsMargins(m);
}
}
这是因为您提到的分组框中的间隔符。将它们的高度设置为零;当组框展开时,它不应该影响他们的行为。
对于组框明显 non-reproducible 的高度,我没有任何解释。但你也可以这样做:
void CollapsibleGroupBox::onVisibilityChanged(bool state)
{
switch (state) {
case true:
m_originalMaxHeight = this->maximumHeight();
this->setMaximumHeight(this->fontMetrics().height());
break;
case false:
this->setMaximumHeight(m_originalMaxHeight);
break;
}
}
并向包含组框的布局添加拉伸。当动态添加或删除小部件时,它会很好地调整大小。
前段时间我回到了我的旧代码,看来我终于想出了一个合理的解决方案。完整代码如下:
** CollapsibleGroupBox.h **
#ifndef COLLAPSIBLEGROUPBOX_H
#define COLLAPSIBLEGROUPBOX_H
#include <QGroupBox>
#include <QMap>
#include <QMargins>
#include <QPair>
class QResizeEvent;
class CollapseExpandButton;
class QSpacerItem;
class CollapsibleGroupBox : public QGroupBox
{
public:
explicit CollapsibleGroupBox(QWidget *parent = nullptr);
protected:
void resizeEvent(QResizeEvent *);
private:
void resizeCollapseButton();
void collapseLayout(QLayout *layout);
void collapseSpacer(QSpacerItem *spacer);
void expandLayout(QLayout *layout);
void expandSpacer(QSpacerItem *spacer);
CollapseExpandButton *m_clExpButton;
QMap<const void *, QMargins> m_layoutMargins;
QMap<const void *, QPair<QSize, QSizePolicy>> m_spacerSizes;
private slots:
void onScreenChanged();
void onVisibilityChanged();
};
#endif // COLLAPSIBLEGROUPBOX_H
** CollapsibleGroupBox.cpp **
#include "collapsiblegroupbox.h"
#include "collapseexpandbutton.h"
#include <QApplication>
#include <QGuiApplication>
#include <QLayout>
#include <QResizeEvent>
#include <QScreen>
#include <QStyle>
#include <QTimer>
#include <QWindow>
#include <cassert>
#include <cmath>
inline
QWindow *findWindowForWidget(const QWidget *widget)
{
for (;;) {
QWindow *wh = widget->window()->windowHandle();
if (wh != nullptr)
return wh;
widget = qobject_cast<const QWidget *>(widget->parent());
if (widget == nullptr)
return nullptr;
}
}
inline
QScreen * findScreenForWidget(const QWidget *widget)
{
for (;;) {
QWindow *wh = widget->window()->windowHandle();
if (wh != nullptr) {
QScreen *scr = wh->screen();
if (scr != nullptr)
return scr;
}
widget = qobject_cast<const QWidget *>(widget->parent());
if (widget == nullptr)
return nullptr;
}
}
CollapsibleGroupBox::CollapsibleGroupBox(QWidget *parent) :
QGroupBox(parent)
{
m_clExpButton = new CollapseExpandButton(this);
connect(m_clExpButton, &CollapseExpandButton::clicked, this, &CollapsibleGroupBox::onVisibilityChanged);
QTimer::singleShot(0, this, [this] {
auto wh = findWindowForWidget(this);
if (wh != nullptr)
connect(wh, &QWindow::screenChanged, this, &CollapsibleGroupBox::onScreenChanged);
});
QTimer::singleShot(0, this, &CollapsibleGroupBox::resizeCollapseButton);
}
void CollapsibleGroupBox::collapseLayout(QLayout *lay)
{
assert(!m_layoutMargins.contains(lay));
const int cnt = lay->count();
for (int idx = 0; idx < cnt; idx++) {
auto lit = lay->itemAt(idx);
if (lit->widget()) {
auto w = lit->widget();
if (w != m_clExpButton)
w->setVisible(false);
}
else if (lit->spacerItem())
collapseSpacer(lit->spacerItem());
else if (lit->layout())
collapseLayout(lit->layout());
}
m_layoutMargins[lay] = lay->contentsMargins();
lay->setContentsMargins(0, 0, 0, 0);
}
void CollapsibleGroupBox::collapseSpacer(QSpacerItem *spacer)
{
assert(!m_spacerSizes.contains(spacer));
m_spacerSizes[spacer] = {spacer->sizeHint(), spacer->sizePolicy()};
spacer->changeSize(0, 0);
}
void CollapsibleGroupBox::expandLayout(QLayout *lay)
{
assert(m_layoutMargins.contains(lay));
const int cnt = lay->count();
for (int idx = 0; idx < cnt; idx++) {
auto lit = lay->itemAt(idx);
if (lit->widget())
lit->widget()->setVisible(true);
else if (lit->spacerItem())
expandSpacer(lit->spacerItem());
else if (lit->layout())
expandLayout(lit->layout());
}
lay->setContentsMargins(m_layoutMargins[lay]);
}
void CollapsibleGroupBox::expandSpacer(QSpacerItem *spacer)
{
assert(m_spacerSizes.contains(spacer));
const auto &sz = m_spacerSizes[spacer].first;
const auto &pol = m_spacerSizes[spacer].second;
spacer->changeSize(sz.width(), sz.height(), pol.horizontalPolicy(), pol.verticalPolicy());
}
void CollapsibleGroupBox::onScreenChanged()
{
resizeCollapseButton();
}
void CollapsibleGroupBox::onVisibilityChanged()
{
assert(this->layout() != nullptr);
CollapseExpandButton::State s = m_clExpButton->state();
switch (s) {
case CollapseExpandButton::State::COLLAPSED:
m_layoutMargins.clear();
m_spacerSizes.clear();
collapseLayout(this->layout());
break;
case CollapseExpandButton::State::EXPANDED:
expandLayout(this->layout());
break;
}
}
void CollapsibleGroupBox::resizeCollapseButton()
{
const QScreen *scr = findScreenForWidget(this);
if (scr == nullptr)
return;
const auto &size = this->size();
#ifdef Q_OS_WIN
qreal baseSize = 15.0;
int yOffset = 5;
#else
qreal baseSize = 22.0;
int yOffset = 0;
#endif
if (scr == nullptr)
return;
if (QString::compare(QApplication::style()->objectName(), "fusion") == 0)
baseSize = 15.0;
const qreal dpi = scr->logicalDotsPerInchX();
const qreal btnSize = floor((baseSize * dpi / 96.0) + 0.5);
m_clExpButton->setGeometry(size.width() - btnSize, yOffset, btnSize, btnSize);
}
void CollapsibleGroupBox::resizeEvent(QResizeEvent *)
{
resizeCollapseButton();
}
这似乎完全按照我的预期折叠并恢复了盒子。
我正在尝试使用 Collapse/Expand 按钮制作 GroupBox,希望在 Qt 中具有明显的功能。我已经使用绘制按钮对 QGroupBox 进行了子类化。单击时,我的代码对 GroupBox 的所有子项调用 setVisible(false)
。此外,它遍历 GroupBox 中的所有 QLayout,并将它们的 contentMargins 设置为零。然而,一些 GroupBoxes 在折叠状态下仍然比其他的大,我不知道是什么原因造成的。
这是我到目前为止的想法(我知道可能的 nullptr
问题):
void CollapsibleGroupBox::onVisibilityChanged()
{
CollapseExpandButton::State s;
s = m_clExpButton->state();
QLayout *master = this->layout();
QList<QObject *> children = this->children();
switch (s) {
case CollapseExpandButton::State::COLLAPSED:
for (QObject *o : children) {
QWidget *w = qobject_cast<QWidget *>(o);
if (w != nullptr) {
if (w != m_clExpButton)
w->setVisible(false);
continue;
}
if (o == master) {
m_layoutMargins.clear();
collapseLayout(master);
}
}
break;
case CollapseExpandButton::State::EXPANDED:
for (QObject *o : children) {
QWidget *w = qobject_cast<QWidget *>(o);
if (w != nullptr) {
w->setVisible(true);
continue;
}
if (o == master)
expandLayout(master);
}
break;
}
}
void CollapsibleGroupBox::collapseLayout(QLayout *layout)
{
for (QObject *o : layout->children()) {
QLayout *l= qobject_cast<QLayout *>(o);
if (l == nullptr)
continue;
collapseLayout(l);
}
if (m_layoutMargins.contains(layout))
return;
QMargins m = layout->contentsMargins();
m_layoutMargins[layout] = m;
layout->setContentsMargins(0, 0, 0, 0);
}
void CollapsibleGroupBox::expandLayout(QLayout *layout)
{
for (QObject *o : layout->children()) {
QLayout *l = qobject_cast<QLayout *>(o);
if (l == nullptr)
continue;
if (m_layoutMargins.contains(l))
expandLayout(l);
}
if (m_layoutMargins.contains(layout)) {
QMargins m = m_layoutMargins[layout];
layout->setContentsMargins(m);
}
}
这是因为您提到的分组框中的间隔符。将它们的高度设置为零;当组框展开时,它不应该影响他们的行为。
对于组框明显 non-reproducible 的高度,我没有任何解释。但你也可以这样做:
void CollapsibleGroupBox::onVisibilityChanged(bool state)
{
switch (state) {
case true:
m_originalMaxHeight = this->maximumHeight();
this->setMaximumHeight(this->fontMetrics().height());
break;
case false:
this->setMaximumHeight(m_originalMaxHeight);
break;
}
}
并向包含组框的布局添加拉伸。当动态添加或删除小部件时,它会很好地调整大小。
前段时间我回到了我的旧代码,看来我终于想出了一个合理的解决方案。完整代码如下:
** CollapsibleGroupBox.h **
#ifndef COLLAPSIBLEGROUPBOX_H
#define COLLAPSIBLEGROUPBOX_H
#include <QGroupBox>
#include <QMap>
#include <QMargins>
#include <QPair>
class QResizeEvent;
class CollapseExpandButton;
class QSpacerItem;
class CollapsibleGroupBox : public QGroupBox
{
public:
explicit CollapsibleGroupBox(QWidget *parent = nullptr);
protected:
void resizeEvent(QResizeEvent *);
private:
void resizeCollapseButton();
void collapseLayout(QLayout *layout);
void collapseSpacer(QSpacerItem *spacer);
void expandLayout(QLayout *layout);
void expandSpacer(QSpacerItem *spacer);
CollapseExpandButton *m_clExpButton;
QMap<const void *, QMargins> m_layoutMargins;
QMap<const void *, QPair<QSize, QSizePolicy>> m_spacerSizes;
private slots:
void onScreenChanged();
void onVisibilityChanged();
};
#endif // COLLAPSIBLEGROUPBOX_H
** CollapsibleGroupBox.cpp **
#include "collapsiblegroupbox.h"
#include "collapseexpandbutton.h"
#include <QApplication>
#include <QGuiApplication>
#include <QLayout>
#include <QResizeEvent>
#include <QScreen>
#include <QStyle>
#include <QTimer>
#include <QWindow>
#include <cassert>
#include <cmath>
inline
QWindow *findWindowForWidget(const QWidget *widget)
{
for (;;) {
QWindow *wh = widget->window()->windowHandle();
if (wh != nullptr)
return wh;
widget = qobject_cast<const QWidget *>(widget->parent());
if (widget == nullptr)
return nullptr;
}
}
inline
QScreen * findScreenForWidget(const QWidget *widget)
{
for (;;) {
QWindow *wh = widget->window()->windowHandle();
if (wh != nullptr) {
QScreen *scr = wh->screen();
if (scr != nullptr)
return scr;
}
widget = qobject_cast<const QWidget *>(widget->parent());
if (widget == nullptr)
return nullptr;
}
}
CollapsibleGroupBox::CollapsibleGroupBox(QWidget *parent) :
QGroupBox(parent)
{
m_clExpButton = new CollapseExpandButton(this);
connect(m_clExpButton, &CollapseExpandButton::clicked, this, &CollapsibleGroupBox::onVisibilityChanged);
QTimer::singleShot(0, this, [this] {
auto wh = findWindowForWidget(this);
if (wh != nullptr)
connect(wh, &QWindow::screenChanged, this, &CollapsibleGroupBox::onScreenChanged);
});
QTimer::singleShot(0, this, &CollapsibleGroupBox::resizeCollapseButton);
}
void CollapsibleGroupBox::collapseLayout(QLayout *lay)
{
assert(!m_layoutMargins.contains(lay));
const int cnt = lay->count();
for (int idx = 0; idx < cnt; idx++) {
auto lit = lay->itemAt(idx);
if (lit->widget()) {
auto w = lit->widget();
if (w != m_clExpButton)
w->setVisible(false);
}
else if (lit->spacerItem())
collapseSpacer(lit->spacerItem());
else if (lit->layout())
collapseLayout(lit->layout());
}
m_layoutMargins[lay] = lay->contentsMargins();
lay->setContentsMargins(0, 0, 0, 0);
}
void CollapsibleGroupBox::collapseSpacer(QSpacerItem *spacer)
{
assert(!m_spacerSizes.contains(spacer));
m_spacerSizes[spacer] = {spacer->sizeHint(), spacer->sizePolicy()};
spacer->changeSize(0, 0);
}
void CollapsibleGroupBox::expandLayout(QLayout *lay)
{
assert(m_layoutMargins.contains(lay));
const int cnt = lay->count();
for (int idx = 0; idx < cnt; idx++) {
auto lit = lay->itemAt(idx);
if (lit->widget())
lit->widget()->setVisible(true);
else if (lit->spacerItem())
expandSpacer(lit->spacerItem());
else if (lit->layout())
expandLayout(lit->layout());
}
lay->setContentsMargins(m_layoutMargins[lay]);
}
void CollapsibleGroupBox::expandSpacer(QSpacerItem *spacer)
{
assert(m_spacerSizes.contains(spacer));
const auto &sz = m_spacerSizes[spacer].first;
const auto &pol = m_spacerSizes[spacer].second;
spacer->changeSize(sz.width(), sz.height(), pol.horizontalPolicy(), pol.verticalPolicy());
}
void CollapsibleGroupBox::onScreenChanged()
{
resizeCollapseButton();
}
void CollapsibleGroupBox::onVisibilityChanged()
{
assert(this->layout() != nullptr);
CollapseExpandButton::State s = m_clExpButton->state();
switch (s) {
case CollapseExpandButton::State::COLLAPSED:
m_layoutMargins.clear();
m_spacerSizes.clear();
collapseLayout(this->layout());
break;
case CollapseExpandButton::State::EXPANDED:
expandLayout(this->layout());
break;
}
}
void CollapsibleGroupBox::resizeCollapseButton()
{
const QScreen *scr = findScreenForWidget(this);
if (scr == nullptr)
return;
const auto &size = this->size();
#ifdef Q_OS_WIN
qreal baseSize = 15.0;
int yOffset = 5;
#else
qreal baseSize = 22.0;
int yOffset = 0;
#endif
if (scr == nullptr)
return;
if (QString::compare(QApplication::style()->objectName(), "fusion") == 0)
baseSize = 15.0;
const qreal dpi = scr->logicalDotsPerInchX();
const qreal btnSize = floor((baseSize * dpi / 96.0) + 0.5);
m_clExpButton->setGeometry(size.width() - btnSize, yOffset, btnSize, btnSize);
}
void CollapsibleGroupBox::resizeEvent(QResizeEvent *)
{
resizeCollapseButton();
}
这似乎完全按照我的预期折叠并恢复了盒子。