Python Tkinter: Progress bar malfunction when using multi-threading - python

Please run the following example.
I have created a progress bar for my application, and by pressing the "Open" button a progress bar pops up. However, the progress bar does not fill up and it appears that the script is halt at
bar.set(i)
when function ProgressBarLoop is called.
from Tkinter import Tk, Frame, BOTH, Label, Toplevel, Canvas, Button
import thread
import time
class ProgressBar:
def __init__(self, parent, width, height):
master = Toplevel(parent)
master.protocol('WM_DELETE_WINDOW', self.hide )
self.master = master
self.master.overrideredirect(True)
ws = self.master.winfo_screenwidth()
hs = self.master.winfo_screenheight()
w = (True and ws*0.2) or 0.2
h = (True and ws*0.15) or 0.15
x = (ws/2) - (w/2)
y = (hs/2) - (h/2)
self.master.geometry('%dx%d+%d+%d' % (width, height * 2.5, x, y))
self.mode = 'percent'
self.ONOFF = 'on'
self.width = width
self.height = height
self.frame = None
self.canvas = None
self.progressBar = None
self.backgroundBar = None
self.progressformat = 'percent'
self.label = None
self.progress = 0
self.createWidget()
self.frame.pack()
self.set(0.0) # initialize to 0%
def createWidget(self):
self.frame = Frame(self.master, borderwidth = 1, relief = 'sunken')
self.canvas = Canvas(self.frame)
self.backgroundBar = self.canvas.create_rectangle(0, 0, self.width, self.height, fill = 'darkblue')
self.progressBar = self.canvas.create_rectangle(0, 0, self.width, self.height, fill = 'blue')
self.setWidth()
self.setHeight()
self.label = Label(self.frame, text = 'Loading...', width = 20)
self.label.pack(side = 'top') # where text label should be packed
self.canvas.pack()
def setWidth(self, width = None):
if width is not None:
self.width = width
self.canvas.configure(width = self.width)
self.canvas.coords(self.backgroundBar, 0, 0, self.width, self.height)
self.setBar() # update progress bar
def setHeight(self, height = None):
if height is not None:
self.height = height
self.canvas.configure(height = self.height)
self.canvas.coords(self.backgroundBar, 0, 0, self.width, self.height)
self.setBar() # update progress bar
def set(self, value):
if self.ONOFF == 'off': # no need to set and redraw if hidden
return
if self.mode == 'percent':
self.progress = value
self.setBar()
return
def setBar(self):
self.canvas.coords(self.progressBar,0, 0, self.width * self.progress/100.0, self.height)
self.canvas.update_idletasks()
def hide(self):
if isinstance(self.master, Toplevel):
self.master.withdraw()
else:
self.frame.forget()
self.ONOFF = 'off'
def configure(self, **kw):
mode = None
for key,value in kw.items():
if key=='mode':
mode = value
elif key=='progressformat':
self.progressformat = value
if mode:
self.mode = mode
def ProgressBarLoop(window, bar):
bar.configure(mode = 'percent', progressformat = 'ratio')
while(True):
if not window.loading:
break
for i in range(101):
bar.set(i)
print "refreshed bar"
time.sleep(0.001)
bar.hide()
class Application(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.pack(fill = BOTH, expand = True)
parent.geometry('%dx%d+%d+%d' % (100, 100, 0, 0))
Button(parent, text = "Open", command = self.onOpen).pack()
def onOpen(self, event = None):
self.loading = True
bar = ProgressBar(self, width=150, height=18)
thread.start_new_thread(ProgressBarLoop, (self, bar))
while(True):
pass
root = Tk()
Application(root)
root.mainloop()
EDIT:
After trying dano's answer, it works but I get the following error:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/mnt/sdev/tools/lib/python2.7/threading.py", line 551, in __bootstrap_inner
self.run()
File "/mnt/sdev/tools/lib/python2.7/threading.py", line 504, in run
self.__target(*self.__args, **self.__kwargs)
File "/home/jun/eclipse/connScript/src/root/nested/test.py", line 88, in ProgressBarLoop
bar.set(i)
File "/home/jun/eclipse/connScript/src/root/nested/test.py", line 61, in set
self.setBar()
File "/home/jun/eclipse/connScript/src/root/nested/test.py", line 64, in setBar
self.canvas.coords(self.progressBar,0, 0, self.width * self.progress/100.0, self.height)
File "/mnt/sdev/tools/lib/python2.7/lib-tk/Tkinter.py", line 2178, in coords
self.tk.call((self._w, 'coords') + args)))
RuntimeError: main thread is not in main loop

