How to do a series of events run by clicks - python

I want the program to run different functions by click. I don't want buttons, just want it to run with left clicks.
In the code below, it runs getorigin by a left click, I don't know how to make it run other_function by next left click, and then run third_function by one more left click.
from tkinter import *
# --- functions ---
def getorigin(event):
x0 = event.x
y0 = event.y
print('getorigin:', x0, y0)
def other_function(event):
print('other function', x0+1, y0+1)
def third_function(event):
print('third function', x0+1, y0+1)
# --- main ---
# create global variables
x0 = 0
y0 = 0
root = Tk()
w = Canvas(root, width=1000, height=640)
w.pack()
w.bind("<Button-1>", getorigin)
root.mainloop()

You could bind the left-click with a function that counts clicks and runs functions based on that.
def userClicked(event):
global clickTimes
clickTimes += 1
if clickTimes == 1:
getorigin(event)
elif clickTimes == 2:
other_function(event)
elif clickTimes == 3:
third_function(event)
You would need to declare that global clickTimes as 0 down in your main

You could put the functions in a list, and then rotate the list each time you process a click.
Sometime after you've created the functions, add them to a list:
def getorigin(event):
...
def other_function(event):
...
def third_function(event):
...
functions = [getorigin, other_function, third_function]
Next, associate a function with a button click that pops the first function off of the list, moves it to the end, and then executes it:
def handle_click(event):
global functions
func = functions.pop(0)
functions.append(func)
func(event)
w.bind("<Button-1>", handle_click)

Related

Is it possible to suspend rendering in tkinter?

I have a GUI with a large number of widgets and each widget has a number of settings. Plus, I have to make a number of calls to get all of the settings. So, it takes a couple of seconds to set the GUI up. As it is now, the GUI starts rendering while I am still setting it up. It looks horrible and it slows down the completion of the final form.
Is there a way I can tell tkinter to stop rendering until the GUI is completely set up and then resume rendering?
I know it's possible to do this in WinForms. In that case, I can just call SuspendLayout() to stop it from rendering while I'm changing it. Does tkinter have a similar mechanism?
Edit ---
#martineau - Here is some test code to give you a better idea of what I am trying to do.
from array import *
from threading import Thread
import tkinter as tk
from tkinter import *
import time
arr=[]
isInit = False
def BuildGUI(value):
"""Draw out inital widgets """
global isInit
rows, cols = (8, 8)
root = Tk()
for i in range(rows):
col = []
for j in range(cols):
led = Label(root, text="", bg="#000000", height = 2, width = 3)
led.grid(row=j, column=i)
col.append(led)
arr.append(col)
print(arr)
isInit = True
#wait for events
root.mainloop()
class AnimTest:
""" Animation Library"""
def begin(self):
if isInit == False:
x = Thread(target=BuildGUI, args=(1,))
x.daemon = True
x.start()
while isInit == False:
print("initializing...")
def clear(self):
for x in range(0,8):
for y in range(0,8):
self.set_pixel(x, y, 0)
#this happens at the start of drawing a new frame
#i want to suspend rendering here
def set_pixel(self, dispValY, dispValX, mode):
pxl = arr[dispValX][dispValY]
if mode == 0: pxl.config(bg="#000000")
else: pxl.config(bg="#00FF00")
#this is used to draw out the fram
def write_display(self):
pass
#i want everything to be rendered when
#this is called.
def runTest():
anim = AnimTest()
anim.begin()
x = 0
y = 2
dx = dy = 1
while True:
x = x + dx
if (x <= 0 or x >= 7): dx = -dx
y = y + dy
if (y <= 0 or y >= 7):dy = -dy
anim.clear()
anim.set_pixel(x,y,1)
anim.write_display()
time.sleep(1/60)
if __name__ == '__main__':
runTest()
This test code should display an animation of a bouncing dot. This particular animation doesn't look that bad. But when I try to make more complex images, it looks very flashy.
I want it to suspend rendering when clear() is called and resume rendering when write_display() is called.
Tkinter can't render anything until either update has been called, or mainloop() is running and allowed to continue. In other words, the default is to not render anything until the code which creates the UI has finished defining the UI.
If you find yourself in the situation where update is being called while you are in the process of defining the UI but you don't want the screen to be updated, the best solution is to either withdraw the window until it is ready, or add one frame between the window and all of the other widgets and then leave adding the frame to the window as your final step.

How to return value of mouse position after event

