Animating QPushButton PyQt - animation

I'm having an issue trying to animate a QPushButtons. I've a queue of buttons that once popped, I would like to animate from one color to another. I've recreated a minimum working example below:
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QHBoxLayout, QPushButton, QApplication, QWidget
from main import comboBox
app = QApplication([])
app.setStyle('Fusion')
window = QWidget()
def change_color():
anim = QtCore.QPropertyAnimation(changeButton, b"color")
anim.setDuration(1000)
anim.setStartValue(QtGui.QColor(255, 144, 0))
anim.setEndValue(QtGui.QColor(255, 255, 255))
anim.start()
hbox = QHBoxLayout()
hbox.addStretch(1)
changeButton = QPushButton()
changeButton.setText("change Grid")
hbox.addWidget((changeButton))
window.setLayout((hbox))
window.show()
app.exec()
changeButton.clicked.connect(lambda event: change_color())
When I go into the debug mode it shows that it's reaching each of the lines, however, it there is no color change happening. Am I doing something wrong here?

You have two problems.
The first is that you're connecting the signal after the app.exec() call. That call starts the event loop, and, as such, it's blocking (nothing after that line will be processed until it returns), so you must move that before starting the event loop.
Then, as the QPropertyAnimation documentation explains:
QPropertyAnimation interpolates over Qt properties.
color is not a Qt property for QPushButton (nor any of its base classes), in fact, if you look at the debug/terminal output for your program (after moving the signal connection as said above), you'll see the following:
StdErr: QPropertyAnimation: you're trying to animate a non-existing property color of your QObject
A simple solution would be to use QVariantAnimation instead, and update the color in a function connected to the animation's valueChanged signal.
def change_color():
def updateColor(color):
# ... change the color
anim = QtCore.QVariantAnimation(changeButton)
anim.setDuration(1000)
anim.setStartValue(QtGui.QColor(255, 144, 0))
anim.setEndValue(QtGui.QColor(255, 255, 255))
anim.valueChanged.connect(updateColor)
anim.start()
# ...
changeButton.clicked.connect(change_color)
The choice becomes the way you change color: if you're not using stylesheets for the button, the more appropriate way would be to use the button palette:
def change_color(button):
def updateColor(color):
palette.setColor(role, color)
button.setPalette(palette)
palette = button.palette()
role = button.foregroundRole()
anim = QtCore.QVariantAnimation(button)
anim.setDuration(1000)
anim.setStartValue(QtGui.QColor(255, 144, 0))
anim.setEndValue(QtGui.QColor(255, 255, 255))
anim.valueChanged.connect(updateColor)
anim.start(anim.DeleteWhenStopped)
# ...
changeButton.clicked.connect(lambda: change_color(changeButton))
Note that I added the button argument (you should not use globals anyway), and also the DeleteWhenStopped flag to start() in order to avoid unnecessary stacking of unused animations when they're finished.
If, instead, you're using stylesheets, you need to override the button's stylesheet. Obviously, you have to be careful: the button shouldn't have any stylesheet set directly on it.
def change_color(button):
def updateColor(color):
button.setStyleSheet('color: {}'.format(color.name()))
# ...
Note that a more appropriate approach would be to subclass the button and implement the animation directly on it. By doing that you can use a QPropertyAnimation by using a custom pyqtProperty, but you will still need to manually set the color as above in the property setter. In this case, you can just have a single animation (possibly creating it in the __init__) and just update its start/end values.
Alternatively, you can just call update in the setter, and override the paintEvent() by using QStyle functions to draw the button primitive and label using the property value.

Related

How can I remove the border on a Dialog Window with Dynamic Layout controls?

