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

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()

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.

TKinter dealing with multiple monitors and image display

I have functioning code but there are a few things which I would like to change about it but don't know how to so thought i'd ask here. My code is as follows:
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#Define the target, source and output arrays. Source has to be completely white otherwise it kills everything
def initialize(x,y):
xarr = np.zeros(x)
yarr = np.zeros(y)
target = np.meshgrid(xarr,yarr)
target = target[0]
source = np.meshgrid(xarr,yarr)
source = source[0]
output = np.meshgrid(xarr,yarr)
output = output[0]
for i in range(x):
for n in range(y):
source[n][i] = 1
return target, source, output
# creates trap between XTrapMin-XTrapMax and YTrapMin-YTrapMax on Array
def trap(xtmi,xtma,xs,ytmi,ytma,ys,array):
for i in range(xs):
if xtmi < i < xtma:
for n in range(ys):
if ytmi < n < ytma:
array[n][i] = 255
return
#Returns the amplitude of a complex number
def Amplitude(x):
if isinstance(x, complex):
return np.sqrt(x.real**2+x.imag**2)
else:
return np.abs(x)
#Returns the phase of a complex number
def Phase(z):
return np.angle(z)
#Main GS algorithm implementation using numpy FFT package
#performs the GS algorithm to obtain a phase distribution for the plane, Source
#such that its Fourier transform would have the amplitude distribution of the plane, Target.
def GS(target,source):
A = np.fft.ifft2(target)
for i in range(5):
B = Amplitude(source) * np.exp(1j * Phase(A))
C = np.fft.fft2(B)
D = Amplitude(target) * np.exp(1j * Phase(C))
A = np.fft.ifft2(D)
output = Phase(A)
return output
#Make array into PIL Image
def mkPIL(array):
im = Image.fromarray(np.uint8(array))
return im
def up():
global ytmi
global ytma
ytmi -= 10
ytma -= 10
return
def down():
global ytmi
global ytma
ytmi += 10
ytma += 10
return
def right():
global xtmi
global xtma
xtmi += 10
xtma += 10
return
def left():
global xtmi
global xtma
xtmi -= 10
xtma -= 10
return
xtmi = 125
xtma = 130
xs = 1024
ytmi = 0
ytma = 5
ys = 768
root = tk.Tk()
root.attributes('-fullscreen', True)
def main():
app = Lower(root)
root.mainloop()
class Lower:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master).pack()
self.displayimg = tk.Button(self.frame, text = 'Display', width = 25, command = self.plot)
self.displayimg.pack()
self.makewidg()
def makewidg(self):
self.fig = plt.figure(figsize=(100,100), frameon=False) #changing figsize doesnt cange the size of the plot display
self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
self.fig.tight_layout()
self.ax = self.fig.add_subplot(111)
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas = FigureCanvasTkAgg(self.fig, master=self.master)
self.canvas.get_tk_widget().pack(expand=True)
self.canvas.figure.tight_layout()
self.canvas.draw()
self.new_window()
def new_window(self):
self.newWindow = tk.Toplevel()
self.app = Display(self.newWindow)
def plot(self):
global xtmi, xtma, xs, ytmi, ytma, ys, i
target,source,output=initialize(xs,ys)
trap(xtmi,xtma,xs,ytmi,ytma,ys,target)
output = GS(target,source)
self.ax.imshow(output, cmap='gray')
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas.draw()
self.ax.clear()
def kill(self):
root.destroy()
class Display:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.frame.pack()
self.up = tk.Button(self.frame, text = 'Up', width = 25, command = up)
self.up.pack()
self.down = tk.Button(self.frame, text = 'Down', width = 25, command = down)
self.down.pack()
self.right = tk.Button(self.frame, text = 'Right', width = 25, command = right)
self.right.pack()
self.left = tk.Button(self.frame, text = 'Left', width = 25, command = left)
self.left.pack()
self.kill = tk.Button(self.frame, text = 'Kill', width = 25, command = self.kill)
self.kill.pack()
def kill(self):
root.destroy()
main()
Currently the button displayimg from the class Lower is displayed above the image, is there a way in which I can have the display button on the Display class and still have it manipulate the image on the Lower screen? Also, I intend to display the window opened by Lower on a separate monitor, but can't drag it seeing as it is fullscreen, is there a way around that I can get it on my second monitor?
I try that as such:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = Lower.plot(Lower))
self.displayimg.pack()
But this causes a misreference I think as I get an error code
AttributeError: type object 'Lower' has no attribute 'ax'
Call to Lower.plot
You are using Lower.plot as your button command. It needs one argument, self which must be an instance of Lower - so Lower.plot(Lower) is passing a class where an instance is expected. Instead you need to use the app instance you've made, and call app.plot(). The arguement self is automatically the instance itself, this is fundamental to OOP in python. Calling the method on an instance passes self as the first arg, so it's missing from the call. Calling Lower.plot(...) is calling the method on the class Lower, so there is no instance, and you have to supply your own. I'd avoid calling methods without an instance like this in this situation, and use your app instance.
Command for the display button
Your button creation becomes something like:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = app.plot)
If you need to pass additional args to plot, you need to delay the function call so it happens on click, not on creation of the button. You can use lambda : app.plot("red", 10, whatever) to make a nameless function, taking no arguments, that when called will go on to call app.plot with the given args.
Positioning the window
You can control the position of the app window using wm_geometry:
app.wm_geometry("200x200+100+500")
Will cause the app window to be 200px by 200px, positioned 100px left and 500px down from the origin, and on a windows machine, this is the top left corner of your primary monitor. You can keep the width and height the same and just move the window with eg
app.wm_geometry("+100+500")
You can use more scripting to build the string +{xpos}+{ypos} with whichever values you like to match your desktop layout.

