PDA

View Full Version : Show/Hide height animation with SetFixedSize



mnunberg
19th September 2010, 23:05
I've been trying for quite a while now to have a window which automatically shrinks to the size of its widgets, and whose child widgets' hide/show operations will be animated.

So far I've managed to get a window which will shrink, using window.layout().setSizeConstraint(QLayout.SetFixed Size) - and then overriding the widget's sizeHint (see the code).

I have been experimenting with different sizePolicies on the widget and different sizeConstraints on the encapsulating layouts -- needless to say, they have each had slightly different (and horrible) results. Here is my code -- in which the animation doesn't work at all:


#!/usr/bin/env python

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys

connect = QObject.connect

app = QApplication(sys.argv)

def print_qsize(sz, title=""):
print title, "W: ", sz.width(), " H: ", sz.height()

class ResizingWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
centralwidget = QWidget(self)
self.setCentralWidget(centralwidget)
layout = QVBoxLayout()
centralwidget.setLayout(layout)

checkbox1 = QCheckBox("Show First Widget", self)
checkbox2 = QCheckBox("Show Second Widget", self)
textedit1 = QPlainTextEdit(self)
textedit2 = QPlainTextEdit(self)

layout.addWidget(checkbox1)
layout.addWidget(textedit1)
layout.addWidget(checkbox2)
layout.addWidget(textedit2)

connect(checkbox1, SIGNAL("toggled(bool)"), lambda b: self.hide_resize(textedit1, b))
connect(checkbox2, SIGNAL("toggled(bool)"), lambda b: self.hide_resize(textedit2, b))

self.modify_widget(textedit1)
self.modify_widget(textedit2)
textedit1.hide()
textedit2.hide()

self.layout().setSizeConstraint(QLayout.SetFixedSi ze)

def modify_widget(self, widget):
#this is needed so that the widget 'inherits' the existing size of the
#layout rather than forcing the layout to expand even further.. don't ask
#me how this works
def sizeHint():
return QSize(1, 1)
widget.sizeHint = sizeHint

def hide_resize(self, widget, show=True):
if show:
#this doesn't work
widget.resize(widget.width(), 1)
widget.show()

#neither does this
widget.resize(widget.width(), 1)

#and neither do these:
a = QPropertyAnimation(widget, "height", widget)
a.setStartValue(1)
a.setEndValue(100)
a.setDuration(250)
a.start()
else:
widget.hide()

mw = ResizingWindow()
mw.show()
app.exec_()

My question is how to make the animation here work? I'm quite sure it involves a magic combination of sizeConstraints and sizePolicy settings, and maybe even a wrapping container widget -- but I haven't found the right combination yet.

Update: Trying to animate the size property rather than just the height yields some results, however I prefer to only dabble with the height, as I would like to leave the width managed by other things - it would be a pain to go through all the layouts and their margins, and then set that width manually by calculating the size of the top-most widget, then subtracting the margins of the layouts

Also, it seems that the animation only works on the widget, but not the window itself (which would be really nice) -- so e.g. the window first expands the full (allowed?) size, and then the widget animates (improperly though)

wysota
20th September 2010, 00:05
Trying with size policies and size constraints is not a good solution. You should just implement your own layout.

mnunberg
20th September 2010, 02:21
I poked around a bit at this idea, but I don't see how it will help me with the animation part, - I've only managed to see that the layout's setGeometry is called whenever a widget is shown/hidden.. I tried to play around with that a bit, (some nasty animation stuff with setGeometry, which didn't work, and didn't seem the right way to do it anyway)
Can you give some pointers?

wysota
20th September 2010, 07:43
If you implement your own layout you are a master of allocating space for all the managed widgets. You can gradually (i.e. using a timer) increase the size hint returned by the layout to make your main widget grow and you can use setGeometry to manage where the child widgets go. You can also hide/show widgets as you see fit. Of course then you won't be able to show()/hide() those widgets directly or it would break your layout - you'd have to add appropriate API for that in your layout class.

