I want to implement a button (already has a custom class) that when clicked, fades out all the widgets on the existing screen before switching to another layout (implemented using QStackedLayout)
I've looked at different documentations and guides on PySide6 on how to animate fading in/out but nothing seems to be working. Not sure what is wrong with the code per se bit I've done the debugging and the animation class is acting on the widget
I assume that to make the Widget fade, I had to create QGraphicsOpacityEffect with the top-level widget being the parent, then adding the QPropertyAnimation for it to work.
main.py
# Required Libraries for PySide6/Qt
from PySide6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QMainWindow, QSystemTrayIcon, QStackedLayout, QGraphicsOpacityEffect
from PySide6.QtGui import QIcon, QPixmap, QFont, QLinearGradient, QPainter, QColor
from PySide6.QtCore import Qt, QPointF, QSize, QVariantAnimation, QAbstractAnimation, QEasingCurve, QPropertyAnimation, QTimer
# For changing the taskbar icon
import ctypes
import platform
# For relative imports
import sys
sys.path.append('../classes')
# Classes and Different Windows
from classes.getStartedButton import getStartedButton
class window(QMainWindow):
# Set up core components of window
def __init__(self,h,w):
# Gets primary parameters of the screen that the window will display in
self.height = h
self.width = w
super().__init__()
# Required to change taskbar icon
if platform.system() == "Windows":
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
"com.random.appID")
# Multiple screens will overlay each other
self.layout = QStackedLayout()
# Init splash screen - this is first
self.splashScreen()
# Function to init other screens
self.initScreens()
# Must create main widget to hold the stacked layout, or another window will appear
main_widget_holder = QWidget()
main_widget_holder.setLayout(self.layout)
self.setCentralWidget(main_widget_holder)
def initScreens(self):
apiScreen = QWidget()
another_button = QPushButton("Test")
another_layout = QVBoxLayout()
another_layout.addWidget(another_button)
apiScreen.setLayout(another_layout)
self.layout.addWidget(apiScreen)
# Window definition for splash screen
def splashScreen(self):
"""Window that displays the splash screen
"""
# Widget that holds all the widgets in the splash screen
self.placeholder = QWidget()
# Logo & Title Component
logo = QLabel("")
logo.setPixmap(QPixmap("image.png"))
# Align logo on right side of the split
logo.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
logo.setStyleSheet(
"border-right:3px solid white;background-color: rgb(22,22,22);")
title = QLabel("Another\nApp")
title.setStyleSheet("padding-left:2px; font-size:36px")
# Header to hold title and logo
header_layout = QHBoxLayout()
header_layout.addWidget(logo)
header_layout.addWidget(title)
header = QWidget()
header.setStyleSheet("margin-bottom:20px")
# Assign header_layout to the header widget
header.setLayout(header_layout)
# Button Component
button = getStartedButton("Get Started ")
# Set max width of the button to cover both text and logo
button.setMaximumWidth(self.placeholder.width())
button.clicked.connect(self.transition_splash)
# Vertical Layout from child widget components
title_scrn = QVBoxLayout()
title_scrn.addWidget(header)
title_scrn.addWidget(button)
# Define alignment to be vertically and horizontal aligned (Prevents button from appearing on the bottom of the app as well)
title_scrn.setAlignment(Qt.AlignCenter)
# Enlarge the default window size of 640*480 to something bigger (add 50px on all sides)
self.placeholder.setLayout(title_scrn)
self.placeholder.setObjectName("self.placeholder")
self.placeholder.setMinimumSize(
self.placeholder.width()+100, self.placeholder.height()+100)
self.setCentralWidget(self.placeholder)
# Grey/Black Background
self.setStyleSheet(
"QMainWindow{background-color: rgb(22,22,22);}QLabel{color:white} #button{padding:25px; border-radius:15px;background: qlineargradient(x1:0, y1:0,x2:1,y2:1,stop: 0 #00dbde, stop:1 #D600ff); border:1px solid white}")
self.setMinimumSize(self.width/3*2,self.height/3*2)
self.layout.addWidget(self.placeholder)
def transition_splash(self):
opacityEffect = QGraphicsOpacityEffect(self.placeholder)
self.placeholder.setGraphicsEffect(opacityEffect)
animationEffect = QPropertyAnimation(opacityEffect, b"opacity")
animationEffect.setStartValue(1)
animationEffect.setEndValue(0)
animationEffect.setDuration(2500)
animationEffect.start()
timer = QTimer()
timer.singleShot(2500,self.change_layout)
def change_layout(self):
self.layout.setCurrentIndex(1)
# Initialise program
if __name__ == "__main__":
app = QApplication([])
page = window(app.primaryScreen().size().height(),app.primaryScreen().size().width())
page.show()
sys.exit(app.exec())
button.py
# Required Libraries for PySide6/Qt
from PySide6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QMainWindow, QSystemTrayIcon, QStackedLayout, QGraphicsOpacityEffect
from PySide6.QtGui import QIcon, QPixmap, QFont, QLinearGradient, QPainter, QColor
from PySide6.QtCore import Qt, QPointF, QSize, QVariantAnimation, QAbstractAnimation, QEasingCurve
# Custom PushButton for splash screen
class getStartedButton(QPushButton):
getStartedButtonColorStart = QColor(0, 219, 222)
getStartedButtonColorInt = QColor(101, 118, 255)
getStartedButtonColorEnd = QColor(214, 0, 255)
def __init__(self, text):
super().__init__()
self.setText(text)
# Setting ID so that it can be used in CSS
self.setObjectName("button")
self.setStyleSheet("font-size:24px")
# Button Animation
self.getStartedButtonAnimation = QVariantAnimation(
self, startValue=0.42, endValue=0.98, duration=300)
self.getStartedButtonAnimation.valueChanged.connect(
self.animate_button)
self.getStartedButtonAnimation.setEasingCurve(QEasingCurve.InOutCubic)
def enterEvent(self, event):
self.getStartedButtonAnimation.setDirection(QAbstractAnimation.Forward)
self.getStartedButtonAnimation.start()
# Suppression of event type error
try:
super().enterEvent(event)
except TypeError:
pass
def leaveEvent(self, event):
self.getStartedButtonAnimation.setDirection(
QAbstractAnimation.Backward)
self.getStartedButtonAnimation.start()
# Suppression of event type error
try:
super().enterEvent(event)
except TypeError:
pass
def animate_button(self, value):
grad = "background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 {startColor}, stop:{value} {intermediateColor}, stop: 1.0 {stopColor});font-size:24px".format(
startColor=self.getStartedButtonColorStart.name(), intermediateColor=self.getStartedButtonColorInt.name(), stopColor=self.getStartedButtonColorEnd.name(), value=value
)
self.setStyleSheet(grad)
For context, I've looked at other questions already on SO and other sites such as
How to change the opacity of a PyQt5 window
https://www.pythonguis.com/tutorials/pyside6-animated-widgets/
Not sure what is wrong with the code
The QPropertyAnimation object is destroyed before it gets a chance to start your animation. Your question has already been solved here.
To make it work, you must persist the object:
def transition_splash(self):
opacityEffect = QGraphicsOpacityEffect(self.placeholder)
self.placeholder.setGraphicsEffect(opacityEffect)
self.animationEffect = QPropertyAnimation(opacityEffect, b"opacity")
self.animationEffect.setStartValue(1)
self.animationEffect.setEndValue(0)
self.animationEffect.setDuration(2500)
self.animationEffect.start()
# Use the finished signal instead of the QTimer
self.animationEffect.finished.connect(self.change_layout)
Related
I found QGraphicsEllipseItem, QGraphicsRectItem and other items when them was selected,The Qt will draw a dotted-white rectangel border around it(like the picture bellow).So is that Qt place a QWidget or use paint method draw a rectange around it?
And sometime I want to change dotted-white rectangel with other style.for example, I want to change it border color, line style(line-width, line-color and so on) or even it's shape,I read Qt doc, not found method to do this, So How Can I change it's default selected border ?
You must override the paint method:
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QPen
from PyQt5.QtWidgets import (
QApplication,
QGraphicsEllipseItem,
QGraphicsItem,
QGraphicsScene,
QGraphicsView,
QStyle,
)
class EllipseItem(QGraphicsEllipseItem):
def paint(self, painter, option, widget):
is_selected = option.state & QStyle.State_Selected
# Remove default paint from selection
option.state &= ~QStyle.State_Selected
super().paint(painter, option, widget)
if is_selected:
# custom paint
itemPenWidth = self.pen().widthF()
pad = itemPenWidth / 2
penWidth = 0
bgcolor = QColor("salmon")
painter.setPen(QPen(bgcolor, penWidth, Qt.SolidLine))
painter.setBrush(Qt.NoBrush)
painter.drawRect(self.boundingRect().adjusted(pad, pad, -pad, -pad))
painter.setPen(QPen(QColor("blue"), 0, Qt.DashLine))
painter.setBrush(Qt.NoBrush)
painter.drawRect(self.boundingRect().adjusted(pad, pad, -pad, -pad))
def main():
app = QApplication([])
scene = QGraphicsScene()
ellipse_item = EllipseItem(0, 0, 500, 500)
ellipse_item.setBrush(QColor("white"))
ellipse_item.setFlag(QGraphicsItem.ItemIsSelectable)
scene.addItem(ellipse_item)
view = QGraphicsView()
view.setScene(scene)
view.show()
app.exec_()
if __name__ == "__main__":
main()
I'm trying to produce an onion skin effect using a QLabel in PyQt. In the simplified example below, three images are loaded in and drawn to the label using QPainter.
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
# -------------------------------------------------------------
# Import the frames
frame_1 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{1}.png')))
frame_2 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{2}.png')))
frame_3 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{3}.png')))
# -------------------------------------------------------------
# Populate the display
frame_1_scaled = frame_1.scaled(self.size(), Qt.KeepAspectRatio)
frame_2_scaled = frame_2.scaled(self.size(), Qt.KeepAspectRatio)
frame_3_scaled = frame_3.scaled(self.size(), Qt.KeepAspectRatio)
base_pixmap = QPixmap(frame_1_scaled.size())
painter = QPainter(base_pixmap)
painter.drawImage(QPoint(), frame_3_scaled)
painter.setOpacity(0.5)
painter.drawImage(QPoint(), frame_2_scaled)
painter.setOpacity(0.3)
painter.drawImage(QPoint(), frame_1_scaled)
painter.end()
self.display.setPixmap(base_pixmap)
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Ideally, the last image would show as fully opaque, with earlier images having an increasingly higher transparency. Instead, I'm getting an output where all three images blend together equally. This seems like a simple problem to fix, but my 'Google Fu' hasn't yielded much this time around.
Edit
Here are the image files. They seem have automatically been converted to .jpg unfortunately. If there's a better way to include them please let me know.
frame_1
frame_2
frame_3
Edit 2
After some experimentation I've decided to compromise and allow some 'blending' of the base image. I'm working with raw images from a camera device, so the background of each image is always going to be non-transparent.
In case anyone is interested here is the code:
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QSizePolicy
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define values used for image painting.
self.first_opacity = 1 # Set first image to fully opaque so it does not blend into background.
self.falloff_value = 0.15 # The opacity of the second image.
self.falloff_rate = 0.5 # A factor used to decrement subsequent image transparencies.
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
self.display.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
self.display.setMinimumSize(1, 1)
# -------------------------------------------------------------
# Import frames.
self.images = []
for i in range(1, 4, 1):
self.images.append(QImage(str(Path(f'fixtures/test_onion_skin/frame_{i}.png'))))
# -------------------------------------------------------------
# Set the display.
self.update_display()
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
def update_display(self):
# -------------------------------------------------------------
# Define the base pixmap on which to merge images.
base_pixmap = QPixmap(self.display.size())
base_pixmap.fill(Qt.transparent)
# -------------------------------------------------------------
# Preform paint cycle for images.
painter = QPainter(base_pixmap)
for (image, opacity) in zip(reversed(self.images), reversed(self.get_opacities(len(self.images)))):
painter.setOpacity(opacity)
painter.drawImage(QPoint(), image.scaled(base_pixmap.size(), Qt.KeepAspectRatio))
painter.end()
# -------------------------------------------------------------
self.display.setPixmap(base_pixmap)
def get_opacities(self, num_images):
# -------------------------------------------------------------
# Define a list to store image opacity values.
opacities = [self.first_opacity]
value = self.falloff_value
# -------------------------------------------------------------
# Calculate additional opacity values if more than one image is desired.
if num_images > 1:
num_decrements = num_images - 1
for i in range(1, num_decrements + 1, 1):
opacities.insert(0, value)
value *= self.falloff_rate
# -------------------------------------------------------------
return opacities
def resizeEvent(self, event):
self.update_display()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Since the OP does not provide the images then the problem can be caused by:
The order of how opacity is set.
The background color of the image is not transparent.
For my demo I will use this gif and since the background is not transparent (which would be ideal) then I will apply a mask when I paint each image.
import os
from pathlib import Path
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QColor, QImageReader, QPainter, QPixmap, QRegion
from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.display = QLabel(alignment=Qt.AlignCenter)
lay = QVBoxLayout(self)
lay.addWidget(self.display)
background_color = QColor("white")
filename = os.fspath(CURRENT_DIRECTORY / "Animhorse.gif")
image_reader = QImageReader(filename)
pixmap = QPixmap(image_reader.size())
pixmap.fill(background_color)
images = []
while image_reader.canRead():
images.append(image_reader.read())
painter = QPainter(pixmap)
for image, opacity in zip(images[3:6], (0.3, 0.7, 1.0)):
painter.setOpacity(opacity)
p = QPixmap.fromImage(image)
mask = p.createMaskFromColor(background_color, Qt.MaskInColor)
painter.setClipRegion(QRegion(mask))
painter.drawImage(QPoint(), image)
painter.end()
self.display.setPixmap(pixmap)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
I have a PyQT application with a toolbar, a set of buttons, and a bottom row of additional buttons. I'd like to add a TextEdit underneath the bottom row that the user can hide or show. I would like the TextEdit to extend the bottom portion when being shown but, when the user hides it, I would like that bottom portion removed without affecting the height, width, or sizing of any other of the buttons. Imagine just taking a pair of scissors to the TextEdit section when the user hides it but then gluing it back on when the user wants it back. Is this even possible to do in PyQt? The closest I've found is the implementation below which resizes all the buttons.
from PyQt5.QtCore import Qt, QPoint, QTimer, QThread, QSize
from PyQt5.QtGui import QFont, QImage, QPainter, QPen, QPixmap
from PyQt5.QtWidgets import (
QAction, QApplication, QCheckBox, QFileDialog, QHBoxLayout, QLabel,
QMainWindow, QMenu, QMenuBar, QPlainTextEdit, QPushButton, QSpacerItem,
QSizePolicy, QFrame,
QTextEdit, QVBoxLayout, QWidget, QGridLayout, QToolButton, QComboBox
)
from PyQt5.QtWidgets import QApplication
import sys
class AppWindow(QMainWindow):
def __init__(self, main_widget):
super(AppWindow, self).__init__()
self.main_widget = main_widget
self.setCentralWidget(self.main_widget)
class AppWidget(QWidget):
def __init__(self, panels=[]):
super(AppWidget, self).__init__()
self.panels = panels
self.main_layout = QVBoxLayout(self)
self.setSizePolicy(
QSizePolicy.MinimumExpanding,
QSizePolicy.MinimumExpanding
)
self.toolbar_frame = QFrame(self)
self.toolbar_frame_layout = QHBoxLayout(self.toolbar_frame)
self.toolbar_frame_layout.addStretch()
self.log_button = QToolButton(self.toolbar_frame)
self.log_button.setText('Toggle Log')
self.toolbar_frame_layout.addWidget(self.log_button)
self.toolbar_frame.setLayout(self.toolbar_frame_layout)
self.project_frame = QFrame(self)
self.project_frame_layout = QHBoxLayout(self.project_frame)
self.project_dropdown = QComboBox(self.project_frame)
self.project_dropdown.setMinimumSize(20, 0)
self.project_refresh = QToolButton(self.project_frame)
self.project_refresh.setText('Refresh')
self.project_frame_layout.addWidget(self.project_dropdown)
self.project_frame_layout.addWidget(self.project_refresh)
self.project_frame.setLayout(self.project_frame_layout)
self.panel_frame = QFrame(self)
self.panel_frame_layout = QVBoxLayout(self.panel_frame)
for panel in panels:
self.panel_frame_layout.addWidget(panel)
self.panel_frame.setLayout(self.panel_frame_layout)
self.bottom_frame = QFrame(self)
self.bottom_frame_layout = QHBoxLayout(self.bottom_frame)
self.bottom_frame_layout.addStretch()
self.sg_button = QToolButton()
self.sg_button.setText('Extra Stuff')
self.bottom_frame_layout.addWidget(self.sg_button)
self.bottom_frame.setLayout(self.bottom_frame_layout)
self.log = QTextEdit()
self.log_frame = QFrame(self)
self.log_frame_layout = QHBoxLayout(self.log_frame)
self.log_frame_layout.addWidget(self.log)
self.log_frame.setLayout(self.log_frame_layout)
self.main_layout.addWidget(self.toolbar_frame)
self.main_layout.addWidget(self.project_frame)
self.main_layout.addWidget(self.panel_frame)
self.main_layout.addWidget(self.bottom_frame)
self.app_widgets = QWidget(self)
self.app_widgets.setLayout(self.main_layout)
self.log_widget = QWidget(self)
self.log_widget.setLayout(self.log_frame_layout)
self.total_layout = QVBoxLayout(self)
self.total_layout.addWidget(self.app_widgets)
self.total_layout.addWidget(self.log_widget)
self.setLayout(self.total_layout)
self.log_button.clicked.connect(self.toggle_log)
def toggle_log(self):
if self.log_widget.isHidden():
self.log_widget.show()
QTimer.singleShot(0, self.resize_show)
else:
self.log_widget.hide()
QTimer.singleShot(0, self.resize_hide)
# self.adjustSize() Also does not work.
def resize_show(self):
self.resize(self.width(), self.sizeHint().height())
def resize_hide(self):
self.resize(self.width(), self.minimumSizeHint().height())
class AppPanel(QWidget):
def __init__(self, sections=[]):
super(AppPanel, self).__init__()
self.setSizePolicy(
QSizePolicy.MinimumExpanding,
QSizePolicy.MinimumExpanding
)
self.layout = QVBoxLayout(self)
self.setLayout(self.layout)
self.sections = sections
for section in self.sections:
self.layout.addWidget(section)
class AppSection(QWidget):
def __init__(self, buttons=[]):
super(AppSection, self).__init__()
self.setSizePolicy(
QSizePolicy.MinimumExpanding,
QSizePolicy.MinimumExpanding
)
self.buttons = buttons
self.layout = QGridLayout()
for i, button in enumerate(self.buttons):
col = i % 2
row = i // 2
self.layout.addWidget(button, row, col)
self.setLayout(self.layout)
class AppButton(QToolButton):
def __init__(self, text=''):
super(AppButton, self).__init__()
self.setText(text)
self.setFocusPolicy(Qt.NoFocus)
self.setIconSize(QSize(50, 50))
self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
if __name__ == '__main__':
app = QApplication(sys.argv)
app_buttons = [AppButton(text='APPS ' + str(i)) for i in range(5)]
custom_btns = [AppButton(text='Custom ' + str(i)) for i in range(5)]
app_section = AppSection(buttons=app_buttons)
custom_section = AppSection(buttons=custom_btns)
panels = [AppPanel(sections=[app_section, custom_section])]
ex = AppWidget(panels=panels)
lw = AppWindow(main_widget=ex)
lw.show()
app.exec_()
Resizing the widget alone is not a valid solution, because it only overrides the geometry set by the layout without notifying the parent widget.
This is also important as you should not resize the widget based on its hint alone when showing the log: if you increase the size of the window while the log is hidden and then show it again, it will not occupy all the available space.
What you need to do is to access the top level window, force its layout to lay out its contents again, and use its hint to for the resize.
def resize_hide(self):
self.window().layout().activate()
self.window().resize(
self.window().width(),
self.window().minimumSizeHint().height()
)
You can set the alignment policy for your top widget:
[...]
self.total_layout.setAlignment(self.app_widgets, Qt.AlignTop)
self.setLayout(self.total_layout)
[...]
The app_widget will not be resized anymore when you hide your text edit.
guys.
Asking for your help to troubleshoot my test script.
I am practicing to make collapsible button with widgets inside.
Script was mainly taken from another question in stackoverflow about collapsible buttons.
So I am trying to put under QTabWidget my class CollpsibleBox(QWidget). Problem is that my CollapsibleBox is acting very weird - buttons are jumping , sometimes it doesn't open/close properly.
I was wondering if it's some mistake in placing correctly my widget under QTabWidget or is there some problem with animation?
import random
from PySide2.QtGui import QPixmap, QBrush, QColor, QIcon, QPainterPath, QPolygonF, QPen, QTransform
from PySide2.QtCore import QSize, Qt, Signal, QPointF, QRect, QPoint, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation
from PySide2.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QFrame, \
QSizePolicy, QGraphicsPixmapItem, QApplication, QRubberBand, QMenu, QMenuBar, QTabWidget, QWidget, QPushButton, \
QSlider, QGraphicsPolygonItem, QToolButton, QScrollArea, QLabel
extraDict = {'buttonSetA': ['test'], 'buttonSetB': ['test']}
tabList = ['Main', 'Extra']
_ui = dict()
class MainWindow(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.create()
def create(self, **kwargs):
_ui['mainLayout'] = QVBoxLayout()
_ui['tabWidget'] = QTabWidget()
_ui['mainLayout'].addWidget(_ui['tabWidget'])
for tab in tabList:
_ui['tab' + tab] = QWidget()
_ui['tabWidget'].addTab(_ui['tab' + tab], tab)
_ui['tabExtra'].layout = QVBoxLayout()
_ui['tabExtra'].setLayout(_ui['tabExtra'].layout)
_ui['content'] = QWidget()
_ui['tabExtra'].layout.addWidget(_ui['content'])
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
box = CollapsibleBox(name)
vlay.addWidget(box)
lay = QVBoxLayout()
for j in range(8):
label = QLabel("{}".format(j))
color = QColor(*[random.randint(0, 255) for _ in range(3)])
label.setStyleSheet(
"background-color: {}; color : white;".format(color.name())
)
label.setAlignment(Qt.AlignCenter)
lay.addWidget(label)
box.setContentLayout(lay)
self.setLayout(_ui['mainLayout'])
class CollapsibleBox(QWidget):
def __init__(self, name):
super(CollapsibleBox, self).__init__()
self.toggle_button = QToolButton(text=name, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QParallelAnimationGroup(self)
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
self.toggle_animation.setDirection(QAbstractAnimation.Forward
if not checked
else QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (self.sizeHint().height() - self.content_area.maximumHeight())
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(500)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(500)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(500, 100, 500, 500)
window.show()
sys.exit(app.exec_())
The problem is that you are only adding two widgets to the full layout, and the layout will try to place them as better as possible (tipically at the center of the area that is available for each widget, based on its size hints).
You could either set the alignment of the widget for the layout (placing the buttons on top of their available space):
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
box = CollapsibleBox(name)
vlay.addWidget(box, alignment=Qt.AlignTop)
Or add a stretch to the bottom of the layout:
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
# ...
vlay.addStretch(1)
which will position all buttons on top of the layout.
As a side note, I'd suggest you to avoid the dictionary logic for the ui, as it might become very confusing and prone to errors. If you really need to do that for some (I hope, very good) reason that's ok, but please avoid it when asking questions: it makes really hard to read your code, and people might end up just ignoring your question at all.
I have this GUI written in python and utilizing PYQT5. I have a functioning version, but I now want to add a QpushButton that executes a method called add_Bladebox. This method adds a doublespinbox to the gui. The method works fine when I call it as part of the code, but when I try to execute it via the QpushButton, nothing happens. I know that without a loop, I could only ever do it once anyway(no position change implemented), but I can add that feature later. So any suggestion as to why it's not working is well appreciated.
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QSizePolicy,
QDoubleSpinBox, QLabel, QCheckBox, QMainWindow,
QGridLayout)
from PyQt5.QtCore import QCoreApplication
import matplotlib
from matplotlib.figure import Figure
import numpy as np
# Mark: QGridlayout importoitu, muttei käytetty
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as F
igureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as
NavigationToolbar
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
# Geometry of main window:
self.setGeometry(200, 200, 1000, 1000)
self.setWindowTitle('Simulation')
# Set canvas:
self.m = PlotCanvas(self, width=5, height=10)
self.m.move(0,0)
# Simulate button:
sim = QPushButton('Simulate', self)
sim.clicked.connect(self.simulate)
sim.resize(sim.sizeHint())
sim.move(550, 700)
# Quit button:
qbtn = QPushButton('Quit', self)
qbtn.clicked.connect(QCoreApplication.instance().quit)
qbtn.resize(qbtn.sizeHint())
qbtn.move(800, 700)
# Input box for x1:
self.x1 = self.inputBox(800,80,10.0,0.01,0.2,2)
self.x1_label = self.label(800,60,"x1")
# Checkbox for sending data via email:
self.send_box = QCheckBox('Send data’, self)
self.send_box.move(550, 600)
self.send_box.toggle()
# Button for adding input boxes
blade_button = QPushButton('Add', self)
# THE PROBLEM IS THE NEXT LINE AS FAR AS I UNDERSTAND
blade_button.clicked.connect(self.add_Bladebox)
self.show()
# Method executes simulation:
def simulate(self):
# TODO Read parameter values. Look explanations of parameters from
simu_func.py
# Method for input box:
def inputBox(self, left, top, maxvalue, step, default,decimals):
box = QDoubleSpinBox(self)
box.move(left,top)
box.setDecimals(decimals)
box.setMaximum(maxvalue)
box.setSingleStep(step)
box.setProperty("value", default)
box.resize(box.sizeHint())
return box
# Method for label:
def label(self, left, top, text):
lbl = QLabel(self)
lbl.setText(text)
lbl.resize(lbl.sizeHint())
lbl.move(left,top)
return lbl
# Method for adding blade boxes
def add_Bladebox(self):
left=900
top=500
maxvalue=3
step=1
default=0
decimals=1
blade_box = self.inputBox(left, top, maxvalue, step, default, decimals)