canvas.itemconfig results in infinite event-loop - python

I try to draw a little 3x3 board with a marker on each cell.
This marker should only show up, when the cell is touched with the mouse.
This works once, twice, sometimes 3 three times - but then the event-loop 'fires' infinitely (always the same event)...
import tkinter as tk
cellsize = 50
class Board(tk.Canvas):
def __init__(self):
tk.Canvas.__init__(self)
for row in range(3):
for column in range(3):
ulx, uly = column*cellsize, row*cellsize
lrx, lry = ulx+cellsize, uly+cellsize
_cell = self.create_rectangle(ulx, uly, lrx, lry,
fill='green')
_right = self.create_rectangle(ulx+39, uly+20, lrx-1, lry-20,
fill='red',
state='hidden')
self.tag_bind(_cell, '<Enter>',
lambda e, r=_right: self.show_pos('on', r))
self.tag_bind(_cell, '<Leave>',
lambda e, r=_right: self.show_pos('off', r))
def show_pos(self, onoff, right):
print('{} {}'.format(onoff, right))
if onoff == 'on':
self.itemconfig(right, state='normal')
elif onoff == 'off':
self.itemconfig(right, state='hidden')
root = tk.Tk()
Board().grid()
root.mainloop()
Perhaps this is sticked to the self.itemconfigure statement, because doing other things (e.g. updating a status line) work as expected.
Is there a solution for this?
Thx in advance
Marvin
Addition:
To be more precise: It seems to stick with the 'state=..."
Changing the itemconfig to 'fill=...' in 'show_pos' works as expected.
So the title should be
'canvas.itemconfig(state='...' results in infinite event-loop'

Using the mouse position approach you could use:
class BoardX(tk.Canvas):
__cells=None
__indicators=None
def __init__(self):
tk.Canvas.__init__(self)
self.__cells=[]
self.__indicators=[]
self.bind('&ltMotion&gt', self.show_pos)
for row in range(3):
for column in range(3):
ulx, uly = column*cellsize, row*cellsize
lrx, lry = ulx+cellsize, uly+cellsize
self.__cells.append(self.create_rectangle(ulx, uly, lrx, lry,
fill='green', tags="cell"))
self.__indicators.append(self.create_rectangle(ulx+39, uly+20, lrx-1, lry-20,
fill='red',
state='hidden', tags="indicator"))
def show_pos(self, event):
""" Get closest widget or widget that we are above,
tagged "cell" and indicate it
"""
# the loop is needed for not to run into value errors
halo=0
widget = self.find_closest(event.x, event.y, halo=halo)
# edit - avoid loop!
if not widget[0] in self.find_withtag("cell"):
return
index = self.__cells.index(widget[0])
for i in range(len(self.__indicators)):
state='hidden'
if i == index:
state='normal'
self.itemconfig(self.__indicators[i], state=state)
This does not fire leave events you bound to in your approach and should therefore solve your problem.
If you do not want to take this approach for whatever reason you could only bind to enter and hide every other indicator using the find_withtag("indicator")-Approach
Edit
Code sample is corrected to avoid the loop.

Related

Tkinter: Image change from blank tile to mark (X/O) is not permanent

