Issue
I'm working on a desktop application for windows using PyQt and Qt creator.
What I want
I want to display messages to the user only when the user gave an input. I also wanted the message to draw the eye, so I'm going for the following animated solution:
A frame that's hidden when not required (with height = 0 and width = the app's width), 'grows' from the bottom of the app when needed, stays visible for 5-6 seconds, then retracts back to the bottom.
The app kind of looks like this without the message:
And kind of like this when the message IS displayed (note how the bottom gray element is 'covered' by the message):
What I tried
So the way I did this was to create what I called "footer frame", which contains another frame that I call "message frame". The message frame contains a label that will hold, in time, the message for the user. Everything has pre-determined height, so to hide the whole thing I set the message frame to have a maximum height of 0.
So for the 'growing' animation I animated the message frame's maximumHeight property.
The current problem
THING IS - since I wanted the app to be responsive I put everything in layouts... and because of that, whenever the message is displayed, the rest of the components are 'compressed' in height. kind of like this (note how the bottom gray element is not covered by the message, but all the elements' heights shrink a little):
Instead, I wanted the messsage to 'cover' whatever is located under the message's coordinates.
I tried to animate the geometry of the message frame, but nothing really happened - probably because the minimum height is still 0. So I tried to change the minimum height right before the animation begins; But that led to that compression again. Tried to do the same with the footer frame, with the same results.
My question is : What is the best / preferred way of achieving the result I intend with Qt?
Solution
Layout managers always try to show all widgets they're managing. If you want a widget to overlap others, you cannot put it inside a layout, you just create the widget with a parent, and that parent will probably be the widget containing the layout above or the top level window.
This cannot be done in Designer/Creator, as it's assumed that once a layout has been set for a parent widget, all child widgets will be managed by that layout. The only solution is to do this programmatically.
In the following example I'm assuming a QMainWindow is used, so the reference parent widget is actually the central widget, not the QMainWindow: that's because the alert should not cover other widgets that are part of a main window's layout, like the status bar or a bottom placed tool bar or dock).
The animation is actually a QSequentialAnimationGroup that shows the rectangle, waits a few seconds, and hides it again. Since the window could be resized while the animation is running, a helper function is used to properly update the start and end values of the warning and eventually update the geometry when in the "paused" state (which is actually a QPauseAnimation); in order to do so, an event filter is installed on the central widget.
from random import randrange
from PyQt5 import QtCore, QtWidgets, uic
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('overlay.ui', self)
self.alerts = []
self.centralWidget().installEventFilter(self)
self.pushButton.clicked.connect(self.showAlert)
QtCore.QTimer.singleShot(2000, self.showAlert)
def showAlert(self, message=None, timeout=250):
# create an alert that is a child of the central widget
alert = QtWidgets.QLabel(message or 'Some message to the user',
self.centralWidget(), wordWrap=True,
alignment=QtCore.Qt.AlignCenter,
styleSheet='background: rgb({}, {}, {});'.format(
randrange(192, 255), randrange(192, 255), randrange(192, 255)))
self.alerts.append(alert)
alert.animation = QtCore.QSequentialAnimationGroup(alert)
alert.animation.addAnimation(QtCore.QPropertyAnimation(
alert, b'geometry', duration=timeout))
alert.animation.addAnimation(QtCore.QPauseAnimation(3000))
alert.animation.addAnimation(QtCore.QPropertyAnimation(
alert, b'geometry', duration=timeout))
# delete the alert when the animation finishes
def deleteLater():
self.alerts.remove(alert)
alert.deleteLater()
alert.animation.finished.connect(deleteLater)
# update all animations, including the new one; this is not very
# performant, as it also updates all existing alerts; it is
# just done for simplicity;
self.updateAnimations()
# set the start geometry of the alert, show it, and start
# the new animation
alert.setGeometry(alert.animation.animationAt(0).startValue())
alert.show()
alert.animation.start()
def updateAnimations(self):
width = self.centralWidget().width() - 20
y = self.centralWidget().height()
margin = self.fontMetrics().height() * 2
for alert in self.alerts:
height = alert.heightForWidth(width) + margin
startRect = QtCore.QRect(10, y, width, height)
endRect = startRect.translated(0, -height)
alert.animation.animationAt(0).setStartValue(startRect)
alert.animation.animationAt(0).setEndValue(endRect)
alert.animation.animationAt(2).setStartValue(endRect)
alert.animation.animationAt(2).setEndValue(startRect)
def eventFilter(self, obj, event):
if obj == self.centralWidget() and event.type() == event.Resize and self.alerts:
self.updateAnimations()
for alert in self.alerts:
ani = alert.animation
# if the animation is "paused", update the geometry
if isinstance(ani.currentAnimation(), QtCore.QPauseAnimation):
alert.setGeometry(ani.animationAt(0).endValue())
return super().eventFilter(obj, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec())
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.