Is there a way to sort the entries in a Tk Treeview by clicking the column? Surprisingly, I could not find any documentation/tutorial for this.
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
patthoyts from #tcl pointed out that the TreeView Tk demo program had the sort functionality. Here’s the Python equivalent of it:
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col), k) for k in tv.get_children('')]
l.sort(reverse=reverse)
# rearrange items in sorted positions
for index, (val, k) in enumerate(l):
tv.move(k, '', index)
# reverse sort next time
tv.heading(col, command=lambda:
treeview_sort_column(tv, col, not reverse))
[...]
columns = ('name', 'age')
treeview = ttk.TreeView(root, columns=columns, show='headings')
for col in columns:
treeview.heading(col, text=col, command=lambda:
treeview_sort_column(treeview, col, False))
[...]
Method 2
This did not work in python3. Since the Variable was passed by reference, all lambdas ended up refering to the same, last, element in columns.
This did the trick for me:
for col in columns:
treeview.heading(col, text=col, command=lambda _col=col:
treeview_sort_column(treeview, _col, False))
Method 3
madonius is right, but here you have the full example and a proper, understandable explanation
The answer provided by Sridhar Ratnakumar does not work in python3 (and apparently in python2.7): since the variable is passed by reference, all lambdas end up referring to the same, last, element in columns.
You just need to change this for loop:
for col in columns:
treeview.heading(col, text=col, command=lambda _col=col:
treeview_sort_column(treeview, _col, False))
And the same change has to be applied to the lambda function inside treeview_sort_column
So the complete solution would look like this:
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col), k) for k in tv.get_children('')]
l.sort(reverse=reverse)
# rearrange items in sorted positions
for index, (val, k) in enumerate(l):
tv.move(k, '', index)
# reverse sort next time
tv.heading(col, text=col, command=lambda _col=col:
treeview_sort_column(tv, _col, not reverse))
[...]
columns = ('name', 'age')
treeview = ttk.TreeView(root, columns=columns, show='headings')
for col in columns:
treeview.heading(col, text=col, command=lambda _col=col:
treeview_sort_column(treeview, _col, False))
[...]
Method 4
I just encountered the same issue while trying to create a view for DB,
Inspired by Sridhar Ratnakumar Answer,
I rather take the same principles that he did and to upgrade the class Treeview.
class MyTreeview(ttk.Treeview):
def heading(self, column, sort_by=None, **kwargs):
if sort_by and not hasattr(kwargs, 'command'):
func = getattr(self, f"_sort_by_{sort_by}", None)
if func:
kwargs['command'] = partial(func, column, False)
return super().heading(column, **kwargs)
def _sort(self, column, reverse, data_type, callback):
l = [(self.set(k, column), k) for k in self.get_children('')]
l.sort(key=lambda t: data_type(t[0]), reverse=reverse)
for index, (_, k) in enumerate(l):
self.move(k, '', index)
self.heading(column, command=partial(callback, column, not reverse))
def _sort_by_num(self, column, reverse):
self._sort(column, reverse, int, self._sort_by_num)
def _sort_by_name(self, column, reverse):
self._sort(column, reverse, str, self._sort_by_name)
def _sort_by_date(self, column, reverse):
def _str_to_datetime(string):
return datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
self._sort(column, reverse, _str_to_datetime, self._sort_by_date)
...
# Some code
...
treeview.heading('number', text='number', sort_by='num')
treeview.heading('name', text='name', sort_by='name')
treeview.heading('date', text='date', sort_by='date')
Just puting this here 🙂
Method 5
make this small change to the function if you have integers in your table
it will look like this.
def treeview_sort_column(treeview: ttk.Treeview, col, reverse: bool):
"""
to sort the table by column when clicking in column
"""
try:
data_list = [
(int(treeview.set(k, col)), k) for k in treeview.get_children("")
]
except Exception:
data_list = [(treeview.set(k, col), k) for k in treeview.get_children("")]
data_list.sort(reverse=reverse)
# rearrange items in sorted positions
for index, (val, k) in enumerate(data_list):
treeview.move(k, "", index)
# reverse sort next time
treeview.heading(
column=col,
text=col,
command=lambda _col=col: treeview_sort_column(
treeview, _col, not reverse
),
)
Method 6
Here is the working code to sort columns that contain string, numbers, numbers with thousand separators, code with multiple decimal values.
import tkinter as objTK
from tkinter import ttk as objTTK
from functools import partial
import datetime as objDateTime
class MyTreeview(objTTK.Treeview):
def heading(self, column, sort_by=None, **kwargs):
if sort_by and not hasattr(kwargs, 'command'):
func = getattr(self, f"_sort_by_{sort_by}", None)
if func:
kwargs['command'] = partial(func, column, False)
# End of if
# End of if
return super().heading(column, **kwargs)
# End of heading()
def _sort(self, column, reverse, data_type, callback):
l = [(self.set(k, column), k) for k in self.get_children('')]
l.sort(key=lambda t: data_type(t[0]), reverse=reverse)
for index, (_, k) in enumerate(l):
self.move(k, '', index)
# End of for loop
self.heading(column, command=partial(callback, column, not reverse))
# End of _sort()
def _sort_by_num(self, column, reverse):
self._sort(column, reverse, int, self._sort_by_num)
# End of _sort_by_num()
def _sort_by_name(self, column, reverse):
self._sort(column, reverse, str, self._sort_by_name)
# End of _sort_by_num()
def _sort_by_date(self, column, reverse):
def _str_to_datetime(string):
return objDateTime.datetime.strptime(string, "%Y-%m-%d")
# End of _str_to_datetime()
self._sort(column, reverse, _str_to_datetime, self._sort_by_date)
# End of _sort_by_num()
def _sort_by_multidecimal(self, column, reverse):
def _multidecimal_to_str(string):
arrString = string.split(".")
strNum = ""
for iValue in arrString:
strValue = f"{int(iValue):02}"
strNum = "".join([strNum, str(strValue)])
# End of for loop
strNum = "".join([strNum, "0000000"])
return int(strNum[:8])
# End of _multidecimal_to_str()
self._sort(column, reverse, _multidecimal_to_str, self._sort_by_multidecimal)
# End of _sort_by_num()
def _sort_by_numcomma(self, column, reverse):
def _numcomma_to_num(string):
return int(string.replace(",", ""))
# End of _numcomma_to_num()
self._sort(column, reverse, _numcomma_to_num, self._sort_by_numcomma)
# End of _sort_by_num()
# End of class MyTreeview
objWindow = objTK.Tk()
arrlbHeader = ["Type" , "Description", "C. Name", "C. code", "Amount", "Day", "Month ", "Year", "Date", "Comments"]
treeview = MyTreeview(columns=arrlbHeader, show="headings")
arrRows = [["Expenses", "Curds milk", "Dairy products", "2.5.2.1", "456", "31", "8", "2021", "2021-08-31", ""],
["Expenses", "Aug", "Maid", "2.12.4", "1,000", "31", "8", "2021", "2021-08-31", ""],
["Expenses", "Aug", "Water", "2.12.8", "200", "31", "8", "2021", "2021-08-31", "AAA"],
["Income", "Aug", "Electricity", "2.12.2", "190", "31", "8", "2021", "2021-08-31", "OMG"],
["Expenses", "Aug - garbage collection", "Miscellaneous", "2.12.9", "20", "31", "8", "2021", "2021-08-31", "Test1"],
["Expenses", "Bread", "Bakery", "2.5.1.1", "10", "29", "8", "2021", "2021-08-29", ""],
["Income", "Veggies", "Vegetables", "2.5.2.7", "21", "28", "8", "2021", "2021-08-28", ""],
["Expenses", "Groceries", "Grains", "2.5.2.3", "76", "28", "8", "2021", "2021-08-28", "Test"],
["Expenses", "Phenyl", "Toiletries", "2.16", "34", "28", "8", "2021", "2021-08-28", ""]]
arrColWidth = [57, 53, 85, 69, 55, 30, 45, 33, 68, 100]
arrColAlignment = ["center", "e", "w", "w", "e", "center", "center", "center", "center", "w"]
arrSortType = ["name", "name", "name", "multidecimal", "numcomma", "num", "num", "num", "date", "name"]
for iCount in range(len(arrlbHeader)):
strHdr = arrlbHeader[iCount]
treeview.heading(strHdr, text=strHdr.title(), sort_by=arrSortType[iCount])
treeview.column(arrlbHeader[iCount], width=arrColWidth[iCount], stretch=True, anchor=arrColAlignment[iCount])
# End of for loop
treeview.pack()
for iCount in range(len(arrRows)):
treeview.insert("", "end", values=arrRows[iCount])
# End of for loop
objWindow.bind("<Escape>", lambda funcWinSer: objWindow.destroy())
objWindow.mainloop()
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0