norobro
20th September 2010, 17:55
Not sure if this is what you are after or not, but I got the animation to work using the "geometry" property.

I couldn't get it to work using "height" either. Could it be because "height" is constant (int height () const), Wysota?
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys

connect = QObject.connect

app = QApplication(sys.argv)

def print_qsize(sz, title=""):
print title, "W: ", sz.width(), " H: ", sz.height()

class ResizingWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
centralwidget = QWidget(self)
self.setCentralWidget(centralwidget)
layout = QVBoxLayout()


checkbox1 = QCheckBox("Show First Widget", self)
checkbox2 = QCheckBox("Show Second Widget", self)
textedit1 = QPlainTextEdit(self)
textedit2 = QPlainTextEdit(self)

layout.addWidget(checkbox1)
layout.addWidget(textedit1)
layout.addWidget(checkbox2)
layout.addWidget(textedit2)
centralwidget.setLayout(layout)
connect(checkbox1, SIGNAL("toggled(bool)"), lambda b: self.hide_resize(textedit1, b))
connect(checkbox2, SIGNAL("toggled(bool)"), lambda b: self.hide_resize(textedit2, b))

textedit1.hide()
textedit2.hide()

self.layout().setSizeConstraint(QLayout.SetFixedSi ze)

def hide_resize(self, widget, show=True):
if show:
widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwa ysOff) #got some flicker as the widget was growing without this
widget.show()
a = QPropertyAnimation(widget, "geometry", widget)
a.setStartValue(QRect(widget.geometry().x(),widget .geometry().y(),0,0)) #use widget's original (x,y)
a.setEndValue(QRect(widget.geometry().x(),widget.g eometry().y(),256,192))
a.setDuration(250)
a.start()
else:
widget.hide()

mw = ResizingWindow()
mw.show()
app.exec_()
HTH

mnunberg
21st September 2010, 01:52
norobo: Your solution works fine, save for the problem that the resizing of the main window itself is not animated - which is also my goal.

I have managed to come up with my own crappy layout implementation. It *works* and does everything i *want* it to, but there must be a better way

a sample run gives me:

increment is 0.5
increment is 0.5
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124
hides
amount_shrunk is 0
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124
hides
amount_shrunk is 0
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124
hides
amount_shrunk is 0
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124
hides
amount_shrunk is 0
show, going for height 125
got begin 0 end 125 step 0.5
amount_grown is 124

real 0m13.645s
user 0m2.369s
sys 0m1.193s

-- way too much CPU time for this

The updated code is:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys

connect = QObject.connect

app = QApplication(sys.argv)

def copy_size(size):
return QSize(size.width(), size.height())

def xfloat_range(begin,end,step):
print "got begin", begin, "end", end, "step", step
while begin < end:
yield begin
begin += step

def xfloat_range_decrement(begin, end, step):
while begin > end and begin > 0:
yield begin
begin -= step


def print_qsize(sz, title=""):
print title, "W: ", sz.width(), " H: ", sz.height()

class AnimatedLayout(QHBoxLayout):
def __init__(self, child, size, parent = None):
QHBoxLayout.__init__(self, parent)
self._name = ""

child.sizeHint = lambda: QSize()
child.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Ignored)

self.child = child
self.addWidget(child)
self.mandated_size = QSize(size.height(), 0)
self.preferred_size = size

self.setContentsMargins(0,0,0,0)
self.setSpacing(0)

self.duration = 250

self.amount_grown = 0
self.amount_shrunk = 0
self.generator = None

self.timer = QTimer(self)
self.child.setVisible(True)

self.show = self.startShow
self.hide = self.startHide
self.child.show = self.show
self.child.hide = self.hide

print "increment is ", self.increment

@property
def increment(self):
return float(float(self.preferred_size.height())/float(self.duration))

