You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CustomTkinter中组件不可见时canvasx(winfo_x())返回值异常问题

问题:CustomTkinter滚动Canvas中子组件坐标获取异常

我用Python的CustomTkinter开发应用,有一个可滚动Canvas,内部嵌套了另一个Canvas作为子组件。需求是获取子组件在父Canvas中的坐标:

  • 子组件可见时,canvasx(widget.winfo_x())能返回稳定值,不受滚动影响;
  • 子组件不可见时,该方法返回值随滚动量变化,和可见时的数值不符。

目标是无论子组件可见与否,都能获取一致的Canvas坐标,滚动后数值保持匹配。尝试过加减滚动量但没解决,发现子组件不可见时widget.winfo_x()不随滚动变化,推测这是问题根源,但不确定。

可复现代码:

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,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

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.frame=ctk.CTkFrame(self)
        self.frame.pack(side='left',fill='both')
        self.B=ctk.CTkButton(self.frame,text="print",command=self.f1)
        self.B.grid(row=0,column=0,pady=100)
        
        #Labels
        self.label1=ctk.CTkLabel(self.frame,text="Winfo_x()          = ")
        self.label1.grid(row=1,column=0)
        self.label2=ctk.CTkLabel(self.frame,text="Canvasx(Winfo_x()) =  ")
        self.label2.grid(row=2,column=0)
        self.label3=ctk.CTkLabel(self.frame,text="Canvasx(scroll)    =  ")
        self.label3.grid(row=3,column=0)



        #Main canvas
        self.c=Canvas_composants( self, "white")
        
        #Object inside canvas
        width  = 107
        height = 190
        bg     = "black"
        self.object=dessin_composant(self.c, width, height, bg,600,350)
        
    def f1(self):
        """Function which prints object position inside the main canvas"""
        #self.c.delete(self.txt)
  
        obj      = self.object
        self.off = self.c.hbar.get() # The amount scrolled

        self.label1.configure(text=f"Winfo_x()          = {obj.winfo_x()} ")
        self.label2.configure(text=f"Canvasx(Winfo_x()) = {self.c.canvasx(obj.winfo_x())} ")
        self.label3.configure(text=f"Canvasx(scroll)    = {self.c.canvasx(self.off[0])} ")
        

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

核心原因

当子组件滚动出Canvas可视区域后,Tkinter会将其winfo_x()固定为边界值(如0),不再同步其实际的Canvas内坐标,导致canvasx()计算结果偏离真实值。

修复方案

放弃依赖winfo_x(),直接通过Canvas的coords()方法获取子组件对应的window对象的绝对坐标,该坐标不受滚动可视区域影响。

具体修改:

  1. 在dessin_composant类中,保存父Canvas创建window时返回的ID:
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
        # 保存window对象的ID,用于后续获取坐标
        self.window_id = self.master.create_window(self.x,self.y, window=self)
        
        self.master = master
        self.canvas_hbar=self.master.hbar
  1. 修改f1()方法,通过coords()获取真实坐标:
def f1(self):
    """Function which prints object position inside the main canvas"""
    obj = self.object
    # 获取子组件在Canvas中的绝对坐标
    canvas_x, canvas_y = self.c.coords(obj.window_id)
    
    self.label1.configure(text=f"Winfo_x()          = {obj.winfo_x()} ")
    self.label2.configure(text=f"Canvas绝对坐标X      = {canvas_x} ")
    self.label3.configure(text=f"滚动偏移量          = {self.c.hbar.get()[0]} ")

效果验证

修改后,无论子组件是否在可视区域内,点击print按钮都会显示稳定的600(即创建时的X坐标),完全符合需求。


内容的提问来源于stack exchange,提问作者Adrien

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 01:47:06