Как сделать расширяемый / складываемый виджет раздела в QT

Я хотел бы создать собственный виджет в QT со следующими функциями:

  • Это контейнер
  • Он может быть заполнен любым макетом QT
  • Может быть внутри любой раскладки QT
  • Кнопка позволяет свернуть / сложить содержимое по вертикали, поэтому видна только кнопка, а весь содержащийся макет невидим.
  • Предыдущая кнопка позволяет снова развернуть / развернуть ее до размера содержимого макета.
  • Расширение / свертывание основано на размерах (не на показе / скрытии), чтобы позволить анимацию.
  • Используется в QDesigner

Чтобы представить идею, вот изображение похожего виджета (не QT):
введите описание изображения здесь

У меня уже есть кадр, который работает правильно и выставлен в QDesigner. Теперь мне нужно сделать так, чтобы он растянулся / развалился, что не так просто.

Я пытался играть с resize (), sizePolicy (), sizeHint (), но это не работает:
Когда кадр свернут, я получил следующие значения:

sizeHint: (500,20)
size    : (500,20)
closestAcceptableSize: (518,150)
Painted size: (518, 150)

QLayout :: closestAcceptableSize не является частью виджета, поэтому я не могу его изменить.

Любая подсказка или / и фрагмент кода для достижения этого?

Редакция:
Вот простой пример. Я удалил все, кроме необходимого.

пример main.cpp

#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

#include "section.hpp"

using namespace myWidgets;
int main(int argc, char *argv[])
{
QApplication a(argc, argv);// Create the main Window
QWidget window;
window.resize(500,500);
window.setStyleSheet("QPushButton:{background-color:rgba(128,128,128,192);}");

// Create the main window layout
QVBoxLayout topLayout(&window);
QWidget *w1 = new QWidget();
w1->setStyleSheet("background-color:rgba(128,128,128,192);");
topLayout.addWidget(w1);

Section section(&window);
topLayout.addWidget(&section);

QVBoxLayout inLayout(&section);
QPushButton *button = new QPushButton();
button->setMinimumHeight(100);
inLayout.addWidget(button);

QWidget *w2 = new QWidget();
w2->setStyleSheet("background-color:rgba(128,128,128,192);");
topLayout.addWidget(w2);window.show();

return a.exec();
}

Section.hpp

#ifndef SECTION_HPP
#define SECTION_HPP

#include <QPushButton> //for the expand/collapse button
#include <QtDesigner/QDesignerExportWidget>
#include <QLayout>
#include <QPainter>
#include <QPaintEvent>
#include <QDebug>// Compatibility for noexcept, not supported in vsc++
#ifdef _MSC_VER
#define noexcept throw()
#endif

#if defined SECTION_BUILD
#define SECTION_BUILD_DLL_SPEC Q_DECL_EXPORT
#elif defined SECTION_EXEC
#define SECTION_BUILD_DLL_SPEC
#else
#define SECTION_BUILD_DLL_SPEC Q_DECL_IMPORT
#endif

namespace myWidgets
{

class SECTION_BUILD_DLL_SPEC Section : public QWidget
{
Q_OBJECT

Q_PROPERTY( bool is_expanded MEMBER isExpanded)

public:
// Constructor, standard
explicit Section( QWidget *parent=0 ): QWidget(parent),
expandButton(this)
{
expandButton.resize(20,20);
expandButton.move(0,0);
expandButton.connect(&expandButton, &QPushButton::clicked,
this, &Section::expandCollapseEvent);

QMargins m= contentsMargins();
m.setTop(m.top()+25);
setContentsMargins(m);
//setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Minimum);

}

virtual void expand( bool expanding ) noexcept
{
resize(sizeHint());
isExpanded = expanding;
updateGeometry();

qDebug() << (isExpanded? "expanded":"collapsed") << sizeHint() << QWidget::size() <<
parentWidget()->layout()->closestAcceptableSize(this, size());
}

virtual QSize sizeHint() const noexcept override
{
if (isExpanded) return QSize(layout()->contentsRect().width(),
layout()->contentsRect().height());
else return QSize(layout()->contentsRect().width(), 20);
}

// Implement custom appearance
virtual void paintEvent(QPaintEvent *e) noexcept override
{
(void) e; //TODO: remove
QPainter p(this);
p.setClipRect(e->rect());
p.setRenderHint(QPainter::Antialiasing );
p.fillRect(e->rect(), QColor(0,0,255,128));
}

protected:

// on click of the expandButton, collapse/expand this widget
virtual void expandCollapseEvent() noexcept
{
expand(!isExpanded);
}bool isExpanded = true; //whenever the section is collapsed(false) or expanded(true)
QPushButton expandButton; //the expanding/collapsing button
};

}#endif // SECTION_HPP

32

Решение

Я наткнулся на ту же проблему и решил ее, реализовав складной виджет в виде QScrollArea чья максимальная высота анимируется QPropertyAnimation,

Но так как я не использую QDesigner, я не могу сказать вам, работает ли он там.

