Search code examples
python-3.xmultithreadingsystem-traypystray

PyStray stops working once the main loop is launched (detached mode)


I am working on a side project which entails a main process that spawns an HTTP server that keeps listening for events and handles them within an infinite loop.

This works as intended and I am now trying to add support for the start and stops of all the components from an icon in the system tray. For this, I am using Pystray which provides a run_detached method that should allow the icon loop to run on a separate thread.

The problem is that when I run the application the icon is created correctly, but as soon as I click on the icon menu to start the main process, the icon is not usable anymore. Right clicks are not detected and no further interactions are possible.

A small example that resembles my use case:

from PIL import Image, ImageDraw
import pystray
import time

class MyClass:
    def __init__(self) -> None:
        self.__running = False
        
    def run(self):
        while self.__running:
            time.sleep(1)
            print("running")
                
    def change_running_state(self):
        print(f"Running: {self.__running}")
        self.__running = not self.__running
        print(f"Running: {self.__running}")
        

if __name__ == "__main__":

    def create_image(width, height, color1, color2):
        image = Image.new('RGB', (width, height), color1)
        dc = ImageDraw.Draw(image)
        dc.rectangle(
            (width // 2, 0, width, height // 2),
            fill=color2)
        dc.rectangle(
            (0, height // 2, width // 2, height),
            fill=color2)
        return image

    def start():
        print("start")
        cl.change_running_state()
        cl.run()
        
    def stop():
        print("stop")
        cl.change_running_state()
        
    def exit_program():
        print("exit")
        import os
        cl.change_running_state()
        icon.stop()
        os._exit(1)
    

    icon = pystray.Icon(name = 'Doata2RuneRemainder', icon=create_image(64, 64, 'black', 'white'), menu=pystray.Menu(
        pystray.MenuItem("Start", start),
        pystray.MenuItem("Stop", stop),
        pystray.MenuItem("Exit", exit_program),
    ))
    cl = MyClass()
    icon.run_detached()

Solution

  • The run_detached() method is intended to be used with a separate thread to run the system tray icon loop while your main process continues running. However, in your current code, you are calling icon.run_detached() at the end of your script, which essentially runs the system tray icon loop on the main thread, blocking the execution of the rest of your code.

    To resolve the issue, you should separate the system tray icon loop into its own thread so that it can run independently while your main process continues executing.

    from PIL import Image, ImageDraw
    import pystray
    import threading
    import time
    import os
    
    class MyClass:
        def __init__(self):
            self.__running = False
            self.__stop_event = threading.Event()
            
        def run(self):
            while not self.__stop_event.is_set():
                time.sleep(1)
                print("running")
                    
        def change_running_state(self):
            print(f"Running: {self.__running}")
            self.__running = not self.__running
            print(f"Running: {self.__running}")
            
            if self.__running:
                self.__stop_event.clear()
                t = threading.Thread(target=self.run)
                t.start()
            else:
                self.__stop_event.set()
    
    if __name__ == "__main__":
    
        def create_image(width, height, color1, color2):
            image = Image.new('RGB', (width, height), color1)
            dc = ImageDraw.Draw(image)
            dc.rectangle(
                (width // 2, 0, width, height // 2),
                fill=color2)
            dc.rectangle(
                (0, height // 2, width // 2, height),
                fill=color2)
            return image
    
        def start(icon, item):
            print("start")
            cl.change_running_state()
    
        def stop(icon, item):
            print("stop")
            cl.change_running_state()
    
        def exit_program(icon, item):
            print("exit")
            cl.change_running_state()
            icon.stop()
            os._exit(1)
    
        cl = MyClass()
        icon = pystray.Icon(name='Doata2RuneRemainder', icon=create_image(64, 64, 'black', 'white'))
        icon.menu = (
            pystray.MenuItem("Start", start),
            pystray.MenuItem("Stop", stop),
            pystray.MenuItem("Exit", exit_program),
        )
        icon.run_detached()