widgets tutorial example ejemplos creator c++ qt widget custom-controls qwidget

c++ - tutorial - qt5 example



Cómo hacer un widget de sección expandible/plegable en Qt (7)

Me gustaría crear un widget personalizado en Qt con las siguientes características:

  • Es un contenedor
  • Puede rellenarse con cualquier diseño Qt
  • Puede estar dentro de cualquier diseño Qt
  • Un botón permite contraer / plegar verticalmente el contenido, por lo que solo el botón es visible, todo el diseño contenido es invisible.
  • El botón anterior permite expandirlo / desplegarlo nuevamente al tamaño del contenido del diseño.
  • La expansión / contracción se basa en tamaños (no en mostrar / ocultar) para permitir la animación.
  • Utilizable en QDesigner

Para proporcionar una idea, aquí hay una imagen de un widget similar (no Qt):

Ya tengo un marco que funciona correctamente y está expuesto en QDesigner. Ahora necesito hacer que se extienda / colapse, lo que no parece tan simple.

Traté de jugar con resize (), sizePolicy (), sizeHint () pero eso no funciona: cuando el marco está colapsado obtuve los siguientes valores:

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

QLayout ::arestAcceptableSize no es parte del widget, por lo que no puedo cambiarlo.

¿Alguna pista o fragmento de código para lograr eso?

EDITADO: Aquí un ejemplo simple. Eliminé todo excepto lo necesario.

Ejemplo de 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


Aunque esto es viejo, encontré este hilo útil. Sin embargo, estoy trabajando en Python, así que tuve que convertir el código C ++. En caso de que alguien esté buscando una versión en python de la solución de x squared. Aquí está mi puerto:

from PyQt4 import QtCore, QtGui class Spoiler(QtGui.QWidget): def __init__(self, parent=None, title='''', animationDuration=300): """ References: # Adapted from c++ version http://.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)


Contribuir con una versión utilizando PySide2 (enlaces Qt5 oficiales para python3)

from PySide2 import QtCore, QtGui, QtWidgets class Expander(QtWidgets.QWidget): def __init__(self, parent=None, title='''', animationDuration=300): """ References: # Adapted from PyQt4 version https://.com/a/37927256/386398 # Adapted from c++ version https://.com/a/37119983/386398 """ super(Expander, self).__init__(parent=parent) self.animationDuration = animationDuration self.toggleAnimation = QtCore.QParallelAnimationGroup() self.contentArea = QtWidgets.QScrollArea() self.headerLine = QtWidgets.QFrame() self.toggleButton = QtWidgets.QToolButton() self.mainLayout = QtWidgets.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(QtWidgets.QFrame.HLine) headerLine.setFrameShadow(QtWidgets.QFrame.Sunken) headerLine.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }") self.contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.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, b"minimumHeight")) toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, b"maximumHeight")) toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self.contentArea, b"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): expandAnimation = self.toggleAnimation.animationAt(i) expandAnimation.setDuration(self.animationDuration) expandAnimation.setStartValue(collapsedHeight) expandAnimation.setEndValue(collapsedHeight + contentHeight) contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1) contentAnimation.setDuration(self.animationDuration) contentAnimation.setStartValue(0) contentAnimation.setEndValue(contentHeight)


He revisado el excelente puntero proporcionado por @LoPiTal y lo he convertido a PyQt5 (Python3). Creo que es muy elegante.

En caso de que alguien esté buscando una solución PyQt, aquí está mi código:

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 solución que apliqué es usar la propiedad MaximumSize del widget para limitar la altura mientras está plegado.

El mayor problema es conocer la altura desplegada mientras está plegada para permitir un paso de animación correcto. Esto no se ha resuelto y actualmente hago una animación con un paso de altura fija (que configuré en un valor apropiado en relación con la altura esperada de la ventana).

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. }


Me topé con el mismo problema y lo resolví implementando el widget plegable como un QScrollArea cuya altura máxima está animada por una QPropertyAnimation .

Pero como no uso QDesigner, no puedo decirte si funciona allí.

Todavía tengo un problema: en lugar de expandirse solo hacia la dirección inferior, el widget plegable puede expandirse hacia la parte superior e inferior. Esto puede hacer que los widgets ubicados encima se encojan si aún no han alcanzado su altura mínima. Pero esto es realmente un detalle en comparación con el hecho de que tenemos que construir esto nosotros mismos ...

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

Cómo usarlo:

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


Sé que esta no es una buena manera de responder una pregunta, solo con un enlace, pero creo que esta publicación de blog es bastante relevante:

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

