Moving shapes from one scene/window to another using Python tkinter canvas - python

Here is a rough example of what I want to do(my code is too long and messy to share here): -
import tkinter as tk
app = tk.Tk()
w, h = 600, 600
canvas = tk.Canvas(app, width = w, height = h)
canvas.pack()
Rec1 = canvas.create_rectangle(0, 0, 100, 100, fill = 'blue', tag = 'move_to_next_window')
Rec2 = canvas.create_rectangle(100, 100, fill='green', tag = 'dont_move_to_next_window')
app.mainloop()
I am sorry if I messed up a couple lines but this program should run by creating 2 rectangles. What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window. If its possible, could I copy all of the object's properties in the second window? Thank you for taking the time to read this (the second window can also use the tkinter canvas).

What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window.
You can't move canvas items from one canvas to another. Your only option is to delete the item in the first canvas and add a new item in the other canvas.
If its possible, could I copy all of the object's properties in the second window?
Yes, you can use the itemconfigure method to get all of the properties of a canvas object, you can use coords to get the coordinates, and you can get the type with type method.
Here's an example function that copies a rectangle from one canvas to another.
def copy_canvas_item(source, item_id, destination):
item_type = source.type(item_id)
coords = source.coords(item_id)
# N.B. 'itemconfigure' returns a dictionary where each element
# has a value that is a list. The currently configured value
# is at index position 4
attrs = {x[0]: x[4] for x in source.itemconfigure(item_id).values()}
if item_type == "rectangle":
item_id = destination.create_rectangle(*coords, attrs)
return item_id

Related

Group widgets in parent container tkinter

