I have a code where every second I generate random circle on canvas. And when you click on the object you get +1 point. But count of your points gets deleted every time new circle is made. How can I do it so count is permanently displayed on canvas?
import tkinter
import random
canvas = tkinter.Canvas(width=300, height=300)
canvas.pack()
count=0
def motion(event):
x,y = event.x, event.y
global count
if q+20 > x > q-20 and w+20 > y > w-20:
count=count+1
canvas.create_text(150,20,text=count)
else:
count=0
canvas.create_text(150,20,text=count)
def game():
canvas.delete('all')
global q,w
q=random.randint(20,280)
w=random.randint(20,280)
canvas.create_oval(q-20,w-20,q+20,w+20)
canvas.after(1000,game)
game()
canvas.bind('<Button-1>', motion,)
If you only want to delete the circles while leaving everything else on the canvas, give the circles a common tag. You can then delete them all via the tag.
canvas.create_oval(q-20,w-20,q+20,w+20, tags=("circle",))
...
canvas.delete("circle")
Also, you probably shouldn't be creating a new text item every time the motion function is called. Instead, create it once and then just change the text. You can use the same tag concept for the text so that you don't have to remember the text id.
Just do this once, outside the function:
canvas.create_text(150,20,text=count, tags=("text",))
Then, inside the function you can change the text:
canvas.itemconfig("text", text=count)
Here's a complete example:
import tkinter
import random
root = tkinter.Tk()
canvas = tkinter.Canvas(root, width=300, height=300)
canvas.pack()
canvas.create_text(150, 20, text="0", tags=("text",))
count=0
def motion(event):
x,y = event.x, event.y
global count
if q+20 > x > q-20 and w+20 > y > w-20:
count=count+1
else:
count=0
canvas.itemconfig("text", text=count)
def game():
canvas.delete('circle')
global q,w
q=random.randint(20,280)
w=random.randint(20,280)
canvas.create_oval(q-20,w-20,q+20,w+20, tags=("circle",))
canvas.after(1000,game)
game()
canvas.bind('<Button-1>', motion,)
root.mainloop()
As a final optimization, you don't have to continuously delete and recreate the one circle. Like with the text object, you can create the one circle and then just reconfigure its coordinates much the same as I showed how to reconfigure the text.
Related
I am trying to make a program that lets me draw on a tkinter window using turtle. For some reason I cannot get the absolute mouse coordinates.
I have done root.winfo_pointerx() - root.winfo_rootx() (and vrootx).
I have also tried:
def mousePos(event):
x,y = event.x , event.y
return x,y
My code:
import turtle
import tkinter as tk
root = tk.Tk()
root.title("Draw!")
cv = tk.Canvas(root, width=500,height=500)
cv.focus_set()
cv.pack(side = tk.LEFT)
pen = turtle.RawTurtle(cv)
window = pen.getscreen()
def main():
window.setworldcoordinates(-500,-500,500,500)
window.bgcolor("white")
frame = tk.Frame(root)
frame.pack(side = tk.RIGHT,fill=tk.BOTH)
pointLabel = tk.Label(frame,text="Width")
pointLabel.pack()
def getPosition(event):
x = root.winfo_pointerx()-root.winfo_vrootx()
y = root.winfo_pointery()-root.winfo_vrooty()
pen.goto(x,y)
cv.bind("<Motion>", getPosition)
cv.pack
tk.mainloop()
pass
I want the cursor to be on top of the arrow, but instead it is always to the right and down. Also, when I move the mouse up, the arrow moves down, and vice versa.
Think hard about how you set the setworldcoordinate(). -500 - 500 means your world has 1,000 in size and window size is 500. Also, the mouse pointer offset from the window root - both absolute coordinates should be used. You mixed up the absolute coordinates - mouse pointer and vrootx which is in different scale so the distance of two makes no sense. Following code is probably closer to what you intended. Note that, I set the world coordinate to match the absolute coordinates of mouse pointer offset from the top/left corner of window.
import turtle
import tkinter as tk
root = tk.Tk()
root.title("Draw!")
cv = tk.Canvas(root, width=500,height=500)
cv.focus_set()
cv.pack(side = tk.LEFT)
pen = turtle.RawTurtle(cv)
window = pen.getscreen()
def main():
window.setworldcoordinates(0,500,500,0)
window.bgcolor("white")
frame = tk.Frame(root)
frame.pack(side = tk.RIGHT,fill=tk.BOTH)
pointLabel = tk.Label(frame,text="Width")
pointLabel.pack()
print(dir(root))
def getPosition(event):
x = root.winfo_pointerx()-root.winfo_rootx()
y = root.winfo_pointery()-root.winfo_rooty()
print(x, y)
pen.goto(x,y)
pass
cv.bind("<Motion>", getPosition)
cv.pack
tk.mainloop()
pass
if __name__ == "__main__":
main()
pass
You've got an issue working against you that isn't of your own making. The general rule is when in a turtle canvas, use turtle methods. But turtle doesn't have an inherent 'Motion' event type, so you were trying to use the raw Canvas one as a substitute. Thus the conflict.
An issue of your own making is that when you're inside a fast moving event handler, you need to disable the event hander as the first thing you do, reenabling on exit. Otherwise, events overlap and bad things happen. (Inadvertant recursions and other wierdness.)
I've rewritten your program below to work as I believe you intended. The fix is adding the missing turtle method so we can stay within the turtle domain:
import tkinter as tk
from turtle import RawTurtle, TurtleScreen
from functools import partial
def onscreenmove(self, fun, add=None): # method missing from turtle.py
if fun is None:
self.cv.unbind('<Motion>')
else:
def eventfun(event):
fun(self.cv.canvasx(event.x) / self.xscale, -self.cv.canvasy(event.y) / self.yscale)
self.cv.bind('<Motion>', eventfun, add)
def getPosition(x, y):
screen.onscreenmove(None) # disable events inside handler
pen.setheading(pen.towards(x, y))
pen.goto(x, y)
screen.onscreenmove(getPosition) # reenable handler on exit
root = tk.Tk()
root.title("Draw!")
cv = tk.Canvas(root, width=500, height=500)
cv.focus_set()
cv.pack(side=tk.LEFT)
screen = TurtleScreen(cv)
screen.onscreenmove = partial(onscreenmove, screen) # install missing method
pen = RawTurtle(screen)
frame = tk.Frame(root)
frame.pack(side=tk.RIGHT, fill=tk.BOTH)
tk.Label(frame, text="Width").pack()
screen.onscreenmove(getPosition)
screen.mainloop()
Mouse position for Tkinter:
import Tkinter as tk
root = tk.Tk()
def motion(event):
x, y = event.x, event.y
print('{}, {}'.format(x, y))
root.bind('<Motion>', motion)
root.mainloop()
Mouse position for turtle:
canvas = turtle.getcanvas()
x, y = canvas.winfo_pointerx(), canvas.winfo_pointery()
Hope this helps.
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.
I'm looking for a way to move multiple images together with the background. Moving the background image works fine, but I can't figure out how to add two images on top (they disappear immediately) and then move together with the background. I guess there is an easy way to do that?
I appreciate any hint!
from tkinter import *
import time
tk = Tk()
w = 1000
h = 800
pos = 0
canvas = Canvas(tk, width=w, height=h)
canvas.pack()
tk.update()
background_image = PhotoImage(file="bg.gif")
background_label = Label(tk, image=background_image)
background_label.place(x=0, y=0)
tk.update()
def addImages(files):
for f in files:
image = PhotoImage(file=f)
label = Label(tk, image=image)
label.place(x=files[f][0],y=files[f][1])
tk.update()
def move(xPos):
pos = background_label.winfo_x() - xPos
while background_label.winfo_x() > pos:
background_label.place(x=background_label.winfo_x()-25)
tk.update()
time.sleep(0.001)
img = {"file1.gif": [10,10], "file2.gif": [50,50]}
addImages(img)
move(100)
tk.mainloop()
I'm having difficulty in understanding your code. Why create a canvas and then not use it? You have also littered your code with tk.update(), most of which are unnecessary. But, the described problem is because you create the labels inside a function and the association between label and image gets garbage collected when the function exits. You have to explicitly remember this association:
def addImages(files):
for f in files:
image = PhotoImage(file=f)
label = Label(tk, image=image)
label.image = image # Lets the label remember the image outside the function
label.place(x=files[f][0],y=files[f][1])
If you are then going to move these labels you might want to keep some kind of reference to them or you won't be able to address them.
Complete example
I changed tk to root because tk is the name usually used as alias for tkinter (eg. import tkinter as tk) which gets confusing.
I'm creating a image_list to hold references to the labels containing images. Later I use the list to loop through the labels and move them.
After I have built the GUI I wait 1000 milliseconds before starting the move function. Also I move the images just 1 pixel at a time to clearer see the action.
from tkinter import *
import time
root = Tk()
root.geometry('800x600') # Setting window size instead of usin canvas to do that
pos = 0
background_image = PhotoImage(file="bg.gif")
background_label = Label(root, image=background_image)
background_label.place(x=0, y=0)
image_list = [] # List for holding references to labels with images
def addImages(files):
for f in files:
image = PhotoImage(file=f)
label = Label(root, image=image)
label.image = image # Remember the image outside the function
label.place(x=files[f][0],y=files[f][1])
image_list.append(label) # Append created Label to the list
def move(xPos):
pos = background_label.winfo_x() - xPos
while background_label.winfo_x() > pos:
background_label.place(x=background_label.winfo_x()-1)
for image in image_list: # Loop over labels in list
image.place(x=image.winfo_x()-1) # Move the label
root.update()
time.sleep(0.001)
img = {"file1.gif": [10,10], "file2.gif": [50,50]}
addImages(img)
root.after(1000, move, 100) # Waits 1 second and then moves images
root.mainloop()
By the way; after is a function much preferred over sleep as sleep suspends the program until its finished, whereas after works by making a call after a time and the program runs meanwhile. But if you are ok with that the program freezes during the move, then why not.
Up until now, whenever I've needed to work with multiple shapes on a tkinter canvas, they were just that - shapes. I get their tags with canvas.find_all() and manipulate their geometry by moving, resizing etc.
But I bumped into this problem which I can't seem to solve like this.
If I define a my own class and draw this object to the canvas, how can I keep track of all the objects on the canvas, in order to call their methods?
Say I define a Bubble class, which draws a bubbly thing to the screen. After every second I want it to change all the bubbles' colour to another colour, using their change_colour methods.
my_list = []
for n in range(10):
bubble = Bubble()
my_list.append(bubble)
while True:
time.sleep(1)
for item in my_list:
item.change_colour()
I could append it to a big 'ol list, then iterate through it like I am doing here, but for cases with more objects this is far too slow!
What is the proper way of doing this?
As usual, thanks for any help!
As pointed out, time.sleep() doesn't make any sense, but it is not the problem I am trying to solve.
My advice is to give each item you create at least two tags. One tag would be "bubble" so that you can reference all bubbles at once, and the second would be a tag unique to each bubble.
For example:
class Bubble():
def __init__(...):
self.tag = "b-%d" % id(self)
...
canvas.create_oval(..., tags=("bubble", self.tag))
...
With that, you can implement a change_color method on the Bubble class like the following, which will change all canvas items created by this instance of the class:
def change_color(self, color):
canvas.itemconfigure(self.tag, fill=color)
You can then create a red bubble like this:
bubble = Bubble()
bubble.change_color("red")
This also lets you change all bubbles at once using the "bubble" tag:
canvas.itemconfigure("bubble", outline="blue")
If you want the bubbles to blink, you should not create a while loop. Instead, take advantage of the loop that is already running.
Do this by creating a function that does whatever you want, and then have that function schedule itself to run again via after. For example:
def blink(color="red"):
canvas.itemconfigure("bubble", fill=color)
new_color = "red" if color == "white" else "white"
canvas.after(1000, blink, new_color)
This will cause all bubbles to blink every second as long as the program is running.
If you want to perform custom individual changes to each item (say, changing each item's color to a brand new random color), then you can't do any better than iterating through each one and calling itemconfig on them individually.
However, if you want to make the same change to each item, you can tag your items and call itemconfig a single time, using that tag as your specifier.
Example:
import Tkinter
import random
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
for i in range(1000):
x = random.randint(0, 400)
y = random.randint(0, 400)
canvas.create_oval((x-5,y-5,x+5,y+5), fill="white", tags=("bubble"))
current_color = "white"
def change_colors():
global current_color
current_color = "white" if current_color == "black" else "black"
canvas.itemconfig("bubble", fill = current_color)
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
Result:
However, as I indicated in an earlier comment, I'm still of the opinion that this is a premature optimization. Even if you have a thousand items, iterating through them and configuring them individually isn't noticeably slower than doing it with tags. Example:
import Tkinter
import random
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
items = []
for i in range(1000):
x = random.randint(0, 400)
y = random.randint(0, 400)
id = canvas.create_oval((x-5,y-5,x+5,y+5), fill="white")
items.append(id)
current_color = "white"
def change_colors():
global current_color
current_color = "white" if current_color == "black" else "black"
for id in items:
canvas.itemconfig(id, fill = current_color)
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
The Canvas.find_withtag() method will return a list of the IDs of the all the matching objects specified by first argument. You can use that in conjunction with a dictionary to map those back to the corresponding instances of your class. Once you have that, you can call any of its methods.
import Tkinter
import random
BUBBLE_TAG = 'Bubble'
current_color = 'white'
class Bubble(object):
def __init__(self, canvas, x, y, size, color):
self.canvas = canvas
self.id = canvas.create_oval((x-5,y-5,x+5,y+5), fill=color,
tags=BUBBLE_TAG)
def change_color(self, new_color):
self.canvas.itemconfigure(self.id, fill=new_color)
root = Tkinter.Tk()
canvas = Tkinter.Canvas(root, width=400, height=400)
canvas.pack()
mapping = {}
for i in range(1000):
x, y = random.randint(0, 400), random.randint(0, 400)
color = 'black' if random.randint(0, 1) else 'white'
obj = Bubble(canvas, x, y, 5, color)
mapping[obj.id] = obj
def change_colors():
for id in canvas.find_withtag(BUBBLE_TAG):
current_color = canvas.itemcget(id, 'fill')
new_color = 'black' if current_color == 'white' else 'white'
mapping[id].change_color(new_color) # calls method of object
root.after(1000, change_colors)
root.after(1000, change_colors)
root.mainloop()
Here's an example of it running:
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.