I have a WIN32 application that uses a main dialog window as background and several alternative dialogs that can appear in front of the main window. These overlay dialogs should not have any border because they need to appear to be part of the main window.
Everything was working well until I activated Dynamic Layout on the controls in an overlay dialog. It then acquired a thin border with drop shadow, a thin top bar that was sometimes windows top bar color and sometimes white, and the dialog became independently resizable. I don't want this, I want the overlay dialog to resize only with the main dialog window.
How can I force the dialog to have No Border?
You can modify the style of a dialog window in your override of the OnInitDialog() member function, if that window is created using a custom class derived from CDialog or CDialogEx, or something similar. (If not, you'll need to somehow 'intercept' the window's creation process.)
Assuming you have overridden OnInitDialog(), the process will be along these lines:
BOOL MyDialog::OnInitDialog()
{
BOOL answer = CDialog::OnInitDialog(); // Call base class stuff
LONG_PTR wStyle = GetWindowLongPtr(m_hWnd, GWL_STYLE); // Get the current style
wStyle &= ~WS_BORDER; // Here, we mask out the style bit(s) we want to remove
SetWindowLongPtr(m_hWnd, GWL_STYLE, wStyle); // And set the new style
// ... other code as required ...
return answer;
}
Note: It is important to call the base class OnInitDialog() before you attempt to modify the window's style; otherwise, the window may not be in a 'completed' state, and any changes you make may be reverted.
As mentioned in the comment by IInspectable, it may be possible (or even better) to modify the style (taking out the WS_BORDER attribute) in an override of the PreCreateWindow() function:
BOOL MyDialog::PreCreateWindow(CREATESTRUCT &cs)
{
if (!CDialog::PreCreateWindow(cs)) return FALSE;
cs.style &= ~WS_BORDER;
return TRUE;
}
Again, as shown here, you should call the base class member before modifying the style.
So the answer to my original question is to put the following code in the overloaded OnInitDialog() after the call to the base class.
LONG_PTR wStyle = GetWindowLongPtr(m_hWnd, GWL_STYLE); // Get the current style
wStyle &= ~WS_SIZEBOX; // Mask out the style bit(s) we don't want
SetWindowLongPtr(m_hWnd, GWL_STYLE, wStyle); // And set the new style

QComboBox popup animation glitch

When using a QComboBox in PySide2 the popup menu seems to initially start about 10 pixels or so to the left until its finished animating down at which point it pops (about) 10 pixels to the right into the correct position.
How can I fix this? Or am I able to disable the animation so the menu just opens without animating? And am I able to control the animation time for the popup?
Here are two screenshots, the top one is while the combobox dropdown is animating down and the bottom one is after the dropdown is open:
Here's the simple example code use to produce the combobox above:
from PySide2 import QtCore, QtWidgets
import sys
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
self.setWindowTitle('Modal Dialogs')
self.setMinimumSize(300,80)
# remove help icon (question mark) from window
self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
# create widgets, layouts and connections (signals and slots)
self.create_widgets()
self.create_layouts()
self.create_connections()
def create_widgets(self):
self.combo = QtWidgets.QComboBox()
self.combo.addItems(['one','two','three'])
def create_layouts(self):
# self must be passed to the main_layout so it is parented to the dialog instance
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(self.combo)
def create_connections(self):
pass
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
my_dialog = MyDialog()
my_dialog.show() # Show the UI
sys.exit(app.exec_())

Play image sequence using Qt QMainWindow