The problem is that you're running an infinite loop immediately after you start the thread:
def onOpen(self, event = None):
self.loading = True
bar = ProgressBar(self, width=150, height=18)
thread.start_new_thread(ProgressBarLoop, (self, bar))
while(True): # Infinite loop
pass
Because of this, control never returns to the Tk event loop, so the progress bar can never be updated. The code should work fine if you remove the loop.
Also, you should use the threading module instead of the thread module, as suggested in the Python docs:
Note The thread module has been renamed to _thread in Python 3. The
2to3 tool will automatically adapt imports when converting your
sources to Python 3; however, you should consider using the high-level
threading module instead.
Putting it altogether, here's what onOpen should look like:
def onOpen(self, event = None):
self.loading = True
bar = ProgressBar(self, width=150, height=18)
t = threading.Thread(target=ProgressBarLoop, args=(self, bar))
t.start()
Edit:
Trying to update tkinter widgets from multiple threads is somewhat finicky. When I tried this code on three different systems, I ended up getting three different results. Avoiding loops in the threaded method helped avoid any errors:
def ProgressBarLoop(window, bar, i=0):
bar.configure(mode = 'percent', progressformat = 'ratio')
if not window.loading:
bar.hide()
return
bar.set(i)
print "refreshed bar"
i+=1
if i == 101:
# Reset the counter
i = 0
window.root.after(1, ProgressBarLoop, window, bar, i)
But really, if we're not using an infinite loop in ProgressBarLoop anyway, there's really no need to use a separate thread at all. This version of ProgressBarLoop can be called in the main thread, and the GUI won't be blocked for any noticeable amount of time.

Related

How to use tkinter protocol("WM_DELETE_WINDOW",function) with a function that runs twice?

This is a function as part of a class to create tkinter Toplevel instances. I am trying to have the X button on each window destroy itself and then create two new windows. Every time I try running this, 'test' is only printed once and only 1 new window will appear. Why is this happening? Thanks!
Here is the class for creating tkinter instances
class App(Toplevel):
nid = 0
def __init__(self, master, title, f, nid):
# Creates Toplevel to allow for sub-windows
Toplevel.__init__(self, master)
self.thread = None
self.f = f
self.nid = nid
self.master = master
self.canvas = None
self.img = None
self.label = None
self.title(title)
self.geometry('300x300')
def window(self):
# Creates play_sound thread for self
self.thread = threading.Thread(target=lambda: play_sound())
# Disables resizing
self.resizable(False, False)
self.img = PhotoImage(file=self.f)
# Creates canvas
self.canvas = Canvas(self, width=300, height=300)
self.canvas.create_image(20, 20, anchor=NW, image=self.img)
self.canvas.pack(fill=BOTH, expand=1)
# Function to move each window to a random spot within the screen bounds
def move(self):
while True:
new_x = random.randrange(0, x)
new_y = random.randrange(0, y)
cur_x = self.winfo_x()
cur_y = self.winfo_y()
dir_x = random.choice(['-', '+'])
dir_y = random.choice(['-', '+'])
# Tests if the chosen position is within the monitor
try:
if (eval(f'{cur_x}{dir_x}{new_x}') in range(0, x)
and eval(f'{cur_y}{dir_y}{new_y}') in range(0, y)):
break
# Prevents crashing if the program happens to exceed the recursion limit
except RecursionError:
pass
# Sets geometry to the new position
self.geometry(f"+{new_x}+{new_y}")
# Repeats every second
self.after(1000, self.move)
# Starts sound thread
def sound(self):
self.thread.start()
# Changes the function of the X button
def new_protocol(self, func):
def run():
#do_twice
def cmd():
print('test')
func()
def both():
self.destroy()
return cmd()
self.protocol('WM_DELETE_WINDOW', both)
run()
Here is the function to create new windows
def create_window():
global num
# Hides root window
root.withdraw()
# Creates a new window with variable name root{num}.
d['root{0}'.format(num)] = App(root, 'HE HE HE HA', r'build\king_image.png', 0)
app_list.append(d['root{0}'.format(num)].winfo_id())
print(d['root{0}'.format(num)])
globals().update(d)
# Starts necessary commands for window
d['root{0}'.format(num)].window()
d['root{0}'.format(num)].move()
d['root{0}'.format(num)].new_protocol(create_window)
d['root{0}'.format(num)].sound()
d['root{0}'.format(num)].mainloop()
num += 1
Here is the decorator function:
def do_twice(func):
#functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
If anyone needs any other parts of my code I will gladly edit this to include them.

