强制 Tkinter.ttk Treeview 小部件在缩小其列宽后调整大小

Posted

技术标签:

【中文标题】强制 Tkinter.ttk Treeview 小部件在缩小其列宽后调整大小【英文标题】:Forcing a Tkinter.ttk Treeview widget to resize after shrinking its column widths 【发布时间】:2018-09-17 19:40:33 【问题描述】:

背景

Python-2.7.14 x64、Tk-8.5.15(捆绑)

有据可查的是,Tk 中的 Treeview 小部件有很多问题,而 Tkinter 作为一个瘦包装器,并没有做太多的事情来处理这些问题。一个常见的问题是让 Treeview 在browse 模式下与水平滚动条一起正常工作。设置整个Treeview的宽度需要设置每一列的宽度。但是如果有很多列,这会将父容器水平拉伸,并且不会激活水平滚动条。

我发现的解决方案是,在设计时,对于每一列,将 width 设置为您想要的所需大小并将此值缓存在安全的地方。当你添加、编辑或删除一行时,你需要遍历列并查询它们当前的宽度值,如果它大于你的缓存宽度,将width设置回原来的缓存值, em> 将minwidth 设置为当前列宽。最后一列也需要将其stretch 属性设置为“True”,因此它可以消耗剩余的任何剩余空间。这将激活水平滚动条并允许它适当地平移 Treeview 的内容,而无需更改整个小部件的宽度。

警告:在某个时间点,Tk 在内部将width 重置为等于minwidth,但不会立即强制重绘。稍后当您更改小部件时会感到惊讶,例如添加或删除一行,因此每次更改小部件时都必须重复上述操作。如果您捕获所有可能发生重绘的地方,这并不是一个真正的大问题。

问题

更改 Ttk 样式的属性会触发整个应用程序的强制重绘,因此会弹出我上面提到的警告,Treeview 小部件水平扩展,并且水平滚动条停用。

演练

以下代码演示:

# imports
from Tkinter import *
from ttk import *
from tkFont import *

# root
root=Tk()

# font config
ff10=Font(family="Consolas", size=10)
ff10b=Font(family="Consolas", size=10, weight=BOLD)

# style config
s=Style()
s.configure("Foo2.Treeview", font=ff10, padding=1)
s.configure("Foo2.Treeview.Heading", font=ff10b, padding=1)

# init a treeview
tv=Treeview(root, selectmode=BROWSE, height=8, show="tree headings", columns=("key", "value"), style="Foo2.Treeview")
tv.heading("key", text="Key", anchor=W)
tv.heading("value", text="Value", anchor=W)
tv.column("#0", width=0, stretch=False)
tv.column("key", width=78, stretch=False)
tv.column("value", width=232, stretch=False)
tv.grid(padx=8, pady=(8,0))

# init a scrollbar
sb=Scrollbar(root, orient=HORIZONTAL)
sb.grid(row=1, sticky=EW, padx=8, pady=(0,8))
tv.configure(xscrollcommand=sb.set)
sb.configure(command=tv.xview)

