簡體   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