Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.7k views
in Technique[技术] by (71.8m points)

python - Forcing a Tkinter.ttk Treeview widget to resize after shrinking its column widths

Background

Python-2.7.14 x64, Tk-8.5.15 (bundled)

It is well-documented that the Treeview widget in Tk has a lot of issues, and Tkinter, being a thin wrapper, doesn't do much to deal with them. A common problem is getting a Treeview in browse mode to work correctly with a horizontal scrollbar. Setting the width of the overall Treeview requires setting the width of each column. But if one has a lot of columns, this stretches the parent container out horizontally, and doesn't activate the horizontal scrollbar.

The solution that I have found is, at design time, for each column, to set width to the desired size you want and cache this value somewhere safe. When you add, edit, or delete a row, you need to walk the columns and query their current width value, and if it's larger than your cached width, set width back to the original cached value and also set minwidth to the current column width. The last column needs its stretch property set to "True" as well, so it can consume any remaining space left over. This activates the horizontal scrollbar and allows it to pan the contents of the Treeview appropriately, without changing the width of the overall widget.

Caveat: At some point, Tk internally resets width to equal minwidth, but does not force a redraw right away. You get surprised later on when you change the widget, such as by adding or deleting a row, so you have to repeat the above every time the widget is changed. This is not a really big issue if you catch all of the places where a redraw can happen.


Problem

Changing a property of a Ttk style triggers a forced redraw of the entire application, so the caveat I mentioned above pops up, the Treeview widget expands horizonatlly, and the horizontal scrollbar deactivates.


Walkthrough

The below code demonstrates:

# 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)


The resulting window will look something like this:

Example Treeview widget w/ horizontal scrollbar


Note that the horizontal scrollbar is active, and that there is a slight overflow of the last column beyond the widget's edge. The last two calls to tv.column are what enable this, but dumping the column properties of the last column, we see that Tk has silently updated width and minwidth to be the same:

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


Changing any property of any style will trigger a forced redraw of the widget. For example, the next line sets the foreground color to red for a derived Checkbutton style class:

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


This is the Treeview widget now:

Treeview widget after changing a ttk.Style property


The problem now is shrinking the entire widget back. The last column can be forced back to its original size by setting width to the originally-cached size, stretch to "False", and minwidth to the maximum column size:

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

Treeview widget after forcing the column size back to original

But the width of the Treeview widget did not shrink back.


I've found two possible solutions, both in a bind to the <Configure> event, using the last display column:

  1. Indicate a change of themes:

    tv.column("value", width=232, stretch=True, minwidth=372)
    tv.event_generate("<<ThemeChanged>>")
    
  2. Hide and re-show:

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


Both work because they are the only ways I can find to invoke the internal Tk function TtkResizeWidget(). The first works because <<ThemeChanged>> forces a complete recalculation of the widget's geometry, while the second works because Treeview will call TtkResizeWidget() if it is in an unmapped state when the column is reconfigured.

The downside to both methods is you can sometimes see the window expand for a single frame and then contract back. This is why I consider both unoptimal and am hoping that someone else knows of a better approach. Or at least of a bindable event that happens either before <Configure> or before the expansion happens, from which I can use one of the above methods with.


Some references indicating that this has been an issue in Tk for a long time:

  1. Treeview's issues w/ a horizontal scrollbar on the Tcl Wiki (search for [ofv] 2009-05-30).
  2. Ticket #3519160.
  3. Message on Tkinter-discuss mailing list.

And, the issue is still reproducible on Python-3.6.4, which includes Tk-8.6.6.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Your instinct of resetting the column widths to initial values is good. Instead of binding those calls to <Configure>, which causes flickering because it redraws the screen a second time right after the first call, let's reset the column widths before the first screen redraw so nothing gets changed and no second call is necessary:

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

Tested on python 3.8.2, tkinter 8.6


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...