Im drawing lines in the canvas, the problem is that the number of lines is not static, so to draw them i use a loop:
def createBars(self):
size = (200, 50);
for i in range(6):
self.canvas.create_line(size[0], i*size[1], size[0], size[1]+(i*size[1]), fill = self.COLORS[i], width = 10);
it works, i get my lines one below each other as i wish, the problem now is that i need to move all of them at the same time when clicking of them, since to move each of them i need to access to the method moveto, i dont know how to move all of them as group, cant find any related like draw all them inside a box then i can move the box with them inside.
tried appending each of the lines in an array but when printing i get an integer instead of the line object, but if creating it in the constructor each of them can access to the method, but doing it in array way dont know how
First off, a bit of terminology. The lines you draw are not widgets. They are called canvas items, and are very different than widgets.
The solution is to apply one or more tags to each item, and then pass the tag name to the moveto method, or any other method that takes an item id or a tag.
Here's an example that adds the tags "line" and the line color to each element. The arrow keys will move all of the items together. You can easily modify it to move only a particular color by changing the tag passed to the move method.
class Example(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.canvas = tk.Canvas(self)
self.canvas.pack(fill="both", expand=True)
self.COLORS=["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
self.createBars()
self.canvas.bind("<Left>", self.go_left)
self.canvas.bind("<Right>", self.go_right)
self.canvas.focus_set()
def go_left(self, event):
self.canvas.move("line", -5, 0)
def go_right(self, event):
self.canvas.move("line", +5, 0)
def createBars(self):
size = (200, 50);
for i in range(6):
self.canvas.create_line(
size[0], i*size[1], size[0], size[1]+(i*size[1]),
fill = self.COLORS[i], width = 10,
tags=("line", self.COLORS[i]),
);
tried appending each of the lines in an array but when printing i get an integer instead of the line object
This is the defined behavior. create_line and the other methods for creating canvas items return an integer identifier for the created object. The lines and other canvas items aren't themselves python objects.

Tkinter Canvas: Change option without changing others OR set all options again

Creating a fairly basic window and trying to keep everything nice and tightly jammed in the window so that there are no gaps between items in the grid requires setting the canvas's border to -2 (annoying, should be 0, but a different complaint for a different day). However, when updating other attributes (in this case bg), that bd attribute gets reset irreparably. Requesting that property returns the -2 it got set to, but the canvas acts like it isn't (i.e., the canvas appears to have a bd value of 0, despite saying it has -2 when asked).
How can one update only one attribute without breaking the bd attribute?
OR,
How can one set all the attributes for that object simultaneously after it already exists similarly to when it was created so that bd actually takes effect?
A simple example that lets you play with it:
import tkinter as tk
from functools import partial
class MyGUI:
def __init__(self, master):
self.master = master
self.buttonx = tk.Button(master, text='goblue', command=partial(self.gocol, 'lightblue'), bg='lightblue')
self.buttonx.grid(row=1,column=0)
self.buttony = tk.Button(master, text='gogrey', command=partial(self.gocol, 'grey'), bg='grey')
self.buttony.grid(row=2,column=0)
self.canvasx_specs = {
'width' : 400,
'height' : 400,
'bg' : 'grey',
'bd' : -2
}
self.canvasx = tk.Canvas(master, **self.canvasx_specs)
self.canvasx.grid(rowspan=9,row=1,column=1)
def gocol(self, col):
## Method 1
self.canvasx['bg']=col
# self.canvasx.configure(bg=col)
## Method 2
# self.canvasx_specs['bg'] = col
# self.canvasx.configureall(**self.canvasx_specs)
top = tk.Tk()
mywin = MyGUI(top)
top.mainloop()
It doesn't appear to matter if you set bd to -2 again, it still acts like it is 0. It also doesn't appear to matter if you use members access or configure function (see Method 1 above), it has the same effect.
I don't want to delete the whole canvas, it may have drawn objects in it already, and I don't want to redraw everything when the background (or some other option) changes.
Canvas (and other widgets) may have two elements - border and highlight.
If you set bd to 0 or -2 then you can still see highlight which you can remove with 'highlightthickness': 0

Fix Text widget size

I have made an application and part of it involves entering a question and answer. I have this code:
import tkinter as tk
root = tk.Tk()
root.geometry("500x250")
#Main question/answer frame
createFrm = tk.Frame(root)
createFrm.pack(expand = True) #To centre the contents in the window
#Create question entry area
cnqFrm = tk.Frame(createFrm)
cnqFrm.pack()
cnqFrm.pack_propagate(False)
#Question entry
cnqLabQ = tk.Label(cnqFrm, text = "Question")
cnqLabQ.grid(column = 0, row = 0)
#Frame for question Text
cnqTxtQFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtQFrm.grid(column = 0, row = 1)
cnqTxtQFrm.grid_propagate(False)
#Question Text
cnqTxtQ = tk.Text(cnqTxtQFrm)
cnqTxtQ.pack()
cnqTxtQ.pack_propagate(False)
#Answer entry
cnqLabA = tk.Label(cnqFrm, text = "Answer")
cnqLabA.grid(column = 1, row = 0)
#Frame for answer text
cnqTxtAFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtAFrm.grid(column = 1, row = 1)
cnqTxtAFrm.grid_propagate(False)
#Answer Text
cnqTxtA = tk.Text(cnqTxtAFrm)
cnqTxtA.pack()
cnqTxtA.pack_propagate(False)
Despite the fact the Text widget is in a Frame with grid_propagate(False) and a fixed height and width, and the Text widget itself has pack_propagate(False), it still expands to far larger than it should be. Why is this and how can I fix it?
You don't give the text widget an explicit size, so it defaults to 40x80 average-sized characters. The most common way to force it to a specific size that is determined by its parent is to give it a size that is smaller than the containing widget, and then let grid or pack expand it to fit the space given to it. So, start by giving the text widget a width and height of 1 (one).
Next, in this specific case you are calling grid_propagate(False) on the containing frame, but you are using pack to manage the window. You should call pack_propagate if you're using pack. You also need to tell pack to expand the text widget to fill its frame.
Finally, there's no point in calling cnqTxtQ.pack_propagate(False) since that only affects children of the text widget and you've given it no children.
All of that being said, I strongly encourage you to not use grid_propagate(False) and pack_propagate(False). Tkinter is really good at arranging widgets. Instead of trying to force the text widget to a specific pixel size, set the text widget to the desired size in lines and characters, and let tkinter intelligently arrange everything else to line up with them.

Struggling to animate an image in Tkinter

I've started coding a Connect 4 game (a click triggers a disc to fall) and I wanted to animate the discs' fall.
So I start by creating a new disc image :
discList[y][x] = can.create_image((xCan,yCurrent), image=imgRedDisc, anchor="nw")
The disc is created at the top of the canvas at xCan; yCurrent. What I want to do now is to animate the disc falling to its destination, yCan. I call my drop function which keeps incrementing the disc's yCurrent until it reaches yCan with recursivity and the after() method :
def drop(disc):
global yCan, yCurrent
can.move(disc, 0, 1)
yCurrent += 1
if yCurrent < yCan:
can.after(5, drop(disc))
Now what my problem is is that the disc's intermediary positions don't display, it only shows up directly at the bottom after a few seconds.
After some research I added a line to update the canvas :
def drop(disc):
global yCan, yCurrent
can.move(disc, 0, 1)
yCurrent += 1
can.update()
if yCurrent < yCan:
can.after(5, drop(disc))
Now I get to see my disc falling, but it gets laggy after each play ; the discs only start falling a few seconds after I've clicked (which triggers their fall). Another problem is that if I trigger two discs to fall almost silmutaneously by double-clicking very quickly, the first one stops its fall midway then simply falls out of the canvas.
My question is, how do I display every step of my discs' fall without can.update() ?
Also I store each disc id in a list (discList), is there a more convenient way to store dynamically created canvas images ?
The proper way to do animation in tkinter is to have a function that does one frame of animation, and then have it call itself repeatedly until some terminal condition.
For example:
def move_object(obj_id):
can.move(obj_id, 0, 1)
x0,y0,x1,y1 = can.coords(obj_id)
if y0 < yCan:
can.after(5, move_obj, obj_id)
The main difference to what you wrote is that you were using after incorrectly. You were doing this:
can.after(5, drop(disc))
... which is exactly the same as this:
result = drop(disc)
can.after(5, drop)
Notice the difference? Like with the command attribute, you must supply a reference to a function. You however were calling the function and then giving the result to after.
Here's a very simple example to experiment with:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=200, height=200)
canvas.pack(fill="both", expand=True)
ball = canvas.create_oval(50,0,100,50, fill="red")
def animate(obj_id):
canvas.move(obj_id, 0, 3)
x0,y0,x1,y1 = canvas.coords(obj_id)
if y0 > canvas.winfo_height():
canvas.coords(obj_id, 50,-50, 100, 0)
canvas.after(50, animate, obj_id)
animate(ball)
root.mainloop()

Get items on canvas

Im working on a GUI for a board game. So I made some objects on the Canvas and I know they are somehow saved as integers because if I create it, it returns the int and if I canvas.delete(i) it gets removed. Summed up I have a mxn board and the squares are $i\in {1,__,m*n}$.
How can I configure the cells now, by only knowing the integers?
For all squares I set the tag: 'cells', therefore I can get the integers and change stuff:
items = canvas.find_withtag('cells')
canvas.itemconfig('cells', ...)
Do I have to set the (i,j) as a tag when I create the squares?
Thanks for reading and good evening.
I don't really often use Canvas quite often but this should be a workaround for what you have asking for:
import tkinter as tk
# Defining all of your constants
width = 10
height = 10
spacing = 2
countX = 20
countY = 10
# Dictionary which will contains all rectangles
_objects = {}
# Creating the window
root = tk.Tk()
root.geometry("200x100")
canvas = tk.Canvas(root, width = 200, height = 100, bg = "white")
canvas.pack()
# Creating all fields and naming than like coordinates
for y in range(countY):
for x in range(countX):
_objects["{0}/{1}".format(x,y)] = canvas.create_rectangle(0+(x*width),
0+(y*height),
width+(x*width) - spacing,
height+(y*height) - spacing)
# Call a ractangle [X/Y]
canvas.itemconfig(_objects["0/2"], fill = "red")
canvas.itemconfig(_objects["0/9"], fill = "yellow")
canvas.itemconfig(_objects["19/9"], fill = "green")
print(_objects)
root.mainloop()
So the idea is to use a simple Dictionary where you store the id corresponding to the correct coordinate. You can address a specific rectangle by using _objects[<X>/<Y>]. And if you need to know the coordinate you can simply iterate over the dic with:
def getCoordById(oid):
# Search with id
for pairs in _objects.items():
if pairs[1] == oid:
return pairs[0]
canvas.itemconfig(_objects[getCoordById(1)], fill = "purple")
Ps.: If you always use the ID to identfy a coordinate I would use the canvas.create(...) as key and the name as value.

Categories