PAYATHON 2026

如何检测 Tkinter Treeview 的列宽变化并更新网格线?

数据阿明

可以通过鼠标事件触发列宽检查,再用 tree.column(column_id, "width") 读取实际宽度。比较新旧列宽,确认发生变化后,移动 Canvas 中对应的竖向分隔线。

拖动期间可监听 <B1-Motion>,拖动结束时可监听 <ButtonRelease-1>。检查应通过 after_idle() 延后执行,让 Treeview 的默认拖动处理先完成。

为什么原来的网格线不会跟着变化?

原代码绘制竖线时使用的是初始化配置中的宽度:

x += col[HDR_WIDTH]

self.headers 是普通 Python 列表,用户调整 Treeview 列宽后,这些初始值不会自动更新,Canvas 中已创建的分隔线也不会自动移动。

列宽变化不一定会改变 Treeview 控件的外部尺寸,因此仅绑定 <Configure> 无法覆盖所有表头拖动操作。它可以作为控件尺寸变化时的补充检查入口。

保存竖向分隔线,并使用实际列宽定位

在 initOverlay() 中保留原有的横线绘制代码,将 # Now do the verticals 后的竖线绘制部分替换为以下代码。代码片段放在方法内部:

self._vertical_lines = []
self._grid_ybeg = int(hstep)
yhgt = hgt - self._grid_ybeg
x = 0

for col in self.headers:
    column_id = col[HDR_TAG]
    x += self.tree.column(column_id, "width")

    sep = ttk.Separator(self.parent, orient=tk.VERTICAL)
    item_id = self.canvas.create_window(
        (x, self._grid_ybeg),
        height=yhgt,
        window=sep,
        anchor="nw",
    )
    self._vertical_lines.append(item_id)

这里保存的是 Canvas.create_window() 返回的项目 ID。后续可以通过 canvas.coords() 修改其位置,无须反复销毁和创建分隔线。

添加列宽检测和网格线更新方法

将以下方法加入 TreeviewTest 类,缩进层级与 initOverlay() 等方法保持一致:

def _column_widths(self):
    return tuple(
        self.tree.column(col[HDR_TAG], "width")
        for col in self.headers
    )


def _watch_column_widths(self):
    self._last_widths = self._column_widths()
    self._width_check_job = None

    for sequence in (
        "<B1-Motion>",
        "<ButtonRelease-1>",
        "<Configure>",
    ):
        self.tree.bind(
            sequence,
            self._queue_column_width_check,
            add="+",
        )


def _queue_column_width_check(self, event=None):
    if self._width_check_job is None:
        self._width_check_job = self.tree.after_idle(
            self._check_column_widths
        )


def _check_column_widths(self):
    self._width_check_job = None
    widths = self._column_widths()

    if widths == self._last_widths:
        return

    self._last_widths = widths
    self._move_vertical_lines(widths)


def _move_vertical_lines(self, widths):
    x = 0
    for item_id, width in zip(self._vertical_lines, widths):
        x += width
        self.canvas.coords(item_id, x, self._grid_ybeg)

然后在 __init__() 中,紧接着 self.initOverlay(rows_visible) 启用检测:

self.initCanvas()
self.initUI(rows_visible)
self.initOverlay(rows_visible)
self._watch_column_widths()

鼠标拖动时会安排列宽检查,只有实际列宽发生变化时才移动网格线。普通的行选择和点击即使触发相同的鼠标事件,也不会导致无意义的重绘。

为什么需要 after_idle()?

在默认的 bindtags 顺序下,控件实例上的绑定通常先于 Treeview 类绑定执行。因此,直接在 <B1-Motion> 回调中读取列宽,可能读到本次默认拖动处理之前的值。

after_idle() 将检查安排在事件处理后的空闲阶段。_width_check_job 用于合并尚未执行的检查,避免连续鼠标事件堆积重复任务。

绑定时使用 add="+",可以保留同一控件上已有的实例事件处理函数。这些回调不应返回 "break",否则可能阻止 Treeview 的默认拖动处理。

如果只需要在用户松开鼠标后更新网格线,可以从监听列表中去掉 <B1-Motion>。

程序修改列宽时如何处理?

直接调用 tree.column() 修改列宽,不一定会触发上述鼠标事件,也不一定会改变控件外部尺寸。此时可以主动安排一次检查:

self.tree.column("upper", width=120)
self._queue_column_width_check()

这套坐标计算适用于原示例:#0 列可见、列顺序与 self.headers 一致,且没有水平滚动。隐藏列、调整 displaycolumns 或增加水平滚动后,需要根据实际显示顺序和水平偏移重新计算竖线位置。

上述修改负责列宽检测和竖线横坐标更新。原代码按控件总高度估算行间距,并使用固定像素偏移;主题、字体或缩放比例变化时,仍需另行校准横线和表头边界。原始示例粘贴时丢失的 Python 缩进也需要先恢复。

备注:内容仅供参考。