Tkinter Canvas items stored cleared and recalled - python

I am a beginner messing around in python 3.6 with tkinter canvas and have built a function that generates a solar system at random using elipses and the random method.
I would like to be able to save these items and all their attributes like their tags and bindings so that I can clear the canvas and draw a new random system. I would then like to go back to the first generated system if I need to.
I do not want to save the canvas as an image as I am binding the items to functions. Any ideas on how to achieve this?

As far as I know, you only get the items "id" as a handle to that item, and while you can delete an item with a given ID, you can not recreate it just with that ID.
What you could do is to given all those elements a common tag and use tag_lower and tag_raise to hide and show the items below and above the "background" pane. The objects are still on the canvas, but can not be seen and do not react to e.g. mouse events.
import tkinter, random
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
space = canvas.create_rectangle(0, 0, 200, 200, fill="#000000")
for _ in range(10):
x, y = random.randint(0, 200), random.randint(0, 200)
s = canvas.create_oval(x, y, x+10, y+10, fill="#ffff00", tags="star")
canvas.tag_bind(s, "<Button>", lambda e: print("twinkle"))
root.after(3000, lambda: canvas.tag_lower("star", space))
root.after(6000, lambda: canvas.tag_raise("star", space))
root.mainloop()
Update: As suggested by OP in comments, one can also set the items' state to hidden, being probably the clearer option and not needing some obscure (-ing) background item.
root.after(3000, lambda: canvas.itemconfig("star", state="hidden"))
root.after(6000, lambda: canvas.itemconfig("star", state="normal"))

Related

Text Animations with Tkinter

I'm designing a GUI to decrease the time which my colleagues spend for reporting tests. But I stuck in the animation of opening screen. I mixed the code that I found on the internet which writes the chosen text letter one by one with a second for loop to enlarge the text but the first loop worked only for the last word of the list. Also, I tried the same code with a while loop both with giving count or just writing "True" but they didn't solve my problem either.
I want to see the chosen texts (now, only a few words but later on I will write my tests) written on the screen one by one and letter by letter. How can I solve this?
My sketch code is as follows:
import tkinter as tk
import random
root = tk.Tk()
root.configure(bg="white")
Words=["Canvas", "Import", "Index", "Random", "Tkinter"]
canvas=tk.Canvas(root, width=400, height=375, bg="white")
canvas.pack()
for word in Words:
x=random.randint(0, 250)
y=random.randint(0, 225)
canvas_text=canvas.create_text(x, y, text=word)
delta=500
delay=0
for i in range(len(word)+1):
s=word[:i]
update_text=lambda s=s: canvas.itemconfigure(canvas_text, text=s)
canvas.after(delay, update_text)
delay+=delta
x=random.randint(0, 250)
y=random.randint(0, 225)
root.mainloop()
There are several issues in your code:
With canvas_text = canvas.create_text(x, y, text=word) you immediately write the full word, it would make more sense to start with an empty string, canvas_text = canvas.create_text(x, y, text=""), then add the letters one by one. Additionally, you can add the option anchor="w" so that adding the letters will not move the word (otherwise the default anchor is the center of the object).
delay is reset to 0 for each word so the letters will appear one by one but at the same time for all words.
update_text is defined as lambda s=s: canvas.itemconfigure(canvas_text, text=s) and therefore canvas_text refers to the last created canvas object (see What do (lambda) function closures capture?). As a consquence, only the last word is animated. To avoid this, you can use an additional argument in the function: lambda s=s, c=canvas_text: canvas.itemconfigure(c, text=s)
A few additional remarks:
delta=500 can be moved out of the for loop
The x = ... and y = ... in the nested for loop are useless and can be removed
update_text=lambda s=s: canvas.itemconfigure(canvas_text, text=s): lambda is typically used to avoid having to give the function a name, so one typically either defines a named function with def or uses a lambda directly, e.g.
canvas.after(delay, lambda c=canvas_text, s=word[:i]: canvas.itemconfigure(c, text=s))
Here is the full code:
import tkinter as tk
import random
root = tk.Tk()
root.configure(bg="white")
Words = ["Canvas", "Import", "Index", "Random", "Tkinter"]
canvas = tk.Canvas(root, width=400, height=375, bg="white")
canvas.pack()
delay = 0
delta = 500
for word in Words:
x = random.randint(0, 250)
y = random.randint(0, 225)
canvas_text = canvas.create_text(x, y, text="", anchor="w")
for i in range(len(word)+1):
canvas.after(delay, lambda c=canvas_text, s=word[:i]: canvas.itemconfigure(c, text=s))
delay += delta
root.mainloop()

