0

I am building a GUI on python and pyqt. The GUI has a lot of pushbuttons, generated through class LED, meaning each led has 3 buttons, for an n number of leds.

In a few of the buttons, I want an effect that changes the opacity of the pushbutton, in a loop from 0 to 1 and back again, so it disappears and appears. I need only one process to manage all, so the effect starts at same time for every button and all blink at the same time.

I've managed to achieve that, through qgraphicseffect in a thread, iterating through a list. The problem is that after a few minutes, the effect stops, although the thread is still running (print(opacity_level)). more pushbuttons with the effect makes even shorter duration. Clicking any button, even others without effect, restarts the gui animation.

My small research in threading on pyqt made me implement this thread manager, although I do not fully understand it.

class WorkerSignals(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    error = QtCore.pyqtSignal(tuple)
    result = QtCore.pyqtSignal(object)
    progress = QtCore.pyqtSignal(tuple)

class Worker(QtCore.QRunnable):
    '''
    Worker thread
    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
    '''
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        # Add the callback to our kwargs
        self.kwargs['progress_callback'] = self.signals.progress

    @pyqtSlot()
    def run(self):
        '''
        Initialise the runner function with passed args, kwargs.
        '''
        # Retrieve args/kwargs here; and fire processing using them
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done

Next the leds class

class LEDs:
    def __init__(self,name,group,frame):
        self.opacity_effect = QtWidgets.QGraphicsOpacityEffect()
        self.button_auto = QtWidgets.QPushButton()
        self.button_auto.setObjectName("button_auto_neutral")
        self.button_auto.clicked.connect(lambda state, x=self: self.AutoMode())
    
    def AutoMode(self):
        print(self.name,"Automode")
        if len(settings.blink) ==0: # start thread only if no previous thread, both thread and 
                                        this reference the size of settings.blink, so not ideal.
            print("start thread")
            settings.ledAutomode()

        settings.blink.append(self)

And finally the settings class, which has the thread with the effect performing action. There is a second thread, which handles the icon of the button, accordingly with a timetable.

class Settings:
    def __init__(self):
        self.blink=[]

    def ledAutomode(self):

        def blink(progress_callback):
            print("opacity")
            op_up=[x/100 for x in range(0,101,5)]
            op_down=op_up[::-1]; op_down=op_down[1:-1]; opacity=op_up+op_down

            while len(self.blink) !=0:
                for i in opacity:
                    print(i)
                    QtCore.QThread.msleep(80)
                    for led in self.blink:
                        led.opacity_effect.setOpacity(i)

        def timeCheck(progress_callback):
            while len(self.blink) != 0:
                QtCore.QThread.msleep(500)
                for led in self.blink:
                    matrix = [v for v in settings.leds_config[led.group][led.name]["Timetable"]]
                    matrix_time=[]

                    ...
                    # some code
                    ...

                    if sum(led_on_time):
                        led.button_auto.setObjectName("button_auto_on")
                        led.button_auto.setStyleSheet(ex.stylesheet)

                    else:
                        led.button_auto.setObjectName("button_auto_off")
                        led.button_auto.setStyleSheet(ex.stylesheet)
                    QtCore.QThread.msleep(int(30000/len(self.blink)))


        worker = Worker(blink)  # Any other args, kwargs are passed to the run function
        ex.threadpool.start(worker)
        worker2 = Worker(timeCheck)  # Any other args, kwargs are passed to the run function
        ex.threadpool.start(worker2)

So, perhaps a limitation on qgraphicseffect, or some problem with the thread (although its keeps printing), or I made some error.

I've read about subclassing the qgraphicseffect but I don't know if that solves the problem. If anyone has another implementation, always eager to learn.

Grateful for your time.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Atma
  • 3
  • 1

1 Answers1

0

Widgets are not thread-safe.
They cannot be created nor accessed from external threads. While it "sometimes" works, doing it is wrong and usually leads to unexpected behavior, drawing artifacts and even fatal crash.

That said, you're making the whole process incredibly and unnecessarily convoluted, much more than it should be, most importantly because Qt already provides both timed events (QTimer) and animations.

class FadeButton(QtWidgets.QPushButton):
    def __init__(self):
        super().__init__()
        self.effect = QtWidgets.QGraphicsOpacityEffect(opacity=1.0)
        self.setGraphicsEffect(self.effect)
        self.animation = QtCore.QPropertyAnimation(self.effect, b'opacity')
        self.animation.setStartValue(1.0)
        self.animation.setEndValue(0.0)
        self.animation.setDuration(1500)
        self.animation.finished.connect(self.checkAnimation)

        self.clicked.connect(self.startAnimation)

    def startAnimation(self):
        self.animation.stop()
        self.animation.setDirection(self.animation.Forward)
        self.animation.start()

    def checkAnimation(self):
        if not self.animation.value():
            self.animation.setDirection(self.animation.Backward)
            self.animation.start()
        else:
            self.animation.setDirection(self.animation.Forward)

If you want to synchronize opacity amongst many widgets, there are various possibilities, but a QVariantAnimation that updates all opacities is probably the easier choice:

class LEDs(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QtWidgets.QHBoxLayout(self)

        self.animation = QtCore.QVariantAnimation()
        self.animation.setStartValue(1.0)
        self.animation.setEndValue(0.0)
        self.animation.setDuration(1500)
        self.animation.valueChanged.connect(self.updateOpacity)
        self.animation.finished.connect(self.checkAnimation)

        self.buttons = []
        for i in range(3):
            button = QtWidgets.QPushButton()
            self.buttons.append(button)
            layout.addWidget(button)
            effect = QtWidgets.QGraphicsOpacityEffect(opacity=1.0)
            button.setGraphicsEffect(effect)
            button.clicked.connect(self.startAnimation)

    # ... as above ...

    def updateOpacity(self, opacity):
        for button in self.buttons:
            button.graphicsEffect().setOpacity(opacity)

Note that you shouldn't change the object name of a widget during runtime, and doing it only because you want to update the stylesheet is wrong. You either use a different stylesheet, or you use the property selector:

    QPushButton {
        /* default state */
        background: #ababab;
    }
    QPushButton[auto_on="true"] {
        /* "on" state */
        background: #dadada;
    }
class FadeButton(QtWidgets.QPushButton):
    def __init__(self):
        super().__init__()
        # ...
        self.setProperty('auto_on', False)

    def setAuto(self, state):
        self.setProperty('auto_on', state)
        self.setStyleSheet(self.styleSheet())
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • @Atma you're welcome. Remember that if an answer solves your problem you should mark it as accepted by clicking the gray tick mark on its left (review the [tour] to better understand how StackOverflow works). Also note that: 1. with PyQt (or PySide) it's better to use subclasses based on widgets (and/or containers) instead of basic python classes, since they provide the proper Qt interface (signals, object hierarchy, etc); 2. There's usually little point in using QRunnable if it's run just once *and* you need signals: just use QThread instead, which already provides signals, being a QObject. – musicamante Jan 28 '22 at 17:56
  • @Atma there's no shame in learning and doing mistakes. But consider that Qt is a very extended toolkit with hundreds of classes and thousands of functions, 95% of the times it already provides what you need, without complicating things (especially if you're a beginner). Take your time to study it, and most importantly read the documentation of the main classes ([QWidget](//doc.qt.io/qt-5/qwidget.html) and [QObject](//doc.qt.io/qt-5/qobject.html)) and all pages listed in the modules pages ([QtWidgets](//doc.qt.io/qt-5/qtwidgets-index.html) and [QtCore](//doc.qt.io/qt-5/qtcore-index.html)) – musicamante Jan 28 '22 at 18:00
  • I dont have experience in programming and especially pyqt. I've found info about animation but could not implementing successfully. After observing the behavior of my app, it seems the problem is about windows focus, since this happens after a while without user input or user changing to another program. Even changing tab when is sleeping doesn't work right away, needing a few clicks or also space key makes it alive. – Atma Jan 28 '22 at 18:02
  • I will try to do the right way always and so, try to implement your animation. I have only one question. Does the animation happens at same time for every button or it starts on its own timing, meaning does it all blink at the same time or by initial press time? – Atma Jan 28 '22 at 18:03
  • @Atma the focus behavior is the *symptom*, the problem is still related to the fact that you used threading; as said, the fact that it "works" is pointless: external threads must *never* create/access/manipulate UI elements; if you try to do it, you are doing it at your own risk, as it's just *wrong*. The animation is set on each widget effect, so it only has effect on the widget. If you need a synchronized animation for all widgets, then you can use a QVariantAnimation that will update all opacities on its `valueChanged`, otherwise create a QParallelAnimationGroup. – musicamante Jan 28 '22 at 18:10
  • I need all in sync. I will research QVariantAnimation and QParallelAnimationGroup. Thanks a lot – Atma Jan 28 '22 at 18:32
  • @Atma see the update. – musicamante Jan 28 '22 at 18:35
  • Im trying to implement and been around in circles by different methods, now I am focused in your answer but I have 2 points to clear: first I generate a group, then I generate Leds for that group with class Leds, so not all leds are in same group. inside class led, i have 3 buttons, auto, on and off. only the auto button will have the effect. On the gui itself, i have rows of 3 buttons, each row for each led, inside various groupboxs. the effect should only apply to that button on that row, when it is clicked, while the sync must be for all leds on all groupboxs – Atma Jan 28 '22 at 19:47
  • Seems your code blink all buttons in sync only in that led (row), not sync for all leds. – Atma Jan 28 '22 at 19:49
  • @Atma I don't know how the whole project is expected to work, I've shown you the basic implementation, I cannot provide you everything you need. Look at the code, study it, and, most importantly, read the documentation related to all classes and functions it uses. Then find a way to implement it based on your needs. – musicamante Jan 28 '22 at 20:08