tkinter Toplevel window won't open from system tray menu - python

I'm trying to open a Toplevel window with tkinter, from a system tray menu.
from cmath import phase
from tkinter import *
from tkinter import messagebox, messagebox
from tracemalloc import start
from pystray import MenuItem as item
import pystray
from PIL import ImageTk,Image
import pickle
def quit_window(icon, item):
icon.stop()
root.destroy()
exit()
def hidden():
global my_img1
top=Toplevel()
top.title("Secret menu, shhh :^)")
top.overrideredirect(True)
top.attributes('-alpha', 0.9)
w = 1100
h = 450
ws = top.winfo_screenwidth()
hs = top.winfo_screenheight()
x = (ws/2) - (w/2)
y = (hs/3) - (h/2)
top.geometry('%dx%d+%d+%d' % (w, h, x, y))
top.iconbitmap('screen.ico')
my_img1 = ImageTk.PhotoImage(Image.open("ITEXTRA.png"))
label1=Label(top,image=my_img1).place(relx=0.01,rely=0.01)
button2=Button(top,text="Close window",bg='#ff4a65',command=top.destroy, relief=GROOVE).place(relx=0.9,rely=0.9)
# Marks window as used
hiddenwindow=1
pickle.dump(hiddenwindow, open("window.dat", "wb"))
Button(root, text="Developer Options", padx=57, bg="#86b3b3",fg="black", command = hidden).grid(row=3,column=0)
def hide_window():
root.withdraw()
image=Image.open("screen.ico")
menu=(item('Dev window', hidden),item('show window', show_window),item('Exit app', quit_window))
icon=pystray.Icon("ITExtra", image, "Program", menu)
icon.run()
def show_window(icon, item):
icon.stop()
root.after(0,root.deiconify())
root.after(0,root.focus_force)
root = Tk()
root.title("ITextra")
root.geometry("400x400")
root.protocol('WM_DELETE_WINDOW', hide_window)
hidden()
root.mainloop()
But this unfortunately will not work, it won't pull up the toplevel window, nor the main one.
If I then open the root window myself, the toplevel window will open, but be unresponsive.
EDIT
Alright, so I tried adding the topwindow as class, but I keep getting error 'Top' object has no attribute 'tk'.
I pasted the updated code below. Any help is always greatly appreciated!
from cmath import phase
from tkinter import *
from tkinter import messagebox, messagebox
from tracemalloc import start
from pystray import MenuItem as item
import pystray
from PIL import ImageTk,Image
import pickle
class Top():
def __init__(self,master=None):
self.hide = True
def hidden(self):
if self.hide:
global my_img1
self.top=Toplevel(root)
self.top.title("Secret menu, shhh :^)")
self.top.attributes('-alpha', 0.9)
w = 1100
h = 450
ws = self.top.winfo_screenwidth()
hs = self.top.winfo_screenheight()
x = (ws/2) - (w/2)
y = (hs/3) - (h/2)
self.top.geometry('%dx%d+%d+%d' % (w, h, x, y))
self.top.iconbitmap('screen.ico')
my_img1 = ImageTk.PhotoImage(Image.open("ITEXTRA.png"))
label1=Label(self.top,image=my_img1).place(relx=0.01,rely=0.01)
button2=Button(self.top,text="Close window",bg='#ff4a65',command=self.top.destroy, relief=GROOVE).place(relx=0.9,rely=0.9)
# Marks window as used
hiddenwindow=1
pickle.dump(hiddenwindow, open("window.dat", "wb"))
self.top.mainloop()
def somewhereelse():
top.hide = True
top.hidden()
def quit_window(icon, item):
icon.stop()
root.destroy()
exit()
def show_window(icon, item):
icon.stop()
root.after(0,root.deiconify())
root.after(0,root.focus_force)
def hide_window():
root.withdraw()
image=Image.open("screen.ico")
try:
if pickle.load(open("window.dat","rb")) ==1:
menu=(item('Dev window', top.hidden),
item('show window', show_window),
item('Exit app', quit_window))
else:
menu=(item('Exit app', quit_window))
except:
menu=(item('Exit app', quit_window))
icon=pystray.Icon("ITextra", image, "Program", menu)
icon.run()
root = Tk()
root.title("ITextra")
root.geometry("400x400")
top = Top(root) #in main part
root.protocol('WM_DELETE_WINDOW', hide_window)
Button(root, text="Developer Options", padx=57, bg="#86b3b3",fg="black", command =top.hidden).grid(row=3,column=0)
root.mainloop()
Top window still unresponsive
It's not when root is open, but when top is open by itself, again it remains unresponsive. It responds however when I click a button, and drag my mouse. I tried adding a mainloop in top, but neither a self.top.mainloop nor a root.mainloop will work.
I tried using binds, but they also showed the same behaviour.
Am I creating something that won't work?
The app I'm creating is multithreaded, and my question is; would this complicate things with other classes? I am very new to coding, and quite frankly don't know.
I have the whole project in a pastebin here, for anyone who's interested. I think it's quite a mess, but I'm still pretty proud of it for a beginner.