У меня все еще есть одна проблема: вместо того, чтобы расширяться только в направлении дна, складной виджет может расширяться в направлении сверху и снизу. Это может привести к уменьшению виджетов, расположенных над ним, если они еще не достигли минимальной высоты. Но это действительно мелочь по сравнению с тем, что мы должны построить эту вещь сами …

Spoiler.h

#include <QFrame>
#include <QGridLayout>
#include <QParallelAnimationGroup>
#include <QScrollArea>
#include <QToolButton>
#include <QWidget>

class Spoiler : public QWidget {
Q_OBJECT
private:
QGridLayout mainLayout;
QToolButton toggleButton;
QFrame headerLine;
QParallelAnimationGroup toggleAnimation;
QScrollArea contentArea;
int animationDuration{300};
public:
explicit Spoiler(const QString & title = "", const int animationDuration = 300, QWidget *parent = 0);
void setContentLayout(QLayout & contentLayout);
};

Spoiler.cpp

#include <QPropertyAnimation>

#include "Spoiler.h"
Spoiler::Spoiler(const QString & title, const int animationDuration, QWidget *parent) : QWidget(parent), animationDuration(animationDuration) {
toggleButton.setStyleSheet("QToolButton { border: none; }");
toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
toggleButton.setArrowType(Qt::ArrowType::RightArrow);
toggleButton.setText(title);
toggleButton.setCheckable(true);
toggleButton.setChecked(false);

headerLine.setFrameShape(QFrame::HLine);
headerLine.setFrameShadow(QFrame::Sunken);
headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);

contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }");
contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
// start out collapsed
contentArea.setMaximumHeight(0);
contentArea.setMinimumHeight(0);
// let the entire widget grow and shrink with its content
toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight"));
toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight"));
toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight"));
// don't waste space
mainLayout.setVerticalSpacing(0);
mainLayout.setContentsMargins(0, 0, 0, 0);
int row = 0;
mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft);
mainLayout.addWidget(&headerLine, row++, 2, 1, 1);
mainLayout.addWidget(&contentArea, row, 0, 1, 3);
setLayout(&mainLayout);
QObject::connect(&toggleButton, &QToolButton::clicked, [this](const bool checked) {
toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow);
toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward);
toggleAnimation.start();
});
}

void Spoiler::setContentLayout(QLayout & contentLayout) {
delete contentArea.layout();
contentArea.setLayout(&contentLayout);
const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight();
auto contentHeight = contentLayout.sizeHint().height();
for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) {
QPropertyAnimation * spoilerAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(i));
spoilerAnimation->setDuration(animationDuration);
spoilerAnimation->setStartValue(collapsedHeight);
spoilerAnimation->setEndValue(collapsedHeight + contentHeight);
}
QPropertyAnimation * contentAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1));
contentAnimation->setDuration(animationDuration);
contentAnimation->setStartValue(0);
contentAnimation->setEndValue(contentHeight);
}

Как это использовать:

…
auto * anyLayout = new QVBoxLayout();
anyLayout->addWidget(…);
…
Spoiler spoiler;
spoiler.setContentLayout(*anyLayout);
…

Пример спойлера

34

Другие решения

Несмотря на то, что это старый, я нашел эту тему полезной. Тем не менее, я работаю в Python, поэтому мне пришлось конвертировать код C ++.
На всякий случай, если кто-то ищет Python-версию решения x squared. Вот мой порт:

from PyQt4 import QtCore, QtGuiclass Spoiler(QtGui.QWidget):
def __init__(self, parent=None, title='', animationDuration=300):
"""References:
# Adapted from c++ version
http://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt
"""super(Spoiler, self).__init__(parent=parent)

self.animationDuration = 300
self.toggleAnimation = QtCore.QParallelAnimationGroup()
self.contentArea = QtGui.QScrollArea()
self.headerLine = QtGui.QFrame()
self.toggleButton = QtGui.QToolButton()
self.mainLayout = QtGui.QGridLayout()

toggleButton = self.toggleButton
toggleButton.setStyleSheet("QToolButton { border: none; }")
toggleButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
toggleButton.setArrowType(QtCore.Qt.RightArrow)
toggleButton.setText(str(title))
toggleButton.setCheckable(True)
toggleButton.setChecked(False)

headerLine = self.headerLine
headerLine.setFrameShape(QtGui.QFrame.HLine)
headerLine.setFrameShadow(QtGui.QFrame.Sunken)
headerLine.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)

self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }")
self.contentArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
# start out collapsed
self.contentArea.setMaximumHeight(0)
self.contentArea.setMinimumHeight(0)
# let the entire widget grow and shrink with its content
toggleAnimation = self.toggleAnimation
toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, "minimumHeight"))
toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, "maximumHeight"))
toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self.contentArea, "maximumHeight"))
# don't waste space
mainLayout = self.mainLayout
mainLayout.setVerticalSpacing(0)
mainLayout.setContentsMargins(0, 0, 0, 0)
row = 0
mainLayout.addWidget(self.toggleButton, row, 0, 1, 1, QtCore.Qt.AlignLeft)
mainLayout.addWidget(self.headerLine, row, 2, 1, 1)
row += 1
mainLayout.addWidget(self.contentArea, row, 0, 1, 3)
self.setLayout(self.mainLayout)