Python tkinter threading and window refresh

I'm using python and tkinter to build a visualization tool that can refresh and visualize an updating object. Right now, the object can't change because the threading is not working. Any help or general knowledge would be appreciated. I'm relatively new to threading and tkinter.
example object I want to ingest
class color1:
def __init__(self, color):
self.color = color
def change_col(self, new_color):
self.color = new_color
def pass_col(self):
return(self)
my visualization code
class my_visual(threading.Thread):
def __init__(self, col1):
threading.Thread.__init__(self)
self.start()
self.col1 = col1
def viz(self):
self.root = Tk()
btn1 = Button(self.root, text = 'Refresh', command = self.refresh)
btn1.pack()
frame = Frame(self.root, width = 100, height = 100, bg = self.col1.color)
frame.pack()
btn2 = Button(self.root, text = 'Close', command = self.exit)
btn2.pack()
self.root.mainloop()
def refresh(self):
self.root.quit()
self.root.destroy()
self.col1 = self.col1.pass_col()
self.viz()
def exit(self):
self.root.quit()
self.root.destroy()
Code that works
c = color1('RED')
test = my_visual(c)
test.viz()
Code that doesn't work
In this version, the refresh works, but the threading doesn't. When the threading is working, the refresh won't pick up that the object has changed.
c.change_col('BLUE')
If you extend the threading.Thread class you need to override the run() method with your custom functionality. With no run method, the thread dies immediately. You can test whether a thread is alive with my_visual.is_alive().
The problem is that your test.viz() is an infinite loop because of self.root.mainloop(), so you cannot do anything once you called that function. The solution is to use a thread for test.viz(), and your thread for the class my_visual is no more necessary.
I added a time.sleep of 2 seconds before the refresh makes it blue, otherwise the color is blue at beginning.
Here you go :
import threading
from tkinter import *
import time
class color1:
def __init__(self, color):
self.color = color
def change_col(self, new_color):
self.color = new_color
def pass_col(self):
return(self)
class my_visual():
def __init__(self, col1):
self.col1 = col1
def viz(self):
self.root = Tk()
btn1 = Button(self.root, text = 'Refresh', command = self.refresh)
btn1.pack()
frame = Frame(self.root, width = 100, height = 100, bg = self.col1.color)
frame.pack()
btn2 = Button(self.root, text = 'Close', command = self.exit)
btn2.pack()
self.root.mainloop()
def refresh(self):
self.root.quit()
self.root.destroy()
self.col1 = self.col1.pass_col()
print("self.col1", self.col1, self.col1.color)
self.viz()
def exit(self):
self.root.quit()
self.root.destroy()
c = color1('RED')
test = my_visual(c)
t2 = threading.Thread(target = test.viz)
t2.start()
time.sleep(2)
print('Now you can change to blue when refreshing')
c.change_col('BLUE')

Why does tkinter's after() function freeze my window?