PysimpleGUI - simple animation

Thank you for reading this.
I'm working on a simple animation that is based on one of two examples from the PysimpleGUI cookbook. The attached code, of course, it is not doing anything. I've looked through many examples trying to figure out how to update the canvas but without success.
My first attempt was based on the sine wave plot example. I have an endless while loop and a display function. The display on the graph area shows the first iteration through the loop but is never updated after that.
The display function contains:
graph.DrawCircle((i,j), 5, line_color='black', etc
A second related question, should I be using the canvas or the graph method (as in the sine wave plot example), or doesn't it matter?
I don't want to overwhelm the reader with too much code. If I can get the following to work then I may have a good chance with the real code.
import PySimpleGUI as sg
import time
layout = [
[sg.Canvas(size=(100, 100), background_color='red', key= 'canvas')],
[sg.T('Change circle color to:'), sg.Button('Red'), sg.Button('Blue')]
]
window = sg.Window('Canvas test')
window.Layout(layout)
window.Finalize()
canvas = window.FindElement('canvas')
cir = canvas.TKCanvas.create_oval(50, 50, 100, 100)
while True:
event, values = window.Read()
'''
if event is None:
break
if event == 'Blue':
canvas.TKCanvas.itemconfig(cir, fill="Blue")
elif event == 'Red':
canvas.TKCanvas.itemconfig(cir, fill="Red")
'''
# this is the part that I need to sort out
for i in range(10):
if i % 2 == 0:
canvas.TKCanvas.itemconfig(cir, fill="Blue")
else:
canvas.TKCanvas.itemconfig(cir, fill="Red")
time.sleep(1)
I discovered the answer and that is, window.Read(timeout = 0).
In order for changes to show up in a window after making changes, you much either call Read or Refresh. I think all you need to do is down in your bottom loop, add the line:
window.Refresh()
From the docs at http://www.PySimpleGUI.org:
Refresh()
Cause changes to the window to be displayed on the screen.
Normally not needed unless the changes are immediately required or if
it's going to be a while before another call to Read.

Which Tkinter functions should i use to find the coordinates of a randomly placed(in the canvas) text entry and how?

Thanks for looking into my question. I'll try to give you a big and small picture of what I'm trying to do here.
Big Picture:
So, basically I'm trying to make a simple mindmapping program where, after the first entry, every text I input unto the Entry widget is linked to the previous text via a line widget. So like this: Hello----There, and then Hello----There----Yo. Actually, I'm hoping for more modifications in the future like being able to rearrange the links via some metric I have yet explored, but this is basically it.
Small/Specific Picture:
I realize that in order to do this, I will have to find a way to acquire all the xy coordinates of every text drawn on the canvas (text I drew on the canvas by using the random function). I need the coordinates of the first text and the coordinates of the second text so I can use those to draw the line to visually link the two texts. I thought of using an array to list down all inputted text, but I realize that only stores the text and not the location of the text on the canvas. I have explored using tags, or using the coords functions or using the bbox function, but to no avail. Any clues on how to go about this? I would highly appreciate it, Thanks. :)
import Tkinter
import random
a = Tkinter.Tk()
b = Tkinter.Canvas(a, width=1000, height=500)
b.pack()
def c(event):
b.create_text(random.randint(50,940), random.randint(50,480), anchor="center", text=d.get())
f.append(d.get())
d.delete(0, 'end')
print f
#this function creates a randomly located text taken from the entry widget below and, at the same time, appends the text in the list known as f''
d = Tkinter.Entry(a)
d.pack()
d.bind("<Return>", c)
d.focus()
b.create_line(300, 250, 600, 290)
#this is my very early attempt at linking text inputted and drawn on the Canvas
f = []
a.mainloop()
Simply assign random values to variables before you use it to create text on canvas, and keep on list with object ID and text.
x = random.randint(...)
y = random.randint(...)
obj_id = b.create_text(x, y, ...)
f.append([x, y, obj_id, d.get()])
BTW: if you have obj_id then you can also do
x,y = b.coords(obj_id)

