次の機能を備えた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を使用していないため、そこで動作するかどうかはわかりません。
私はまだ1つの問題があります。折り畳み可能なウィジェットは、下方向にのみ拡大するのではなく、上下に拡大できます。これにより、最小の高さにまだ達していない場合、その上にあるウィジェットが縮小する可能性があります。しかし、これは私たちがこのことを自分で構築しなければならないという事実と比較して、本当に詳細です...
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, QtGui
class 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 section
if __== "__main__":
app = QApplication(sys.argv)
window = CollapsibleDialog()
window.show()
sys.exit(app.exec_())
私が書いているStyleSheetクラスをテストするために、Python3/Qt5スタイルの例を作成しました。また、エキスパンダーボタンのサイズ変更を考慮しないサイズ計算の問題も修正しました。
また、メソッドをsetLayout()
に変更して、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 __== '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
PySide2(python3の公式Qt5バインディング)を使用してバージョンを提供する
from PySide2 import QtCore, QtGui, QtWidgets
class Expander(QtWidgets.QWidget):
def __init__(self, parent=None, title='', animationDuration=300):
"""
References:
# Adapted from PyQt4 version
https://stackoverflow.com/a/37927256/386398
# Adapted from c++ version
https://stackoverflow.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)
私が適用した解決策は、ウィジェットの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.
}