Search code examples
pythontkinterline

How to move points of a line to specific locations?


I'm trying visualise the sine waves addition in python using tkinter, and I'm trying to build lines between each circles center, but what I've tried so far didnt work as I thought it would. is there a way to fix what I've tried (see code), or a way to move only one coordinates point of a line independantly from the other?

As you'll see in the code if you run it, I've tried a method where each iteration the previous line is erased and a new one is created. When I run the code there is actually a line between each center of the circles just like I want, but facts are those lines persist and won't erase themselves; for some reason it seems like the canvas.delete(line) doesnt work as I expected it to.

here's the full code. The interesting part is in the 'updateline' fonction, into 'act()' func.

import math
import tkinter as tk

##important to know! -- the way I'm creating the circles is by setting an object, the bounds of the circle, depending on amplitude asked by user.
##then the programs calculates the path of these bounds, depending on circles, amplitude, phase and frequency of the sine waves asked by the user from the tkinter GUI.
##finally, the program creates and moves along this path a circle, representing visually the sine wave.

top = tk.Tk()
top.title('Superposition')
choice = tk.Tk()
choice.title('Parametres')

f = tk.Frame(choice,bd=3)
f.pack(side='top')

g = tk.Frame(choice,bd=3)
g.pack(side='bottom')

tk.Label(f,text="nbre ondes:",width = 10).grid(row=0,column=0)
sines = tk.Spinbox(f,from_=1,to=50,width=10,textvariable=tk.DoubleVar(value=2))
sines.grid(row=0,column=1)
sines.delete(0,5)
sines.insert(0,2)

delai = tk.Scale(g, orient='vertical', from_=100, to=1,resolution=1, length=100,label='delai')
delai.grid(row=0,column=0)
hauteur = tk.Scale(g, orient='vertical', from_=1100, to=100,resolution=100, length=100,label='fenetre')
hauteur.grid(row=0,column=1)
taillec1 = tk.Scale(g, orient='vertical', from_=3.5, to=0.1,resolution=0.1, length=100,label='taille')
taillec1.grid(row=0,column=2)
delai.set(20)
hauteur.set(600)
taillec1.set(1.5)

def grilledechoix():
    numberofsines = int(sines.get())
    for i in f.grid_slaves():
        if int(i.grid_info()["row"]) > numberofsines+2:
            i.grid_forget()

    for i in range(1,numberofsines+1):
        tk.Label(f,text="phase n."+str(i),width = 10).grid(row=i+2,column=4)
        phase = tk.Spinbox(f,from_=-180,to=180,width=10)
        phase.grid(row=i+2,column=5)
        phase.delete(0,5)
        phase.insert(0, 0)
    for i in range(1,numberofsines+1):
        tk.Label(f,text="amp. n."+str(i),width = 10).grid(row=i+2,column=0)
        ampli = tk.Spinbox(f,from_=1,to=10000000,width=10)
        ampli.grid(row=i+2,column=1)
        ampli.delete(0,5)
        ampli.insert(0,10)
    for i in range(1,numberofsines+1):
        tk.Label(f,text="freq n."+str(i),width = 10).grid(row=i+2,column=2)
        freq = tk.Spinbox(f,from_=-1000,to=1000,width=10)
        freq.grid(row=i+2,column=3)
        freq.delete(0,5)
        freq.insert(0,5)


def act():        
    h = g.grid_slaves()[1].get()
    delai = g.grid_slaves()[2].get()
    taillec1 = g.grid_slaves()[0].get()
    w = h

    ampdict = {'box1':100 * ((h/700)*taillec1)}
    frqdict = {}
    aaadict = {}
    fffdict = {}
    phadict = {}

    numberofsines = int(sines.get())

    sin = lambda degs: math.sin(math.radians(degs))
    cos = lambda degs: math.cos(math.radians(degs))

    for i in range(1,numberofsines+1):
        fffdict['box'+str(numberofsines-i+1)] = f.grid_slaves()[(2*i)-2].get()
        aaadict['box'+str(numberofsines-i+1)] = f.grid_slaves()[(2*i)-2+2*numberofsines].get()
        phadict['box'+str(numberofsines-i+1)] = f.grid_slaves()[(2*i)-2+4*numberofsines].get()
    for i in range(1,numberofsines+1):
        ampdict['box'+str(i)] = (float(ampdict['box1'])/float(aaadict['box1'])) * float(aaadict['box'+str(i)])
        frqdict['box'+str(i)] = float(fffdict['box'+str(i)])/float(fffdict['box1'])

    class obj(object):
        cos0, cos180 = cos(0), cos(180)
        sin90, sin270 = sin(90), sin(270)

        def __init__(i, x, y, rayon):
            i.x, i.y = x, y
            i.rayon = rayon

        def bounds(i):
            return (i.x + i.rayon*i.cos0,   i.y + i.rayon*i.sin270,
                    i.x + i.rayon*i.cos180, i.y + i.rayon*i.sin90)

    def updateposition(canvas, id, cent, obj, path):
        obj.x, obj.y = next(path)
        x0, y0, x1, y1 = canvas.coords(id)
        oldx, oldy = (x0+x1) // 2, (y0+y1) // 2
        dx, dy = obj.x - oldx, obj.y - oldy
        canvas.move(id, dx, dy)
        canvas.move(cent, dx, dy)
        canvas.after(delai, updateposition, canvas, id, cent, obj, path)

    def updateline(canvas, line, robj0, cent0, robj1, cent1):
        x00, y00, x01, y01 = canvas.coords(cent0)  ##defining coords of the two ovals asked, representing centers of circles
        x10, y10, x11, y11 = canvas.coords(cent1)
        oldx0, oldy0 = (x00+x01) // 2, (y00+y01) // 2 ##defining center coords of the two ovals
        oldx1, oldy1 = (x10+x11) // 2, (y10+y11) // 2
        dx0, dy0 = robj0.x - oldx0, robj0.y - oldy0 ##defining the deltax and deltay, difference of movements between frames, of the two ovals
        dx1, dy1 = robj1.x - oldx1, robj1.y - oldy1
        canvas.after(delai, canvas.delete, line) ##deleting previous line, does not work and I don't know why. I've also tried 'canvas.delete(line)', giving same results
        canvas.create_line(oldx0+dx0, oldy0+dy0, oldx1+dx1, oldy1+dy1) ##creating new line
        canvas.after(delai, updateline, canvas, line, robj0, cent0, robj1, cent1) ##function invoking itself after delay 'delai'

    def posobj(pt,ang,deltang):
        while True:
            yield pt.x + pt.rayon*cos(ang), pt.y + pt.rayon*sin(ang)
            ang = (ang+deltang)%360

    try:
        top.pack_slaves()[0].destroy()
    except:
        pass

    canvas = tk.Canvas(top, bg='white', height=h, width=w)
    canvas.pack(side='right')

    robj = {}
    r = {}
    posobjet = {}
    line = {}
    cent = {}