I'm making a scalable tic-tac-toe program using Tkinter. When I press an empty square button, the image changes, as it should. When I press a different button, the image from the button first pressed disappears completely, when I'd expect it to stay, and the only button with the X/O image is the latest button pressed. Any help in fixing this? I'm a beginner.
If you have any tips on how to make a scalable win check as well (with scalable I mean the user can input board size, from 2x2 to as large as fits the screen), that would be extremely helpful.
Here are the images I used, if they're useful (link works for 24h, they're just basic 125x125 images for a box, X and O) https://picresize.com/b5deea04656747
from tkinter import *
class XOGame:
def main_game(self):
self.__game_window = Tk()
self.__grid_size = 3 # User inputted in a different part of the code
self.__game_window.title("Tic Tac Toe (" + str(self.__grid_size) + "x"
+ str(self.__grid_size) + ")")
self.build_board(self.__game_window)
self.__game_window.mainloop()
def build_board(self, window):
self.__size = self.__grid_size ** 2
self.__empty_square = PhotoImage(master=window,
file="rsz_empty.gif")
self.__squares = [None] * self.__size
# Building the buttons and gridding them
for i in range(self.__size):
self.__squares[i] = (Button(window, image=self.__empty_square))
row = 0
column = 0
number = 1
for j in self.__squares:
j.grid(row=row, column=column)
j.config(command=lambda index=self.__squares.index(j):
self.change_mark(index))
column += 1
if number % 3 == 0:
row += 1
column = 0
number += 1
# This is the part where the picture changing happens.
# I have yet to implement the turn system, thus self.__x being the only image being configured.
# This is the part with the issue.
def change_mark(self, i):
self.__x = PhotoImage(master=self.__game_window,
file="rsz_cross.gif")
self.__o = PhotoImage(master=self.__game_window,
file="rsz_nought.gif")
self.__squares[i].configure(image=self.__x, state=DISABLED)
def start(self):
# Function starts the first window of the game.
self.main_game()
def main():
ui = XOGame()
ui.start()
main()
Question: the image from the button first pressed disappears completely
You overwrite self.__x = PhotoImage(... at every click, which results in garbage collection of the previous image and tkinter looses the image reference.
Create the images only once and reuse it like self.empty_square.
Simplify to the following:
def build_board(self, window):
self.empty_square = PhotoImage(master=window, file="empty.gif")
self.x_square = PhotoImage(master=window, file="cross.gif")
self.squares = []
for row in range(3):
for column in range(3):
self.squares.append(Button(window, image=self.empty_square))
self.squares[-1].grid(row=row, column=column)
self.squares[-1].bind("<Button-1>", self.change_mark)
def change_mark(self, event):
w = event.widget
w.configure(image=self.x_square, state=DISABLED)

I'm trying to make a simple line drawing program with tkinter but it won't work

I'm trying to make this really simple program, all it does is store the current x/y pos of the mouse on the canvas and then use them to draw a line when you click for the second time. I've already bound it and I'm not getting any errors, it seems like it's not even being activated. Any help is greatly appreciated
from tkinter import *
main = Tk()
c = Canvas(main, width=600, height=600)
c.pack()
#For colored lines
presses = 0
def click(event):
if presses == 0:
initX = int(c.canvasx(event.x))
initY = int(c.canvasy(event.y))
presses == 1
elif presses == 1:
c.create_line(initX, initY,
int(c.canvasx(event.x)),
int(c.canvasy(event.y)))
presses == 0
c.bind("<Button-1>", click)
mainloop()
How does something like this work for you?
from tkinter import *
main = Tk()
c = Canvas(main, width=600, height=600)
c.pack()
line = []
def click(event):
global line
X = int(c.canvasx(event.x))
Y = int(c.canvasy(event.y))
line.append((X,Y))
if len(line) > 1:
startX,startY = line[-2]
c.create_line(startX, startY, X, Y)
c.bind("<Button-1>", click)
mainloop()
I've changed around your code a bit to store a list of the X,Y coordinates that have been clicked on. If more than 1 point on the screen has been clicked, it will draw a line between the current point clicked on and the last point clicked on.
Reason your code wasn't working was that initX and initY are forgotten in between calls on the the click function. Adding them to a list solves this.

Python tkinter - closing of new window

I made this program, I recommend run it first. The main idea is popping window, it should pop after clicking on text info. This works correctly, than, an user needs to close this window, so I made red closing button (rectangle with lines) in top right corner. After clicking on this button, new window should disappear, but first window with ovals should stay.
I need function for closing this window, I was trying to find solution a long time, but I am not able to. Any idea please?
import tkinter
class Desktop:
def __init__(self):
self.canvas = tkinter.Canvas()
self.canvas.pack()
x, y = 25,25
for i in range(1,61):
self.canvas.create_oval(x-10, y-10, x+10, y+10, fill='blue')
self.canvas.create_text(x, y, text=i)
x += 30
if i%10==0:
x = 25
y += 40
self.canvas.create_text(340, 150, text='info', font='arial 17', tags='info')
self.canvas.tag_bind('info', '<Button-1>', self.window)
def window(self, event):
self.canvas.create_rectangle(40,40,310,220,fill='white')
x, y = 75,100
for i in range(1,9):
self.canvas.create_rectangle(x-30,y-30,x+30,y+30)
self.canvas.create_text(x,y,text='number ' + str(i), font='arial 9')
x += 65
if i == 4:
x = 75
y += 70
self.rec = self.canvas.create_rectangle(310,40,290,60, fill="red", tags="cancel")
self.line = self.canvas.create_line(310,40,290,60,310,60,290,40, tags="cancel")
self.canvas.tag_bind('cancel', '<Button-1>', self.close)
def close(self,event):
#?
print('should close this part of screen')
d = Desktop()
What I use in my code is:
def quit(self):
root.destroy()
self.quit()
This might be a really long winded way to do it but it works for me every time.
EDIT: Just thinking about it now, depending on the way you open a new frame,root.destroy would be the way I would do it.

How to use .after with .create_rectangle?

I am trying to create a program that draws rectangles on a canvas on at a time, and I want to use the .after function to stop them from being drawn (near) instantly.
Currently my (stripped down) code looks like this:
root = Tk()
gui = ttk.Frame(root, height=1000, width=1000)
root.title("Test GUI")
rgb_colour = "#%02x%02x%02x" % (0, 0, 255)
def func(*args):
for item in sorted_list:
canvas.create_rectangle(xpos, ypos, xpos+10, ypos+10, fill=rgb_colour)
xpos += 10
canvas = Canvas(gui, width=1000, height=1000)
canvas.grid()
func() # Code doesn't actually look exactly like this
root.mainloop()
and I want there to be a delay between each rectangle being drawn. I have gathered that I should be doing:
def draw(*args):
canvas.create_rectangle(xpos, ypos, xpos+10, ypos+10, fill=rgb_colour)
for item in sorted_list:
root.after(10, draw)
However I cannot do this because my original for loop is nested within a function that contains the xpos, ypos, and colour variables, so a new function to create the rectangle would lack the required variables. I realise I could solve this by nesting my entire function within a class, and then calling the variables from the class, however I want to keep this code very simple and I was wondering if there was a way to delay the creation of rectangles without the use of a class?
Edit: This:
from tkinter import *
root = Tk()
canvas = Canvas(root, width=400, height=400, bg="white")
canvas.pack()
items = [1, 2, 3, 4, 5]
delay = 100
def draw_all(*args):
global delay
x, y = 0, 10
for item in items:
canvas.after(delay, canvas.create_rectangle(x, y, x+10, y+10, fill="red"))
delay += 10
x += 10
root.bind('<Return>', draw_all)
root.mainloop()
still returns an AttributeError
The simplest solution is to create a function that takes a list of items along with any other data it needs, pops one item from the list and creates the rectangle, and then calls itself until there are no more items.
def draw_one(items):
item = items.pop()
canvas.create_rectangle(...)
if len(items) > 0:
canvas.after(10, draw_one, items)
If you prefer to not use a function that calls itself, simply call the create_rectangle method using after:
def draw_all():
delay = 0
for item in items:
canvas.after(delay, canvas.create_rectangle, x0, y0, x1, y1, ...)
delay += 10
With that, the first will be drawn immediately, the next in 10ms, the next in 20ms, etc.

Python (Tkinter) - canvas for-loop color change

I generated a grid using a for-loop in Tkinter, but want to know how I would be able to bind an on-click function to such that when I click on each individual generated rectangle, the rectangle will change color.
from Tkinter import *
master = Tk()
def rowgen(row, col):
for i in range(row):
for j in range(col):
w.create_rectangle(25+50*i, 25+50*j, 50+50*i, 50+50*j, fill="green")
w = Canvas(master, width=225, height=225)
w.pack()
rowgen(4, 4)
master.resizable(0,0)
mainloop()
I'm thinking that I have to first iterate through another for-loop to make an event, where if I click within these coordinates, I'd reconfig the color of one of the rectangles.
By following Curly Joe's hints and making some mistakes, I got the following, which requires only one tag_bind. You might want to try it yourself first.
from tkinter import *
master = Tk()
def box_click(event):
box = event.widget.find_closest(event.x, event.y)
print(box) # remove later
w.itemconfig(box, fill='red')
def rowgen(row, col):
for i in range(row):
for j in range(col):
w.create_rectangle(25+50*i, 25+50*j, 50+50*i, 50+50*j,
fill="green", tag='BOX')
w = Canvas(master, width=225, height=225)
w.pack()
rowgen(4, 4)
w.tag_bind('BOX', '<Button-1>', box_click)
master.resizable(0,0)
mainloop()

Categories

Resources