nathancy
nathancy

Reputation: 46600

Pass in dynamic widget object to QVariantAnimation valueChanged signal handler

I'm trying to make an invalid animation tool. I have a button that when pressed, animates the background color of a field from red to the "normal" field color. This works great but now I want to pass in any arbitrary PyQt widget object (could be QLineEdit, QComboBox, and so on). Here's what my animation handler looks like:

enter image description here

@QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
    field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")

Currently it requires that the field object be already named before calling the function to change the background. I would like to be able to dynamically pass in a widget and set the stylesheet on the fly by passing in a parameter, something like this:

@QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
    widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")

When I try to do this, the widget is able to be passed but the constant color change from WARNING_COLOR to NORMAL_COLOR doesn't work anymore. I also do not want to put this into a class since it has to be on-the-fly. My goal is being able to call a function to start the animation from anywhere instead of having to press the button. The desired goal is like this:

class VariantAnimation(QtCore.QVariantAnimation):
    """VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
    def updateCurrentValue(self, value):
        pass

@QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
    widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")

def invalid_animation(widget):
    return VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=lambda: invalid_animation_handler(widget))

def start_invalid_animation(animation_handler) -> None:
    if animation_handler.state() == QtCore.QAbstractAnimation.Running:
        animation_handler.stop()
    animation_handler.start()

field = QtGui.QLineEdit()
field_animation_handler = invalid_animation(field)

# Goal is to make a generic handler to start the animation
start_invalid_animation(field_animation_handler)

Minimal working example

import sys
from PyQt4 import QtCore, QtGui

class VariantAnimation(QtCore.QVariantAnimation):
    """VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
    def updateCurrentValue(self, value):
        pass

@QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
    field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")

def start_field_invalid_animation() -> None:
    if field_invalid_animation.state() == QtCore.QAbstractAnimation.Running:
        field_invalid_animation.stop()
    field_invalid_animation.start()
    
NORMAL_COLOR = QtGui.QColor(25,35,45)
SUCCESSFUL_COLOR = QtGui.QColor(95,186,125)
WARNING_COLOR = QtGui.QColor(251,188,5)
ERROR_COLOR = QtGui.QColor(247,131,128)
ANIMATION_DURATION = 1500

if __name__== '__main__':
    app = QtGui.QApplication(sys.argv)
    button = QtGui.QPushButton('Animate field background')
    button.clicked.connect(start_field_invalid_animation)
    field = QtGui.QLineEdit()
    field_invalid_animation = VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=invalid_animation_handler)

    mw = QtGui.QMainWindow()
    layout = QtGui.QHBoxLayout()
    layout.addWidget(button)
    layout.addWidget(field)
    window = QtGui.QWidget()
    window.setLayout(layout)
    mw.setCentralWidget(window)
    mw.show()
    sys.exit(app.exec_())

Upvotes: 1

Views: 264

Answers (1)

eyllanesc
eyllanesc

Reputation: 243907

I do not understand exactly why you do not want a class but IMO is the most suitable solution. The logic is to store the callable that allows to change the property and invoke it in updateCurrentValue.

Currently I don't have PyQt4 installed so I implemented the logic with PyQt5 but I don't think it is difficult to change the imports.

import sys
from dataclasses import dataclass
from functools import partial
from typing import Callable

from PyQt5.QtCore import QAbstractAnimation, QObject, QVariant, QVariantAnimation
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QWidget,
)


@dataclass
class VariantAnimation(QVariantAnimation):
    widget: QWidget
    callback: Callable[[QWidget, QVariant], None]
    start_value: QVariant
    end_value: QVariant
    duration: int
    parent: QObject = None

    def __post_init__(self) -> None:
        super().__init__()
        self.setStartValue(self.start_value)
        self.setEndValue(self.end_value)
        self.setDuration(self.duration)
        self.setParent(self.parent)

    def updateCurrentValue(self, value):
        if isinstance(self.widget, QWidget) and callable(self.callback):
            self.callback(self.widget, value)


def invalid_animation_handler(widget: QWidget, color: QColor) -> None:
    widget.setStyleSheet(f"background-color: {QColor(color).name()}")


def start_field_invalid_animation(animation: QAbstractAnimation) -> None:
    if animation.state() == QAbstractAnimation.Running:
        animation.stop()
    animation.start()


NORMAL_COLOR = QColor(25, 35, 45)
SUCCESSFUL_COLOR = QColor(95, 186, 125)
WARNING_COLOR = QColor(251, 188, 5)
ERROR_COLOR = QColor(247, 131, 128)
ANIMATION_DURATION = 1500

if __name__ == "__main__":
    app = QApplication(sys.argv)
    button = QPushButton("Animate field background")

    field = QLineEdit()
    animation = VariantAnimation(
        widget=field,
        callback=invalid_animation_handler,
        start_value=ERROR_COLOR,
        end_value=NORMAL_COLOR,
        duration=ANIMATION_DURATION,
    )

    button.clicked.connect(partial(start_field_invalid_animation, animation))

    mw = QMainWindow()
    layout = QHBoxLayout()
    layout.addWidget(button)
    layout.addWidget(field)
    window = QWidget()
    window.setLayout(layout)
    mw.setCentralWidget(window)
    mw.show()
    sys.exit(app.exec_())

Upvotes: 2

Related Questions