It gets your mouse position after clicks, but I can't return the X and Y value to use in other functions. Such as the codes below, it prints at the first time, but do nothing with the second print. I think x0, y0 are not returned and they are still local variables.
from tkinter import *
root = Tk()
w = Canvas(root, width=1000, height=640)
w.pack()
def getorigin(eventorigin):
global x0, y0
x0 = eventorigin.x
y0 = eventorigin.y
print(x0, y0)
return x0, y0
w.bind("<Button 1>",getorigin)
print(x0, y0)
You can't return from function assigned to event (or used in command=, bind() or after()). You can only assign to global variable and later use in other function.
Your print() after bind() is executed before mainloop() shows window and it is NOT "other function".
I use two functions: one to get values when left mouse button is pressed, and second to use these values when right mouse button is pressed. Second function uses values from first function. It shows that value from first function are assigned to global variables.
from tkinter import *
# --- functions ---
def getorigin(event):
global x0, y0 # inform function to assing to global variables instead of local variables
x0 = event.x
y0 = event.y
print('getorigin:', x0, y0)
def other_function(event):
#global x0, y0 # this function doesn't assign to variables so it doesn't need `global`
print('other function', x0, y0)
# --- main ---
# create global variables
x0 = 0
y0 = 0
root = Tk()
w = Canvas(root, width=1000, height=640)
w.pack()
w.bind("<Button-1>", getorigin)
w.bind("<Button-3>", other_function)
print('before mainloop:', x0, y0) # executed before mainloop shows window
root.mainloop()

How to use function in tkinter mainloop() [duplicate]

