Home > front end >  Pass in dynamic widget object to QVariantAnimation valueChanged signal handler
Pass in dynamic widget object to QVariantAnimation valueChanged signal handler

Time:10-05

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_())

CodePudding user response:

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_())

CodePudding user response:

Another solution instead of using @eyllanesc's method of using partials. A workaround was to change the scope of the function and put the actual function that sets the stylesheet within the same scope as the parent function.

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

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

    return VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=invalid_animation_handler)

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)
  • Related