I am creating a replica of dodger using tkinter. I am facing a problem with timing object movement. I was told the time module does not work well with tkinter, therefore I should use after() instead. However, I face the same problem with the after() function as I did with the time module. Here is my code:
from tkinter import *
from random import randint
class Window(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.master = master
self.initWindow()
def initWindow(self):
self.master.title('Dodger')
self.pack(fill=BOTH, expand=1)
self.master.geometry('600x800')
self.master.config(bg='black')
menu = Menu(self.master)
self.master.config(menu=menu)
def clientExit():
exit()
file = Menu(menu)
file.add_command(label='Exit', command=clientExit)
file.add_command(label='Start', command=self.game)
menu.add_cascade(label='File', menu=file)
def game(self):
canvas = Canvas(self.master, width='600', height='800', borderwidth='0', highlightthickness='0')
canvas.pack()
canvas.create_rectangle(0, 0, 600, 800, fill='black', outline='black')
character = canvas.create_rectangle(270, 730, 330, 760, fill='magenta', outline='cyan', width='2')
def left(event):
cord = canvas.coords(character)
if cord[0] <= 5:
pass
else:
canvas.move(character, -10, 0)
def right(event):
cord = canvas.coords(character)
if cord[2] >= 595:
pass
else:
canvas.move(character, 10, 0)
self.master.bind('<Left>', left)
self.master.bind('<Right>', right)
class variables:
sizeMin = 10
sizeMax = 80
y = 10
minX = 5
maxX = 545
def createShape():
size = randint(variables.sizeMin, variables.sizeMax)
x = randint(variables.minX, variables.maxX)
topLeft = [x, variables.y]
bottomRight = [x + size, variables.y + size]
shape = canvas.create_rectangle(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1],
fill='red', outline='red')
return shape
def moveShape(shape):
canvas.move(shape, 0, 800)
for x in range(5):
x = createShape()
self.master.after(1000, moveShape(x))
root = Tk()
app = Window(root)
app.mainloop()
As you can see, at the bottom of the game instance, I created a square and moved it down five times at 1 second intervals. However, this did not work; my window just froze for the allotted time, then resumed afterwards. I am not sure if this is because my computer sucks or if I did something wrong. Please run my code in you editor and explain to me if I did something wrong.
The reason it freezes is because you're calling after wrong.
Consider this code:
self.master.after(1000, moveShape(x))
... it is exactly the same as this code:
result = moveShape(x)
self.master.after(1000, result)
... which is the same as this, since moveShape returns None:
result = moveShape(x)
self.master.after(1000, None)
... which is the same as this:
result = moveShape(x)
self.master.after(1000)
... which is the same as
result = moveShape(x)
time.sleep(1)
In other words, you're telling it to sleep, so it sleeps.
after requires a callable, or a reference to a function. You can pass additional args as arguments to after. So the proper way to call it is this:
self.master.after(1000, moveShape, x)
Though, I doubt that is exactly what you want, since all five iterations will try to run the code 1000ms after the loop starts, rather than 1000ms apart. That's just a simple matter of applying a little math.
You need to replace the sleep function and the loop with after. Try this:
def moveShape():
if self.step < 5:
canvas.move(shape, 0, 10)
self.master.after(1000, moveShape)
self.step += 1
self.step = 0
shape = createShape()
moveShape()
Also, if you move it by 800 pixels you won't see it after the first tick, so I reduced the amount to move to 10 pixels.
Edit: this plus a lot of other bugfixes and improvements:
import tkinter as tk
from random import randint
class variables:
sizeMin = 10
sizeMax = 80
y = 10
minX = 5
maxX = 545
class Window(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.master = master
self.initWindow()
def initWindow(self):
self.master.title('Dodger')
self.pack(fill=tk.BOTH, expand=1)
self.master.geometry('600x800')
self.master.config(bg='black')
menu = tk.Menu(self.master)
self.master.config(menu=menu)
file = tk.Menu(menu)
file.add_command(label='Exit', command=self.quit)
file.add_command(label='Start', command=self.game)
menu.add_cascade(label='File', menu=file)
def game(self):
self.canvas = tk.Canvas(self.master, width='600', height='800', borderwidth='0', highlightthickness='0')
self.canvas.pack()
self.canvas.create_rectangle(0, 0, 600, 800, fill='black', outline='black')
self.character = self.canvas.create_rectangle(270, 730, 330, 760, fill='magenta', outline='cyan', width='2')
self.createShape()
self.moveShape() # start the moving
self.master.bind('<Left>', self.left)
self.master.bind('<Right>', self.right)
def left(self, event):
cord = self.canvas.coords(self.character)
if cord[0] <= 5:
pass
else:
self.canvas.move(self.character, -10, 0)
def right(self, event):
cord = self.canvas.coords(self.character)
if cord[2] >= 595:
pass
else:
self.canvas.move(self.character, 10, 0)
def createShape(self):
size = randint(variables.sizeMin, variables.sizeMax)
x = randint(variables.minX, variables.maxX)
topLeft = [x, variables.y]
bottomRight = [x + size, variables.y + size]
self.shape = self.canvas.create_rectangle(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1],
fill='red', outline='red')
def moveShape(self, x=0):
if x < 5: # loop 5 times
self.canvas.move(self.shape, 0, 10)
self.after(1000, self.moveShape, x+1) # run this method again in 1,000 ms
root = tk.Tk()
app = Window(root)
app.mainloop()

