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 !
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()