def setGeometry(self, rect):
#print_qsize(rect.size(), self._name)
super(type(self), self).setGeometry(rect)

def sizeHint(self):
return self.mandated_size

def setAnimationDuration(self, msecs):
self.duration = msecs

def timer_fire_show(self):
if not self.generator:
raise Exception("Generator not initialized!")
try:
h = int(self.generator.next())
#print h
if h > self.amount_grown:
self.mandated_size.setHeight(h)
self.amount_grown = h
self.update()
except StopIteration:
#finished:
print "amount_grown is", self.amount_grown
self.timer.stop()
#do any cleanup, just a final check
self.mandated_size = copy_size(self.preferred_size)
self.update()
QObject.disconnect(self.timer, SIGNAL("timeout()"), self.timer_fire_show)
del self.generator

def startShow(self):
print "show, going for height", self.preferred_size.height()
#start an animation here using a timer that will update both our sizeHint
#as well as the widget's size accordingly
self.generator = xfloat_range(0, self.preferred_size.height(), self.increment)
QObject.connect(self.timer, SIGNAL("timeout()"), self.timer_fire_show)
self.amount_grown = 0
self.timer.start(1)

def timer_fire_hide(self):
if not self.generator:
raise Exception("Generator not initialized")
try:
h = int(self.generator.next())
#print h
if h < self.amount_shrunk:
self.mandated_size.setHeight(h)
self.amount_shrunk = h
self.update()
except StopIteration:
print "amount_shrunk is", self.amount_shrunk
self.timer.stop()
self.mandated_size.setHeight(0)
self.update()
QObject.disconnect(self.timer, SIGNAL("timeout()"), self.timer_fire_hide)

def startHide(self):
print "hides"
self.generator = xfloat_range_decrement(self.preferred_size.height( ), 0, self.increment)
QObject.connect(self.timer, SIGNAL("timeout()"), self.timer_fire_hide)
self.amount_shrunk = self.preferred_size.height()
self.timer.start(1)

def setVisible(self, b):
"convenience method"
if b:
self.startShow()
else:
self.startHide()

class ResizingWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.layout().setSizeConstraint(QLayout.SetFixedSi ze)

centralwidget = QWidget(self)
self.setCentralWidget(centralwidget)
layout = QVBoxLayout()


checkbox1 = QCheckBox("Show First Widget", self)
checkbox2 = QCheckBox("Show Second Widget", self)
textedit1 = QPlainTextEdit(self)
textedit2 = QPlainTextEdit(self)

anim_wrapper1 = AnimatedLayout(textedit1, QSize(150, 125))
anim_wrapper1._name = "anim_wrapper1"
self.anim_wrapper1 = anim_wrapper1
anim_wrapper2 = AnimatedLayout(textedit2, QSize(150, 125))
anim_wrapper2._name = "anim_wrapper2"
self.anim_wrapper2 = anim_wrapper2


layout.addWidget(checkbox1)
layout.addLayout(anim_wrapper1)
layout.addWidget(checkbox2)
layout.addLayout(anim_wrapper2)
centralwidget.setLayout(layout)

connect(checkbox1, SIGNAL("toggled(bool)"), anim_wrapper1.setVisible)
connect(checkbox2, SIGNAL("toggled(bool)"), anim_wrapper2.setVisible)
mw = ResizingWindow()
mw.show()
app.exec_()

mnunberg
21st September 2010, 04:40
Well, I've reworked a cleaner example, but this one jitters, and it doesn't seem to work in my actual code -- I get the children widgets in their own windows?:


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys

connect = QObject.connect

app = QApplication(sys.argv)

def print_qsize(sz, title=""):
print title, "W: ", sz.width(), " H: ", sz.height()

class AnimatedLayout(QLayout):
STATE_HIDDEN, STATE_VISIBLE, STATE_ANIMATING = range(3)

def __init__(self, child, size, parent = None):
super(type(self),self).__init__(parent)
self._name = ""