Does pyautogui.keydown() not register as an event in python's bind() function?

I am making a game in which you dodge falling object by using the tkinter library. In my code, I am trying to make an object fall by binding a canvas.move() function with pressing the down arrow key, then using pyautogui to hold down the key. Here is my code:
from tkinter import *
from random import randint
import pyautogui
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 not cord[0] <= 5:
canvas.move(character, -10, 0)
def right(event):
cord = canvas.coords(character)
if not cord[2] >= 595:
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(event):
cord = canvas.coords(x)
if cord[1] != 800:
canvas.move(x, 0, 10)
x = createShape()
self.master.bind('<Down>', moveShape)
pyautogui.keyDown('down')
root = Tk()
app = Window(root)
app.mainloop()
As you can see, at the bottom of the class, I binded the down arrow key and moving a shape down. However, the pyautogui does not work; the object does not move down unless I manually press the down arrow key. Am I forgetting something or is pyautogui not compatible with bind()? I know there are more efficient ways to move the object down, however with all the methods I have tried, none show the actual movement of the object heading down the screen; they just show the object being re-created in another position. Please let me know how I can fix this.
I wouldn't bother with it. Maybe it misses the _all argument. Try to simply bind canvas.move() function to a canvas.bind_all()

How to code an animation to run mutliple times in parallel in tkinter?

