python threading and tkinter.tix.NoteBook tabs not accessible beyond first open tab
In the following example I'm unable to write to the other tabs aside from the open-by-default Log tab:
#!/usr/bin/python
import os, sys
import threading
import time
from itertools import count
import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk
TCL_DONT_WAIT = 1<<1
TCL_WINDOW_EVENTS = 1<<2
TCL_FILE_EVENTS = 1<<3
TCL_TIMER_EVENTS = 1<<4
TCL_IDLE_EVENTS = 1<<5
TCL_ALL_EVENTS = 0
class GUI(threading.Thread):
def __init__(self):
""" thread 开发者_StackOverflowinit
defines some vars and starts stuff when the class is called (gui=GUI())
"""
self.root=tkinter.tix.Tk()
z = self.root.winfo_toplevel()
z.wm_title('minimal example')
if z.winfo_screenwidth() <= 800:
z.geometry('790x590+10+10')
else:
z.geometry('890x640+10+10')
frame1 = self.MkMainNotebook()
frame2 = self.MkMainStatus()
frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
frame2.pack(side=BOTTOM, fill=X)
z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())
threading.Thread.__init__(self)
def run(self):
""" thread start
kick starts the main loop when the thread start()
"""
self.root.mainloop()
def stop(self):
""" escape plan
Exits gui thread
"""
self._stop()
raise SystemExit
def MkMainStatus(self):
""" status bar
"""
top = self.root
w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)
self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())
self.exitbutton.grid(row=0, column=0, sticky=E, padx=3, pady=3)
return w
def MkMainNotebook(self):
""" the tabs frame
defines the tabs
"""
top = self.root
w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
tagPadX 6
tagPadY 4
borderWidth 2
""")
top['bg'] = w['bg']
w.add('log', label='Log', underline=0,
createcmd=lambda w=w, name='log': self.MkLog(w, name))
w.add('pro', label='Progress', underline=0,
createcmd=lambda w=w, name='pro': self.MkProgress(w, name))
w.add('set', label='Settings', underline=0,
createcmd=lambda w=w, name='set': self.MkSettings(w, name))
return w
def MkSettings(self, nb, name):
""" TODO: settings tab
"""
w = nb.page(name)
options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.SettingsFrame = settings_scr_win.window
def MkProgress(self, nb, name):
""" TODO: progress tab
"""
w = nb.page(name)
options = "label.padX 4"
progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.ProgressFrame = progress_scr_win.window
def MkLog(self, nb, name):
""" log
"""
w = nb.page(name)
options = "label.padX 4"
log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.LogFrame = log_scr_win.window
def main(argv):
""" main function
Keyword arguments:
args[0] --
args[1] --
Returns:
None
"""
#GUI
gui=GUI()
gui.start()
time.sleep(1) # give it a sec to draw the gui...
tix.Label(gui.LogFrame, text=("log")).pack()
tix.Label(gui.SettingsFrame, text=("settings")).pack()
tix.Label(gui.ProgressFrame, text=("progress")).pack()
return None
if __name__ == "__main__":
sys.exit(main(sys.argv))
The console traceback:
Traceback (most recent call last):
File "C:\minimal example.py", line 132, in <module>
sys.exit(main(sys.argv))
File "C:\minimal example.py", line 126, in main
tix.Label(gui.SettingsFrame, text=("settings")).pack()
AttributeError: 'GUI' object has no attribute 'SettingsFrame'
This happens whenever I try to create widgets in gui.SettingsFrame or gui.ProgressFrame. If I increase the time.sleep(1) in main() and click through the tabs before the tix.Label part starts, the code works since now the tabs command were invoked.
So, how do I declare the gui.SettingsFrame and gui.ProgressFrame in advance ? can I pass through the tabs in the code before I reach the tix.Label in main() ?
Note: I need it threaded, it's part of a larger program that does a dozen different things and is both multi threaded and has several processes.
Thanks
Edit 1: I could add methods to the class instead of referring to the frames:
def print_log(self, text):
""" log printer
"""
tix.Label(self.LogFrame, text=(text)).pack()
def print_progress(self, text):
""" log printer
"""
tix.Label(self.ProgressFrame, text=(text)).pack()
def print_settings(self, text):
""" log printer
"""
tix.Label(self.SettingsFrame, text=(text)).pack()
and call them in main:
#tix.Label(gui.LogFrame, text=("log")).pack()
gui.print_log("log")
#tix.Label(gui.SettingsFrame, text=("settings")).pack()
gui.print_settings("settings")
#tix.Label(gui.ProgressFrame, text=("progress")).pack()
gui.print_progress("progress")
but the results are the same:
Traceback (most recent call last):
File "C:\minimal example.py", line 150, in <module>
sys.exit(main(sys.argv))
File "C:\minimal example.py", line 143, in main
gui.print_settings("settings")
File "C:\minimal example.py", line 124, in print_settings
tix.Label(self.SettingsFrame, text=(text)).pack()
AttributeError: 'GUI' object has no attribute 'SettingsFrame'
Edit 2: a very nice quick and easy fix by Bryan Oakley :
#!/usr/bin/python
import os, sys
import threading
import time
from itertools import count
import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk
TCL_DONT_WAIT = 1<<1
TCL_WINDOW_EVENTS = 1<<2
TCL_FILE_EVENTS = 1<<3
TCL_TIMER_EVENTS = 1<<4
TCL_IDLE_EVENTS = 1<<5
TCL_ALL_EVENTS = 0
class GUI(threading.Thread):
def __init__(self):
""" thread init
defines some vars and starts stuff when the class is called (gui=GUI())
"""
self.root=tkinter.tix.Tk()
z = self.root.winfo_toplevel()
z.wm_title('minimal example')
if z.winfo_screenwidth() <= 800:
z.geometry('790x590+10+10')
else:
z.geometry('890x640+10+10')
frame1 = self.MkMainNotebook()
frame2 = self.MkMainStatus()
frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
frame2.pack(side=BOTTOM, fill=X)
z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())
threading.Thread.__init__(self)
def run(self):
""" thread start
kick starts the main loop when the thread start()
"""
self.root.mainloop()
def stop(self):
""" escape plan
Exits gui thread
"""
self._stop()
raise SystemExit
def MkMainStatus(self):
""" status bar
"""
top = self.root
w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)
self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())
self.exitbutton.grid(row=0, column=0, sticky=E, padx=3, pady=3)
return w
def MkMainNotebook(self):
""" the tabs frame
defines the tabs
"""
top = self.root
w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
tagPadX 6
tagPadY 4
borderWidth 2
""")
top['bg'] = w['bg']
w.add('log', label='Log', underline=0)
#createcmd=lambda w=w, name='log': self.MkLog(w, name))
w.add('pro', label='Progress', underline=0)
#createcmd=lambda w=w, name='pro': self.MkProgress(w, name))
w.add('set', label='Settings', underline=0)
#createcmd=lambda w=w, name='set': self.MkSettings(w, name))
#log_it = w.subwidget('log')
#pro_it = w.subwidget('pro')
#set_it = w.subwidget('set')
self.MkLog(w, 'log')
self.MkProgress(w, 'pro')
self.MkSettings(w, 'set')
return w
def MkSettings(self, nb, name):
""" TODO: settings tab
"""
w = nb.page(name)
options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.SettingsFrame = settings_scr_win.window
def MkProgress(self, nb, name):
""" TODO: progress tab
"""
w = nb.page(name)
options = "label.padX 4"
progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.ProgressFrame = progress_scr_win.window
def MkLog(self, nb, name):
""" log
"""
w = nb.page(name)
options = "label.padX 4"
log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.LogFrame = log_scr_win.window
def print_log(self, text):
""" log printer
"""
tix.Label(self.LogFrame, text=(text)).pack()
def print_progress(self, text):
""" log printer
"""
tix.Label(self.ProgressFrame, text=(text)).pack()
def print_settings(self, text):
""" log printer
"""
tix.Label(self.SettingsFrame, text=(text)).pack()
def main(argv):
""" main function
Keyword arguments:
args[0] --
args[1] --
Returns:
None
"""
#GUI
gui=GUI()
gui.start()
time.sleep(1) # give it a sec to draw the gui...
tix.Label(gui.LogFrame, text=("log")).pack()
#gui.print_log("log")
tix.Label(gui.SettingsFrame, text=("settings")).pack()
#gui.print_settings("settings")
tix.Label(gui.ProgressFrame, text=("progress")).pack()
#gui.print_progress("progress")
return None
if __name__ == "__main__":
sys.exit(main(sys.argv))
Thanks !
Edit 3: The following is a thread safe implementation. I added a bonus timer:
#!/usr/bin/python
import os, sys
import threading
import queue
from queue import Empty
import time
from itertools import count
import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk
TCL_DONT_WAIT = 1<<1
TCL_WINDOW_EVENTS = 1<<2
TCL_FILE_EVENTS = 1<<3
TCL_TIMER_EVENTS = 1<<4
TCL_IDLE_EVENTS = 1<<5
TCL_ALL_EVENTS = 0
class GUI(threading.Thread):
def __init__(self):
""" thread init
defines some vars and starts stuff when the class is called (gui=GUI())
"""
self.root=tkinter.tix.Tk()
z = self.root.winfo_toplevel()
z.wm_title('minimal example')
if z.winfo_screenwidth() <= 800:
z.geometry('790x590+10+10')
else:
z.geometry('890x640+10+10')
frame1 = self.MkMainNotebook()
frame2 = self.MkMainStatus()
frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
frame2.pack(side=BOTTOM, fill=X)
z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())
threading.Thread.__init__(self)
def run(self):
""" thread start
kick starts the main loop when the thread start()
"""
self.root.mainloop()
def stop(self):
""" escape plan
Exits gui thread
"""
self._stop()
raise SystemExit
def MkMainStatus(self):
""" status bar
"""
top = self.root
w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)
self.status = tkinter.tix.Label(w, anchor=E, bd=1)
self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())
self.print_queue=queue.Queue()
self.print_label()
self.status.grid(row=0, column=0, sticky=W, padx=3, pady=3)
self.exitbutton.grid(row=0, column=1, sticky=E, padx=3, pady=3)
return w
def print_label(self):
""" listner
listner
"""
rate=0.5 # seconds to re-read queue; 0.5=half a second, 1=a full second
counter = count(0, rate)
def update_func():
secs= str(counter.__next__())
try:
self.status.config(text=str("%s(secs): Processing queue..." % (secs.split('.'))[0]), fg=str("red"))
a = tix.Label(self.LogFrame, text=(self.print_queue.get(False)))
except Empty:
self.status.config(text=str("%s(secs): Waiting for queue..." % (secs.split('.'))[0]), fg=str("black"))
self.status.after(int(rate*1000), update_func)
else:
a.pack()
a.after(int(rate*1000), update_func)
update_func()
def MkMainNotebook(self):
""" the tabs frame
defines the tabs
"""
top = self.root
w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
tagPadX 6
tagPadY 4
borderWidth 2
""")
top['bg'] = w['bg']
w.add('log', label='Log', underline=0)
self.MkLog(w, 'log')
w.add('pro', label='Progress', underline=0)
self.MkProgress(w, 'pro')
w.add('set', label='Settings', underline=0)
self.MkSettings(w, 'set')
return w
def MkSettings(self, nb, name):
""" TODO: settings tab
"""
w = nb.page(name)
options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.SettingsFrame = settings_scr_win.window
def MkProgress(self, nb, name):
""" TODO: progress tab
"""
w = nb.page(name)
options = "label.padX 4"
progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.ProgressFrame = progress_scr_win.window
def MkLog(self, nb, name):
""" log
"""
w = nb.page(name)
options = "label.padX 4"
log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
self.LogFrame = log_scr_win.window
def main(argv):
""" main function
Keyword arguments:
args[0] --
args[1] --
Returns:
None
"""
#GUI
gui=GUI()
gui.start()
gui.print_queue.put("log")
time.sleep(10) # timed release test
gui.print_queue.put("timed release test")
return None
if __name__ == "__main__":
sys.exit(main(sys.argv))
It's probably the simplest method for implementing a threaded tkinter gui to a console application. The gui.print_queue.put() replaces the print() and the rate the gui updates can be modified in print_label(self) by adjusting the rate variable.
Enjoy !
For one, Tkinter is not thread safe. You can't access the tk widgets from more than one thread. Doing so yields unpredictable behavior (though one can usually predict that it will cause crashes).
As for the other question -- how to declare gui.SettingsFrame
in advance -- I'm not sure how to answer it without stating the obvious. To "declare" it you must create it, and to create it you must call the method that creates it. And you must do this before using gui.SettingsFrame
as an argument to some other method.
Why are you creating a label in a frame from outside of the method where the frame is created? I think the solution to that problem is to move the creation of the "settings" label from Main
to MkSettings
(and likewise for the "progress" and "log" labels).
精彩评论