Reputation: 524
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()
Upvotes: 11
Views: 8589
Reputation: 63
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 :)
Upvotes: 4
Reputation: 462
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()
Upvotes: 4
Reputation: 731
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()
Upvotes: 3
Reputation: 524
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()
Upvotes: 36