## the following 'for' loop creates a number of circles corresponding to sine waves, as much as the user asked.
    for i in range(1,int(sines.get())+2):
        if i != int(sines.get())+1:
            if i == 1:
                robj[str(i)] = obj(h/2,h/2,float(ampdict['box'+str(i)]))
                r[str(i)] = canvas.create_oval(robj[str(i)].bounds(),fill='',outline='black')
                cent[str(i)] = canvas.create_oval(h/2+h/200,h/2+h/200.,h/2-h/200,h/2-h/200, fill='white', outline='red')
                posobjet[str(i)] = posobj(robj[str(i)],float(phadict['box'+str(i)]),float(frqdict['box'+str(i)]))
            else:
                robj[str(i)] = obj(robj[str(i-1)].x,robj[str(i-1)].y,float(ampdict['box'+str(i)]))
                r[str(i)] = canvas.create_oval(robj[str(i)].bounds(),fill='',outline='black')
                cent[str(i)] = canvas.create_oval(robj[str(i)].x+h/200,robj[str(i)].y+h/200,robj[str(i)].x-h/200,robj[str(i)].y-h/200, fill='white', outline='blue')
                line[str(i)] = canvas.create_line(0,0,0,0)
                posobjet[str(i)] = posobj(robj[str(i)],float(phadict['box'+str(i)]),float(frqdict['box'+str(i)]))
                top.after(delai, updateposition, canvas, r[str(i)], cent[str(i)], robj[str(i)], posobjet[str(i-1)])
                ##here I'm invoking the updateline function using the constant 'delai', the line i, and objects defining the bounds of the center objects, the little blue/red dots appearing as the center of each circles(run the code, it'll be easier to understand)
                top.after(delai, updateline, canvas, line[str(i)], robj[str(i-1)], cent[str(i-1)], robj[str(i)], cent[str(i)])
        else:
            robj[str(i)] = obj(robj[str(i-1)].x,robj[str(i-1)].y,h/200)
            r[str(i)] = canvas.create_oval(robj[str(i)].bounds(),fill='white',outline='red')
            cent[str(i)] = canvas.create_oval(robj[str(i)].x+h/200,robj[str(i)].y+h/200,robj[str(i)].x-h/200,robj[str(i)].y-h/200, fill='white', outline='red')
            line[str(i)] = canvas.create_line(0,0,0,0)
            top.after(delai, updateposition, canvas, r[str(i)], cent[str(i)], robj[str(i)], posobjet[str(i-1)])
            ##2nd and last time invoking the updateline function, for the line between the last circle's point and the final red point.
            top.after(delai, updateline, canvas, line[str(i)], robj[str(i-1)], cent[str(i-1)], robj[str(i)], cent[str(i)])
    top.mainloop()

ok = tk.Button(f,text='NBRE',command=grilledechoix)
ok.grid(row=0,column=2)
ac = tk.Button(f,text='APPLY',command=act)
ac.grid(row=0,column=3)
grilledechoix()
act()

I expected the lines to disappear once the updateline function called itself again, because of that 'canvas.delete(line)' line into updateline, and I can't really understand why it does that. anyway if you have a solution to make the lines move, without creating and deleting them each time the function is called, feel free to tell me.

Thanks!


Solution

  • If I understand the problem correctly, I believe the issue is with this code:

    canvas.after(delai, canvas.delete, line)
    canvas.create_line(oldx0+dx0, oldy0+dy0, oldx1+dx1, oldy1+dy1)
    canvas.after(delai, updateline, canvas, line, robj0, cent0, robj1, cent1)
    

    It fails to reassign the new line to the line variable for the next call. Instead try:

    canvas.after(delai, canvas.delete, line)
    line = canvas.create_line(oldx0+dx0, oldy0+dy0, oldx1+dx1, oldy1+dy1)
    canvas.after(delai, updateline, canvas, line, robj0, cent0, robj1, cent1)
    

    Which gets rid of the extra lines when I run it. Let me know if I've missed the point.