Running a Tkinter window and PysTray Icon together
Asked Answered
P

4

11

I'm building a tkinter gui project and i'm looking for ways to run a tray icon with the tkinter window.
I found Pystray library that does it, But now i'm trying to figure it out how to use this library (tray Icon) together with tkinter window,
I set it up when the user exit winodw it's only will withdraw window:
self.protocol('WM_DELETE_WINDOW', self.withdraw)
I want to bring it back with the tray icon.. anyone know how to do it?
EDIT:untill now I just wrote this code so far (they're not running together but it's also fine):

from pystray import MenuItem as item
import pystray
from PIL import Image
import tkinter as tk

def quit_window(icon, item):
    icon.stop()
    #window.destroy()

def show_window(icon, item):
    icon.stop()
    #window.deiconify()

def withdraw_window(window):    
    window.withdraw()
    image = Image.open("image.ico")
    menu = (item('Quit', quit_window), item('Show', show_window))
    icon = pystray.Icon("name", image, "title", menu)
    icon.run()

def main():
    window = tk.Tk() 
    window.title("Welcome")
    window.protocol('WM_DELETE_WINDOW', lambda: withdraw_window(window))
    window.mainloop()
main()
Percent answered 22/2, 2019 at 21:24 Comment(4)
@stovfl pythonhosted.org/pystray brief: Python library for tray icon on taskbarPercent
Do you run your tkinter app within pystray? Search Q&A pystraySharonsharona
@Sharonsharona I edited the question, that's where I've managed to get this farPercent
@Sharonsharona Thanks for trying help me hehe :)Percent
P
35

Finally I figure it out,
Now I just need to combine this with my main code, I hope this code will help to other people too...

from pystray import MenuItem as item
import pystray
from PIL import Image
import tkinter as tk

window = tk.Tk()
window.title("Welcome")

def quit_window(icon, item):
    icon.stop()
    window.destroy()

def show_window(icon, item):
    icon.stop()
    window.after(0,window.deiconify)

def withdraw_window():  
    window.withdraw()
    image = Image.open("image.ico")
    menu = (item('Quit', quit_window), item('Show', show_window))
    icon = pystray.Icon("name", image, "title", menu)
    icon.run()

window.protocol('WM_DELETE_WINDOW', withdraw_window)
window.mainloop()
Percent answered 23/2, 2019 at 11:21 Comment(3)
I don't know why this answer has not received more votes. Everywhere you just find that it is not possible to do this. Excellent contribution.Piceous
I agree, this is awesome!Alumna
Actually this solution is not "running together". In my interpretation "running together" is running tkinter and pystray concurrently, without stopping any of then to start another. I think the only solution to this is using threads but I want to know what is the right way to do this since in tkinter and pystray documentation, both of then say that the code must run in the main thread.Junkie
E
3

For anyone wanting to run both at the same time it's possible to run pystray in it's own thread by using run_detached() instead of run(). Based on Osher's answer here's an example that keeps the icon in the system tray even after closing the window.

from pystray import MenuItem as item
import pystray
from PIL import Image
import tkinter as tk


def quit_window(icon, item):
    icon.visible = False
    icon.stop()
    window.quit()


def show_window(icon, item):
    window.after(0, window.deiconify)


def withdraw_window():
    window.withdraw()


window = tk.Tk()
window.title("Welcome")
image = Image.open("free.ico")

menu = (item('Quit', quit_window), item('Show', show_window))
icon = pystray.Icon("name", image, "title", menu)
icon.run_detached()
window.protocol('WM_DELETE_WINDOW', withdraw_window)
window.mainloop()
Earley answered 7/1, 2023 at 0:3 Comment(1)
For me the run_detached() did not work. I had to run it like this instead: threading.Thread(daemon=True, target=lambda: self.icon.run()).start()Yeomanry
F
2

Thanks to Oshers solution, I adapted it into my own project.

One issue I fixed was that you could only hide the main window once, then the loop would crash. With this solution, it has no limit.

import tkinter as tk
from PIL import Image

import pystray


class Gui():

    def __init__(self):
        self.window = tk.Tk()
        self.image = Image.open("./assets/icons/ready.png")
        self.menu = (
            pystray.MenuItem('Show', self.show_window),
            pystray.MenuItem('Quit', self.quit_window)
            )
        self.window.protocol('WM_DELETE_WINDOW', self.withdraw_window)
        self.window.mainloop()


    def quit_window(self):
        self.icon.stop()
        self.window.destroy()


    def show_window(self):
        self.icon.stop()
        self.window.protocol('WM_DELETE_WINDOW', self.withdraw_window)
        self.window.after(0, self.window.deiconify)


    def withdraw_window(self):
        self.window.withdraw()
        self.icon = pystray.Icon("name", self.image, "title", self.menu)
        self.icon.run()


if __name__ in '__main__':
    Gui()
Flinty answered 1/12, 2022 at 17:17 Comment(0)
S
2

As suggested in Can't Get A Menu to Work On Windows 10 #99 and pystray Icon in a thread will not end the thread with Icon.stop() #94 on official pystray Github: if using Windows or Linux, the run_detached() method is unnecessary, even if using alongside Tkinter or other GUIs that utilize its own mainloop (ignore if using or intend to deploy on macs).

Also, other methods seem cluttered or unnecessarily complicated...

For a clean, simple and extensible solution that you can implement universally in any program, see the below code; it uses a variable (self.active) to inform whether the tray icon is still running (ie. user hasn't exited the program via tray icon), one function for you to allocate MenuItems, and another function that creates the actual icon itself, implemented using Thread.threading module. It uses some of the code from the above linked Github issues.

For your easy testing purposes, I've also included pystray's function from issue #94 linked above that creates a icon for you. Of course, you may replace this with the path to your icon file, which would make this look cleaner and more succinct.

import threading
from PIL import Image, ImageDraw
from pystray import Icon, Menu, MenuItem


def create_image(color1, color2, width=64, height=64):
    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


class TrayIcon:
    def __init__(self):
        self.active = True

    def _on_clicked(self, icon, item):
        if str(item) == "Settings":
            print("Opening Settings")
        elif str(item) == "Open ReadMe":
            print("Opening ReadMe")
        elif str(item) == "Exit":
            icon.visible = False
            icon.stop()
            self.active = False

    def create_icon(self):
        thread = threading.Thread(
            daemon=True,
            target=lambda: Icon(
                "test",
                create_image("black", "white"),
                menu=Menu(
                    MenuItem("Settings", self._on_clicked),
                    MenuItem("Open ReadMe", self._on_clicked),
                    MenuItem("Exit", self._on_clicked),
                ),
            ).run(),
        )
        thread.start()

And then, all you need to do in your view or tkinter main file, is include this (don't forget to import your TrayIcon class first):

tray_icon = TrayIcon()
tray_icon.create_icon()
while tray_icon.active == True:
    print("whatever you want to run here, put it here.")
    print("including tkinter mainloops.")

Let me know if this helps you :)

Substantive answered 29/7, 2023 at 3:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.