Why is the generated tkinter button-1 event not recognized?
Asked Answered
S

2

1

I have 2 canvas rectangles, which have a binding for Button-1. Sometimes I want that when one rectangle gets this event, then the other rectangle should also get this event. So I have to duplicate the Button-1 event by the widget method "event_generate".

My example code is kind of minimal, so you can only press once the left mouse button. When you press the left mouse button over the red rectangle, this event is recognized, which is proved by the message "button1 red " showing up. Because I have added a second binding to each of the rectangles, also the method "create_event_for_rectangle" is started, which is proved by the message "Create event for other rectangle" showing up. This method also generates a new Button-1 event at the green rectangle. The coordinates of the generated event seem to be correct, as additionally a small rectangle is created at the calculated coordinates. This generated event at the the green rectangle now should create the message "button1 green 45 45", but nothing happens.

This is my code:

import tkinter as tk
class rectangle():
    def __init__(self, factor, color):
        self.canvas_id=canvas.create_rectangle(factor*10,factor*10,
                       (factor+1)*10,(factor+1)*10,fill=color)
        canvas.tag_bind(self.canvas_id, "<Button-1>", self.button1)
        self.color = color
    def button1(self, event):
        print("button1", self.color, event.x, event.y)
    def get_canvas_id(self):
        return self.canvas_id
    def get_center(self):
        coords = canvas.coords(self.canvas_id)
        return (coords[0]+coords[2])/2, (coords[1]+coords[3])/2
def create_event_for_rectangle(not_clicked):
    print("Create event for other rectangle")
    canvas.tag_unbind(red.get_canvas_id()  , "<Button-1>", func_id_red)
    canvas.tag_unbind(green.get_canvas_id(), "<Button-1>", func_id_green)
    not_clicked_center_x, not_clicked_center_y = not_clicked.get_center()
    canvas.event_generate("<Button-1>",
           x=not_clicked_center_x, y=not_clicked_center_y)
    canvas.create_rectangle(not_clicked_center_x-1, not_clicked_center_y-1,
                            not_clicked_center_x+1, not_clicked_center_y+1)
root   = tk.Tk()
canvas = tk.Canvas(width=100, height=100)
canvas.grid()
red    = rectangle(1, "red"  )
green  = rectangle(4, "green")
func_id_red   = canvas.tag_bind(red.get_canvas_id()  , "<Button-1>",
                lambda event: create_event_for_rectangle(green), add="+multi")
func_id_green = canvas.tag_bind(green.get_canvas_id(), "<Button-1>",
                lambda event: create_event_for_rectangle(red  ), add="+multi")
root.mainloop()

What am I doing wrong?

Stress answered 13/4, 2023 at 15:36 Comment(0)
C
0

That's because the tag_unbind() or unbind() clears all the existing handlers. The backend Tcl/Tk library does not provide a command for unbinding, and the current implementation of the unbind() binds a dummy empty script. (There is an old open issue on this problem. What about pinging the issue?)

Changing event handlers and synthesizing events make the program unnecessarily complex. In your case, it's much better to use the after_idle() and closures, like the following example.

import tkinter as tk

root = tk.Tk()

canvas = tk.Canvas(width=100, height=100)
canvas.grid()

class rectangle():
    def __init__(self, factor, color):
        self.canvas_id = canvas.create_rectangle(...)
        canvas.tag_bind(self.canvas_id, "<Button-1>", self.button1)
        self.other = None
    def get_center(self):
        coords = canvas.coords(self.canvas_id)
        return (coords[0]+coords[2])/2, (coords[1]+coords[3])/2
    def button1(self, event=None):
        print("button1", self.color, event)
        other = self.other
        if other and event:
            x, y = other.get_center()
            canvas.create_rectangle(x-1, y-1, x+1, y+1)
            print("Schedule the button1() of other rectangle")
            root.after_idle(other.button1)

red = rectangle(1, "red"  )
green = rectangle(4, "green")
red.other = green
green.other = red

root.mainloop()
Chinua answered 16/4, 2023 at 9:21 Comment(6)
I tested tag_unbind, and yes, you are right. But when I read the documentation it sounds different: "Removes bindings for handler funcId and event sequence from the canvas object or objects specified by tagOrId."Stress
I found a solution #37903003. Instead of canvas.tag_unbind(red.get_canvas_id() , "<Button-1>", func_id_red) I can use root.unbind("<Button-1>", func_id_red). Then only the additional bindings are removed and everything works as expected. But then when I close the window I get this error: "_tkinter.TclError: can't delete Tcl command"Stress
The documentation is incorrect. See the implementation for a detail. And the point of the answer of the linked question is using the returned function id, not calling the unbind() on the root.Chinua
@Matthias Schweikart, I found an old issue on this problem. Check the link in the updated answer.Chinua
Can you "ping the issue"? I don't know how to do that.Stress
You can log in to Github and post a new comment on the issue. If there's no response after two weeks, you can post a message on the Python forum.Chinua
S
0

I found a solution at https://github.com/python/cpython/issues/75666 and updated my code. The patch overloads the method tag_unbind. The new tag_unbind reads first the bound function ids, removes the function which shall be removed, and binds then again. Now it works as expected.

import tkinter as tk
class CanvasPatched(tk.Canvas):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
    def tag_unbind(self, tagOrId, sequence, funcid=None):
        funcs_string = self.tk.call(self._w, 'bind', tagOrId, sequence, None).split('\n')
        bound = '\n'.join([f for f in funcs_string if not f.startswith('if {"[' + funcid)])
        self.tk.call(self._w, 'bind', tagOrId, sequence, bound)
        if funcid:
            self.deletecommand(funcid)
class rectangle():
    def __init__(self, factor, color):
        self.canvas_id=canvas.create_rectangle(factor*10,factor*10,
                                               (factor+1)*10,(factor+1)*10,fill=color)
        canvas.tag_bind(self.canvas_id, "<Button-1>", self.button1)
        self.color = color
    def button1(self, event):
        print("button1", self.color, event.x, event.y)
    def get_canvas_id(self):
        return self.canvas_id
    def get_center(self):
        coords = canvas.coords(self.canvas_id)
        return (coords[0]+coords[2])/2, (coords[1]+coords[3])/2
def create_event_for_rectangle(not_clicked):
    print("Create event for other rectangle")
    canvas.tag_unbind(red.get_canvas_id()  , "<Button-1>", func_id_red)
    canvas.tag_unbind(green.get_canvas_id(), "<Button-1>", func_id_green)
    not_clicked_center_x, not_clicked_center_y = not_clicked.get_center()
    canvas.event_generate("<Button-1>", x=not_clicked_center_x, y=not_clicked_center_y, state=3)
    canvas.create_rectangle(not_clicked_center_x-1, not_clicked_center_y-1,
                            not_clicked_center_x+1, not_clicked_center_y+1)
root   = tk.Tk()
canvas = CanvasPatched(width=100, height=100)
canvas.grid()
red    = rectangle(1, "red"  )
green  = rectangle(4, "green")
func_id_red   = canvas.tag_bind(red.get_canvas_id()  , "<Button-1>",
                lambda event: create_event_for_rectangle(green), add="+multi")
func_id_green = canvas.tag_bind(green.get_canvas_id(), "<Button-1>",
                lambda event: create_event_for_rectangle(red  ), add="+multi")
root.mainloop()
Stress answered 18/4, 2023 at 17:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.