I am trying to wrap my head around parallel animations.
In the following code, clicking on a square will cause a small animation.
But declaring 2 boxes (or more) makes things more difficult: The animation called last will run and cause the other to pause and resume only after it is complete.
How to change my code so that all animation calls can run independently and in parallel?
#!python3
import tkinter as tk
import time
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# create a canvas
self.canvas = tk.Canvas(width=400, height=400)
self.canvas.pack()
# create a couple of movable objects
self._create_token(100, 100, "green")
self._create_token(200, 100, "black")
def _create_token(self, x, y, color):
self.canvas.create_rectangle(x-25, y-25, x+25, y+25, outline=color, fill=color, tags=color)
self.canvas.tag_bind(color, "<ButtonPress-1>", self.on_token_press)
def on_token_press(self,event):
Rx = self.canvas.find_closest(event.x, event.y)
x = 0
y = 5
for i in range(25):
time.sleep(0.025)
self.canvas.move(Rx, x, y)
self.canvas.update()
for i in range(25):
time.sleep(0.025)
self.canvas.move(Rx, x, -y)
self.canvas.update()
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack()
root.mainloop()
You should not use blocking tasks in a GUI, they live inside an event loop that allows them to verify events such as keyboard, mouse, etc, if you use those tasks the GUI will probably freeze. what you should do is use after() for periodic tasks, in the following solution I have proposed to create a class that manages the animation in a simple way.
#!python3
import tkinter as tk
import time
class AbstractAnimation:
def __init__(self, canvas, id_item, duration, _from = 0, _to = 1):
self.canvas = canvas
self.id_item = id_item
self._progress = 0
self._from = _from
self._to = _to
self.t = max(10, int(duration/(self._to -self._from)))
def start(self):
self.canvas.after(self.t, self.on_timeout)
def on_timeout(self):
if self._from <= self._progress < self._to:
self.interpolated(self._from, self._to, self._progress)
self._progress += 1
self.canvas.after(self.t, self.on_timeout)
def interpolated(self, _from, _to, _progress):
pass
class Animation(AbstractAnimation):
def interpolated(self, _from, _to, _progress):
x, y = 0, 5
if _progress < 25:
self.canvas.move(self.id_item, x, y)
else:
self.canvas.move(self.id_item, x, -y)
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# create a canvas
self.canvas = tk.Canvas(width=400, height=400)
self.canvas.pack()
# create a couple of movable objects
self._create_token(100, 100, "green")
self._create_token(200, 100, "black")
def _create_token(self, x, y, color):
self.canvas.create_rectangle(x-25, y-25, x+25, y+25, outline=color, fill=color, tags=color)
self.canvas.tag_bind(color, "<ButtonPress-1>", self.on_token_press)
def on_token_press(self,event):
Rx = self.canvas.find_closest(event.x, event.y)
animation = Animation(self.canvas, Rx, 1250, 0, 50)
animation.start()
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack()
root.mainloop()
As someone mention is the comments and #eyllanesc also mentions in his answer, you generally shouldn't call time.sleep() in a tkinter program because doing so temporarily halts its mainloop() which essentially halts the running GUI for the duration. Instead you should use the universal after() method.
However, you don't really need to use to get delays to animation. Instead it can be used to periodically run an arbitrary function within the mainloop(), which provides the leverage to animate things if desired.
In the code below, this is done by first defining a Token class to encapsulate the values associated with one, and then creating a list of them named _self._tokens, and finally using after() to schedule moving all the items in it that are currently active. The function to be called by after() in this case is the Example._update_tokens() method.
Here's code showing how to implement this approach:
import tkinter as tk
UPDATE_RATE = 10 # Updates-per-second.
UPDATE_DELAY = 1000//UPDATE_RATE # msec delay between updates.
class Token:
WIDTH, HEIGHT, INCR = 25, 25, 1
def __init__(self, canvas, x, y, color, max_value, dx, dy):
self.canvas, self.x, self.y = canvas, x, y
self.color, self.max_value, self.dx, self.dy = color, max_value, dx, dy
self.value, self.moving, self.saved_direction = 0, 0, 1
self.id = self.canvas.create_rectangle(x-self.WIDTH, y-self.HEIGHT,
x+self.WIDTH, y+self.HEIGHT,
outline=color, fill=color)
self.canvas.tag_bind(self.id, "<ButtonPress-1>", self._toggle)
def _toggle(self, _event):
""" Start movement of object if it's paused otherwise reverse its
direction.
"""
if self.moving:
self.moving = -self.moving # Reverse movement.
else: # Start it moving.
self.moving = self.saved_direction
def start(self):
self.moving = self.saved_direction
def pause(self):
if self.moving:
self.saved_direction = self.moving
self.moving = 0
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(width=400, height=400)
self.canvas.pack()
# Create list of movable objects.
self._tokens = []
self._tokens.append(Token(self.canvas, 100, 100, "green", 25, 0, 5))
self._tokens.append(Token(self.canvas, 200, 100, "black", 25, 0, 5))
tk.Button(self, text='Go', command=self._start_paused_tokens).pack(side=tk.LEFT)
tk.Button(self, text='Pause', command=self._pause_tokens).pack(side=tk.LEFT)
# Start the updating of active objects in _tokens list.
self.after(UPDATE_DELAY, self._update_tokens)
def _start_paused_tokens(self):
""" Start any paused Tokens. """
for token in self._tokens:
if token.moving == 0:
token.start()
def _pause_tokens(self):
""" Stop any moving Tokens. """
for token in self._tokens:
if token.moving != 0:
token.pause()
def _update_tokens(self):
""" Update any objects in Tokens lst that aren't paused. """
for token in self._tokens:
if token.moving > 0:
if token.value < token.max_value:
token.value += token.INCR
token.canvas.move(token.id, token.dx, token.dy)
else:
token.value = token.max_value
token.moving = -token.moving # Reverse moving.
token.canvas.move(token.id, token.dx, -token.dy)
elif token.moving < 0:
if token.value > 0:
token.value -= token.INCR
token.canvas.move(token.id, token.dx, -token.dy)
else:
token.value = 0
token.moving = -token.moving # Reverse moving.
token.canvas.move(token.id, token.dx, token.dy)
self.after(UPDATE_DELAY, self._update_tokens) # Continue doing updates.
def on_token_press(self, event):
closest_token = self.canvas.find_closest(event.x, event.y)
dx, dy = 0, 5
for i in range(25):
time.sleep(0.025)
self.canvas.move(closest_token, dx, dy)
self.canvas.update()
for i in range(25):
time.sleep(0.025)
self.canvas.move(closest_token, dx, -dy)
self.canvas.update()
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack()
root.mainloop()

Python Tkinter: Progress bar malfunction when using multi-threading

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.

Categories

Resources