Tkinter/CustomTkinter组件悬停缩放时位移异常求助
问题:CustomTkinter组件悬停放大后无法回到初始位置
尝试实现CustomTkinter组件鼠标悬停放大、离开缩小的效果,但组件位移行为异常,无法回到初始位置。运行下方代码,将鼠标悬停在红色块上可复现问题:
import customtkinter import threading import itertools import decimal def frange(x, y, jump): if x < y: while x < y: yield float(x) x = decimal.Decimal(x)+decimal.Decimal(jump) elif x > y: while x > y: yield float(x) x = decimal.Decimal(x)+decimal.Decimal(jump) def dfly(widget,target_x,target_y,speed): t1=threading.Thread(target=lambda:dfly_main(widget,target_x,target_y,speed)) t1.start() dfly_main_global=1 def dfly_main(widget,target_x,target_y,speed): global dfly_main_global if dfly_main_global==1: dfly_main_global=0 init_x=widget.winfo_x() init_y=widget.winfo_y() diff_x=float(target_x-init_x) diff_y=float(target_y-init_y) time=(((diff_x**2)+(diff_y**2))**(1/2))*speed print(time) for frame in frange(1,time,time/abs(time)): widget.forget() widget.place(x=init_x+(frame*(diff_x/time)),y=init_y+(frame*(diff_y/time))) dfly_main_global=1 def hover_zoom(widget,zoom,speed): t2=threading.Thread(target=lambda:hover_zoom_main(widget,zoom,speed)) t2.start() def hover_zoom_main(widget,zoom,speed): width=widget.cget(attribute_name='width') height=widget.cget(attribute_name='height') dfly(widget,widget.winfo_x()-((width*zoom-width)/2),widget.winfo_y()-((height*zoom-height)/2),95.5/speed) if speed>0: for width_,height_ in itertools.zip_longest(frange(width,width*zoom,speed),frange(height,height*zoom,speed)): widget.configure(width=width_,height=height_) elif speed<0: for width_,height_ in zip(frange(width,width*zoom,speed),frange(height,height*zoom,speed)): widget.configure(width=width_,height=height_) root=customtkinter.CTk() root.configure(bg='grey') root.geometry('1200x675') #----- red_frame=customtkinter.CTkLabel(root,150,150,10,fg_color='red') red_frame.place(x=60,y=230) red_frame.bind('<Enter>', lambda event:hover_zoom(red_frame,1.2,2)) red_frame.bind('<Leave>', lambda event:hover_zoom(red_frame,0.8,-2)) root.mainloop()
问题根源分析
- 初始状态未保存:位移和缩放计算依赖组件当前的宽高/位置,而非初始值,导致每次操作都是基于当前状态叠加,无法回到原点。
- 线程操作UI风险:用线程更新UI会导致竞态问题,
dfly_main_global全局变量的互斥逻辑不可靠,容易出现动画中断或错位。 - 缩放参数错误:离开时设置
zoom=0.8是相对于当前尺寸缩小,而非恢复到初始尺寸。 - 不必要的
widget.forget():place布局下直接修改x/y即可,调用forget()会导致组件短暂消失且可能干扰布局计算。
修复方案
改用CustomTkinter推荐的after方法实现动画(避免线程问题),保存组件的初始位置和尺寸,修正缩放/位移逻辑:
import customtkinter class HoverZoomWidget: def __init__(self, widget, init_x, init_y, init_width, init_height, zoom_factor=1.2, animation_speed=10): self.widget = widget self.init_x = init_x self.init_y = init_y self.init_width = init_width self.init_height = init_height self.zoom_factor = zoom_factor self.animation_speed = animation_speed self.is_animating = False # 绑定事件 widget.bind('<Enter>', self.start_zoom_in) widget.bind('<Leave>', self.start_zoom_out) def animate(self, target_width, target_height, target_x, target_y, current_width, current_height, current_x, current_y): if self.is_animating: # 计算步长 width_step = (target_width - current_width) / 10 height_step = (target_height - current_height) / 10 x_step = (target_x - current_x) / 10 y_step = (target_y - current_y) / 10 # 更新当前值 new_width = current_width + width_step new_height = current_height + height_step new_x = current_x + x_step new_y = current_y + y_step # 判断是否接近目标值 if abs(new_width - target_width) < 0.5: new_width = target_width new_height = target_height new_x = target_x new_y = target_y self.is_animating = False # 更新组件 self.widget.configure(width=new_width, height=new_height) self.widget.place(x=new_x, y=new_y) if self.is_animating: self.widget.after(self.animation_speed, self.animate, target_width, target_height, target_x, target_y, new_width, new_height, new_x, new_y) def start_zoom_in(self, event): if not self.is_animating: self.is_animating = True target_width = self.init_width * self.zoom_factor target_height = self.init_height * self.zoom_factor # 计算中心偏移:向左上移动,保证组件中心不变 target_x = self.init_x - (target_width - self.init_width)/2 target_y = self.init_y - (target_height - self.init_height)/2 current_width = self.widget.cget('width') current_height = self.widget.cget('height') current_x = self.widget.winfo_x() current_y = self.widget.winfo_y() self.animate(target_width, target_height, target_x, target_y, current_width, current_height, current_x, current_y) def start_zoom_out(self, event): if not self.is_animating: self.is_animating = True # 恢复到初始尺寸和位置 target_width = self.init_width target_height = self.init_height target_x = self.init_x target_y = self.init_y current_width = self.widget.cget('width') current_height = self.widget.cget('height') current_x = self.widget.winfo_x() current_y = self.widget.winfo_y() self.animate(target_width, target_height, target_x, target_y, current_width, current_height, current_x, current_y) root = customtkinter.CTk() root.configure(bg='grey') root.geometry('1200x675') # 创建组件并初始化悬停缩放 red_frame = customtkinter.CTkLabel(root, width=150, height=150, corner_radius=10, fg_color='red') init_x, init_y = 60, 230 red_frame.place(x=init_x, y=init_y) HoverZoomWidget(red_frame, init_x, init_y, 150, 150, zoom_factor=1.2) root.mainloop()
关键改动说明
- 封装为类:保存组件的初始位置和尺寸,确保每次动画都基于初始值计算。
- 用
after替代线程:避免线程操作UI的竞态问题,符合Tkinter/CustomTkinter的UI更新规范。 - 中心对齐逻辑:缩放时计算偏移量,保证组件中心位置不变,动画更自然。
- 动画状态控制:
is_animating标志防止重复触发动画,避免冲突。
内容的提问来源于stack exchange,提问作者Manasmay Singh
相关产品推荐
相关产品推荐

