I have written an GUI with tkinter. In the background I have to perfom some intensive calculations. From time to time I want to write some information from the calculation thread to the GUI window.
First I thought it was a good idea to add the computation code into the "mainloop". But this doesn't work, because the mainloop is resposible for keeping the GUI reactive. It seams to be no good idea to mainpulate it.
Below I have created a dummy app that scatches my new idea. The Sample app has a container. Inside that container, there is a TitleBar. The class TitleBar is defined below. It contains only one label.
Next I define a simple thread. It simulates a timeconsuming computation that wants to write some information to the GUI.
import tkinter as tk
import threading
import time
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
# initialize the main window
tk.Tk.__init__(self, *args, **kwargs)
# add a container which will take all the widgets
container = tk.Frame(self, bg="green")
container.pack(side="top", fill="both", expand=True)
# Add a titlebar object (defined below)
titleBar = TitleBar(container, controller=self)
titleBar.grid(row=0, column=0, columnspan=2, sticky=tk.N+tk.W+tk.E)
class TitleBar(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
# the title bar contains only one label element
titleLabel = tk.Label(self, text="This is the initial text")
titleLabel.pack(side=tk.LEFT)
# Define a thread that runs in the background to perform intensive calculations
class MyTestThread(threading.Thread):
def run(self):
for i in range(10):
time.sleep(1)
a = i+100 # intensive calculation
# from time to time: inform use about status
print(a) # printing to console works fine
app.titleBar.titleLabel['text'] = "test 1" # --- FAILS ---
if __name__ == "__main__":
app = SampleApp()
app.titleBar.titleLabel['text'] = "test 2" # --- FAILS ---
t = MyTestThread()
t.start()
app.mainloop()
The problem is that I cannot access the label to write information to it. The writing fails, both, from within the thread and from the app itself. In both cases it fails with the following error:
AttributeError: '_tkinter.tkapp' object has no attribute 'titleBar'
How can I access and change the properties of the label-object?
Thank you
Bernd
With the help of eyllanesc and Mark_Bassem I was able to solve the problem. It seems that the problem was indeed very simple.
Just in case people will wisit this post in the future, I leave the correct code here:
import tkinter as tk
import threading
import time
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
# initialize the main window
tk.Tk.__init__(self, *args, **kwargs)
# add a container which will take all the widgets
container = tk.Frame(self, bg="green")
container.pack(side="top", fill="both", expand=True)
# Add a titlebar object (defined below)
self.titleBar = TitleBar(container, controller=self)
self.titleBar.grid(row=0, column=0, columnspan=2, sticky=tk.N+tk.W+tk.E)
class TitleBar(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
# the title bar contains only one label element
self.titleLabel = tk.Label(self, text="This is the initial text")
self.titleLabel.pack(side=tk.LEFT)
# Define a thread that runs in the background to perform intensive calculations
class MyTestThread(threading.Thread):
def run(self):
for i in range(10):
time.sleep(1)
a = i+100 # intensive calculation
# from time to time: inform use about status
print(a) # printing to console works fine
app.titleBar.titleLabel['text'] = "status: " + str(a)
if __name__ == "__main__":
app = SampleApp()
#app.titleBar.titleLabel['text'] = "test 2"
t = MyTestThread()
t.start()
app.mainloop()
You forgot to put self before the titleBar attribute
Related
This app is supposed to destroy the old frame to draw a new one.
In my case the frames are image containers and I show them in 4 different windows, but in the example I only show one as an example, also each instance from Img_1 to Img_60 has its own widgets and these have their own links. Now I have two problems, I hope you can help me please.
When it comes to destroying and drawing the new frame, there are conflicts due to the links of the previous frame, I was thinking of deactivating them but '''''' doesn't work for me, I'm kind of gross.
Where should I initialize the images? [150], I ask this question due to a performance issue as I have noticed that the window takes a long time to open. I appreciate all the people who took the time to read, it doesn't matter if they didn't help me. Thanks.
from tkinter import *
class Crtl(Frame):
def__init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self._frame_1 = None
self._open_1 = False
self.button1 = Button(self, text='pack 1',
command= lambda:self.windows(ImG)
self.button1 .pack()
def windows(self, var_1):
if not self._open_1:
self.top1 = Toplevel(self.master)
self.top1 .geometry('200x200')
container = var_1(self.top1)
if self._frame_1 is not None:
self._frame_1 .destroy()
self._frame_1 = container
self._frame_1 .pack()
self._open_1 = True
class ImG(Frame):
def__init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.img1 = PhotoImage(file= '60.png')
self.lbl1 = Label(self, image=self.img1)
self.master.bind("<Button-1>", self.img_2)
def img_2(self):
print('new image N°24')
root = Tk()
app = Ctrl(root, bg='black')
app .pack()
root .mainloop()
Immediately during the first ~1 sec after I've landed on a new page in my GUI, I'm seeing numerous visual glitches before the window arrives at the proper layout (before/after screenshots below).
UPDATE:
The below code will give the desired error. I feel like the initial frame is being merged with the next frame upon calling the show_frame function, and posit that the initial frame must actually be manually hidden from view before preloading (hiding/loading) the next frame. Any help on this is greatly appreciated - and thank you to all who have taken a look so far.
import tkinter as Tk
from tkinter import font as tkfont
from tkinter import filedialog, ttk #browse directories; widget support
class Manifold(Tk.Tk):
def __init__(self, *args, **kwargs):
Tk.Tk.__init__(self, *args, **kwargs)
#custom font options:
self.title1_font = tkfont.Font(family='Helvetica',size=13) #normal type
#customized ttk GUI theme:
GUItheme = ttk.Style()
GUItheme.theme_use('alt')
container = Tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F,geometry,title,options,wait in zip((StartPage,PageOne),
("532x279","528x270"),
("","Experimental Data"),
((False,False),(True,False)),
(100,100)):
page_name = F.__name__
frame = F(container, self)
self.frames[page_name] = (frame,geometry,title,options,wait) #puts all pages in stacked order
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name): #show a frame for the given page name
frame, geometry, title, options, wait = self.frames[page_name]
self.geometry(geometry) #changes between window sizes
self.title(title) #changes titles of windows
self.resizable(*options) #changes ability of user to resize each window
self.withdraw()
frame.tkraise() #raises window to top
self.after(wait,self.deiconify) #preload page before viewing
self.update_idletasks()
class StartPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self, parent)
self.controller = controller
self.configure(background='black') #set window background color
#page one button:
button1 = ttk.Button(self, text="Page 1",
command=lambda: controller.show_frame("PageOne"))
button1.grid(row=0,column=0,sticky='W')
class PageOne(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self, parent)
self.controller = controller
self.configure(background='gray15') #set window background color
self.grid_columnconfigure(0,weight=1000) #for resizing window horizontally
#average volume filename:
self.label1 = Tk.Label(self, text="Average Volume Filename:",fg="gray90",bg="gray25",font=controller.title1_font)
self.label1.grid(row=0,column=0,ipadx=10,ipady=0,sticky='W')
self.label1.config(height=1)
self.entry1 = Tk.Entry(self,textvariable=Tk.StringVar(),highlightbackground="black",width=50)
self.entry1.grid(row=1,column=0,sticky="WE")
self.entry1.insert(0," .mrc, .spi")
self.entry1.configure(state="disabled") #prevent typing
self.browse1 = ttk.Button(self,text="Browse",
command=self.browse_button1)
self.browse1.grid(row=1,column=1,sticky='W')
#gathers volume input:
def browse_button1(self):
self.entry1.configure(state="normal") #unlocks typing for program
self.label1.config(fg="gray90",bg='gray25') #standard colors, or reset on additional wrong input
content_initial = self.entry1.get()
if __name__ == "__main__":
app = Manifold()
app.mainloop()
Page One:
Transition:
Page Two:
Okay, I've figured it out. Using my above code (in the Update section of my initial question), I made two changes. The first is right under the main Class structure:
class Manifold(Tk.Tk):
def __init__(self, *args, **kwargs):
Tk.Tk.__init__(self, *args, **kwargs)
#preload windows (avoids watching widgets load in real time):
self.withdraw() #hide window
self.after(0,self.deiconify) #unhide window asap
The second is in the show_frame loop, with the preceding wait tuple (within the chunk starting with for F, geometry,etc. set to (10,10,10,10):
def show_frame(self, page_name): #show a frame for the given page name
frame, geometry, title, scaling, wait = self.frames[page_name]
self.quit() #seems to be important in exiting out of previous window entirely
self.geometry(geometry) #changes between window sizes
self.title(title) #changes titles of windows
self.resizable(*scaling) #changes ability of user to resize each window
self.withdraw()
frame.tkraise() #raises window to top
self.after(wait,self.deiconify) #preload page before viewing
self.update_idletasks()
If anyone else ends up running into something like this, I hope this helps!
Remove all calls to update until your are ready for your UI to be visible, and don't use after(0, ...) because the zero means that code will run before tkinter has had a chance to process any other events, including requests to redraw portions of the screen.
Hide your Toplevel or Tk instance(s) that is/are glitchy until after a certain amount of time has passed:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except:
import Tkinter as tk
def hide(*toplevel_or_tk_instance_s_):
for each in toplevel_or_tk_instance_s_:
each.withdraw()
def show(*toplevel_or_tk_instance_s_):
for each in toplevel_or_tk_instance_s_:
each.iconify()
each.deiconify()
if __name__ == '__main__':
root = tk.Tk()
my_toplevel1 = tk.Toplevel(root, bg='red')
my_toplevel2 = tk.Toplevel(root, bg='green')
hide(root, my_toplevel1, my_toplevel2)
root.after(1000, show, root)
root.after(2000, show, my_toplevel1)
root.after(3000, show, my_toplevel2)
root.mainloop()
In the following code, when I press the button to add some secondary windows, and then try to close a window by using "Command-w" it does not always close the active window. But if I disable the menu creation by commenting the line self.gerar_menu(), windows are opened and closed as expected (I mean, by clicking the red 'x' button or by pressing Command-W in OS X). Any idea about what is wrong here?
Here is my current test code:
#!/usr/bin/env python3.6
# encoding: utf-8
import tkinter as tk
import tkinter.font
from tkinter import ttk
class baseApp(ttk.Frame):
"""
Parent classe for main app window (will include some aditional methods and properties).
"""
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
class App(baseApp):
""" Base class for the main application window """
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
#self.gerar_menu() # This line breaks "Command-w" functionality
self.lbl_text = ttk.Label(self.mainframe, text="This is the Main Window")
self.lbl_text.pack()
self.btn = ttk.Button(self.mainframe, text="Open Second window",
command=lambda: self.create_detail_window(self, number=0))
self.btn.pack()
self.newDetailsWindow = {}
self.windows_count=0
def gerar_menu(self):
""" generate the application menu """
self.menu = tk.Menu(root)
root.config(menu=self.menu)
self.fileMenu = tk.Menu(self.menu)
self.menu.add_cascade(label="File", menu=self.fileMenu)
self.fileMenu.add_command(label="New Document", command=None, accelerator="Command+n")
def create_detail_window(self, *event, number=None):
self.windows_count += 1
self.newDetailsWindow[self.windows_count]=tk.Toplevel()
self.newDetailsWindow[self.windows_count].geometry('900x600+80+130')
self.newDetailsWindow[self.windows_count].title(f'Detail: {self.windows_count}')
self.newDetailsWindow[self.windows_count].wm_protocol("WM_DELETE_WINDOW", self.newDetailsWindow[self.windows_count].destroy)
self.newDetailsWindow[self.windows_count].bind("Command-w", lambda event: self.newDetailsWindow[-1].destroy())
self.detail_window = detailWindow(self.newDetailsWindow[self.windows_count], self.windows_count)
self.newDetailsWindow[self.windows_count].focus()
print(self.newDetailsWindow)
class detailWindow(ttk.Frame):
""" Base class for secondary windows """
def __init__(self, master, rep_num, *args,**kwargs):
super().__init__(master,*args,**kwargs)
self.num_rep = rep_num
self.master.minsize(900, 600)
self.master.maxsize(900, 600)
print(f"Showing details about nr. {self.num_rep}")
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
self.lbl_text = ttk.Label(self.mainframe,
text=f"Showing details about nr. {self.num_rep}")
self.lbl_text.pack()
if __name__ == "__main__":
root = tk.Tk()
janela_principal = App(root)
root.title('Main Window')
root.bind_all("<Mod2-q>", exit)
root.mainloop()
If you want to create a binding for closing a window, you need the function to act upon the actual window that received the event. Your code is always deleting the last window that was opened no matter which window received the event.
The first step is to bind to a function rather than using lambda. While lambda has its uses, binding to a named function is much easier to debug and maintain.
Once the function is called, the event object can tell you which window got the event via the widget attribute of the event object. Given this window, you can get the toplevel window that this window is in (or itself, if it's a toplevel window) via the winfo_toplevel command.
For example:
window = tk.Toplevel(...)
...
window.bind("<Command-w>", self.destroy_window)
...
def destroy_window(self, event):
window = event.widget.winfo_toplevel()
window.destroy()
I want to create a GUI that shows a message and it is automatically destroyed after some time. I saw this question in different posts but none of the solutions proposed worked out for my App. Here a small part of the code
class MessageShort(tkSimpleDialog.Dialog):
def __init__(self, parent, text, time):
self.top=Toplevel.__init__(self, parent)
self.transient(parent)
self.parent = parent
self.text=text
self.time=time
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=10, pady=10)
if not self.initial_focus:
self.initial_focus = self
self.geometry("+%d+%d" % (parent.winfo_rootx()+200,
parent.winfo_rooty()+75))
self.initial_focus.focus_set()
self.wait_window(self)
def body(self, master):
m=Message(master, text=self.text).grid(row=0,column=0,sticky=W)
master.after(self.time,master.destroy())
MessageShort(root,"Select date and decimal format",2000)#call from another part to the class to create the GUI message
root = Tk()
app = App(root) #main App
root.mainloop()
The App have different Menus and Tkinter classes to display the different tools
With the current code I close the App and I just want to close the message but not the App
Create a timer and set destroying the root as it's callback:
from threading import Timer
import time
def called_after_timer():
if(root != None):
root.destroy()
t = Timer(20 * 60, called_after_timeout)
t.start()
# do something else, such as
time.sleep(1500)
Finally I got something that seems to work
class MessageShort(tkSimpleDialog.Dialog):
def __init__(self, parent, text, time):
self.top=Toplevel.__init__(self, parent)
self.transient(parent)
self.parent = parent
self.text=text
self.time=time
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=10, pady=10)
if not self.initial_focus:
self.initial_focus = self
self.geometry("+%d+%d" % (parent.winfo_rootx()+200,
parent.winfo_rooty()+75))
self.initial_focus.focus_set()
self.wait_window(self)
def body(self, master):
m=Message(master, text=self.text).grid(row=0,column=0,sticky=W)
master.after(self.time,self.destroy)
MessageShort(root,"Select date and decimal format",2000)#call from another part to the class to create the GUI message
root = Tk()
app = App(root) #main App
root.mainloop()
In the last line self.master.destroy destroys m but not the container itself. So it has to call to self.destroy
I want to script two widgets which would be stacked and I would switch from one to another with a key. Each widget would be a Frame containing several Labels. I have the following code so far (only one Label per Frame):
import Tkinter as tk
import ttk
import datetime
def main():
# initialize root
root = tk.Tk()
# initialize widgets
dash = Dashboard(root)
notepad = Notepad(root)
# set key actions
root.bind('<F11>', root.lift)
root.bind('<F1>', dash.raiseme)
root.bind('<F2>', notepad.raiseme)
root.mainloop()
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.dashframe = tk.Frame(parent)
self.labone = tk.Label(self.dashframe, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.dashframe.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.noteframe = tk.Frame(parent)
self.laboneone = tk.Label(self.noteframe, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.noteframe.lift()
if __name__ == '__main__':
main()
Pressing F1 and F2 reach the correct routines but the only thing I get is the main window, empty. There are no errors displayed so I guess that the code runs fine (just not the way O would like to :)).
Can I achieve the switch using the skeleton above?
There are at least two big problems with your code.
First, you're creating all these new frames, but not placing them anywhere, so they will never show up anywhere. If you have a main window with nothing placed on it, of course you will just "get the main window, empty". You need to call pack or some other layout method on any widget to get it to show up on its parent. In this case, it sounds like you want to put them both in the exact same place, so grid or place is probably what you want.
Second, your Dashboard and Notepad classes are themselves Frames, but they don't do any Frame-ish stuff; instead, they each create another, sibling Frame and attach a label to that sibling. So, even if you packed the Dashboard and Notepad, they're just empty frame widgets, so that wouldn't do any good.
If you fix both of those, I think your code does what you want:
class Dashboard(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # voodoo
self.labone = tk.Label(self, text="lab1", fg='black', bg='blue')
self.labone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme dash"
self.labone.configure(text=datetime.datetime.now())
self.lift()
class Notepad(ttk.Frame):
def __init__(self, parent):
ttk.Frame.__init__(self, parent) # also voodoo
self.laboneone = tk.Label(self, text="lab11", fg='white', bg='red')
self.laboneone.grid(row=0, column=0)
self.grid(row=0, column=0)
def raiseme(self, event):
print "raiseme notepad"
self.laboneone.configure(text=datetime.datetime.now())
self.lift()
However, you might also want to set a fixed size for everything; otherwise you could end up lifting the red widget and, e.g., only covering 96% of the blue one because the current time is a bit narrower than the previous one…
The code you linked to for inspiration attempted to do this:
newFrame = tkinter.Frame(root).grid()
newFrame_name = tkinter.Label(newFrame, text="This is another frame").grid()
That won't work, because grid returns None, not the widget. But at least it calls grid; yours doesn't even do that.