Python tkinter widget image animation function with after in a loop misbehaving

I am slowly learning my way through Python and tkinter. :)
In a game I'm making there are animations of images displayed within widgets (namely buttons).
Animating frame-by-frame is mundane, so I came up with a function to help me automate a 10-frame animation.
In a loop of range(10) (as I have 10 frames) the function calls the after() method which has a function callback, each displaying next frame of animation.
Since time within the after method is larger for each consecutive iteration of the loop, each new frame should be displayed nicely after given time (here it's 34ms).
That's all fine in theory, however when I run the code and appropriate functions are called, the button does not animate properly. Only the last frame seems to pop out.
The way I see it, after some reading on how tkinter works, is that each after in a loop should set independent callback in tkinter's "timeline" to be called after some time. Thus in my opinion this code should work.
What do you make of it? What've I got wrong, is my logic about after() in a loop off?
#Python 3.4.3
def animateMine(object):
global firstAnimateMineCall
for frame in range(10):
frame += 1
time = 34 * frame
root.after(time, lambda: mineAnimationFrame(object, frame))
if firstAnimateMineCall and frame == 10:
root. after(500 , lambda: animateAllMines(object))
firstAnimateMineCall = False
In the doubtful event this'd be useful:
def mineAnimationFrame(object, frame):
tempDir = "Resources/Mine/saperx_mine_%s.png" % (frame)
tempImage = PhotoImage(file=tempDir)
object.configure(image=tempImage)
object.image = tempImage
object.disabled = True
A simplistic, good-looking and easy to implement solution to this problem I came up with (thanks to CurlyJoe's advice).
*A major pro of this design is that it's easy to adjust it to your frames quantity... you got 5? Just change 1 value and it's good to go! Got 900? Still easy. 6,02*10^23 frames? Still just 1 change ;]*
To adjust to your frame size, just change the list comprehension range(10) to whatever quantity you wish, the code will take care of the rest.
from tkinter import Tk, PhotoImage, Button, FLAT
root = Tk()
mineImagesList = [PhotoImage(file="Resources/Mine/saperx_mine_%s.png" % (frame)) for frame in range(1, 11)]
button = Button(root, bd=0, relief=FLAT, command= lambda: func(button))
def func (object, frame=0):
object.configure(image=mineImagesList[frame])
object.image = mineImagesList[frame]
print("Object image:", object.image)
if frame+1 < len(mineImagesList):
frame += 1
root.after(34, lambda frame=frame, object=object: func(object=object, frame=frame))
button.pack()
root.mainloop()

Finding widgets on a grid (tkinter)

Using the tkinter module, suppose I create a grid with 50 button widgets and each of those widgets has different text. I need to be able to specify some way of typing in a row and column so I can get that widget's text at that location.
For example, if I need the widget's text at the third row in the second column of the grid. I've searched the docs but that tells me how to get info about widgets, when I need info about the grid.
There's no need to create your own function or keep a list/dictionary, tkinter already has a built-in grid_slaves() method.
It can be used as frame.grid_slaves(row=some_row, column=some_column)
Here's an example with a grid of buttons showing how grid_slaves() retrieves the widget, as well as displaying the text.
import tkinter as tk
root = tk.Tk()
# Show grid_slaves() in action
def printOnClick(r, c):
widget = root.grid_slaves(row=r, column=c)[0]
print(widget, widget['text'])
# Make some array of buttons
for r in range(5):
for c in range(5):
btn = tk.Button(root, text='{} {}'.format(r, c),
command=lambda r=r, c=c: printOnClick(r, c))
btn.grid(row=r, column=c)
tk.mainloop()
You got a previous answer relative to a method to save button objects in a dictionary in order to recover them using their (column, row) position in a grid.
So if self.mybuttons is your dictionary of lists of buttons as described in previous answer, then you can get the text at position row, col as this:
abutton = self.mybuttons[arow][acolumn]
text_at_row_col = abutton["text"]
On the other hand, if what you need is to get the text from the button callback:
button.bind("<Button-1>", self.callback)
then you can get the button text from the event, you do not need to know its row/col position, only to press it:
def callback(self, event):
mybutton = event.widget
text_at_row_col = mybutton["text"]

Categories

Resources