I'm creating a Tkinter-based GUI in Python. I would like the window to hide to system tray when it is minimized (using pystray module). It hides, but it only appears on the screen and hangs, when I'm trying to restore it.
Here's what I have tried:
from tkinter import *
from PIL import Image
import pystray
def hide_to_tray(_event=None):
tray_icon = pystray.Icon("MyTrayIcon", title="My tray icon") # create the tray icon
tray_icon.icon = Image.open("app_icon.ico") # open the icon using PIL
tray_icon.menu = pystray.Menu(pystray.MenuItem("Open", lambda: tray_icon.stop(), default=True)) # create the menu
root.withdraw() # hide the window
tray_icon.run() # run the icon's main loop
# icon mainloop
root.deiconify() # when the icon mainloop had been stopped, show the window again
root.focus_force() # focus on it
root = Tk()
btn = Button(root, text="Sample button")
btn.grid()
root.bind("<Unmap>", hide_to_tray) # hide to tray on minimizing
root.mainloop()
How can I solve this problem?
1. Use infi.systray
It runs on a seperate thread and thus won't block, install it with pip:
pip install infi.systray
Don't call tkinter methods from the info.systray thread:
Since infi.systray runs on a seperate thread, you mustn't call tkinter methods directly in the callback functions that you pass to the systray icon on creation. Use a thread safe way (e.g. queue) to inform the main thread about events in the systray icon instead!
2. Don't run pystray and tkinter at the same time
You can't run them both because they block the running thread and both have to run on the mainthread.
See Osher's answer that only displays the system tray icon when the tkinter app is closed.
Related
I've been trying to create a message box when a button turns from disabled to active but when the button actually turns from disabled to active for some reason my callback is not even being called I've tried to get it working so for quite a bit of time now and I'm stuck.
Here is an example of the problem:
from tkinter import *
from tkinter import Tk
def disable_and_activate():
b.config(state = DISABLED)
b.config(state = ACTIVE)
def is_working(event):
print('working')
root = Tk()
b = Button (root, text = 'click me', command = disable_and_activate)
b.pack()
b.bind('<Activate>', is_working)
root.mainloop()
Console:
the button is clicked but there's nothing printed on the console
The <Activate> event is not triggered when you set the state of the button to "active". The event is triggered when the window becomes the active window.
For example, when I run your code on my OSX machine, if I click on some other application to give it focus and then I click back to the tkinter window, the event will fire when the tkinter window becomes the active window.
This is explained in the canonical tcl/tk documentation which says this:
Activate, Deactivate
These two events are sent to every sub-window of a toplevel when they change state. In addition to the focus Window, the Macintosh platform and Windows platforms have a notion of an active window (which often has but is not required to have the focus). On the Macintosh, widgets in the active window have a different appearance than widgets in deactive windows. The Activate event is sent to all the sub-windows in a toplevel when it changes from being deactive to active. Likewise, the Deactive event is sent when the window's state changes from active to deactive. There are no useful percent substitutions you would make when binding to these events.
Here the problem was just the code inside the function , It seems like you needed to call EventGenerate('<<Activate>>') I also recommend adding 2 << and 2 >>
So I rewrote the code and its now working perfectly fine:
from tkinter import *
from tkinter import Tk
import tkinter
def disable_and_activate():
b.configure(state=tkinter.DISABLED)
b.configure(state=tkinter.ACTIVE)
b.event_generate("<<Activate>>")
def is_working(event):
print('working')
root = Tk()
b = Button (root, text = 'click me', command = disable_and_activate)
b.pack()
b.bind('<<Activate>>', is_working)
root.mainloop()
I have a very simple python code: a tkitner button that process some images in the background. I wanted to open a tkinter toplevel to show the user that it was doing something, but for my surprise is not working as I thought it would. The command on the tk.Button is the next method:
def processing(self):
"""Starts the images processing"""
# Open a Tk.Toplevel
aux_topLevel = Splash(self.window) # a simple Tk.Toplevel class, that works perfectly
self._process_images() # starts processing the images
# I wanted to kill here the topLevel created before
aux_topLevel.destroy()
My surprise: the window is displayed once the processing images is done (tried it out adding prints and time.sleep), however, i couldn't display the TopLevel when I wanted to.
Is there anything am I doing wrong? Any help is appreciated. Thank you.
Consider the following example and try to run it.
What you'd think should happen is that the new Toplevel window should open, some event happens for a period of time and then the window is destroyed.
What actually happens is the window is opened, but never displayed, the task occurs and then the window is destroyed.
from tkinter import *
import time
def processing():
new = Toplevel(root)
new.geometry("200x150")
lbl = Label(new,text="--")
lbl.grid()
for i in range(50):
time.sleep(0.1)
#Un-comment the line below to fix
#root.update()
print(i)
lbl['text'] = "{}".format(i)
new.destroy()
root = Tk()
root.geometry('200x100')
btnGo = Button(root,text="Go",command=processing)
btnGo.grid()
root.mainloop()
If you un-comment out the root.update() line and re-run the code, the window will be displayed.
There are better ways to deal with tasks that takes a while to process, such as threading.
I've made a program in python with Tkinter that allows you to free draw and choose different colors. I decided to make a button that would close the window instead of clicking the exit button in the top right corner. My question is how do I make the window close when the button is pressed?
If you are using a main loop for your application, then you can use the .destroy() method to release all the resources associated with the window and close the application. You call this method within the command function for your button like so:
from tkinter import *
root = Tk()
frame = Frame(root)
frame.pack(side=LEFT)
button = Button(frame, text="Exit", command=exit)
button.pack()
root.mainloop()
def exit():
root.destroy()
That should close your window. Optionally, the destroy() method may also be used at the end of your main loop if the X button of your application won't close the window immediately.
See these examples for more info:
http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.destroy-method
http://effbot.org/tkinterbook/tkinter-hello-again.htm
I am using python3 on a mac and run scripts with the IDLE which comes automatically with the python3 installation.
I am trying to make an alert to the user and found the command
tkinter.messagebox.showinfo("title","some text")
So I i tried a minimal script to check if I can get along with that command
import tkinter
tkinter.messagebox.showinfo("test" , "blabla")
The window is displayed correctly but it doesn't respond when I click on the "OK" button.
Addtionally there is a second empty window which appears when I start the script.
What is the explanation for this or at least how can I fix that?
tkinter isn't designed to work this way. Every tkinter requires a root window. If you don't explicitly create one (and you didn't), one will be created for you. That's what the blank window is.
Also, a tkinter GUI can't function properly unless it has a running event loop. This is necessary because some functions, such as responding to buttons and redrawing the window, only happens in response to events. If the event loop isn't running, events can't be processed.
Bottom line: the dialogs aren't designed to be used outside of the context of a proper tkinter app.
Wrapper for standalone use
The following code can be used to display one of the dialogs in standalone mode. It works by creating and hiding a root window, displaying the dialog, and then destroying the root window.
import tkinter as tk
from tkinter import messagebox
def show_dialog(func, *args, **kwargs):
# create root window, then hide it
root = tk.Tk()
root.withdraw()
# create a mutable variable for storing the result
result = []
# local function to call the dialog after the
# event loop starts
def show_dialog():
# show the dialog; this will block until the
# dialog is dismissed by the user
result.append(func(*args, **kwargs))
# destroy the root window when the dialog is dismissed
# note: this will cause the event loop (mainloop) to end
root.destroy()
# run the function after the event loop is initialized
root.after_idle(show_dialog)
# start the event loop, then kill the tcl interpreter
# once the root window has been destroyed
root.mainloop()
root.quit()
# pop the result and return
return result.pop()
To use it, pass the dialog you want as the first option, followed by dialog-specific options.
For example:
result = show_dialog(messagebox.askokcancel, "title", "Are you sure?")
if result:
print("you answered OK")
else:
print("you cancelled")
#!/usr/bin/env python
# Display window with toDisplayText and timeOut of the window.
from Tkinter import *
def showNotification(notificationTimeout, textToDisplay):
## Create main window
root = Tk()
Button(root, text=textToDisplay, activebackground="white", bg="white", command=lambda: root.destroy()).pack(side=LEFT)
root.update_idletasks()
# Remove window decorations
root.overrideredirect(1)
timeOut = int(notificationTimeout*1000) # Convert to ms from s
## Run appliction
root.after(timeOut,root.destroy)
root.mainloop()
The above code creates a notification, with a timout. However on windows - the notification does not automatically pop up above all other present windows automatically. One has to click on the kill button (the text), and focus it the first time, after which the root window will be displayed above all other windows.
Is there a way to make the notification automatically appear above all other windows - on windows?
It seems to work on linux just fine (ubuntu 9.10).
According to this message you should be able to add the following after root.overridedirect(1). A quick test here suggests it should work for you.
root.wm_attributes("-topmost", 1)