I have an image sequence rendered out. which I want to payback in a simple QMainWindow or QDialog. This is what I have sofar. It loads the images into the qlabel, but I cant see the label being updated, its just show the last loaded image, and nothing in between.
Maybe someone knows something?
from PySide import QtCore, QtGui
import shiboken
import maya.OpenMayaUI as apiUI
import time
def getMayaWindow():
"""
Get the main Maya window as a QtGui.QMainWindow instance
#return: QtGui.QMainWindow instance of the top level Maya windows
"""
ptr = apiUI.MQtUtil.mainWindow()
if ptr is not None:
return shiboken.wrapInstance(long(ptr), QtGui.QWidget)
class Viewer(QtGui.QMainWindow):
def __init__(self, parent = getMayaWindow()):
super(Viewer, self).__init__(parent)
self.setGeometry(400, 600, 400, 300)
self.setUi()
def setUi(self):
self.label = QtGui.QLabel()
self.setCentralWidget(self.label)
def showUi(self):
self.show()
def loadImage(self, path):
self.label.clear()
image = QtGui.QImage(path)
pp = QtGui.QPixmap.fromImage(image)
self.label.setPixmap(pp.scaled(
self.label.size(),
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
x = Viewer()
x.showUi()
for i in range(1, 11):
x.loadImage("C://anim%03d.png" % i)
time.sleep(0.5)
You change pixmaps in loop and sleep (stop) all GUI thread, that's why your GUI freeze.
http://www.tutorialspoint.com/python/time_sleep.htm
It is not correct. qLabel.repaint() it is bad solution because it still blocks GUI. Of course you can use processEvents but it is bad approach too.
You should use QTimer for this purpose, use timeout() signal, create slot and change pixmaps in this slot. In this case your GUI will not be blocked because QTimer works asynchronously and images will be successfuly changed.
Same code with loop and sleep can help you only when this code will execute in another thread (multi threading) but it is not necessary because there is special class QTimer.

How to handle sprite animation in Kivy

I am coding a game using Kivy. I have a Screen class where I put my animation code. It's not a usual game, it's more like several screens, each with its own animation, with button commands for going back and forth to different screens.
It works ok, but when I make more classes like this and put it all in a ScreenManager, the animation is disrupted with random white screens.
class Pas(Screen):
def __init__(self, **kwargs):
super(Pas, self).__init__(**kwargs)
Clock.schedule_interval(self.update, 1 / 60.0)
self.ani_speed_init = 15
self.ani_speed = self.ani_speed_init
self.ani = glob.glob("img/pas_ani*.png")
self.ani.sort()
self.ani_pos = 0
self.ani_max = len(self.ani)-1
self.img = self.ani[0]
self.update(1)
back = Button(
background_normal=('img/back-icon.png'),
background_down=('img/back-icon.png'),
pos=(380, 420))
self.add_widget(back)
def callback(instance):
sm.current = 'game'
back.bind(on_press=callback)
def update(self, dt):
self.ani_speed -= 1
if self.ani_speed == 0:
self.img = self.ani[self.ani_pos]
self.ani_speed = self.ani_speed_init
if self.ani_pos == self.ani_max:
self.ani_pos = 0
else:
self.ani_pos += 1
with self.canvas:
image = Image(source=self.img, pos=(0, 0), size=(320, 480))
What am I doing wrong? I am also accepting ideas for a different way of doing this.
If you want to use Screen and ScreenManager for your screens, it would be better to use the transition system they define and use, so, to define your own Transitions, and apply them. If you want more control, i would advise getting ride of Screen and ScreenManager, and just using Widgets, to control the whole drawing/positioning process.
Also, Clock.schedule_interval(self.update, 0) is equivalent to the call you are making, the animation will be called each frame, and you can use dt to manage the animation progress.
Also, kivy can manage gifs, as well as zip archives of images to directly do animations (useful to have animated pngs), you can let kivy manage the whole animation process this way.

qwidget.render drawing with offset in QStyledItemDelegate

I'm trying to create a delegate to draw custom widgets as elements in a listview on icon mode. I have it more or less working but I can't get the widgets to draw in the right place, it seems they are getting drawn considering (0,0) the origin on the main window not the origin of the list view. What do I need to pass to render the widget on the right place? I know I can pass an offset... how can I calculate the offset between the main window and the listview?
This is my paint method on my delegate (derived from QStyledItemDelegate)
def paint(self, painter, option, index):
painter.save()
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight());
model = index.model()
myWidget = model.listdata[index.row()]
myWidget.setGeometry(option.rect)
myWidget.render(painter, option.rect.topLeft() )
painter.restore()
Thanks
/J
In case this is useful for someone else I'll post my solution...
I don't know if this is the best way of doing it, but I'm calculating the offset by mapping the orgin of my parent to the main window:
offset = self._parent.mapTo(self._mainWindow, QPoint(0,0))
myWidget.render(painter, option.rect.topLeft() + offset)
It works, so I'll use it until I find a better way for doing this.
You can render your Widget into a temporary pixmap and then draw the pixmap instead. That solves the shift issue:
def paint(self, painter, option, index):
pic = QPixmap( option.rect.width(), option.rect.height() )
w = ItemWidget()
w.setGeometry( option.rect )
w.render(pic)
painter.drawPixmap( option.rect, pic )
I use another alternative method, and it works.
painter.translate(option.rect.topLeft())
myWidget.render(painter, QtCore.QPoint(0, 0))

Resources