Search code examples
pythontkintercanvasscrollcustomtkinter

Problem getting widget coordinates after scrolling the canvas by a certain amount in python and customtkinter


I am new here and with python!

I am trying to create an application in python using customtkinter. The goal of the app is to define an optical bench with multiple components through a drag and drop interface and which calculate propagation of a laser beam. To create this drag and drop interface I have decided to implement a canvas that is scrollable. Within this canvas I have other canvas representing the optical components. These canvas are draggable and I need to get their positions within the canvas for further calculations.

My problem is when I try to get the position of the object within the main canvas : I've managed to get its position when the main canvas is not scrolled. However, when the canvas is scrolled by a certain amount (i.e when the widget disappears from the screen), the position of the component is modified.

Here is a reproducible code :

import customtkinter as ctk
import tkinter as tk
import matplotlib


class Canvas_composants(ctk.CTkCanvas):
    """The main canvas which is scrollable"""
    def __init__(self,master,bg):
        super().__init__(master=master,bg=bg)
        self.pack(fill="both",expand=True)
    
        # Scrollbar 
        self.hbar=ctk.CTkScrollbar(self,orientation="horizontal")
        self.hbar.pack(side='top',fill='x')
        
        #Configuration scrollbar and canvas
        self.configure(xscrollcommand=self.hbar.set)
        self.hbar.configure(command=self.xview)
        
        #Configuration of the scrollregion
        self.configure(scrollregion=(0,0,50000,50000))
        
        # Bind scroll to mousewheel
        self.bind("<MouseWheel>", self.scroll_canvas)
        
    def scroll_canvas(self,event):
        """Function executed when the mousewheel is used"""
        if event.delta:
            self.xview_scroll(-1 * (event.delta // 120), "units")

class dessin_composant(ctk.CTkCanvas):
    """How to define the object inside the main canvas"""
    def __init__(self,master,width,height,bg):
        super().__init__(master,width=width,height=height,bg=bg)
        
        
        #Positionning the object 
        self.master.create_window(150,150, window=self)
        
        #Attributes 
        self.master = master
        self.canvas_hbar=self.master.hbar # The scrollbar of the main canvas
        
        #Binding drag function
        self.bind("<B1-Motion>", self.drag)
      
    def drag(self,event):
        """How to drag the object inside the main canvas"""
        self.off=self.canvas_hbar.get() # The amount scrolled
        x = self.master.canvasx(event.x) + self.master.canvasx(event.widget.winfo_x())-self.master.canvasx(self.off[0])
        y = self.master.canvasy(event.y) + self.master.canvasy(event.widget.winfo_y())-self.master.canvasy(self.off[0])
        
        widg=event.widget
        self.master.create_window(x,y,window=widg)

class App(ctk.CTk):
    
    def __init__(self):
        super().__init__()
        
        # Executed when closed
        self.protocol("WM_DELETE_WINDOW", self.Fermeture) 
        
        # Screen resolution
        screen_x  = int(self.winfo_screenwidth())
        screen_y  = int(self.winfo_screenheight())
        
        self_X = screen_x
        self_Y = screen_y
        
        posX = (screen_x // 2) -(self_X // 2)
        posY = (screen_y // 2) -(self_Y // 2)
        
        geo="{}x{}+{}+{}".format(self_X,self_Y,posX,posY)
        self.geometry(geo)
        
        #Button
        self.B=ctk.CTkButton(self,text="print",command=self.f1)
        self.B.pack(side='left',padx=20)
        
        #Main canvas
        self.c=Canvas_composants( self, "white")
        
        #Object inside canvas
        width=27+80
        height=100+90
        bg="black"
        self.object=dessin_composant(self.c, width, height, bg)
        self.txt=None
    def f1(self):
        """Function which prints object position inside the main canvas"""
        self.c.delete(self.txt)
        obj=self.object
        x=int(self.c.canvasx(obj.winfo_x()))
        y=int(self.c.canvasy(obj.winfo_y()))
        self.txt=self.c.create_text((800,200),text=f"x={x} pixel, y={y} pixel")
        
        print(f"x={x} pixel,y={y} pixel")
        
    def Fermeture(self):
        """Executed when the window is closed"""
        if tk.messagebox.askokcancel("Quit", "Do you really want to quit ? "):
            matplotlib.use('module://matplotlib_inline.backend_inline')
            self.quit()
            self.destroy()

if __name__ == "__main__":
    Fenetre = App()
    Fenetre.mainloop()     

I have tried various things such as : substracting the amount scrolled, different methods to get the widget position (winfo_x,winfo_rootx,canvasx/y etc...) but none of them seems to work. I don't understand why the position is modified when the widget disappears from the screen because for me, the coordinate origin is fixed.

On the pictures below you can see the position of the widget before and after scrolling : Before scrolling / after scrolling

Any help would be appreciated

Thank you !


Solution

  • Following acw1668 comment, here is the working code :

    class Canvas_composants(ctk.CTkCanvas):
        """The main canvas which is scrollable"""
        def __init__(self,master,bg):
            super().__init__(master=master,bg=bg)
            self.pack(fill="both",expand=True)
        
            # Scrollbar 
            self.hbar=ctk.CTkScrollbar(self,orientation="horizontal")
            self.hbar.pack(side='top',fill='x')
            
            #Configuration scrollbar and canvas
            self.configure(xscrollcommand=self.hbar.set)
            self.hbar.configure(command=self.xview)
            
            #Configuration of the scrollregion
            self.configure(scrollregion=(0,0,50000,50000))
            
            # Bind scroll to mousewheel
            self.bind("<MouseWheel>", self.scroll_canvas)
            
        def scroll_canvas(self,event):
            """Function executed when the mousewheel is used"""
            if event.delta:
                self.xview_scroll(-1 * (event.delta // 120), "units")
    
    class dessin_composant(ctk.CTkCanvas):
        """How to define the object inside the main canvas"""
        def __init__(self,master,width,height,bg,x,y):
            super().__init__(master,width=width,height=height,bg=bg)
            
            self.x=x
            self.y=y
            #Positionning the object 
            self.master.create_window(self.x,self.y, window=self)
            
            #Attributes 
            self.master = master
            self.canvas_hbar=self.master.hbar # The scrollbar of the main canvas
            
            #Binding drag function
            self.bind("<B1-Motion>", self.drag)
          
        def drag(self,event):
            """How to drag the object inside the main canvas"""
            self.off=self.canvas_hbar.get() # The amount scrolled
            self.x = self.master.canvasx(event.x) + self.master.canvasx(event.widget.winfo_x())-self.master.canvasx(self.off[0])
            self.y = self.master.canvasy(event.y) + self.master.canvasy(event.widget.winfo_y())-self.master.canvasy(self.off[0])
            
            widg=event.widget
            self.master.create_window(self.x,self.y,window=widg)
    
    class App(ctk.CTk):
        
        def __init__(self):
            super().__init__()
            
            # Executed when closed
            self.protocol("WM_DELETE_WINDOW", self.Fermeture) 
            
            # Screen resolution
            screen_x  = int(self.winfo_screenwidth())
            screen_y  = int(self.winfo_screenheight())
            
            self_X = screen_x
            self_Y = screen_y
            
            posX = (screen_x // 2) -(self_X // 2)
            posY = (screen_y // 2) -(self_Y // 2)
            
            geo="{}x{}+{}+{}".format(self_X,self_Y,posX,posY)
            self.geometry(geo)
            
            #Button
            self.B=ctk.CTkButton(self,text="print",command=self.f1)
            self.B.pack(side='left',padx=20)
            
            #Main canvas
            self.c=Canvas_composants( self, "white")
            
            #Object inside canvas
            width=27+80
            height=100+90
            bg="black"
            self.object=dessin_composant(self.c, width, height, bg,150,150)
            self.txt=None
        def f1(self):
            """Function which prints object position inside the main canvas"""
            self.c.delete(self.txt)
            obj=self.object
            # x=int(self.c.canvasx(obj.winfo_x()))
            # y=int(self.c.canvasy(obj.winfo_y()))
            
            x=obj.x
            y=obj.y
                
            self.txt=self.c.create_text((800,200),text=f"x={x} pixel, y={y} pixel")
            
            print(f"x={x} pixel,y={y} pixel")
            
        def Fermeture(self):
            """Executed when the window is closed"""
            if tk.messagebox.askokcancel("Quit", "Do you really want to quit ? "):
                matplotlib.use('module://matplotlib_inline.backend_inline')
                self.quit()
                self.destroy()
    
    if __name__ == "__main__":
        Fenetre = App()
        Fenetre.mainloop()