# insert a row that has data longer than the initial column width and
# then update width/minwidth to activate the scrollbar.
tv.insert("", END, values=("foobar", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"))
tv.column("key", width=78, stretch=False, minwidth=78)
tv.column("value", width=232, stretch=True, minwidth=372)

生成的窗口将如下所示:

请注意,水平滚动条处于活动状态,并且最后一列在小部件边缘之外有轻微溢出。对tv.column 的最后两次调用是启用此功能的原因,但是在转储最后一列的列属性时,我们看到 Tk 已经默默地将 widthminwidth 更新为相同:

tv.column("value")
'minwidth': 372, 'width': 372, 'id': 'value', 'anchor': u'w', 'stretch': 1

更改 any 样式的 any 属性将触发小部件的强制重绘。例如,下一行将派生的 Checkbutton 样式类的 foreground 颜色设置为红色:

s.configure("Foo2.TCheckbutton", foreground="red")

这是现在的 Treeview 小部件:

现在的问题是缩小整个小部件。可以通过将width 设置为最初缓存的大小,将stretch 设置为“False”,将minwidth 设置为最大列大小,可以将最后一列强制恢复为原始大小:

tv.column("value", width=232, stretch=False, minwidth=372)

但是 Treeview 小部件的宽度并没有缩小。

我找到了两种可能的解决方案,都在绑定到<Configure> 事件中,使用最后一个显示列:

    表明主题的变化:

    tv.column("value", width=232, stretch=True, minwidth=372)
    tv.event_generate("<<ThemeChanged>>")
    

    隐藏并重新显示:

    tv.grid_remove()
    tv.column("value", width=232, stretch=True, minwidth=372)
    tv.grid()
    

两者都有效,因为它们是我能找到调用内部 Tk 函数TtkResizeWidget() 的唯一方法。第一个有效是因为&lt;&lt;ThemeChanged&gt;&gt; 强制重新计算小部件的几何图形,而第二个有效是因为如果在重新配置列时 Treeview 处于未映射状态,则 Treeview 将调用 TtkResizeWidget()

这两种方法的缺点是您有时会看到窗口展开一帧然后又收缩回来。这就是为什么我认为两者都不是最佳方法,并希望其他人知道更好的方法。或者至少是在&lt;Configure&gt; 之前或扩展发生之前发生的可绑定事件,我可以从中使用上述方法之一。

一些参考资料表明这在 Tk 中已存在很长时间的问题:

    Treeview 在Tcl Wiki 上带有水平滚动条的问题(搜索[ofv] 2009-05-30)。 票务#3519160. Message 在 Tkinter-discuss 邮件列表中。

而且,该问题仍然可在 Python-3.6.4(包括 Tk-8.6.6)上重现。

【问题讨论】:

【参考方案1】:

您将列宽重置为初始值的直觉很好。不要将这些调用绑定到&lt;Configure&gt;,这会导致闪烁,因为它会在第一次调用后立即重绘屏幕,让我们在第一次屏幕重绘之前重置列宽,这样什么都不会改变,也不需要第二次调用:

# imports
from tkinter import *
from tkinter.ttk import *
from tkinter.font import *

# subclass treeview for the convenience of overriding the column method
class ScrollableTV(Treeview):
  def __init__(self, master, **kw):
    super().__init__(master, **kw)
    self.columns=[]

  # column now records the name and details of each column in the TV just before they're added
  def column(self, column, **kw):
    if column not in [column[0] for column in self.columns]:
      self.columns.append((column, kw))
    super().column(column, **kw)

  # keep a modified, heavier version of Style around that you can use in cases where ScrollableTVs are involved
  class ScrollableStyle(Style):
    def __init__(self, tv, *args, **kw):
      super().__init__(*args, **kw)
      self.tv = tv

    # override Style's configure method to reset all its TV's columns to their initial settings before it returns into TtkResizeWidget(). since we undo the TV's automatic changes before the screen redraws, there's no need to cause flickering by redrawing a second time after the width is reset
    def configure(self, item, **kw):
      super().configure(item, **kw)
      for column in self.tv.columns:
        name, kw = column
        self.tv.column(name, **kw)

# root
root=Tk()

# font config
ff10=Font(family="Consolas", size=10)
ff10b=Font(family="Consolas", size=10, weight=BOLD)

# init a scrollabletv
tv=ScrollableTV(root, selectmode=BROWSE, height=8, show="tree headings", columns=("key", "value"), style="Foo2.Treeview")
tv.heading("key", text="Key", anchor=W)
tv.heading("value", text="Value", anchor=W)
tv.column("#0", width=0, stretch=False)
tv.column("key", width=78, stretch=False)
tv.column("value", minwidth=372, width=232, stretch=True)
tv.grid(padx=8, pady=(8,0))

# style config. use a ScrollableStyle and pass in the ScrollableTV whose configure needs to be managed. if you had more than one ScrollableTV, you could modify ScrollableStyle to store a list of them and operate configure on each of them
s=ScrollableTV.ScrollableStyle(tv)
s.configure("Foo2.Treeview", font=ff10, padding=1)
s.configure("Foo2.Treeview.Heading", font=ff10b, padding=1)

# init a scrollbar
sb=Scrollbar(root, orient=HORIZONTAL)
sb.grid(row=1, sticky=EW, padx=8, pady=(0,8))
tv.configure(xscrollcommand=sb.set)
sb.configure(command=tv.xview)

# insert a row that has data longer than the initial column width and
# then update width/minwidth to activate the scrollbar.
tv.insert("", END, values=("foobar", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"))
# we don't need to meddle with the stretch and minwidth values here

#click in the TV to test
def conf(event):
  s.configure("Foo2.TCheckbutton", foreground="red")
tv.bind("<Button-1>", conf, add="+")
root.mainloop()

在 python 3.8.2、tkinter 8.6 上测试

【讨论】:

以上是关于强制 Tkinter.ttk Treeview 小部件在缩小其列宽后调整大小的主要内容,如果未能解决你的问题,请参考以下文章

如何在 tkinter.ttk Treeview 上完全更改背景颜色

Python如何改变tkinter.ttk.Treeview表格组件的每行的行高?

tkinter ttk treeview 如何设置固定宽度?为啥它会随着列数而变化?

在 tkinter ttk treeview 中格式化单个单元格/项目而不是整行

使用透明背景、ttk 框架背景颜色配置 tkinter/ttk 小部件?

tkinter学习01