Se basa en QTreeWidget y utiliza sus funciones de expansión / contracción, que ya están implementadas. Explica cómo se pueden agregar widgets a los elementos de widgets del árbol y cómo agregar un botón para contraerlos / expandirlos.

Por supuesto, todo el crédito va para el autor del post.


Creé el ejemplo de estilo Python3 / Qt5 para probar una clase de StyleSheet que estoy escribiendo. También solucioné un problema con los cálculos de tamaño que no consideraban los cambios de tamaño en el botón expansor.

También cambié el método para establecerLayout setLayout() para que sea consistente con Qt.

import sys import inspect import textwrap from collections import OrderedDict, UserString from PyQt5 import QtCore, QtGui from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import * class QStyleSheet(UserString): """ Represent stylesheets as dictionary key value pairs. Update complex stylesheets easily modifying only the attributes you need Allow for attribute inheritance or defaulting of stylesheets. # TODO support [readOnly="true"] attribute-selectors QTextEdit, QListView <-- you can have multiple classes. QCheckBox::indicator <-- some psuedo classes have double colons """ def __init__(self, cls=None, name=None, psuedo=None, **styles): """ Arguments to the constructor allow you to default different properties of the CSS Class. Any argument defined here will be global to this StyleSheet and cannot be overidden later. :param cls: Default style prefix class to ``cls`` :param name: Default object name to ``name`` (hashtag) is not needed. :param psuedo: Default psuedo class to ``psuedo``, example: ``:hover`` """ self.cls_scope = cls self.psuedo_scope = psuedo self.name_scope = name self._styles = OrderedDict() # we''ll preserve the order of attributes given - python 3.6+ if styles: self.setStylesDict(OrderedDict(styles)) def _ident(self, cls=None, name=None, psuedo=None): # -- ensure value is of correct type ---------------------------------------- if cls is not None and not inspect.isclass(cls): raise ValueError(f''cls must be None or a class object, got: {type(cls)}'') if name is not None and not isinstance(name, str): raise ValueError(f''name must be None or a str, got: {type(name)}'') if psuedo is not None and not isinstance(psuedo, str): raise ValueError(f''psuedo must be None or a str, got: {type(psuedo)}'') # -- ensure not overiding defaults ------------------------------------------- if cls is not None and self.cls_scope is not None: raise ValueError(f''cls was set in __init__, you cannot override it'') if name is not None and self.name_scope is not None: raise ValueError(f''name was set in __init__, you cannot override it'') if psuedo is not None and self.psuedo_scope is not None: raise ValueError(f''psuedo was set in __init__, you cannot override it'') # -- apply defaults if set --------------------------------------------------- if cls is None and self.cls_scope is not None: cls = self.cls_scope if name is None and self.name_scope is not None: name = self.name_scope if psuedo is None and self.psuedo_scope is not None: psuedo = self.psuedo_scope # return a tuple that can be used as a dictionary key. ident = tuple([getattr(cls, ''__name__'', None), name or None, psuedo or None]) return ident def _class_definition(self, ident): """Get the class definition string""" cls, name, psuedo = ident return ''%s%s%s'' % (cls or '''', name or '''', psuedo or '''') def _fix_underscores(self, styles): return OrderedDict([(k.replace(''_'', ''-''), v) for k,v in styles.items()]) def setStylesStr(self, styles): """ Parse styles from a string and set them on this object. """ raise NotImplementedError() self._update() def setStylesDict(self, styles, cls=None, name=None, psuedo=None): """ Set styles using a dictionary instead of keyword arguments """ styles = self._fix_underscores(styles) if not isinstance(styles, dict): raise ValueError(f''`styles` must be dict, got: {type(styles)}'') if not styles: raise ValueError(''`styles` cannot be empty'') ident = self._ident(cls, name, psuedo) stored = self._styles.get(ident, OrderedDict()) stored.update(styles) self._styles[ident] = stored self._update() def setStyles(self, cls=None, name=None, psuedo=None, **styles): """ Set or update styles according to the CSS Class definition provided by (cls, name, psuedo) using keyword-arguments. Any css attribute with a hyphen ``-`` character should be changed to an underscore ``_`` when passed as a keyword argument. Example:: Lets suppose we want to create the css class: QFrame#BorderTest { background-color: white; margin:4px; border:1px solid #a5a5a5; border-radius: 10px;} >>> stylesheet.setStyle(cls=QFrameBorderTest, background_color=''white'', margin=''4px'', border_radius=''10px'') >>> print(stylesheet) QFrame#BorderTest { background-color: white; margin:4px; border:1px solid #a5a5a5; border-radius: 10px;} """ styles = OrderedDict(styles) self.setStylesDict(styles=styles, cls=cls, name=name, psuedo=psuedo) def getStyles(self, cls=None, name=None, psuedo=None): """ Return the dictionary representations of styles for the CSS Class definition provided by (cls, name, psuedo) :returns: styles dict (keys with hyphens) """ ident = self._ident(cls, name, psuedo) return self._styles.get(ident) def getClassIdents(self): """Get all class identifier tuples""" return list(self._styles.keys()) def getClassDefinitions(self): """Get all css class definitions, but not the css attributes/body""" return [self._class_definition(ident) for ident in self.getClassIdents()] def validate(self): """ Validate all the styles and attributes on this class """ raise NotImplementedError() def merge(self, stylesheet, overwrite=True): """ Merge another QStyleSheet with this QStyleSheet. The QStyleSheet passed as an argument will be left un-modified. :param overwrite: if set to True the matching class definitions will be overwritten with attributes and values from ``stylesheet``. Otherwise, the css attributes will be updated from ``stylesheet`` :type overwrite: QStyleSheet """ for ident in stylesheet.getClassIdents(): styles = stylesheet.getStyles(ident) cls, name, psuedo = ident self.setStylesDict(styles, cls=cls, name=name, psuedo=psuedo) self._update() def clear(self, cls=None, name=None, psuedo=None): """ Clear styles matching the Class definition The style dictionary cleared will be returned None will be returned if nothing was cleared. """ ident = self._ident(cls, name, psuedo) return self._styles.pop(ident, None) def _update(self): """Update the internal string representation""" stylesheet = [] for ident, styles in self._styles.items(): if not styles: continue css_cls = self._class_definition(ident) css_cls = css_cls + '' '' if css_cls else '''' styles_str = ''/n''.join([f''{k}: {v};'' for k, v in styles.items()]) styles_str = textwrap.indent(styles_str, ''''.ljust(4)) stylesheet.append(''%s{/n%s/n}'' % (css_cls, styles_str)) self.data = ''/n/n''.join(stylesheet) class Expander(QWidget): def __init__(self, parent=None, title=None, animationDuration=200): super().__init__(parent=parent) self.animationDuration = animationDuration self.toggleAnimation = QtCore.QParallelAnimationGroup() self.contentArea = QScrollArea() self.headerLine = QFrame() self.toggleButton = QToolButton() self.mainLayout = QGridLayout() toggleButton = self.toggleButton self.toggleButtonQStyle = QStyleSheet(QToolButton, border=''none'') toggleButton.setStyleSheet(str(self.toggleButtonQStyle)) toggleButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) toggleButton.setArrowType(QtCore.Qt.RightArrow) toggleButton.setText(title or '''') toggleButton.setCheckable(True) toggleButton.setChecked(False) toggleButton.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) headerLine = self.headerLine self.headerLineQStyle = QStyleSheet(QFrame) headerLine.setFrameShape(QFrame.NoFrame) # see: https://doc.qt.io/archives/qt-4.8/qframe.html#Shape-enum headerLine.setFrameShadow(QFrame.Plain) # see: https://doc.qt.io/archives/qt-4.8/qframe.html#Shape-enum headerLine.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.contentAreaQStyle = QStyleSheet(QScrollArea, border=''none'') self.contentArea.setStyleSheet(str(self.contentAreaQStyle)) self.contentArea.setSizePolicy(QSizePolicy.Expanding, 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, b"minimumHeight")) toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, b"maximumHeight")) toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self.contentArea, b"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) super().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 setHeaderFrameStyles(self, styles): self._setWidgetStyles(self.headerLine, self.headerLineQStyle, styles) def setToggleButtonStyles(self, styles): self._setWidgetStyles(self.toggleButton, self.toggleButtonQStyle, styles) def setContentAreaStyles(self, styles): self._setWidgetStyles(self.contentArea, self.contentAreaQStyle, styles) def _setWidgetStyles(self, widget, qstylesheet, var): if isinstance(var, QStyleSheet): qstylesheet.merge(var) widget.setStyleSheet(str(qstylesheet)) elif isinstance(var, dict): qstylesheet.setStylesDict(var) widget.setStyleSheet(str(qstylesheet)) elif isinstance(var, str): widget.setStyleSheet(var) else: raise ValueError(''invalid argument type: {type(var)}'') def setLayout(self, contentLayout): """ Set the layout container that you would like to expand/collapse. This should be called after all styles are set. """ # Not sure if this is equivalent to self.contentArea.destroy() self.contentArea.destroy() self.contentArea.setLayout(contentLayout) collapsedHeight = self.toggleButton.sizeHint().height() 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) class MainWindow(QMainWindow): LIGHT_BLUE = ''#148cc1'' MED_BLUE = ''#0c6a94'' DARK_BLUE = ''#0a3a6b'' PALE_SALMON = ''#fd756d'' LIGHT_GREY = ''#d2d5da'' SLATE = ''#525863'' def __init__(self): super().__init__() self.WINDOW_STYLE = QStyleSheet(QMainWindow, background_color=self.SLATE) self.WINDOW_STYLE = str(self.WINDOW_STYLE) self.LABEL_STYLE = QStyleSheet(QLabel, color=self.DARK_BLUE, font_weight=400, font_size=''9pt'') self.LABEL_STYLE = str(self.LABEL_STYLE) # -- QPushButton stylesheet --------------------- self.BUTTON_STYLE = s1 = QStyleSheet() s1.setStyles(cls=QPushButton, color=''white'', font_weight=400, border_style=''solid'', padding=''4px'', background_color=self.LIGHT_BLUE) s1.setStyles(cls=QPushButton, psuedo='':pressed'', background_color=self.PALE_SALMON) s1.setStyles(cls=QPushButton, psuedo='':focus-pressed'', background_color=self.PALE_SALMON) s1.setStyles(cls=QPushButton, psuedo='':disabled'', background_color=self.LIGHT_GREY) s1.setStyles(cls=QPushButton, psuedo='':checked'', background_color=self.PALE_SALMON) s1.setStyles(cls=QPushButton, psuedo='':hover:!pressed:!checked'', background_color=self.MED_BLUE) self.BUTTON_STYLE = str(self.BUTTON_STYLE) self.BUTTON_GROUPBOX_STYLE = QStyleSheet(QGroupBox, border=''none'', font_weight=''bold'', color=''white'') self.BUTTON_GROUPBOX_STYLE = str(self.BUTTON_GROUPBOX_STYLE) self.TEXT_EDIT_STYLE = QStyleSheet(QTextEdit, color=''white'', border=f''1px solid {self.LIGHT_BLUE}'', background_color=self.MED_BLUE) self.TEXT_EDIT_STYLE = str(self.TEXT_EDIT_STYLE) self.initUI() def initUI(self): contents_vbox = QVBoxLayout() label_box = QHBoxLayout() for text in (''hello'', ''goodbye'', ''adios''): lbl = QLabel(text) lbl.setStyleSheet(self.LABEL_STYLE) lbl.setAlignment(Qt.AlignCenter) label_box.addWidget(lbl) button_group = QButtonGroup() button_group.setExclusive(True) button_group.buttonClicked.connect(self._button_clicked) self.button_group = button_group button_hbox = QHBoxLayout() for _id, text in enumerate((''small'', ''medium'', ''large'')): btn = QPushButton(text) btn.setCheckable(True) btn.setStyleSheet(self.BUTTON_STYLE) button_group.addButton(btn) button_group.setId(btn, _id) button_hbox.addWidget(btn) button_group.buttons()[0].toggle() text_area = QTextEdit() text_area.setPlaceholderText(''Type a greeting here'') text_area.setStyleSheet(self.TEXT_EDIT_STYLE) contents_vbox.addLayout(label_box) contents_vbox.addLayout(button_hbox) contents_vbox.addWidget(text_area) collapsible = Expander(self, ''Expander'') collapsible.setToggleButtonStyles({''padding'': ''4px'', ''background-color'': ''white''}) collapsible.setContentAreaStyles({''background-color'': ''white''}) collapsible.setLayout(contents_vbox) vbox = QVBoxLayout() vbox.addWidget(collapsible) vbox.setAlignment(Qt.AlignTop) widget = QWidget() widget.setLayout(vbox) self.setCentralWidget(widget) self.setGeometry(200, 200, 500, 400) self.setWindowTitle(''Expander'') self.setStyleSheet(self.WINDOW_STYLE) self.show() def _button_clicked(self, button): """ For the toggle behavior of a QButtonGroup to work you must connect the clicked signal! """ print(''button-active'', self.button_group.id(button)) if __name__ == ''__main__'': app = QApplication(sys.argv) ex = MainWindow() sys.exit(app.exec_())