I have an existing application that I am polishing off and I want to add some animation to a few of the widgets. Animating widgets with QPropertyAnimation outside of layouts is easy and fun, however when they are in a layout I am having various difficulties. The current one giving me a headache is that when I animate the size of a widget, the layout does not adjust to it's new size.
So lets say I have a QVBoxLayout with three widgets: a label which should expand to all available space, a treeview, and a button. When I click the button I want the tree to collapse and the label to take over it's space. Below is this example in code, and as you can see while the tree animates it's size nothing happens, and then when I hide it at the end of the animation the label pops to fill the now vacant space. So it seems that during the animation the layout does not "know" the tree is resizing. What I would like to happen is that AS the tree shrinks, the label expands to fill it.
Could this could be done not by absolute sizing of the label, but by calling a resize on the layout or something like that? I ask because I want to animate several widgets across my application and I want to find the best way to do this without having to make too many widgets interdependent upon each other.
Example code:
import sys
from PyQt4 import QtGui, QtCore
class AnimatedWidgets(QtGui.QWidget):
def __init__(self):
super(AnimatedWidgets, self).__init__()
layout1 = QtGui.QVBoxLayout()
self.setLayout(layout1)
expanding_label = QtGui.QLabel("Expanding label!")
expanding_label.setStyleSheet("border: 1px solid red")
layout1.addWidget(expanding_label)
self.file_model = QtGui.QFileSystemModel(self)
sefl.file_model.setRootPath("C:/")
self.browse_tree = QtGui.QTreeView()
self.browse_tree.setModel(self.file_model)
layout1.addWidget(self.browse_tree)
shrink_tree_btn = QtGui.QPushButton("Shrink the tree")
shrink_tree_btn.clicked.connect(self.shrink_tree)
layout1.addWidget(shrink_tree_btn)
#--
self.tree_size_anim = QtCore.QPropertyAnimation(self.browse_tree, "size")
self.tree_size_anim.setDuration(1000)
self.tree_size_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.tree_pos_anim = QtCore.QPropertyAnimation(self.browse_tree, "pos")
self.tree_pos_anim.setDuration(1000)
self.tree_pos_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.tree_anim_out = QtCore.QParallelAnimationGroup()
self.tree_anim_out.addAnimation(self.tree_size_anim)
self.tree_anim_out.addAnimation(self.tree_pos_anim)
def shrink_tree(self):
self.tree_size_anim.setStartValue(self.browse_tree.size())
self.tree_size_anim.setEndValue(QtCore.QSize(self.browse_tree.width(), 0))
tree_rect = self.browse_tree.geometry()
self.tree_pos_anim.setStartValue(tree_rect.topLeft())
self.tree_pos_anim.setEndValue(QtCore.QPoint(tree_rect.left(), tree_rect.bottom()))
self.tree_anim_out.start()
self.tree_anim_out.finished.connect(self.browse_tree.hide)
def main():
app = QtGui.QApplication(sys.argv)
ex = AnimatedWidgets()
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The layouts handle the geometry() of the widgets so that when wanting to change the pos property these are interfacing with their handles so it is very common that you get that type of behavior, a better option is to use a QVariantAnimation to establish a fixed height:
import sys
from PyQt4 import QtGui, QtCore
class AnimatedWidgets(QtGui.QWidget):
def __init__(self):
super(AnimatedWidgets, self).__init__()
layout1 = QtGui.QVBoxLayout(self)
expanding_label = QtGui.QLabel("Expanding label!")
expanding_label.setStyleSheet("border: 1px solid red")
layout1.addWidget(expanding_label)
self.file_model = QtGui.QFileSystemModel(self)
self.file_model.setRootPath(QtCore.QDir.rootPath())
self.browse_tree = QtGui.QTreeView()
self.browse_tree.setModel(self.file_model)
layout1.addWidget(self.browse_tree)
shrink_tree_btn = QtGui.QPushButton("Shrink the tree")
shrink_tree_btn.clicked.connect(self.shrink_tree)
layout1.addWidget(shrink_tree_btn)
#--
self.tree_anim = QtCore.QVariantAnimation(self)
self.tree_anim.setDuration(1000)
self.tree_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
def shrink_tree(self):
self.tree_anim.setStartValue(self.browse_tree.height())
self.tree_anim.setEndValue(0)
self.tree_anim.valueChanged.connect(self.on_valueChanged)
self.tree_anim.start()
def on_valueChanged(self, val):
h, isValid = val.toInt()
if isValid:
self.browse_tree.setFixedHeight(h)
def main():
app = QtGui.QApplication(sys.argv)
ex = AnimatedWidgets()
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Related
can you help me please with set of possiotion? I have a problem, because the labels don't move :(
Detail in screen.
label_result.move (X, Y) doesnť work.
from datetime import datetime as datetime, timedelta as timedelta
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
# Hlavní okno
main = QtWidgets.QWidget()
main.setWindowTitle('Set tag')
main.setGeometry(60, 60, 300, 600)
# Layout 1
layout = QtWidgets.QHBoxLayout()
main.setLayout(layout)
# Nápis
label = QtWidgets.QLabel('Číslo sezení')
layout.addWidget(label)
#Input
input_session = QtWidgets.QLineEdit()
input_session.move(0,0)
layout.addWidget(input_session)
# Tlačítko
button = QtWidgets.QPushButton('Zaznamenat tag')
layout.addWidget(button)
#Sesion Result
label_result = QtWidgets.QLabel('Výsledky \n')
label_result.move (20, 20)
layout.addWidget(label_result)
button.clicked.connect(lambda: test(input_session.text()))
# Spuštění
main.show()
app.exec()
Thanks a lot.
Using layout managers means that all positioning and sizing of widgets is left to it, so any attempt to manually set the geometry of widget is ignored everytime the widget on which the layout is set is resized, which is what happens at least once as soon as the widget is mapped (shown) the first time.
If you want to position widgets in a different layouts, then you need to understand how layout(s) could be structured in order to achieve the desired result. In your case, it could be done in at least 3 slightly different ways:
using a main vertical boxed layout, then adding a horizontal layout for the first three widgets, another one for the last label, and finally a stretch (which is an empty layout item that tries to expand itself up to the maximum available extent);
again a main vertical layout, then a grid layout and again a stretch;
a grid layout with an expanding spacer at the bottom;
I'll show a possible implementation of the first, leaving the others up to you. Note that I will use a subclass of QWidget, which gives a better code/object structure and allows better implementation.
from PyQt5 import QtWidgets
class MyWidget(QtWidgets.QWidget):
def __init__():
super().__init__()
self.setWindowTitle('Set tag')
mainLayout = QtWidgets.QVBoxLayout(self)
topLayout = QtWidgets.QHBoxLayout()
mainLayout.addLayout(topLayout)
label = QtWidgets.QLabel('Číslo sezení')
topLayout.addWidget(label)
input_session = QtWidgets.QLineEdit()
topLayout.addWidget(input_session)
button = QtWidgets.QPushButton('Zaznamenat tag')
topLayout.addWidget(button)
midLayout = QtWidgets.QHBoxLayout()
mainLayout.addLayout(midLayout)
label_result = QtWidgets.QLabel('Výsledky')
midLayout.addWidget(label_result)
mainLayout.addStretch()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
myWidget = MyWidget()
myWidget.show()
sys.exit(app.exec_())
I’m trying to make a GUI and I have a trouble with widget positioning. I would like to tie position of one widget (A) to the position of another (B) that is added in GridLayout. So, main idea: A.pos() = B.pos() + DELTA.
In detail:
Let us have such QWidget with QGridLayout containing four Qlabels:
import sys
from PySide2.QtWidgets import*
from PySide2.QtGui import*
from PySide2.QtCore import*
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
w.resize(500, 300)
lab1 = QLabel("label1")
lab2 = QLabel("label2")
lab3 = QLabel("label3")
lab4 = QLabel("label4")
lay = QGridLayout()
lay.addWidget(lab1,0,0)
lay.addWidget(lab2,0,1)
lay.addWidget(lab3,1,0)
lay.addWidget(lab4,1,1)
w.setLayout(lay)
w.show()
sys.exit(app.exec_())
Screen of GUI is here
So, I would like to create QLineEdit that will be placed right above lab4 and trying to realize something like:
le = QLineEdit(w)
le.setGeometry(QRect(lab4.pos().x(),lab4.pos().y()+10,20,20))
But if make print(lab4.pos().x(), lab4.pos().y()) it will print (0,0)... and my GUI will look like this
But I want it to look like this
Please, help me set my widget correctly. As you have noticed, I write using PyQT, but if you answer me using C++ QT I would be grateful too!
If, as I suspect, the intention is to place a QLineEdit over the top-left corner of one of the grid elements then you can take advantage of the fact that...
QGridLayout allows multiple items to be added to the same cell.
It also allows you to add a QLayout to a cell.
Firstly, create a class that inherits from QGridLayout and really does nothing more than add a QLineEdit to cell(0, 0) and a stretch to the right and below the QLineEdit...
class line_edit_overlay(QGridLayout):
def __init__(self):
super(line_edit_overlay, self).__init__()
self.line_edit = QLineEdit()
self.addWidget(self.line_edit, 0, 0);
self.setRowStretch(1, 1);
self.setColumnStretch(1, 1);
Now your code would be something like...
import sys
from PySide2.QtWidgets import*
from PySide2.QtGui import*
from PySide2.QtCore import*
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
w.resize(500, 300)
lab1 = QLabel("label1")
lab2 = QLabel("label2")
lab3 = QLabel("label3")
lab4 = QLabel("label4")
lay = QGridLayout()
lay.addWidget(lab1,0,0)
lay.addWidget(lab2,0,1)
lay.addWidget(lab3,1,0)
lay.addWidget(lab4,1,1)
w.setLayout(lay)
# Added the following. Note the calls to lay.setRowStretch(...)
# and lay.setColumnStretch(...) which seem to be required to
# maintain even grid sizing.
for row in range(0, lay.rowCount()):
lay.setRowStretch(row, 1)
for col in range(0, lay.columnCount()):
lay.setColumnStretch(col, 1)
lay.addLayout(line_edit_overlay(), 1, 1)
w.show()
sys.exit(app.exec_())
The above results in the following...
Using Qt5 I am trying to make a widget work using absolute positioning. The code below is a minimum working example of something I am trying to do. A quick walk through of the code:
CentralWidget is the central widget of the main window and holds MyWidget using absolute positioning, e.g. without using any layouts.
MyWidget does not set its child widgets immediately but provides a SetData method which first removes all current child widgets and then adds the new child widgets to its layout.
SetData is triggered using a timer in the main window.
I commented out two lines of code. The first "enables" relative positioning using layouts by adding a layout to CentralWidget. This line shows what I am trying to achieve but with absolute positioning. The second comment enables some debug information:
MyWidget
layout.count: 3
size: PyQt5.QtCore.QSize(-1, -1)
sizeHint: PyQt5.QtCore.QSize(200, 100)
CentralWidget
size: PyQt5.QtCore.QSize(18, 18)
sizeHint: PyQt5.QtCore.QSize(18, 18)
What I am doing wrong in order for MyWidget to be visible using absolute positioning?
Code:
from PyQt5 import QtCore, QtWidgets
import sys
class MyWidget(QtWidgets.QWidget):
z = 0
def __init__(self, parent):
super().__init__(parent)
self._layout = QtWidgets.QVBoxLayout(self)
def SetData(self):
while self._layout.count() > 0:
widget = self._layout.takeAt(0).widget()
widget.hide()
widget.deleteLater()
for i in range(3):
self._layout.addWidget(QtWidgets.QLabel(str(MyWidget.z * 10 + i)))
MyWidget.z += 1
class CentralWidget(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self._myWidget = MyWidget(self)
# QtWidgets.QHBoxLayout(self).addWidget(self._myWidget)
def SetData(self):
self._myWidget.SetData()
# print("MyWidget\n layout.count: {}\n size: {}\n sizeHint: {}\n\nCentralWidget\n size: {}\n sizeHint: {}\n\n".format(self._myWidget.layout().count(), self.sizeHint(), self.size(), self._myWidget.sizeHint(), self._myWidget.size()))
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
centralWidget = CentralWidget(self)
self.setCentralWidget(centralWidget)
self._timer = QtCore.QTimer(self)
self._timer.timeout.connect(centralWidget.SetData)
self._timer.start(500)
def main():
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec_()
if __name__ == "__main__":
main()
The reason for this behavior is directly related to the fact that the widget is not added to a layout and its contents are added after being shown.
In fact, if you call centralWidget.SetData() upon initialization and before mainWindow.show(), it will work as expected.
A lot of things happen when you add a child widget to a layout, and this usually involves multiple calls to the children size hints, allowing the parent to adapt its own size hint, and, after that, adapt its size and that of its children.
If that "container widget" is itself contained in another layout, that widget will be automatically resized (based on its hint) in the next cycle of events, but this doesn't happen in your case, since yours is a "free" widget.
The function you are looking for is QWidget.adjustSize(), but, for the aforementioned reasons, you cannot call it immediately after adding the children widgets.
To overcome your issue, you can call QApplication.processEvents() before adjustSize(), or, eventually, use a 0-based single shot QTimer:
def SetData(self):
while self._layout.count() > 0:
widget = self._layout.takeAt(0).widget()
widget.hide()
widget.deleteLater()
for i in range(3):
self._layout.addWidget(QtWidgets.QLabel(str(MyWidget.z * 10 + i)))
MyWidget.z += 1
QtCore.QTimer.singleShot(0, self.adjustSize)
EDIT: There are a number of similar posts on PyQt4 progress bars not updating. They all focus on the issue of threads & where the program actually updates the window. Although helpful, my code was so structured that the replies were not practical. The accepted answer given here is simple, to the point & works.
I am using Python 2.7 and PyQT 4 on a Win 7 x64 machine.
I am trying to clear my window of one widget, an 'Accept' button, see code, and replace it with a progress bar.
Even though I close the 'Accept' button & add the progress bar before the processing loop is entered into. The window is only updated after the loop has finished & the progress bar jumps straight to 100%.
My code,
from PyQt4 import QtCore, QtGui
import sys
import time
class CentralWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(CentralWidget, self).__init__(parent)
# set layouts
self.layout = QtGui.QVBoxLayout(self)
# Poly names
self.pNames = QtGui.QLabel("Import file name", self)
self.polyNameInput = QtGui.QLineEdit(self)
# Polytype selection
self.polyTypeName = QtGui.QLabel("Particle type", self)
polyType = QtGui.QComboBox(self)
polyType.addItem("")
polyType.addItem("Random polyhedra")
polyType.addItem("Spheres")
polyType.addItem("Waterman polyhedra")
polyType.activated[str].connect(self.onActivated)
# Place widgets in layout
self.layout.addWidget(self.pNames)
self.layout.addWidget(self.polyNameInput)
self.layout.addWidget(self.polyTypeName)
self.layout.addWidget(polyType)
self.layout.addStretch()
# Combobox choice
def onActivated(self, text):
if text=="Random polyhedra":
self.randomPolyhedra(text)
if text=="Spheres": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
if text=="Waterman polyhedra": # not implementaed yet
self.polyTypeName.setText("Not implemented yet.")
self.polyTypeName.adjustSize()
# New options for random polyhedra choice
def randomPolyhedra(self, text):
self.polyNumberLbl = QtGui.QLabel("How many: ", self)
self.polyNumber = QtGui.QLineEdit(self)
self.acceptSeed = QtGui.QPushButton('Accept') # Accept button created
self.acceptSeed.clicked.connect(lambda: self.ranPolyGen())
self.layout.addWidget(self.polyNumberLbl)
self.layout.addWidget(self.polyNumber)
self.layout.addWidget(self.acceptSeed) # Accept button in layout
self.randFlag = True
self.polyTypeName.setText(text)
self.polyTypeName.adjustSize()
# Act on option choices for random polyhedra
def ranPolyGen(self):
polyCount = int(self.polyNumber.text())
self.progressBar = QtGui.QProgressBar() # Progress bar created
self.progressBar.setMinimum(1)
self.progressBar.setMaximum(polyCount)
self.acceptSeed.close() # Accept button closed
self.layout.addWidget(self.progressBar) # Add progressbar to layout
for poly in range(1, polyCount+1):
time.sleep(1) # Calls to main polyhedral generating code go here
print poly
self.progressBar.setValue(poly)
self.doneLbl = QtGui.QLabel("Done", self)
self.layout.addWidget(self.doneLbl)
# Creates GUI
class Polyhedra(QtGui.QMainWindow):
def __init__(self):
super(Polyhedra, self).__init__()
# Place central widget in layout
self.central_widget = CentralWidget(self)
self.setCentralWidget(self.central_widget)
# Set up window
self.setGeometry(500, 500, 300, 300)
self.setWindowTitle('Pyticle')
self.show()
# Combo box
def onActivated(self, text):
self.central_widget.onActivated(text)
def main():
app = QtGui.QApplication(sys.argv)
poly = Polyhedra()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Below is a picture of during loop execution & after completion.
I dont think I have got my head around the addWidget() method. I was under the impression that this would add another widget to the present layout (a vbox layout here) & that the .close() method removed a widget when directed to do so.
What am I missing?
You can add:
from PyQt4.QtGui import QApplication
Then in your for loop:
QApplication.processEvents()
Your app is actually becoming unresponsive, you need to call processEvents() to process the events and redraw the gui. I am not overly familiar with pyqt but I imagine another alternative is using a thread.
I have realized a python simple application, without any animation on it.
Now I want to add a simple animation, triggered by a signal (a button click for example), which on trigger enlarges the width of the windows and shows a new text area with some text in it.
Honestly, I am quite new to python/pyqt4, and I do not know much about the animation framework.
I tried to add this to my class code, for example in a method called clicking on the about menu :) :
self.anim = QPropertyAnimation(self, "size")
self.anim.setDuration(2500)
self.anim.setStartValue(QSize(self.width(), self.height()))
self.anim.setEndValue(QSize(self.width()+100, self.height()))
self.anim.start()
and this enlarge my window as I want.
Unfortunately I have no idea how to insert a new text area, avoiding the widgets already present to fill the new space (actually, when the window enlarge, the widgets use
all the spaces, thus enlarging themselves)
Could someone help me knowing how to add the text area appearance animation?
Any help is appreciated...really...
One way to achieve this is to animate the maximumWidth property on both the window and the text-edit.
The main difficulty is doing it in a way that plays nicely with standard layouts whilst also allowing resizing of the window. Avoiding flicker during the animation is also quite tricky.
The following demo is almost there (the animation is slightly jerky at the beginning and end):
from PyQt4 import QtGui, QtCore
class Window(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self._offset = 200
self._closed = False
self._maxwidth = self.maximumWidth()
self.widget = QtGui.QWidget(self)
self.listbox = QtGui.QListWidget(self.widget)
self.button = QtGui.QPushButton('Slide', self.widget)
self.button.clicked.connect(self.handleButton)
self.editor = QtGui.QTextEdit(self)
self.editor.setMaximumWidth(self._offset)
vbox = QtGui.QVBoxLayout(self.widget)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addWidget(self.listbox)
vbox.addWidget(self.button)
layout = QtGui.QHBoxLayout(self)
layout.addWidget(self.widget)
layout.addWidget(self.editor)
layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
self.animator = QtCore.QParallelAnimationGroup(self)
for item in (self, self.editor):
animation = QtCore.QPropertyAnimation(item, 'maximumWidth')
animation.setDuration(800)
animation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
self.animator.addAnimation(animation)
self.animator.finished.connect(self.handleFinished)
def handleButton(self):
for index in range(self.animator.animationCount()):
animation = self.animator.animationAt(index)
width = animation.targetObject().width()
animation.setStartValue(width)
if self._closed:
self.editor.show()
animation.setEndValue(width + self._offset)
else:
animation.setEndValue(width - self._offset)
self._closed = not self._closed
self.widget.setMinimumSize(self.widget.size())
self.layout().setSizeConstraint(QtGui.QLayout.SetFixedSize)
self.animator.start()
def handleFinished(self):
if self._closed:
self.editor.hide()
self.layout().setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
self.widget.setMinimumSize(0, 0)
self.setMaximumWidth(self._maxwidth)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.move(500, 300)
window.show()
sys.exit(app.exec_())