Search code examples
pythontkintermouseevent

Drag and click management tkinter


I try in vain, to allow the user to blacken several boxes of a grid in a single click-and-drag, but it only blackens the first clicked box, how to do it?

My code is quite substantial, and I'm not going to post everything, but here is a functional example of my problem

Here is an excerpt from my code:

import tkinter as tk
from tkinter import ttk


class GrilleFenetre(tk.Tk):
    """ interface graphique pour resoudre les hanjies """
    def __init__(self):
        super().__init__()
        self.title("Hanjie Solver")
        self.minsize(800, 500)
        self.content = ttk.Frame(self, padding=(3,3,12,12))
        self.grille_frame = ttk.Frame(self.content, borderwidth=2, relief="solid")
        self.content.grid(row=0, column=0, sticky="nsew")
        self.grille_frame.grid(row=1, column=3, columnspan=3, sticky="nsew", padx=5)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.content.columnconfigure(0, weight=1)
        self.content.columnconfigure(1, weight=1)
        self.content.columnconfigure(2, weight=1)
        self.content.columnconfigure(3, weight=3, minsize=100)
        self.content.rowconfigure(1, weight=3)
        
        self.creer_hanjie()

    def creer_hanjie(self):
            """ affiche une grille de cases blanches """
            #self.resoudre_bouton['state'] = 'normal'
            #self.flag_crea = False
            self.hanjie = []
            self.rows = 10
            self.cols = 10
            self.hanjie = [[0] * self.cols for _ in range(self.rows)]
            self.cellule_taille = 50
            #self.grille_frame = ttk.Frame(self.content)
            
            for r in range(self.rows):
                
                for c in range(self.cols):
                    self.cell = tk.Frame(self.grille_frame, borderwidth=1, relief="solid", bg="white", width=self.cellule_taille, height=self.cellule_taille)
                    # detection du click sur une case  
                    self.cell.bind("<B1-Motion>", lambda event, row=r, col=c: self.click_cases(event, row, col))             
                    self.hanjie[r][c] = self.cell
                    
                    self.cell.grid(row=r+1, column=c+1, sticky="nsew")
            
            self.grille_frame.grid(row=1, column=3, columnspan=2, sticky="nsew", padx=10, pady=10)
            #self.update_interface(2)
            
            
    #%% click cases
    def click_cases(self, event, row, col):
        """ change la couleur de la case cliquée """
        #print(row, col)
        case = self.hanjie[row][col]
        couleur_actuelle = case.cget("bg")
        nouvelle_couleur = "black" if couleur_actuelle == "white" else "white"
        case.configure(bg=nouvelle_couleur)
            
if __name__ == "__main__":
    fenetre = GrilleFenetre()
    fenetre.geometry("800x600+350+150")  # Taille initiale de la fenêtre
    fenetre.mainloop()

I wanted to try to retrieve the coordinates of the mouse pointer, but since I don't know the exact coordinates of the boxes, it's a little complicated...


Solution

  • Even though you create a binding for the motion event of each cell, while in the processing of a motion event, the event will always only be triggered for the one that received the button press. That means that you will need to compute the widget under the mouse rather than relying on a passed-in row and column since the passed-in value won't change as you move the mouse around.

    You can get the widget under the cursor with winfo_containing, passing it the root coordinates of the event. You can then use that information to know which cell to change.

    Step 1: stop passing the row and column to the event handler

    for c in range(self.cols):
        ...
        self.cell.bind("<B1-Motion>", click_cases)
        ...
    

    Step two: modify the callback to get the widget under the cursor.

    This will unconditionally set the color to black, but you could add some logic to toggle it. However, since the callback could happen dozens of time while over a single cell, this will result in a lot of unpleasant flashing.

    I don't know what you actually want to happen, but this simplified example shows the basic technique of determining what widget is under the mouse in a callback for a motion event.

    def click_cases(self, event):
        case = event.widget.winfo_containing(event.x_root, event.y_root)
        case.configure(bg="black")