Tkinter <enter> and <leave> Events Not Working Properly

Forgive me in advance for my code, but I'm simply making this for friend of mine to automatically populate a GUI interface with song information, channel information for each song, and things such as images attached to the songs. Right now I'm only scraping from a playlist on Youtube and a playlist on Soundcloud. I have all of that properly working, but me being new to frontend development left me in a horrible spot to make a decent application for him. I had a lot in mind that I could have done, but now I'm simply creating buttons with each song title as the text. Here is an image of my progress. I still have to find a way to attach each image to each button for the on_enter event, but that is for later on. As you can see, I have a on_leave function commented out. I was using that to delete the self.image_window each time I left a button. Problem is even a minuscule amount of mouse movement would cause the window to be delete and recreated dozens of times. How do I make it static so when I am hovering over a button it doesn't spam create/delete the window?
Thanks!
from Tkinter import *
import json
import os
import webbrowser
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.column = 0
self.row = 0
def on_enter(self, event):
self.image_window = Toplevel(self)
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.img = PhotoImage(file=self.img_path)
#self.image_window.minsize(width=200, height=250)
self.image_window.title("Preview")
canvas = Canvas(self.image_window, width=200, height=200)
canvas.pack()
canvas.create_image(100, 100, image=self.img)
#def on_leave(self, enter):
def addTab(self, id):
tabslen = len(self.tabs)
tab = {}
if self.row < 30:
btn = Button(self, text=id,highlightbackground='PeachPuff2' ,command=lambda: self.raiseTab(id))
btn.grid(row=self.row, column=self.column, sticky=W+E)
btn.bind("<Enter>", self.on_enter)
#btn.bind("<Leave>", self.on_leave)
tab['id']=id
tab['btn']=btn
self.tabs[tabslen] = tab
self.raiseTab(id)
self.row +=1
else:
self.row = 0
self.column +=1
btn = Button(self, text=id,highlightbackground='PeachPuff2' ,command=lambda: self.raiseTab(id))
btn.grid(row=self.row, column=self.column, sticky=W+E)
tab['id']=id
tab['btn']=btn
self.tabs[tabslen] = tab
self.raiseTab(id)
def raiseTab(self, tabid):
with open(os.getcwd() + '/../PlaylistListener/CurrentSongs.json') as current_songs:
c_songs = json.load(current_songs)
print(tabid)
if self.curtab!= None and self.curtab != tabid and len(self.tabs)>1:
try:
#webbrowser.open(c_songs[tabid]['link'])
webbrowser.open_new('http://youtube.com')
except:
pass
def main():
root = Tk()
root.title('Playlist Scraper')
root.geometry("1920x1080+300+300")
t = GUIPopulator(root)
with open(os.getcwd() + '/../PlaylistListener/CurrentSongs.json') as current_songs:
c_songs = json.load(current_songs)
for song in c_songs:
t.addTab(song)
root.mainloop()
if __name__ == '__main__':
main()
Example of JSON file provided:
{
"F\u00d8RD - Shadows (feat. Samsaruh)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=CNiV6Pne50U&index=32&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "CNiV6Pne50U",
"channel": "youtube.com/watch?v=CNiV6Pne50U&index=32&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/CNiV6Pne50U/hqdefault.jpg",
"CNiV6Pne50U.jpg"
]
},
"Katelyn Tarver - You Don't Know (tof\u00fb remix)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=7pPNv38JzD4&index=43&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "7pPNv38JzD4",
"channel": "youtube.com/watch?v=7pPNv38JzD4&index=43&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/7pPNv38JzD4/hqdefault.jpg",
"7pPNv38JzD4.jpg"
]
},
"Illenium - Crawl Outta Love (feat. Annika Wells)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=GprXUDZrdT4&index=7&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "GprXUDZrdT4",
"channel": "youtube.com/watch?v=GprXUDZrdT4&index=7&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/GprXUDZrdT4/hqdefault.jpg",
"GprXUDZrdT4.jpg"
]
}
}
After some testing I have come up with some code I think you can use or is what you are looking for.
I have changes a few things and add some others.
1st we needed to create a place holder for the top window that we can use later in the code. So in the __init__ section of GUIPopulatior add self.image_window = None. We will talk about this part soon.
next I created the image path as a class attribute on init this can be changed later with update if need be.
next I added a bind() to the init that will help us keep the self.image_window placed in the correct location even if we move the root window. so we add self.parent.bind("<Configure>", self.move_me) to the __init__ section as well.
next we create the method move_me that we just created a bind for.
the way it is written it will only take effect if self.image_window is not equal to None this should prevent any errors while self.image_window is being used however I have not created the error handling to deal with what happens after the toplevel window is closed by the user. Its not difficult but I wanted to answer for the main issue at hand.
Here is the move_me method:
def move_me(self, event):
if self.image_window != None:
h = self.parent.winfo_height() # gets the height of the window in pixels
w = self.parent.winfo_width() # gets the width of the window in pixels
# gets the placement of the root window then uses height and width to
# calculate where to place the window to keep it at the bottom right.
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
next we need to modify the on_enter method to create the toplevel window if out class attribute self.image_window is equal to None if it is not equal to None then we can use the else portion of the if statement to just update the image.
Here is the modified on_enter method:
def on_enter(self, event):
if self.image_window == None:
self.image_window = Toplevel(self)
#this keeps the toplevel window on top of the program
self.image_window.attributes("-topmost", True)
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
self.img = PhotoImage(file=self.img_path)
self.image_window.title("Preview")
self.canvas = Canvas(self.image_window, width=200, height=200)
self.canvas.pack()
self.canv_image = self.canvas.create_image(100, 100, image=self.img)
else:
self.img = PhotoImage(file= self.img_path)
self.canvas.itemconfig(self.canv_image, image = self.img)
all that being said there are some other issues with your code that need to be addressed however this answer should point you in the right direction.
Below is a section of your code you need to replace:
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.column = 0
self.row = 0
def on_enter(self, event):
self.image_window = Toplevel(self)
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.img = PhotoImage(file=self.img_path)
#self.image_window.minsize(width=200, height=250)
self.image_window.title("Preview")
canvas = Canvas(self.image_window, width=200, height=200)
canvas.pack()
canvas.create_image(100, 100, image=self.img)
#def on_leave(self, enter):
With this:
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.image_window = None
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.parent.bind("<Configure>", self.move_me)
self.column = 0
self.row = 0
def move_me(self, event):
if self.image_window != None:
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
def on_enter(self, event):
if self.image_window == None:
self.image_window = Toplevel(self)
self.image_window.attributes("-topmost", True)
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
self.img = PhotoImage(file=self.img_path)
self.image_window.title("Preview")
self.canvas = Canvas(self.image_window, width=200, height=200)
self.canvas.pack()
self.canv_image = self.canvas.create_image(100, 100, image=self.img)
else:
self.img = PhotoImage(file= self.img_path)
self.canvas.itemconfig(self.canv_image, image = self.img)

