在缩小列宽

时间:2018-04-08 07:19:31

标签: python tkinter

背景

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

有充分证据表明,Tk中的Treeview小部件存在很多问题,而Tkinter作为一个薄的包装器,并没有做很多事情来处理它们。一个常见的问题是使browse模式的Treeview与水平滚动条一起正常工作。设置整个Treeview的宽度需要设置每列的宽度。但是如果一个列有很多列,则会将父容器水平拉伸,并且不会激活水平滚动条。

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


生成的窗口将如下所示:

Example Treeview widget w/ horizontal scrollbar


请注意,水平滚动条处于活动状态,并且在窗口小部件边缘之外的最后一列略有溢出。对tv.column的最后两次调用是启用此功能,但转储最后一列的列属性,我们发现Tk已默默更新widthminwidth

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


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

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


现在这是Treeview小部件:

Treeview widget after changing a ttk.Style property


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

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

Treeview widget after forcing the column size back to original

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

  3. 两者都有效,因为它们是我发现调用内部Tk函数TtkResizeWidget()的唯一方法。第一个起作用是因为<<ThemeChanged>>强制完全重新计算窗口小部件的几何体,而第二个起作用是因为Treeview将调用TtkResizeWidget(),如果它在重新配置列时处于未映射状态。

    这两种方法的缺点是你有时可以看到窗口展开为一个框架然后收缩。这就是为什么我认为两者都不理想,我希望其他人知道更好的方法。或者至少是在<Configure>之前或扩展发生之前发生的可绑定事件,我可以使用上述方法之一。


    一些参考文献表明,这是Tk中时间的一个问题:

    1. Treeview的问题,Tcl Wiki上有水平滚动条(搜索[ofv] 2009-05-30)。
    2. 票证#3519160
    3. Message在Tkinter讨论邮件列表。
    4. 而且,Python-3.6.4上的问题仍然可重现,其中包括Tk-8.6.6。

1 个答案:

答案 0 :(得分:0)

将列宽重置为初始值的本能很好。与其将这些调用绑定到<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上测试