child.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
#self.setSizeConstraint(QLayout.SetFixedSize)

self.child = child
self.mandated_size = QSize(size.width(), 0)
self.preferred_size = size

self.setContentsMargins(0,0,0,0)
self.setSpacing(0)

self.duration = 250
self.child.setVisible(True)

self.show = self.startShow
self.hide = self.startHide
self.child.show = self.show
self.child.hide = self.hide
self.child.setVisible = self.setVisible

self.child_state = self.STATE_VISIBLE

#these two segfaulted if i tried to do anything legit with them?
def count(self):
return 0
def itemAt(self, index):
return None

def setGeometry(self, rect):
if not self.child_state in (self.STATE_ANIMATING, self.STATE_VISIBLE):
return
#super(type(self), self).setGeometry(rect)
self.child.setGeometry(rect)

def sizeHint(self):
return self.mandated_size
#return self.child.sizeHint()

def setAnimationDuration(self, msecs):
self.duration = msecs

def startShow(self):
self._start_animation(True)
def startHide(self):
self._start_animation(False)

def _start_animation(self, show=True):
a = QPropertyAnimation(self.child, "geometry", self)
g = self.child.geometry()
g.setHeight(0)
a.setStartValue(g)
g.setHeight(self.preferred_size.height())
a.setEndValue(g)
a.setEasingCurve(QEasingCurve.OutQuad)
a.setDuration(self.duration)
def valueChanged(qv):
r = qv.toRect()
self.mandated_size.setHeight(r.height())
self.update()
connect(a, SIGNAL("valueChanged(QVariant)"), valueChanged)

if not show:
a.setDirection(a.Backward)
connect(a, SIGNAL("finished()"), lambda: setattr(self, "child_state", self.STATE_HIDDEN))
else:
a.setDirection(a.Forward)
connect(a, SIGNAL("finished()"), lambda: setattr(self, "child_state", self.STATE_VISIBLE))
a.start(a.DeleteWhenStopped)


def setVisible(self, b):
"convenience method"
if b:
self.startShow()
else:
self.startHide()

class ResizingWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.layout().setSizeConstraint(QLayout.SetFixedSi ze)

centralwidget = QWidget(self)
self.setCentralWidget(centralwidget)
layout = QVBoxLayout()


checkbox1 = QCheckBox("Show First Widget", self)
checkbox2 = QCheckBox("Show Second Widget", self)
textedit1 = QPlainTextEdit(self)
textedit2 = QPlainTextEdit(self)
textedit1.setVerticalScrollBarPolicy(Qt.ScrollBarA lwaysOff)
textedit2.setVerticalScrollBarPolicy(Qt.ScrollBarA lwaysOff)

anim_wrapper1 = AnimatedLayout(textedit1, QSize(150, 125))
anim_wrapper1._name = "anim_wrapper1"
self.anim_wrapper1 = anim_wrapper1
anim_wrapper2 = AnimatedLayout(textedit2, QSize(150, 125))
anim_wrapper2._name = "anim_wrapper2"
self.anim_wrapper2 = anim_wrapper2


layout.addWidget(checkbox1)
layout.addLayout(anim_wrapper1)
layout.addWidget(checkbox2)
layout.addLayout(anim_wrapper2)
centralwidget.setLayout(layout)

connect(checkbox1, SIGNAL("toggled(bool)"), anim_wrapper1.setVisible)
connect(checkbox2, SIGNAL("toggled(bool)"), anim_wrapper2.setVisible)
mw = ResizingWindow()
mw.show()
app.exec_()

junky
24th November 2010, 11:11
I have same kind of problem and would like to resize window when i hide a widget from layout(so this will give user dock kind of experience), this can be easily done by setting layout property QLayout::setSizeConstraint(QLayout::SetFixedSize), but then i can't resize window.

so is there any way where i can re size window without loosing dock kind of effect when i hide or show child widget from layout.

Please note that i don't want to implement re size event handling.
thanks.