configure tkinter object from separate class

I have a simple program with two classes, one controls a relay board via a serial.serial connection. The other class is for a GUI which will send commands to the relay class and then display the status of the relay board.
I'm having an issue with sending messages from the relay class to the tkinter class. The messages only appear once the relay command has finished. I've cut down my program below. Test.test() represents a function in my relay class where as the MainWindow class is my GUI.
Someone has pointed out to use threading to handle the messages being passed between the classes. Is that my only option? I have not delved into threading yet.
from Tkinter import *
import time
import ScrolledText
class Test():
def test(self):
main.textboxupdate(" test start ")
time.sleep(2)
main.textboxupdate(" test middle ")
time.sleep(2)
main.textboxupdate(" test end ")
class MainWindow(Frame):
def __init__(self, *args, **kwargs):
Frame.__init__(self, *args, **kwargs)
self.canvas = Canvas(width=1200,height=700)
self.canvas.pack(expand=YES,fill=BOTH)
self.frame = Frame(self.canvas)
self.TextBox = ScrolledText.ScrolledText(self.frame)
self.open = Button(self.frame, text="Open Cover",
command=test.test)
def createtextbox(self, statusmsg):
self.frame.place(x=0,y=0)
self.TextBox.config(state = NORMAL)
self.TextBox.insert(END, statusmsg,('error'))
self.TextBox.config(state = 'disabled', height = 2, width = 35)
self.TextBox.see(END)
self.TextBox.grid(columnspan=2, rowspan = 1)
self.open.grid()
def textboxupdate(self, statusmsg):
statusmsg = statusmsg +'\n'
self.TextBox.config(state = NORMAL)
self.TextBox.insert(END, statusmsg,('error'))
self.TextBox.config(state = 'disabled', height = 10, width = 50)
self.TextBox.see(END)
test = Test()
root = Tk()
main = MainWindow(root)
main.createtextbox('Startup\n')
root.mainloop()
Here's one option:
from Tkinter import *
import time
import ScrolledText
import threading, Queue
class Test():
def __init__(self):
self.msg_queue = Queue.Queue()
def test(self):
self.msg_queue.put(" test start ")
time.sleep(2)
self.msg_queue.put(" test middle ")
time.sleep(2)
self.msg_queue.put(" test end ")
class MainWindow(Frame):
def __init__(self, *args, **kwargs):
Frame.__init__(self, *args, **kwargs)
self.canvas = Canvas(width=1200,height=700)
self.canvas.pack(expand=YES,fill=BOTH)
self.frame = Frame(self.canvas)
self.TextBox = ScrolledText.ScrolledText(self.frame)
self.open = Button(self.frame, text="Open Cover",
command=self.create_thread)
self.test_thread = None
self.createtextbox("")
def create_thread(self):
self.test_thread = threading.Thread(target=test.test)
self.test_thread.start()
self.after(10, self.update_textbox)
def update_textbox(self):
while not test.msg_queue.empty():
self.textboxupdate(test.msg_queue.get())
if self.test_thread.is_alive():
self.after(10, self.update_textbox)
else:
self.test_thread = None
def createtextbox(self, statusmsg):
self.frame.place(x=0,y=0)
self.TextBox.config(state = NORMAL)
self.TextBox.insert(END, statusmsg,('error'))
self.TextBox.config(state = 'disabled', height = 2, width = 35)
self.TextBox.see(END)
self.TextBox.grid(columnspan=2, rowspan = 1)
self.open.grid()
def textboxupdate(self, statusmsg):
statusmsg = statusmsg +'\n'
self.TextBox.config(state = NORMAL)
self.TextBox.insert(END, statusmsg,('error'))
self.TextBox.config(state = 'disabled', height = 10, width = 50)
self.TextBox.see(END)
self.update_idletasks()
test = Test()
main = MainWindow()
main.pack()
main.mainloop()
The first change is that instead of calling a function, Test.test puts the messages onto a queue. Test.test is started in a separate thread by MainWindow.start_thread. MainWindow.start_thread also schedules a check on the thread by asking tkinter to call update_textbox after 10 milliseconds (self.after(10, self.update_textbox)). This function takes all the new messages off the queue, and displays them. Then, if the thread is still running, it reschedules itself, otherwise it resets the MainWindow.

Categories

Resources