Comment faire un widget de section extensible/repliable dans QT
j'aimerais créer un widget personnalisé dans QT avec les fonctionnalités suivantes:
- il s'agit d'un conteneur
- elle peut être peuplée avec N'importe quel layout QT
- C'est peut-être à l'intérieur de toute QT mise en page
- un bouton permet d'effondrer/plier verticalement le contenu, de sorte que seul le bouton est visible, toute la mise en page contenue est invisible.
- Le bouton précédent permet d'agrandir/déplier encore une fois à la taille du contenu de mise en page.
- l'extension/effondrement est basé sur les tailles (pas sur show/hide) pour permettre l'animation.
- utilisable en QDesigner
pour fournir une idée, voici une image d'un widget similaire (pas QT):
j'ai déjà un cadre qui fonctionne correctement et qui est exposé dans QDesigner. Je dois maintenant le faire pour étendre/réduire, ce qui ne semble pas si simple.
j'ai essayé de jouer avec resize(), sizePolicy(), sizeHint() mais cela ne fonctionne pas: Quand le cadre est effondré j'ai obtenu les valeurs suivantes:
sizeHint: (500,20)
size : (500,20)
closestAcceptableSize: (518,150)
Painted size: (518, 150)
QLayout:: closestAcceptableSize ne fait pas partie du widget donc je ne peux pas le changer.
y a-t-il une indication ou/et un extrait de code pour y parvenir?
édité: Voici un exemple simple. Je l'ai enlevé tout sauf nécessaire.
principal.exemple 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(§ion);
QVBoxLayout inLayout(§ion);
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
5 réponses
je suis tombé sur le même problème et l'ai résolu en mettant en œuvre le widget pliable comme un QScrollArea
dont la hauteur maximale est animée par un QPropertyAnimation
.
mais comme je n'utilise pas QDesigner, Je ne peux pas vous dire s'il fonctionne là.
j'ai encore un problème: au lieu de seulement étendre vers le bas, le widget pliable peut étendre vers le haut et le bas. Cela peut faire rétrécir les widgets situés au-dessus de lui ils n'ont pas encore atteint leur taille minimale. Mais c'est vraiment un détail par rapport au fait que nous avons à construire cette chose nous-mêmes...
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);
}
comment l'utiliser:
…
auto * anyLayout = new QVBoxLayout();
anyLayout->addWidget(…);
…
Spoiler spoiler;
spoiler.setContentLayout(*anyLayout);
…
bien que ce soit ancien, j'ai trouvé ce fil utile. Cependant, je travaille en python, donc j'ai dû convertir le code C++. Juste au cas où quelqu'un serait à la recherche d'une version python de la solution de x squared. Voici mon port:
from PyQt4 import QtCore, QtGui
class Spoiler(QtGui.QWidget):
def __init__(self, parent=None, title='', animationDuration=300):
"""
References:
# Adapted from c++ version
/q/how-to-make-an-expandable-collapsable-section-widget-in-qt-6757/"""
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)
je sais que ce n'est pas une bonne façon de répondre à une question, juste avec un lien, mais je pense que ce billet de blog est tout à fait pertinent:
http://www.fancyaddress.com/blog/qt-2/create-something-like-the-widget-box-as-in-the-qt-designer /
il est basé sur QTreeWidget, et utilise ses fonctionnalités expand / collapse, qui sont déjà implémentées. Il explique comment les widgets peuvent être ajoutés à l'arbre widget éléments, et comment ajouter un bouton à utiliser pour développer / réduire.
bien sûr, tout le mérite revient à l'auteur du billet.
j'ai creusé à travers l'excellent pointeur fourni par @LoPiTal et je l'ai converti en PyQt5 (Python3). Je pense que c'est très élégant.
si quelqu'un cherche une solution PyQt, voici mon code:
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 section
if __name__ == "__main__":
app = QApplication(sys.argv)
window = CollapsibleDialog()
window.show()
sys.exit(app.exec_())
la solution que j'ai appliquée est d'utiliser la propriété MaximumSize du widget pour limiter la hauteur lorsque plié.
le plus gros problème est de connaître la hauteur dépliée lorsque plié pour permettre une étape d'animation correcte. Ceci n'a pas été résolu et je réalise actuellement une animation avec un pas de hauteur fixe (que je mets à une valeur appropriée par rapport à la hauteur attendue de la fenêtre).
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.
}