def start_animation(checked):
arrow_type = QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow
direction = QtCore.QAbstractAnimation.Forward if checked else QtCore.QAbstractAnimation.Backward
toggleButton.setArrowType(arrow_type)
self.toggleAnimation.setDirection(direction)
self.toggleAnimation.start()

self.toggleButton.clicked.connect(start_animation)

def setContentLayout(self, contentLayout):
# Not sure if this is equivalent to self.contentArea.destroy()
self.contentArea.destroy()
self.contentArea.setLayout(contentLayout)
collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight()
contentHeight = contentLayout.sizeHint().height()
for i in range(self.toggleAnimation.animationCount()-1):
spoilerAnimation = self.toggleAnimation.animationAt(i)
spoilerAnimation.setDuration(self.animationDuration)
spoilerAnimation.setStartValue(collapsedHeight)
spoilerAnimation.setEndValue(collapsedHeight + contentHeight)
contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1)
contentAnimation.setDuration(self.animationDuration)
contentAnimation.setStartValue(0)
contentAnimation.setEndValue(contentHeight)
14

Я знаю, что это не очень хороший способ ответить на вопрос, просто с помощью ссылки, но я думаю, что это сообщение в блоге весьма актуально:

http://www.fancyaddress.com/blog/qt-2/create-something-like-the-widget-box-as-in-the-qt-designer/

Он основан на QTreeWidget и использует его функции развертывания / свертывания, которые уже реализованы.
В нем объясняется, как виджеты могут быть добавлены в элементы дерева виджетов, и как добавить кнопку, чтобы использовать их для свертывания / развертывания.

Конечно, вся заслуга автора поста.

4

Я перерыл превосходный указатель, предоставленный @LoPiTal, и преобразовал его в PyQt5 (Python3). Я думаю, что это очень элегантно.

Если кто-то ищет PyQt-решение, вот мой код:

import sys
from PyQt5.QtWidgets import (QPushButton, QDialog, QTreeWidget,
QTreeWidgetItem, QVBoxLayout,
QHBoxLayout, QFrame, QLabel,
QApplication)

class SectionExpandButton(QPushButton):
"""a QPushbutton that can expand or collapse its section
"""def __init__(self, item, text = "", parent = None):
super().__init__(text, parent)
self.section = item
self.clicked.connect(self.on_clicked)

def on_clicked(self):
"""toggle expand/collapse of section by clicking
"""if self.section.isExpanded():
self.section.setExpanded(False)
else:
self.section.setExpanded(True)class CollapsibleDialog(QDialog):
"""a dialog to which collapsible sections can be added;
subclass and reimplement define_sections() to define sections and
add them as (title, widget) tuples to self.sections
"""def __init__(self):
super().__init__()
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
layout = QVBoxLayout()
layout.addWidget(self.tree)
self.setLayout(layout)
self.tree.setIndentation(0)

self.sections = []
self.define_sections()
self.add_sections()

def add_sections(self):
"""adds a collapsible sections for every
(title, widget) tuple in self.sections
"""for (title, widget) in self.sections:
button1 = self.add_button(title)
section1 = self.add_widget(button1, widget)
button1.addChild(section1)

def define_sections(self):
"""reimplement this to define all your sections
and add them as (title, widget) tuples to self.sections
"""widget = QFrame(self.tree)
layout = QHBoxLayout(widget)
layout.addWidget(QLabel("Bla"))
layout.addWidget(QLabel("Blubb"))
title = "Section 1"self.sections.append((title, widget))

def add_button(self, title):
"""creates a QTreeWidgetItem containing a button
to expand or collapse its section
"""item = QTreeWidgetItem()
self.tree.addTopLevelItem(item)
self.tree.setItemWidget(item, 0, SectionExpandButton(item, text = title))
return item

def add_widget(self, button, widget):
"""creates a QWidgetItem containing the widget,
as child of the button-QWidgetItem
"""section = QTreeWidgetItem(button)
section.setDisabled(True)
self.tree.setItemWidget(section, 0, widget)
return sectionif __name__ == "__main__":
app = QApplication(sys.argv)
window = CollapsibleDialog()
window.show()
sys.exit(app.exec_())
2

Решение, которое я применил, заключается в использовании свойства MaximumSize виджета, чтобы ограничить высоту в сложенном виде.

Самая большая проблема заключается в том, чтобы узнать высоту в сложенном состоянии, чтобы обеспечить правильный шаг анимации. Это не было решено, и в настоящее время я делаю анимацию с шагом фиксированной высоты (которому я установил соответствующее значение по отношению к ожидаемой высоте окна).

if (toBeFolded)
{
unfoldedMaxHeight = maximumHeight();
previousUnfoldedHeight = height();
setMaximumHeight(25);
}
else
{
// animate maximumHeight from 25 up to where the height do not change
// A hint of the final maximumHeight is the previousUnfoldedHeight.
// After animation, set maximumHeight back to unfoldedMaxHeight.
}
0