Search code examples
pythontkintertkinter-canvas

How can I move a tkinter canvas-window by the mouse-pointer?


I have a tkinter Canvas where some objects are placed and can be moved by the mouse pointer. One of these objects is a text, which is not implemented as canvas-text-item but as a text-widget in a canvas-window-item, as I want to implement syntax highlighting for the text. I know how to implement the move-feature for a canvas-text-item but I am not able to implement the same feature for a canvas-window-item. I believe the reason is that when the mouse pointer is inside the canvas-window-item, it not inside the canvas anymore. This is my example code, where moving text1 works, but moving text2 fails:

    import tkinter as tk
    event_x = None
    event_y = None
    
    def end_move_text(event, item_id):
        canvas_id.tag_unbind(item_id, "<Motion>"         )
        canvas_id.tag_unbind(item_id, "<ButtonRelease-1>")
    
    def move_text(event, item_id):
        global event_x, event_y
        canvas_id.move(item_id, event.x-event_x, event.y-event_y)
        event_x, event_y = event.x, event.y
    
    def move_start_text(event, item_id):
        global event_x, event_y
        event_x, event_y = event.x, event.y
        canvas_id.tag_bind(item_id, "<Motion>"         , lambda event : move_text    (event, item_id))
        canvas_id.tag_bind(item_id, "<ButtonRelease-1>", lambda event : end_move_text(event, item_id))
    
    root = tk.Tk()
    canvas_id = tk.Canvas(root, width=300, height=200)
    
    canvas_text = canvas_id.create_text(50,50, text="text1", tag="text1", font=("Courier", 10))
    canvas_rect = canvas_id.create_rectangle(canvas_id.bbox(canvas_text), outline="black", tag="text1")
    canvas_id.tag_bind(canvas_text, "<Button-1>", lambda event: move_start_text(event, "text1"))
    
    text_widget = tk.Text(canvas_id, height=1, width=5, relief="flat", font=("Courier", 10))
    text_widget.insert("1.0", "text2")
    canvas_wind = canvas_id.create_window(150, 50)
    canvas_id.itemconfig(canvas_wind, window=text_widget)
    canvas_id.tag_bind(canvas_wind, "<Button-1>", lambda event: move_start_text(event, canvas_wind)) # does not work
    
    canvas_id.grid()
    root.mainloop()

As a workaround I implemented a binding of Button-1 directly to the canvas, where I check if there is any canvas-item near the mouse-pointer and if yes this canvas item is moved. But then the user has to click "near" the object to move and not "at" the object to move, which is bad user experience.


Solution

  • You can simply bind <B1-Motion> on the text widget instead:

    def move_item_window(event, item_id):
        # get the bounding box of the widget
        x1, y1, x2, y2 = canvas_id.bbox(item_id)
        # calculate the center point inside the bounding box
        cx, cy = (x2-x1)/2, (y2-y1)/2
        # move the widget by the distance between the mouse position and the center point
        canvas_id.move(item_id, event.x-cx, event.y-cy)
    
    text_widget.bind("<B1-Motion>", lambda e: move_item_window(e, canvas_wind))