Customtkinter表格行上下移动功能实现及优化方案问询
CustomTkinter表格行交换问题修复及拖拽排序方案
问题说明
- 首次交换行后再次移动出现显示异常
- 无法限制首行执行上移操作
- 寻求比上下按钮更优的动态行重排方案(如拖拽排序)
修复后完整代码
import customtkinter customtkinter.FontManager.load_font("Industry-Book.otf") customtkinter.FontManager.load_font("Industry-Medium.otf") customtkinter.FontManager.load_font("Industry-Demi.otf") def append(): global row, rows, currenth row = rows for column in range(columns): index = (row, column) e = customtkinter.CTkEntry(framedash, validate="key", justify='c', fg_color='#27333B', border_color='#D1D3D4') e.grid(row=row+1, column=column+1, stick="nsew") _entry[index] = e l1 = customtkinter.CTkLabel(framedash, text=row + 1, font=("Industry-Medium", 14), bg_color='#8F8F8F', width=40) l1.grid(row=row+1, column=0, padx=1, pady=1, stick="nsew") cb = customtkinter.CTkCheckBox(framedash, text='', width=0) cb.grid(row=row + 1, column=5, padx=1, pady=1, stick="nsew") downbutton = customtkinter.CTkButton(framedash, text='DW', width=20, height=20, command=lambda row=row: down(row)) downbutton.grid(row=row + 1, column=6, padx=1, pady=1, stick="nsew") upbutton = customtkinter.CTkButton(framedash, text='UP', width=20, height=20, command=lambda row=row: up(row)) upbutton.grid(row=row + 1, column=7, padx=1, pady=1, stick="nsew") # 统一行组件索引格式 _entry1[(row, 0)] = l1 _entry2[(row, 0)] = cb _entry3[(row, 0)] = downbutton _entry4[(row, 0)] = upbutton # 绑定拖拽事件 bind_drag_events(l1, row) currenth = currenth + 30 dashboard.geometry(f'450x{currenth}') rows += 1 def addrow(): append() def delete(): global row, rows, currenth if rows == 1: return row = rows-1 # 删除Entry组件 for column in range(columns): index = (row, column) _entry[index].grid_remove() del _entry[index] # 删除行号、复选框、按钮 idx = (row, 0) _entry1[idx].grid_remove() del _entry1[idx] _entry2[idx].grid_remove() del _entry2[idx] _entry3[idx].grid_remove() del _entry3[idx] _entry4[idx].grid_remove() del _entry4[idx] currenth = currenth - 30 dashboard.geometry(f'450x{currenth}') rows -= 1 update_row_numbers() def deleterow(): delete() def update_row_numbers(): # 更新所有行号标签文本 for row in range(rows): _entry1[(row, 0)].configure(text=row+1) def up(row): # 限制首行不能上移 if row == 0: return # 交换当前行与上一行的Entry组件 for column in range(columns): curr_idx = (row, column) prev_idx = (row-1, column) _entry[curr_idx].grid_configure(row=row) _entry[prev_idx].grid_configure(row=row+1) _entry[curr_idx], _entry[prev_idx] = _entry[prev_idx], _entry[curr_idx] # 交换行号、复选框、按钮 curr_idx = (row, 0) prev_idx = (row-1, 0) _entry1[curr_idx].grid_configure(row=row) _entry1[prev_idx].grid_configure(row=row+1) _entry2[curr_idx].grid_configure(row=row) _entry2[prev_idx].grid_configure(row=row+1) _entry3[curr_idx].grid_configure(row=row) _entry3[prev_idx].grid_configure(row=row+1) _entry4[curr_idx].grid_configure(row=row) _entry4[prev_idx].grid_configure(row=row+1) # 交换字典索引 _entry1[curr_idx], _entry1[prev_idx] = _entry1[prev_idx], _entry1[curr_idx] _entry2[curr_idx], _entry2[prev_idx] = _entry2[prev_idx], _entry2[curr_idx] _entry3[curr_idx], _entry3[prev_idx] = _entry3[prev_idx], _entry3[curr_idx] _entry4[curr_idx], _entry4[prev_idx] = _entry4[prev_idx], _entry4[curr_idx] # 更新按钮绑定的行索引 _entry4[curr_idx].configure(command=lambda r=row: up(r)) _entry4[prev_idx].configure(command=lambda r=row-1: up(r)) _entry3[curr_idx].configure(command=lambda r=row: down(r)) _entry3[prev_idx].configure(command=lambda r=row-1: down(r)) # 更新拖拽事件绑定的行索引 bind_drag_events(_entry1[curr_idx], row) bind_drag_events(_entry1[prev_idx], row-1) update_row_numbers() def down(row): # 限制末行不能下移 if row == rows-1: return # 交换当前行与下一行的Entry组件 for column in range(columns): curr_idx = (row, column) next_idx = (row+1, column) _entry[curr_idx].grid_configure(row=row+2) _entry[next_idx].grid_configure(row=row+1) _entry[curr_idx], _entry[next_idx] = _entry[next_idx], _entry[curr_idx] # 交换行号、复选框、按钮 curr_idx = (row, 0) next_idx = (row+1, 0) _entry1[curr_idx].grid_configure(row=row+2) _entry1[next_idx].grid_configure(row=row+1) _entry2[curr_idx].grid_configure(row=row+2) _entry2[next_idx].grid_configure(row=row+1) _entry3[curr_idx].grid_configure(row=row+2) _entry3[next_idx].grid_configure(row=row+1) _entry4[curr_idx].grid_configure(row=row+2) _entry4[next_idx].grid_configure(row=row+1) # 交换字典索引 _entry1[curr_idx], _entry1[next_idx] = _entry1[next_idx], _entry1[curr_idx] _entry2[curr_idx], _entry2[next_idx] = _entry2[next_idx], _entry2[curr_idx] _entry3[curr_idx], _entry3[next_idx] = _entry3[next_idx], _entry3[curr_idx] _entry4[curr_idx], _entry4[next_idx] = _entry4[next_idx], _entry4[curr_idx] # 更新按钮绑定的行索引 _entry4[curr_idx].configure(command=lambda r=row: up(r)) _entry4[next_idx].configure(command=lambda r=row+1: up(r)) _entry3[curr_idx].configure(command=lambda r=row: down(r)) _entry3[next_idx].configure(command=lambda r=row+1: down(r)) # 更新拖拽事件绑定的行索引 bind_drag_events(_entry1[curr_idx], row) bind_drag_events(_entry1[next_idx], row+1) update_row_numbers() # 拖拽排序相关变量与函数 drag_start_row = None drag_start_y = 0 def bind_drag_events(widget, row): widget.bind("<ButtonPress-1>", lambda e: on_drag_start(e, row)) widget.bind("<B1-Motion>", on_drag_motion) widget.bind("<ButtonRelease-1>", on_drag_release) def on_drag_start(event, row): global drag_start_row, drag_start_y drag_start_row = row drag_start_y = event.y_root def on_drag_motion(event): # 可选:添加拖拽时的视觉反馈,比如临时移动组件 pass def on_drag_release(event): global drag_start_row if drag_start_row is None: return # 计算鼠标释放时对应的目标行 target_row = None for idx, label in _entry1.items(): row_y = label.winfo_rooty() row_height = label.winfo_height() if row_y <= event.y_root <= row_y + row_height: target_row = idx[0] break if target_row is not None and target_row != drag_start_row: # 根据目标行位置执行多次上下移动 if target_row > drag_start_row: for _ in range(target_row - drag_start_row): down(drag_start_row) drag_start_row +=1 else: for _ in range(drag_start_row - target_row): up(drag_start_row) drag_start_row -=1 drag_start_row = None # 初始化窗口 currenth=380 dashboard = customtkinter.CTk(fg_color='#27333b') dashboard.geometry(f"450x{currenth}") dashboard.title('Itipack Systems Cals') dashboard.attributes('-topmost', True) dashboard.resizable(False, False) # dashboard.after(201, lambda: dashboard.iconbitmap('Itipack_icon_cmyk_rev_l.ico')) _entry = {} _entry1 = {} # 行号标签 _entry2 = {} # 复选框 _entry3 = {} # 下移按钮 _entry4 = {} # 上移按钮 rows=9 columns =2 framedash = customtkinter.CTkFrame(dashboard, width=600, height=600, fg_color='#27333b') framedash.grid(row=2, column=0, padx=10, pady=10, columnspan=columns+4, rowspan=rows+1, sticky='nsew') # 表头 c1 = customtkinter.CTkLabel(framedash, text='FUNCTION', font=("Industry-Medium",14), bg_color='grey', width=60) c1.grid(row=0, column=1, padx=1, pady=1, sticky='nesw') c2 = customtkinter.CTkLabel(framedash, text='DURATION', font=("Industry-Medium",14), bg_color='grey', width=40) c2.grid(row=0, column=2, padx=1, pady=1, sticky='nesw') # 初始化表格行 for row in range(rows): l1 = customtkinter.CTkLabel(framedash, text=row+1, font=("Industry-Medium",14), bg_color='#8F8F8F', width=40) l1.grid(row=row+1, column=0, padx=1, pady=1, stick="nsew") cb= customtkinter.CTkCheckBox(framedash, text='', width=0) cb.grid(row=row+1, column=5, padx=1, pady=1, stick="nsew") downbutton = customtkinter.CTkButton(framedash, text='DW', width=20, height=20, command=lambda row=row: down(row)) downbutton.grid(row=row + 1, column=6, padx=1, pady=1, stick="nsew") upbutton = customtkinter.CTkButton(framedash, text='UP', width=20, height=20, command=lambda row=row: up(row)) upbutton.grid(row=row + 1, column=7, padx=1, pady=1, stick="nsew") for column in range(columns): index = (row, column) e = customtkinter.CTkEntry(framedash, validate="key", justify='c', fg_color='#27333B', border_color='#D1D3D4') e.grid_configure(row=row+1, column=column+1, padx=1, pady=1, stick="nsew") _entry[index] = e _entry1[(row, 0)] = l1 _entry2[(row, 0)] = cb _entry3[(row, 0)]= downbutton _entry4[(row, 0)]=upbutton # 绑定拖拽事件 bind_drag_events(l1, row) # 配置网格权重 for column in range(8): framedash.grid_columnconfigure(column, weight=1) framedash.grid_rowconfigure(rows+1, weight=1) # 底部按钮 button1 = customtkinter.CTkButton(dashboard, width=50, height=50, text='A', font=("Industry-Medium", 16), corner_radius=1, fg_color='#00AAE9', command=addrow) button1.grid(row=0, column=0, padx=5, pady=5, sticky='e') button1 = customtkinter.CTkButton(dashboard, width=50, height=50, text='D', font=("Industry-Medium", 16), corner_radius=1, fg_color='#00AAE9', command=deleterow) button1.grid(row=0, column=1, padx=5, pady=5, sticky='w') button1 = customtkinter.CTkButton(dashboard, width=50, height=50, text='S', font=("Industry-Medium", 16), corner_radius=1, fg_color='#00AAE9') button1.grid(row=0, column=2, padx=5, pady=5) label12=customtkinter.CTkEntry(dashboard, width=50, height=30, justify='c') label12.grid(row=0, column=4, padx=5, pady=5, sticky='w') label12=customtkinter.CTkLabel(dashboard, text='# STRAPS', font=("Industry-Medium", 16)) label12.grid(row=0, column=3, padx=5, pady=5, sticky='e') # 配置窗口列权重 for col in range(6): dashboard.grid_columnconfigure(col, weight=1) dashboard.mainloop()
问题修复说明
- 限制无效移动:在
up/down函数开头添加行索引判断,直接阻止首行上移、末行下移的无效操作。 - 解决显示异常:
- 交换行时同步处理所有组件(Entry、行号、复选框、按钮)的网格位置
- 交换字典中的组件索引,确保后续操作能正确定位组件
- 更新按钮绑定的行索引,保证按钮点击逻辑正确
- 添加行号更新函数,交换后自动同步显示的行号
- 优化数据清理:删除行时同时清理字典中的组件引用,避免残留无效数据。
拖拽排序实现说明
代码中已集成拖拽排序功能,核心逻辑:
- 给每行的行号标签绑定鼠标按下、移动、释放事件
- 按下时记录起始行和鼠标位置
- 释放时计算鼠标落点对应的目标行
- 通过循环调用
up/down函数完成行交换 - 拖拽操作比按钮点击更直观高效,适合频繁调整行顺序的场景
内容的提问来源于stack exchange,提问作者RafaelTech
相关产品推荐
相关产品推荐

