Я хотел бы создать собственный виджет в QT со следующими функциями:
Чтобы представить идею, вот изображение похожего виджета (не 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(§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
Я наткнулся на ту же проблему и решил ее, реализовав складной виджет в виде 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);
…
Несмотря на то, что это старый, я нашел эту тему полезной. Тем не менее, я работаю в 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)
Я знаю, что это не очень хороший способ ответить на вопрос, просто с помощью ссылки, но я думаю, что это сообщение в блоге весьма актуально:
http://www.fancyaddress.com/blog/qt-2/create-something-like-the-widget-box-as-in-the-qt-designer/
Он основан на QTreeWidget и использует его функции развертывания / свертывания, которые уже реализованы.
В нем объясняется, как виджеты могут быть добавлены в элементы дерева виджетов, и как добавить кнопку, чтобы использовать их для свертывания / развертывания.
Конечно, вся заслуга автора поста.
Я перерыл превосходный указатель, предоставленный @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_())
Решение, которое я применил, заключается в использовании свойства 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.
}