0

I am trying to make Visual programming system using tkinter. That is what I have for now:

I need to be able to move nodes by mouse. All nodes have a main frame, that I am moving using place method. I have created a funcion with place method and attached it to the "Motion" event. But then if I move nodes too fast, I can see the temporary "path" made of last positions of a node: I am not sure why this happens. My guess is that it may be hard to complete lots of place function at small time, so just can't update screen too fast. Or maybe that is somehow related to the bindtags. Parts from code. From init:

[widget.bind("<Button-1>", self.start_motion) for (name, widget) in self.base_f.children.items()]
[widget.bind("<ButtonRelease-1>", self.stop_motion) for (name, widget) in self.base_f.children.items()]

Then start, stop motion and move node functions:

def start_motion(self, e: tk.Event):
    [widget.bind("<Motion>", self.move_node) for (name, widget) in self.base_f.children.items()]

def stop_motion(self, e: tk.Event):
    [widget.unbind("<Motion>") for (name, widget) in self.base_f.children.items()]

def move_node(self, e):
    m_pos_x = root.winfo_pointerx() - root.winfo_rootx()
    m_pos_y = root.winfo_pointery() - root.winfo_rooty()
    
    self.base_f.place(x=m_pos_x, y=m_pos_y)

Some explanation: All widgets from "node" class are children from a main_base_frame(base_f). I want binding happens only on specific widgets touched by a mouse (that is where that lists come from, only touched children of base_f will make node to be able to move). Move node just move node to position of cursor. Here is a simple code example to try:

import tkinter as tk


class SimpleClass:
    def __init__(self):
        self.frame = tk.Frame(width=200, height=100, bg="black")
        self.frame.place(x=0, y=0)
        self.frame.bind("<Button-1>", self.pressed)
        self.frame.bind("<ButtonRelease-1>", self.release)

    def pressed(self, e):
        self.frame.bind("<Motion>", self.move)

    def release(self, e):
        self.frame.unbind("<Motion>")

    def move(self, event: tk.Event):
        m_pos_x = root.winfo_pointerx() - root.winfo_rootx()
        m_pos_y = root.winfo_pointery() - root.winfo_rooty()
        self.frame.place(x=m_pos_x, y=m_pos_y)


root = tk.Tk()
root.geometry('1200x1000')
cl = SimpleClass()

root.mainloop()

I am really interested why this happens and how can I fix this. Maybe there is a function that will freeze place method until window has finished all updates or something like this. Thanks.

P.S. Don't hate me for using the tkinter library for purposes it was not intended for.

Danya K
  • 165
  • 1
  • 10

2 Answers2

1

I remember encountering this issue under Windows and my conclusion was that only the area "needed" is rendered and redrawn by tkinter. You have to redraw the background as well.

The easiest way of doing so is to set the background color of the widget behind the moving/dynamic part of your GUI.

def move(self, event: tk.Event): 
    m_pos_x = root.winfo_pointerx() -  root.winfo_rootx() 
    m_pos_y =  root.winfo_pointery() - root.winfo_rooty() 
    self.frame.place(x=m_pos_x, y=m_pos_y)
    root.configure(background = "yellow"

This should solve your issue.

Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
  • Thanks a lot! It is indeed working. I even tried to use a label with an image as for background and just using label.config(image=) in move function it removes glithes. Still confused about the explanation. Are there any sources where I can read more about _only the area "needed" is rendered_? – Danya K Jul 22 '23 at 11:50
  • @DanyaK I think I read it in the source code. But not entirely sure anymore – Thingamabobs Jul 22 '23 at 12:24
0

I've also found an alternative solution, which seems to work, but may be less "correct". It uses update of the root and afteridle:

import tkinter as tk


class SimpleClass:
    def __init__(self):
        self.frame = tk.Frame(width=200, height=100, bg="black")
        self.frame.place(x=0, y=0)
        self.frame.bind("<Button-1>", self.pressed)
        self.frame.bind("<ButtonRelease-1>", self.release)
        self.run = False

    def pressed(self, e):
        self.run = True
        self.move()

    def release(self, e):
        self.run = False

    def move(self):
        if self.run:
            m_pos_x = root.winfo_pointerx() - root.winfo_rootx()
            m_pos_y = root.winfo_pointery() - root.winfo_rooty()
            self.frame.place(x=m_pos_x, y=m_pos_y)
            root.update()
            root.after_idle(self.move)


root = tk.Tk()
root.geometry('1200x1000')
cl = SimpleClass()

root.mainloop()

it seems not to be completely right, as in some sources (e.g. there) I've read that it is better not to use root.update() at all, and sometimes it could lead to a different errors (I once had a recursion error, probably not in this example, as afteridle happens after root has finished updating)

Danya K
  • 165
  • 1
  • 10