The Toplevel() remains unresponsive because it has no event loop attached (mainloop()) because in this code the Toplevel acts as a standalone main window.
Need to attach this Toplevel to the root - top = Toplevel(root) where root is passed as argument to hidden(root). This way the root event loop works for all widget children such as a Toplevel. This would help towards your main part of the question.
(#added...) so there is no need for top.mainloop() because now that the root is the master/parent top is inside root.mainloop().
The event loop is for checking in to any events that happen on your widget which you would normally program with bind(). eg
top.bind('<Button>',dosomething) where dosomething is a defined function.
(...#added)
If you want a title for top then you need to create your own title baror label if you are using overrideredirect(True) because this removes the platform window manager.
(#added...)
The platform window manager is not so much removed as it is not being used when using overrideredirect(True). This is probably another reason why your window seems unresponsive with this stage of code. Need to code for events attached to the widget yourself - as you have done with the Button widget to close.
(...#added)
For main part of question:
there is nothing that refers to top widget in show_window in this code.
(#added...)
could look at making top a class and instantiate that in the root. The default status of hidden for top could be an attribute of this class. Then you can change the class attribute to hide or show functionally inside the body of the code somewhereelse.
eg skeleton sketch:
class Top():
def __init__(self,master=None):
...
self.hide = True
...
def hidden(self):
if self.hide:
...
def somewhereelse():
top.hide = true
top.hidden()
top = Top(root) #in main part
!!! obviously very brief general idea that needs work here of a way to maintain your design which seems quite good to me. There are several ways to incorporate the Toplevel widget into the class but that digresses a bit from the original question.
(...#added)
added 28Jan...
I recommend to study class more thoroughly rather than only putting in my example. But here is a bit more
class Top():
def __init__(self,master=None):
super().__init__()
self.master = master
self.hide = True
def hidden(self):
...
self.top = Toplevel(self.master)
...
In my words, but please check Python docs, super().__init__() will call the initialising function of the inherited object which in this case goes back to self.master which is root and then back through to tk.__init__ which is called in Tk().
I recommend looking at the code __init__.py file in the Lib\tkinter\ folder in the Python download to get a good understanding of how tkinter works.
I think this is definitely achievable but might need a different GUI - agree it is an excellent start for a beginner and thus not really a mess!!
Using class is not essential to achieving what you want to do but classes are very useful for encapsulating an object so that any extra attributes and methods relevant to that object can be customised for your project. This makes further or future development easier.
...added 28Jan

Related

Python/Tkinter: need code to define / import a widget as a class outside root window

I have been wrestling for a very long time with the issue of creating a Tkinter gui in modular fashion using classes. While there are many examples on this site - and believe me, I have read them all - they have all been too complex for me to understand. In particular, I could not work out how the imported modules could 'talk to' functions in the main application. I have finally had a eureka moment - I created a class in a module that defines a root window and a button. Then, I wrote a main python file that imports the root window / button module, and tests interactions between the imported module and the main app. All of those tests were successful, which is huge progress for me, as far as it goes. Here is the module code, saved as 'fmod.py':
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('Tkinter titlebar title')
self.geometry('300x50')
# create button within root window
self.button = tk.Button(self, text='Click Me')
self.button.pack()
Here is the python file written to import the module, and test all the interactions I was interested in understanding:
import tkinter as tk
# import module
import fmod
# create gui root window as an instance of imported MainWindow class
MainWin = fmod.MainWindow()
# define a local function for testing purposes
def ButtonClicked():
print("Button clicked")
# alter attributes of imported root/button from within this file
MainWin.geometry("700x400")
MainWin.config(bg = "yellow")
MainWin.button.config(bg="lightblue")
# add a new widget to root from within this file
NewLabel = tk.Label(MainWin,text="Label")
NewLabel.pack()
# connect an imported widget to the above local function
MainWin.button.config(command=ButtonClicked)
# mainloop
MainWin.mainloop()
As mentioned, all of the tests in the above code worked successfully. The question is this: I would rather have the MainWindow class define the root window and nothing else. So rather than including the button in that code, I'd like to write another entirely separate class that simply defines a button, which could be imported into my app separately. Would anyone be kind enough to help me write that code? I tried copying code from the MainWindow class, and it worked, but it opened an entirely new window (probably because of the init / super init code, which I do not fully understand, and don't really need to understand at the moment - it works, and I'm fine with that). I want code for a simple button that I could import into the main app, as a widget, and that I could place in the MainWin window inside the app.
You can define the class in a separate module and then import it, but you will still want to initiate the button inside of your main window like you are doing now. After all the button does belong on the window.
To create a button class it would be similar to how you created the MainWindow...
import tkinter as tk
class MyButton(tk.Button):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
... do something
def buttonClicked(self, *args):
print("Button Clicked")
Then you could just leave you mainwindow the way it is except switch out the class for your class. I also suggest moving a lot of your logic that is in the global scope inside of your MainWindow, and minimizing your use of the global scope as much as possible. For example:
import tkinter as tk
from mybuttonmodule import MyButton
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('Tkinter titlebar title')
self.geometry('300x50')
# create button within root window
self.button = MyButton(self, text='Click Me')
self.button.pack()
self.button.config(command=self.button.buttonClicked)
self.newlabel = tk.Label(self, text="Label")
self.newlabel.pack()
self.geometry("700x400")
self.config(bg = "yellow")
self.button.config(bg="lightblue")
if __name__ == "__main__":
window = MainWindow()
window.mainloop()

Is there a way to reopen a window after closing it using destroy() in tkinter?

window6.after(1,lambda:window6.destroy())
is what I've been using to close my windows, is there any way to get them back after doing this?
basically, is there something that is the opposite of this?
ps. these are the libraries that I've imported, if it helps in any way
import tkinter as tk
from tkinter import *
import time
from tkinter import ttk
Is there a way to reopen a window after closing it using destroy() in tkinter?
The short answer is "no". Once it has been destroyed, it is impossible to get back. You should either create the window via a frame or class so that it's easy to recreate, or hide the window by calling .withdraw() rather than .destroy().
If you put the window code into a class or a function then after destroying it you can create a new instance of it by
1: creating a new instance of the class with the window code in the init function
2: call the function the has the code for the window
By doing this you are essentially creating a new instance of the program, but without initiating the script.
from tkinter impot *
from tkinter import ttk
#creating window function, not class
def main_window():
#window code here
root = Tk()
Label(root, text = "Hello World").pack()
#destroying main window
root.destroy()
root.mainloop()
main_window()
Of course, there are a few hurdles such as the window shutting down as soon as it opens, but this is to show that you can create a new instance of a window from your program.
You can wait for user input to see whether or not the window will open or close.
If you took an OOP approach, you can pass a reference to the Parent Widget as argument to the New_Window and store it in a class attribute.
You´ll have a two way reference: Parent knows child and child knows parent.
Then you can set the Parent Reference to the New_Window to None, from within the child Widget self.parent.new_window = None in a close_me() method right after you call self.destroy() on the New_Window:
1st Bonus: this code prevents the opening of more than 1 instance of a Window at a time. You won´t get more than 1 New_Window on the screen. I don´t think having two loggin windows opened or two equal options window makes sense.
2nd Bonus: It is possible to close the window from other parts of the code, as in a MVC patter, the Controller can close the window after doing some processing.
Here´s a working example:
import tkinter as tk
class Toolbar(tk.Frame):
'''Toolbar '''
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
# to store the New Window reference
self.new_window = None
self.button_new_window = tk.Button(self, text = 'New Window', command = lambda : self.get_window(self))
self.configure_grid()
def configure_grid(self):
'''Configures the Grid layout'''
self.grid(row=1, column=0, columnspan=3, sticky=(tk.N,tk.S,tk.E,tk.W))
self.button_new_window.grid(row = 2, column = 2, padx=5, pady=5)
def get_window(self, parent):
''' If window exists, return it, else, create it'''
self.new_window = self.new_window if self.new_window else Window(parent)
return self.new_window
class Window(tk.Toplevel):
'''Opens a new Window.
#param parent -- tk.Widget that opens/reference this window
'''
def __init__ (self, parent : tk.Widget):
# Stores reference to the Parent Widget, so you can set parent.new_window = None
self.parent = parent
super().__init__(master = parent.master)
self.title('New Window')
self.button_dummy = tk.Button(self, text = 'Do the thing', width = 25, command = lambda : print("Button pressed on window!"))
self.button_close = tk.Button(self, text = 'Close', width = 25, command = self.close_me)
self.configure_grid()
def configure_grid(self):
'''Grid'''
self.button_dummy.grid(row = 1, column = 0)
self.button_close.grid(row = 2, column = 0)
def close_me(self):
'''Tkinter widgets are made of two parts. 1. The python Object and 2. The GUI Widget.
The destroy() method gets rid of the widget part, but leaves the object in memory.
To also destroy the object, you need to set all of its references count to ZERO on
the Parent Widget that created the new Window, so the Garbage Collector can collect it.
'''
# Destroys the Widget
self.destroy()
# Decreasses the reference count on the Parent Widget so the Garbage Collector can destroy the python object
self.parent.new_window = None
if __name__ == '__main__':
root = tk.Tk()
toolbar = Toolbar(root)
root.mainloop()
I don´t know if this: .destroy() and re-instantiate approach is more efficient than the .withdraw() and .deiconify(). Maybe if you have a program that runs for long periods of time and opens a lot of windows it can be handy to avoid stackoverflow or heapoverflow.
It sure frees up the object reference from memory, but it has the additional cost of the re-instantiation, and that is processing time.
But as David J. Malan would say on CS50, “There´s always a tradeoff”.

Tkinter - My custom icon "disturbs" the Top Level window when it opens

when I opened my Top Level window, I always saw an annoying and weird behaviour.. at the end I realized that it was because of my custom icon.
below an exaple code:
from tkinter import *
from tkinter import ttk
class MainWindow:
def __init__(self):
self.parent=Tk()
self.parent.geometry("494x410+370+100")
self.parent.title("My Software - WITH ICON")
self.parent.iconbitmap("icon.ico")
Button = ttk.Button(self.parent, text="open a new widnow", command=self.OpenNewWindow)
Button.place(x=16, y=16)
def OpenNewWindow(self):
self.obj = NewWindow(self)
class NewWindow:
def __init__(self, mw):
self.window, self.mw = Toplevel(mw.parent), mw
self.window.geometry("200x150+360+200")
self.window.title("New Window")
self.window.iconbitmap("icon.ico") # it creates the issue..
self.window.protocol("WM_DELETE_WINDOW", self.on_close)
self.window.focus()
self.mw.parent.attributes('-disabled', 1)
self.window.transient(mw.parent)
self.window.grab_set()
self.mw.parent.wait_window(self.window)
def on_close(self):
self.mw.parent.attributes('-disabled', 0)
self.window.destroy()
def main():
app=MainWindow()
app.parent.mainloop()
if __name__=="__main__":
main()
to make it clear where is the issue, I create a GIF:
here we have two softwares, "without icon.py" and "with icon.py". they are the same, but the first one doesn't use my custom icon for his second window.
as you can see, if I run "with icon.py" the second window will be always affected by something when I will open it, but for "without icon.py" this "something" doesn't exist.
what is the issue? the software opens his second window, focus the root one (it's the issue), and then focus the second window again. you can see it clearly from the GIF.
how can I solve the issue? and why with the default icon this weird behaviour doesn't happen?

Opening TKinter window pauses rest of the program

I have a python script which has an object of a class which basically opens a Tkinter window. The problem is that when I create the object, the rest of the program stops running because when I create the Tkinter object, it basically starts an infinite while loop.
The thing is that I want to change, i.e. the text in a label, but from my other class.
My two files look roughly like this:
from tkinter import *
class Panel():
def __init__(self):
self.root = Tk()
width = 300
screen_width = int(self.root.winfo_screenwidth())
screen_height = int(self.root.winfo_screenheight())
self.root.geometry(str(width)+"x50+"+str(screen_width-width)+"+0")
self.root.overrideredirect(True)
#Create Label
self.label = Label(self.root, text="Text")
self.label.pack()
self.root.mainloop()
def closePanel(self):
self.root.quit()
def editText(self,new_text):
self.label.configure(text=new_text)
And my other class:
from Panel import *
outputPanel = Panel()
outputPanel.editText("New Text")
When you create a tkinter application, your whole application should run as a tkinter application. Therefore you need to create the main window at the highest level. Everything else must run within the mainloop of the tkinter program.
In your case, the Panel module instantiates a class which instantiates a tkinter window. You must move that instantiation to the the Panel module itself. Here is my version of you modified code, just to give a basic idea:
from tkinter import *
class Panel():
def __init__(self, root): # root is passed when instantiating Panel()
self.root=root # make root an instance variable
width = 300
screen_width = int(self.root.winfo_screenwidth())
screen_height = int(self.root.winfo_screenheight())
self.root.geometry(str(width)+"x50+"+str(screen_width-width)+"+0")
self.root.overrideredirect(True)
#Create Label
self.label = Label(self.root, text="Text")
self.label.pack()
def closePanel(self):
self.root.quit()
def editText(self,new_text):
self.label.configure(text=new_text)
root = Tk() # create a window at highest level
outputPanel = Panel(root) # Panel may be imported and accessed here
outputPanel.editText("New Text")
outputPanel.editText("New Text")
root.mainloop()
There is an excellent post about how to structure a tkinter application here: Best way to structure a tkinter application?
Put outputPanel.root.mainloop() after outputPanel.editText("New Text") and remove self.root.mainloop().

Tkinter exit command with infinite loop

I'm having some issue with the close button of the interface window with tkinter. My tool displays some video in real time, I do that with an infinite loop with the after function.
When I close the tkinter window by clicking on the cross, the program freezes. However, when I click on the button, the same function is called but it closes properly.
Here is the most simplified code I came up to show you the problem. Does anyone have an explanation and a way to solve it?
(BTW, I'm using Python 2.7.8 on OSX)
from Tkinter import *
from PIL import Image, ImageTk
import numpy as np
class Test():
def __init__(self, master):
self.parent = master
self.frame = Frame(self.parent)
self.frame.pack(fill=BOTH, expand=1)
self.mainPanel = Label(self.frame)
self.mainPanel.pack(fill=BOTH, expand=1)
self.closeButton = Button(self.frame, command=self.closeApp)
self.closeButton.pack(fill=BOTH, expand=1)
def closeApp(self):
print "OVER"
self.parent.destroy()
def task(tool):
print 'ok'
im = Image.fromarray(np.zeros((500, 500, 3)), 'RGB')
tool.tkim = ImageTk.PhotoImage(im)
tool.mainPanel['image'] = tool.tkim
root.after(1, task, tool)
def on_closing():
print "OVER"
root.destroy()
root = Tk()
root.wm_protocol("WM_DELETE_WINDOW", on_closing)
tool = Test(root)
root.after(1, task, tool)
root.mainloop()
Now if you try again with a smaller image (say 100*100), it works. Or if you put a delay of 100 in the after function, it also works. But in my application, I need a really short delay time as I'm displaying a video and my image size is 900px*500px.
Thanks!
Edit (08/19) : I have not found the solution yet. But I may use root.overrideredirect(1) to remove the close button and then recreate it in Tk, and also add drag a window using : Python/Tkinter: Mouse drag a window without borders, eg. overridedirect(1)
Edit (08/20) : Actually, I can not even drag the window. The tool is also freezing!
You probably just need to kill your animation loop. after returns a job id which can be used to cancel pending jobs.
def task():
global job_id
...
job_id = root.after(1, task, tool)
def on_closing():
global job_id
...
root.after_cancel(job_id)
Your code could be a bit cleaner if these functions were methods of the object so you didn't have to use a global variable. Also, you should have one quit function rather than two. Or, have one call the other so you are certain both go through exactly the same code path.
Finally, you shouldn't be calling a function 1000 times a second unless you really need to. Calling it so often will make your UI sluggish.
I found a solution, I'm not sure it is really clean, but at least it is working for what I want to do. I no longer use after but I loop and update the gui at each iteration.
from Tkinter import *
from PIL import Image, ImageTk
import numpy as np
class Test():
def __init__(self, master):
self.parent = master
self.frame = Frame(self.parent)
self.frame.pack(fill=BOTH, expand=1)
self.mainPanel = Label(self.frame)
self.mainPanel.pack(fill=BOTH, expand=1)
self.parent.wm_protocol("WM_DELETE_WINDOW", self.on_closing)
self.close = 0
def on_closing(self):
print "Over"
self.close = 1
def task(self):
print "ok"
im = Image.fromarray(np.zeros((500, 500, 3)), 'RGB')
self.tkim = ImageTk.PhotoImage(im)
self.mainPanel['image'] = self.tkim
root = Tk()
tool = Test(root)
while(tool.close != 1):
tool.task()
root.update()
root.destroy()

Categories