Search code examples
pythontkintercustomtkinterpystray

Pystray icon inappropriate behavior with Tkinter window withdraw


Description: So I am using customtkinter library instead of tkinter, the issue I am facing is that I want my tkinter app to be withdrawn when closed if the minimize window checkbox is checked in settings of the app. The first time I perform this action the app withdraws as well as pystray icon appears in the taskbar, when I open the window again using the "open app" button the app deiconifies but when I try the same method second time I get an error.

Source Code: Below is my code through which I am implementing this operation

def save_checkbox_state(state):
    with open('.\\_internal\\src\\checkbox_state.pickle', 'wb') as file:
        pickle.dump(state, file)

def read_checkbox_state():
    try:
        with open('.\\_internal\\src\\checkbox_state.pickle', 'rb') as file:
            state = pickle.load(file)
            # Modify this section according to your UI framework
            if state == 'on':
                checkbox.select()
                check_var.set('on')
            else:
                checkbox.deselect()
                check_var.set('off')
    except FileNotFoundError:
        return None
    
def app_destroy():
    icon.stop()
    app.destroy()

def open_app():
    icon.stop()
    app.after(0,app.deiconify())
    
image = Image.open(".\\_internal\\assets\\ico.png")
menu=pystray.Menu(pystray.MenuItem("Open enigma:guard",open_app),pystray.MenuItem("Exit", app_destroy))
icon = pystray.Icon('enigma:guard', image, "My system tray icon", menu)

def destroy_window():
    checkbox_state = check_var.get()
    save_checkbox_state(checkbox_state)
    if checkbox_state == 'on':
        app.withdraw()
        
        icon.run()
    else:
        app.destroy()
        icon.stop()


settings_minimize_frame = customtkinter.CTkLabel(settings_frame_win,text="",image=settings_minimize)
settings_minimize_frame.place(x=420,y=121)

check_var = customtkinter.StringVar(value="off")
checkbox = customtkinter.CTkCheckBox(settings_minimize_frame, text="", variable=check_var, onvalue="on", offvalue="off",width=30,height=35,checkbox_height=35,checkbox_width=35,border_color="#1FFFA9",bg_color="#000000")
checkbox.place(x=1078,y=33)

app.protocol("WM_DELETE_WINDOW", destroy_window)

Error: Below is the error I am facing when I close the window and it minimizes and I reopen it the second time:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\[hidden]\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "D:\python_encryptor_decryptor\project2\enigma_guard.py", line 1228, in destroy_window
    icon.run()
  File "C:\Users\[hidden]\AppData\Local\Programs\Python\Python311\Lib\site-packages\pystray\_base.py", line 212, in run
    self._run()
  File "C:\Users\[hidden]\AppData\Local\Programs\Python\Python311\Lib\site-packages\pystray\_win32.py", line 120, in _run
    self._hwnd = self._create_window(self._atom)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\[hidden]\AppData\Local\Programs\Python\Python311\Lib\site-packages\pystray\_win32.py", line 244, in _create_window
    hwnd = win32.CreateWindowEx(
           ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\[hidden]\AppData\Local\Programs\Python\Python311\Lib\site-packages\pystray\_util\win32.py", line 204, in _err
    raise ctypes.WinError()
OSError: [WinError 6] The handle is invalid.

Already tried: I tried to destroy the window instead of withdrawing it but that makes the useability a lot awkward since I am giving a splash screen at startup.


Solution

  • Solution: So after a lots of debugging I finally found the issue, the code of the pystray works in a loop in a certain way that it works only once. To repeat the loop everytime I created the function inside the destroy_window function itself so everytime the window closed the function runs creating the icon from pystray from scratch.

    Source Code:

    def save_checkbox_state(state):
        with open('.\\_internal\\src\\checkbox_state.pickle', 'wb') as file:
            pickle.dump(state, file)
    
    def read_checkbox_state():
        try:
            with open('.\\_internal\\src\\checkbox_state.pickle', 'rb') as file:
                state = pickle.load(file)
                if state == 'on':
                    checkbox.select()
                    check_var.set('on')
                else:
                    checkbox.deselect()
                    check_var.set('off')
        except FileNotFoundError:
            return None
        
    def destroy_window():
        checkbox_state = check_var.get()
        save_checkbox_state(checkbox_state)
        if checkbox_state == 'on':
            app.withdraw()
    
            def open_app():
                icon.stop()
                app.after(0,app.deiconify)
    
            def app_destroy():
                icon.stop()
                app.destroy()
    
            menu=pystray.Menu(pystray.MenuItem("Open enigma:guard",open_app),pystray.MenuItem("Exit", app_destroy))
            image = Image.open(".\\_internal\\assets\\ico.png")
            icon = pystray.Icon('enigma:guard', image, "My system tray icon", menu)
            icon.run()
    
        else:
            app.destroy()
            icon.stop()
    
    settings_minimize_frame = customtkinter.CTkLabel(settings_frame_win,text="",image=settings_minimize)
    settings_minimize_frame.place(x=420,y=121)
    
    check_var = customtkinter.StringVar(value="off")
    checkbox = customtkinter.CTkCheckBox(settings_minimize_frame, text="", variable=check_var, onvalue="on", offvalue="off",width=30,height=35,checkbox_height=35,checkbox_width=35,border_color="#1FFFA9",bg_color="#000000")
    checkbox.place(x=1078,y=33)
    
    app.protocol("WM_DELETE_WINDOW", destroy_window)