This question already has answers here:
Tkinter — executing functions over time
(2 answers)
Closed 4 years ago.
I decided to try out Python and it's been fun so far. However while messing around with tkinter I encountered a problem which I haven't been able to solve for hours. I've read some things and tried different stuff but nothing works.
I've got the code so far that I think the program should run fine. Except for the fact that I can't make it loop and thus update automatically.
So my question is: how can I call a function with tkinters loop options in an infite loop fashion?
Simple game of life:
I wrote basicly 2 classes. A Matrix which stores and handles the single cells
and the game itself which utilizes the matrix class through some game logic and basic user input.
First the game class as there is my loop problem:
from tkinter import *
from ButtonMatrix import *
class Conway:
def __init__(self, master, size = 20, cell_size = 2):
self.is_running = True
self.matrix = ButtonMatrix(master,size,cell_size)
self.matrix.randomize()
self.matrix.count_neighbours()
self.master = master
# playbutton sets boolean for running the program in a loop
self.playbutton = Button(master, text = str(self.is_running), command = self.stop)
self.playbutton.grid(row = 0 , column = size +1 )
#Test button to trigger the next generation manually. Works as itended.
self.next = Button(master, text="next", command = self.play)
self.next.grid(row = 1, column = size +1)
def play(self): # Calculates and sets the next generation. Intended to be used in a loop
if self.is_running:
self.apply_ruleset()
self.matrix.count_neighbours()
self.apply_colors()
def apply_ruleset(self):
#The ruleset of conways game of life. I wish i knew how to adress each element
#without using these two ugly loops all the time
size = len(self.matrix.cells)
for x in range (size):
for y in range (size):
if self.cell(x,y).is_alive():
if self.cell(x,y).neighbours < 2 or self.cell(x,y).neighbours > 3:
self.cell(x,y).toggle()
if not self.cell(x,y).is_alive() and self.cell(x,y).neighbours == 3:
self.cell(x,y).toggle()
def apply_colors(self): #Some flashy colors just for fun
size = len(self.matrix.cells)
for x in range (size):
for y in range (size):
if self.cell(x,y).is_alive():
if self.cell(x,y).neighbours < 2 or self.cell(x,y).neighbours > 3:
self.cell(x,y).button.configure(bg = "chartreuse3")
if not self.cell(x,y).is_alive() and self.cell(x,y).neighbours == 3:
self.cell(x,y).button.configure(bg = "lightgreen")
def cell(self,x,y):
return self.matrix.cell(x,y)
def start (self): #start and stop set the boolean for the loop. They work and switch the state properly
self.is_running = True
self.playbutton.configure(text=str(self.is_running), command =self.stop)
def stop (self):
self.is_running = False
self.playbutton.configure(text=str(self.is_running), command =self.start)
#Test program. I can't make the loop work. Manual update via next button works however
root = Tk()
conway = Conway(root)
root.after(1000, conway.play())
root.mainloop()
The Matrix (only for interested readers):
from tkinter import *
from random import randint
class Cell:
def __init__(self,master, cell_size = 1):
self.alive = False
self.neighbours = 0
# initializes a squares shaped button that fills the grid cell
self.frame = Frame(master, width= cell_size*16, height = cell_size*16)
self.button = Button(self.frame, text = self.neighbours, command = self.toggle, bg ="lightgray")
self.frame.grid_propagate(False)
self.frame.columnconfigure(0, weight=1)
self.frame.rowconfigure(0,weight=1)
self.button.grid(sticky="wens")
def is_alive(self):
return self.alive
def add_neighbour(self):
self.neighbours += 1
def toggle (self):
if self.is_alive() :
self.alive = False
self.button.configure( bg = "lightgray")
else:
self.alive = True
self.button.configure( bg = "green2")
class ButtonMatrix:
def __init__(self, master, size = 3, cell_size = 3):
self.master = master
self.size = size
self.cell_size = cell_size
self.cells = []
for x in range (self.size):
row = []
self.cells.append(row)
self.set_cells()
def cell(self, x, y):
return self.cells[x][y]
def set_cells(self):
for x in range (self.size):
for y in range (self.size):
self.cells[x] += [Cell(self.master, self.cell_size)]
self.cell(x,y).frame.grid(row=x,column=y)
def count_neighbours(self): # Checks 8 sourounding neighbours for their stats and sets a neighbour counter
for x in range(self.size):
for y in range(self.size):
self.cell(x,y).neighbours = 0
if y < self.size-1:
if self.cell(x,y+1).is_alive(): self.cell(x,y).add_neighbour() # Right
if x > 0 and self.cell(x-1,y+1).is_alive(): self.cell(x,y).add_neighbour() #Top Right
if x < self.size-1 and self.cell(x+1,y+1).is_alive(): self.cell(x,y).add_neighbour() #Bottom Right
if x > 0 and self.cell(x-1,y).is_alive(): self.cell(x,y).add_neighbour()# Top
if x < self.size-1 and self.cell(x+1,y).is_alive():self.cell(x,y).add_neighbour() #Bottom
if y > 0:
if self.cell(x,y-1).is_alive(): self.cell(x,y).add_neighbour() # Left
if x > 0 and self.cell(x-1,y-1).is_alive(): self.cell(x,y).add_neighbour() #Top Left
if x < self.size-1 and self.cell(x+1,y-1).is_alive(): self.cell(x,y).add_neighbour() #Bottom Left
self.cell(x,y).button.configure(text = self.cell(x,y).neighbours)
def randomize (self):
for x in range(self.size):
for y in range(self.size):
if self.cell(x,y).is_alive(): self.cell(x,y).toggle()
rando = randint(0,2)
if rando == 1: self.cell(x,y).toggle()
There are two problems with your code:
root.after(1000, conway.play())
First, you're not telling Tkinter to call conway.play after 1 second, you're calling conway.play() right now, which returns None, and then telling Tkinter to call None after 1 second. You want to pass the function, not call it:
root.after(1000, conway.play)
Meanwhile, after does not mean "call this function every 1000ms", it means "call this function once, after 1000ms, and then never again". The easy way around this is to just have the function ask to be called again in another 1000ms:
def play(self): # Calculates and sets the next generation. Itended to use in a loop
if self.is_running:
self.apply_ruleset()
self.matrix.count_neighbours()
self.apply_colors()
self.master.after(1000, self.play)
This is explained in the docs for after:
This method registers a callback function that will be called after a given number of milliseconds. Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer.
The callback is only called once for each call to this method. To keep calling the callback, you need to reregister the callback inside itself:
class App:
def __init__(self, master):
self.master = master
self.poll() # start polling
def poll(self):
... do something ...
self.master.after(100, self.poll)
(I'm assuming nobody's going to care if your timing drifts a little bit, so after an hour you might have 3549 steps or 3627 instead of 3600+/-1. If that's a problem. you have to get a bit more complicated.)

"after" looping indefinitely: never entering mainloop

This is my first post. I started coding when considering a career swap two months ago and am working on a Tetris clone. I've implemented most of the core features, but cannot get the game to refresh continually with an after loop.
I'm using Tkinter to produce my Gui and am trying out event oriented programming.
My understanding is that after(Time, Event) from Tkinter should schedule whatever the Event callback function is to occur after a delay specified by Time. I think that the code is supposed to continue executing subsequent items after this.
My frame refresh function (game.updateBoard()) does most of the necessary events for tetris to work, then calls itself using after. I call it once when initializing an instance of the game.
Instead of proceeding to mainloop(), the game.updateboard() function calls itself via after indefinitely.
I suspect that it is not behaving how I thought after worked which would be to continue to execute the script until the specified delay occurs. I think it is waiting for the callback to terminate to continue.
I tried to find a resource on this but could not.
If you have suggestions for fixing this question, the attached code, or for coding in general, I am very happy to hear them! This is a learning process and I'll gladly try pretty much anything you suggest.
Here is the relevant portion of the code:
class game():
def __init__(self): #Set up board and image board
self.pieces = ["L","J","S","Z","T","O","I"]
self.board = boardFrame()
self.root = Tk()
self.root.title("Tetris")
self.root.geometry("250x525")
self.frame = Frame(self.root)
#set up black and green squares for display
self.bSquare = "bsquare.gif"
self.gSquare = "square.gif"
self.rSquare = "rsquare.gif"
self.image0 = PhotoImage(file = self.bSquare)
self.image1 = PhotoImage(file = self.gSquare)
self.image2 = PhotoImage(file = self.rSquare)
#get an initial piece to work with
self.activeBlock = piece(self.pieces[random.randint(0,6)])
#Tells program to lower block every half second
self.blockTimer = 0
self.updateBoard()
self.root.bind('<KeyPress-Up>', self.turn)
self.root.bind('<KeyPress-Right>', self.moveR)
self.root.bind('<KeyPress-Left>', self.moveL)
self.root.bind('<KeyPress-Down>',self.moveD)
print("Entering mainloop")
self.root.mainloop()
def turn(self, event):
self.activeBlock.deOccupy(self.board)
self.activeBlock.turn()
self.activeBlock.occupy(self.board)
self.drawGrid(self.board.grid)
def moveR(self, event):
self.activeBlock.deOccupy(self.board)
self.activeBlock.updatePos([1,0], self.board)
self.activeBlock.occupy(self.board)
self.drawGrid(self.board.grid)
def moveL(self, event):
if self.activeBlock.checkLeft(self.board) == False:
self.activeBlock.deOccupy(self.board)
self.activeBlock.updatePos([-1,0], self.board)
self.activeBlock.occupy(self.board)
self.drawGrid(self.board.grid)
def moveD(self, event): #find
self.activeBlock.deOccupy(self.board)
self.activeBlock.updatePos([0,-1],self.board)
if self.activeBlock.checkBottom(self.board) == True:
self.activeBlock.occupy(self.board)
self.activeBlock = piece(self.pieces[random.randint(0,6)])
## self.activeBlock = piece(self.pieces[1])
print("bottomed")
self.activeBlock.occupy(self.board)
self.activeBlock.occupy(self.board)
self.drawGrid(self.board.grid)
def drawGrid(self, dGrid):
#Generate squares to match tetris board
for widget in self.frame.children.values():
widget.destroy()
self.activeBlock.occupy(self.board)
for x in range(9,-1,-1):
for y in range(20,-1,-1):
if self.board.grid[x][y] == 1:
self.frame.displayA = Label(self.frame, image=self.image1)
## self.frame.displayA.image = self.image1
self.frame.displayA.grid(row=21-y, column=x)
else:
self.frame.displayA = Label(self.frame, image = self.image0)
## self.frame.displayA.image = self.image0
self.frame.displayA.grid(row=21-y, column=x)
self.frame.displayA = Label(self.frame, image = self.image2)
self.frame.displayA.grid(row = 21 - self.activeBlock.center[1], column = self.activeBlock.center[0])
self.frame.grid()
def updateBoard(self):
self.blockTimer += 1
"print updateBoard Loop"
## 1)check for keyboard commands
#1.1 move block by keyboard commands
#2) see if block has bottomed out, if it has, have it enter itself into the grid and generate a new block.
if self.activeBlock.checkBottom(self.board) == True:
self.activeBlock.occupy(self.board)
self.activeBlock = piece(self.pieces[random.randint(0,6)])
print("bottomed")
self.activeBlock.occupy(self.board)
#2.2 - if block has not bottomed and 50 frames (~.5 seconds) have passed, move the active block down a square after clearing its old space.
elif self.blockTimer%12 == 0:
self.activeBlock.deOccupy(self.board)
self.activeBlock.updatePos([0,-1], self.board)
self.activeBlock.occupy(self.board)
## 4) check for filled rows
for y in range(1,21):
for x in range(10):
rowFull = True
if self.board.grid[x][y] == 0:
rowFull == False
#4.1 if any row is filled, delete it and then move all rows above the deleted row down by one
if rowFull == True:
for x2 in range(10):
self.board.grid[x2][y] = 0
for y2 in range(y+1,21):
if self.board.grid[x2][y2] == 1:
self.board.grid[x2][y2] = 0
self.board.grid[x2][y2-1] = 1
#4.11 if the row is full and the row above it was full, delete the row again as well as the row above it, and move all rows down by 2
for x in range(10):
rowFull = True
if self.board.grid[x][y] == 0:
rowFull == False
if rowFull == True:
for x2 in range(10):
try:
self.board.grid[x2][y] = 0
self.board.grid[x2][y+1] = 0
except:
pass
for y2 in range(y+2,21):
try:
if self.board.grid[x2][y2] == 1:
self.board.grid[x2][y2] = 0
self.board.grid[x2][y2-2] = 1
except:
pass
#5) if there is a block in the top row, end the game loop
for x in range(10):
if self.board.grid[x][20] == 1:
game = "over"
#6) update image
self.activeBlock.occupy(self.board)
self.drawGrid(self.board.grid)
self.frame.after(500, self.updateBoard())
Game = game()
You want to do self.frame.after(500, self.updateBoard).
The difference here is subtle, (self.updateBoard instead of self.updateBoard()). In your version, you're passing the result of your function to the after method instead of passing the function. This results in the infinite recursion that you described.

Tkinter events outside of the mainloop?

The program I am writing has a tkinter window that is constantly being fed with data manually rather than being part of a mainloop. It also needs to track mouse location. I havn't found a workaround for tracking the mouse outside of mainloop yet, but if you have one please do tell.
from Tkinter import *
import random
import time
def getCoords(event):
xm, ym = event.x, event.y
str1 = "mouse at x=%d y=%d" % (xm, ym)
print str1
class iciclePhysics(object):
def __init__(self, fallrange, speed=5):
self.speed = speed
self.xpos = random.choice(range(0,fallrange))
self.ypos = 0
def draw(self,canvas):
try:
self.id = canvas.create_polygon(self.xpos-10, self.ypos, self.xpos+10, self.ypos, self.xpos, self.ypos+25, fill = 'lightblue')
except:
pass
def fall(self,canvas):
self.ypos+=self.speed
canvas.move(self.id, 0, self.ypos)
root = Tk()
mainFrame = Frame(root, bg= 'yellow', width=300, height=200)
mainFrame.pack()
mainCanvas = Canvas(mainFrame, bg = 'black', height = 500, width = 500, cursor = 'circle')
mainCanvas.bind("<Motion>", getCoords)
mainCanvas.pack()
root.resizable(0, 0)
difficulty = 1500
#root.mainloop()
currentIcicles = [iciclePhysics(difficulty)]
root.update()
currentIcicles[0].draw(mainCanvas)
root.update_idletasks()
time.sleep(0.1)
currentIcicles[0].fall(mainCanvas)
root.update_idletasks()
tracker = 0
sleeptime = 0.04
while True:
tracker+=1
time.sleep(sleeptime)
if tracker % 3 == 0 and difficulty > 500:
difficulty -= 1
elif difficulty <= 500:
sleeptime-=.00002
currentIcicles.append(iciclePhysics(difficulty))
currentIcicles[len(currentIcicles)-1].draw(mainCanvas)
for i in range(len(currentIcicles)):
currentIcicles[i].fall(mainCanvas)
root.update_idletasks()
for i in currentIcicles:
if i.ypos >= 90:
currentIcicles.remove(i)
root.update_idletasks()
There is no way. Mouse movement is presented to the GUI as a series of events. In order to process events, the event loop must be running.
Also, you should pretty much never do a sleep inside a GUI application. All that does is freeze the GUI during the sleep.
Another hint: you only need to create an icicle once; to make it fall you can use the move method of the canvas.
If you are having problems understanding event based programming, the solution isn't to avoid the event loop, the solution is to learn how event loops work. You pretty much can't create a GUI without it.

Categories