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

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

问题修复说明

  1. 限制无效移动:在up/down函数开头添加行索引判断,直接阻止首行上移、末行下移的无效操作。
  2. 解决显示异常:
    • 交换行时同步处理所有组件(Entry、行号、复选框、按钮)的网格位置
    • 交换字典中的组件索引,确保后续操作能正确定位组件
    • 更新按钮绑定的行索引,保证按钮点击逻辑正确
    • 添加行号更新函数,交换后自动同步显示的行号
  3. 优化数据清理:删除行时同时清理字典中的组件引用,避免残留无效数据。

拖拽排序实现说明

代码中已集成拖拽排序功能,核心逻辑:

  • 给每行的行号标签绑定鼠标按下、移动、释放事件
  • 按下时记录起始行和鼠标位置
  • 释放时计算鼠标落点对应的目标行
  • 通过循环调用up/down函数完成行交换
  • 拖拽操作比按钮点击更直观高效,适合频繁调整行顺序的场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 12:54:53