Search code examples
pythontkintertkinter-canvas

Mouse position offset problem in a tkinter canvas


I have several problems. I don't understand why in this code, if I click on matrix, I do have a grid that starts at x=30, and y=30. But if I click on create grid, the grid is shifted, moreover the mouse click does not blacken the right box. There is also a problem because there is always a spinbox when it should be deleted by grille_frame.delete("all").

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
btn_frame = tk.Frame(root)
btn_frame.grid(row=0, column=0, columnspan=3)
grille_frame = tk.Canvas(root, width=400, height=600)
grille_frame.grid(row=1, column = 0)
yscrollbar_grille = ttk.Scrollbar(root, orient="vertical", command=grille_frame.yview)
yscrollbar_grille.grid(row=0, column=3, rowspan=3, sticky="ns")
xscrollbar_grille = ttk.Scrollbar(root, orient="horizontal", command=grille_frame.xview)
xscrollbar_grille.grid(row=2, column=0, columnspan=3, sticky="ew")
grille_frame.config(yscrollcommand=yscrollbar_grille.set, xscrollcommand=xscrollbar_grille.set)
rows = 12
cols = 12
cell_size = 40
x = 30
y = 30

def zoom():
    grille_frame.scale("all", 30, 30, 0.5, 0.5)

scale_btn = tk.Button(btn_frame, text="scale", command=zoom)
scale_btn.grid(row=0, column=0)

def mouse_position(event):
    grille_frame.event_generate("<Motion>", warp=True, x=30, y=30)
    print("mouse position on canvas", event.x, event.y)
    print("mouse postition on screen", event.x_root, event.y_root)
    

root.bind("<Key>", mouse_position)
def matrice():
    grille_frame.delete("all")
    y = 30
    for r in range(rows):
        x = 30
        
        #y_temp = y +(cell_size // 5)
        spinbox_row = tk.Spinbox(grille_frame, from_=0, to=cols, width=2)
        #spinbox_row.grid(row=r, column=0)
        grille_frame.create_window((x, y+cell_size/2), window=spinbox_row, anchor="e")
    
        temp = []
        for c in range(cols):
            cell = grille_frame.create_rectangle((x, y), (x + cell_size, y + cell_size), fill="white", outline= "black")
            temp.append(cell)
            x = x + cell_size
        y = y + cell_size
    
    x = y = 30
    for c in range(cols):
        spinbox_col = tk.Spinbox(grille_frame, from_=0, to=rows, width=2)
        grille_frame.create_window((x+cell_size/2, y), window=spinbox_col, anchor="s")
        x = x + cell_size
    
    grille_frame.update_idletasks()
    grille_frame.config(scrollregion=grille_frame.bbox("all"))

matrice_btn = tk.Button(btn_frame, text="Matrice", command=matrice)
matrice_btn.grid(row=0, column=2)
def creer_hanjie():
    """ affiche une grille de cases blanches """
    grille_frame.delete("all")
    x = 30
    y = 30
    
    for r in range(rows):
        x = 30
        for c in range(cols):
            cell = grille_frame.create_rectangle((x, y), (x + cell_size, y + cell_size ), fill="white", outline= "black")
            x = x + cell_size
            print("coordonnées case:", grille_frame.coords(cell))
            
            grille_frame.bind("<Button-1>", click_1case)
        y = y + cell_size
    grille_frame.update_idletasks()
    print("canvaas taille", grille_frame.bbox("all"))
    grille_frame.config(scrollregion=grille_frame.bbox("all"))

grille_btn = tk.Button(btn_frame, text="create grid", command=creer_hanjie)
grille_btn.grid(row=0, column=1)
    
def click_1case(event):
    """ change la couleur de la case par click simple, 1 case à la fois """
    print("clic souris", event.x, event.y)
    case = grille_frame.find_closest(event.x, event.y)
    couleur_actuelle = grille_frame.itemcget(case, "fill")
    if couleur_actuelle == "white":
        nouvelle_couleur = "black"
    else:
        nouvelle_couleur = "white"
    grille_frame.itemconfigure(case, fill=nouvelle_couleur)
root.mainloop()

Solution

  • Note that (event.x, event.y) is a coordinates relative to the top-left corner of the canvas, and it may not be the same as the coordinates relative to the origin or canvas if the view is scrolled.

    You need to convert (event.x, event.y) to the real coordinates using .canvasx() and .canvasy():

    def click_1case(event):
        """ change la couleur de la case par click simple, 1 case à la fois """
        ### get the real coordinates
        x, y = grille_frame.canvasx(event.x), grille_frame.canvasy(event.y)
        print("clic souris", event.x, event.y, x, y)
        case = grille_frame.find_closest(x, y) # used the real coordinates
        couleur_actuelle = grille_frame.itemcget(case, "fill")
        if couleur_actuelle == "white":
            nouvelle_couleur = "black"
        else:
            nouvelle_couleur = "white"
        grille_frame.itemconfigure(case, fill=nouvelle_couleur)
    

    Note that you can also get the item under the clicked point by .find_withtag("current") instead:

    def click_1case(event):
        ### get the item under mouse cursor
        case = grille_frame.find_withtag("current")
        couleur_actuelle = grille_frame.itemcget(case, "fill")
        nouvelle_couleur = "black" if couleur_actuelle == "white" else "white"
        grille_frame.itemconfigure(case, fill=nouvelle_couleur)