繁体   English   中英

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

[英]Forcing a Tkinter.ttk Treeview widget to resize after shrinking its column widths

背景

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

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

我发现的解决方案是,在设计时,对于每一列,将width设置为您想要的所需大小并将该值缓存在安全的地方。 添加、编辑或删除行时,需要遍历列并查询其当前宽度值,如果大于缓存宽度,则将width设置回原始缓存值,并将minwidth 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)


生成的 window 将如下所示:

带有水平滚动条的示例 Treeview 小部件


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

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


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

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


这是现在的 Treeview 小部件:

更改 ttk.Style 属性后的 Treeview 小部件


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

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

将列大小强制恢复为原始大小后的 Treeview 小部件

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


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

  1. 表示主题的变化:

     tv.column("value", width=232, stretch=True, minwidth=372) tv.event_generate("<<ThemeChanged>>")
  2. 隐藏并重新显示:

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


两者都有效,因为它们是我能找到调用内部 Tk function TtkResizeWidget()的唯一方法。 第一个有效是因为<<ThemeChanged>>强制重新计算小部件的几何形状,而第二个有效是因为 Treeview 如果它在未映射的 state 中,则在重新配置列时将调用TtkResizeWidget()

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


一些参考资料表明这在 Tk 中长期以来一直是一个问题:

  1. Treeview 在Tcl Wiki上带有水平滚动条的问题(搜索[ofv] 2009-05-30 )。
  2. #3519160
  3. Tkinter-讨论邮件列表上的消息

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

您将列宽重置为初始值的直觉很好。 不要将这些调用绑定到<Configure> ,因为它会在第一次调用后第二次重绘屏幕而导致闪烁,让我们在第一次屏幕重绘之前重置列宽,这样没有任何改变,不需要第二次调用:

# 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 上测试

已在 04.2020 中通过此修复关闭,并在 tk 8.6.10 中发布。 我可以确认,对于 python 3.10 和 tkinter 8.6.12,此错误不可重现。

检查您的补丁级别

import tkinter as tk

root = tk.Tk()

print(root.tk.call("info", "patchlevel"))

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM