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:
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:
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)
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:
Indicate a change of themes:
tv.column("value", width=232, stretch=True, minwidth=372)
tv.event_generate("<<ThemeChanged>>")
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:
- Treeview's issues w/ a horizontal scrollbar on the Tcl Wiki (search for
[ofv] 2009-05-30
).
- Ticket #3519160.
- 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