Reputation: 85262
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.
Upvotes: 20
Views: 34493
Reputation: 1
Also, the solution that madonius proposed works nicely but sorts strings in alphabetical order by first placing upper case letters then lowercase letters.
This is an expected behaviour if you know how computers treat letters (for more information about this behaviour Why does Python sort put upper case items first?).
To sort alphabetically strings without considering upper case letters different from lowercase letter, add .lower() in the treeview_sort_column function.
Full code:
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col).lower(), 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))
[...]
Upvotes: 0
Reputation: 647
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()
Upvotes: 1
Reputation: 55
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
),
)
Upvotes: 1
Reputation: 155
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 :)
Upvotes: 3
Reputation: 784
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))
[...]
Upvotes: 6
Reputation: 461
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))
Upvotes: 28
Reputation: 85262
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))
[...]
Upvotes: 30