1

I have a MultiBoxList from TkinterTreeCtrl in a window with multiple columns, some are numeric, some alphabetical (I believe as far as this cares, they're all alphanumeric). This has been populated from a database.

I would like to be able to click the column headers to either: switch which column is being used to sort the list, or: reverse the sort order (i.e. descending ascending) if that column is already being used to sort.

Below is an outline of one of these lists.

from tkinter import *
from tkinter import ttk
import TkTreectrl as treectrl

root = Tk()
root.title("Columns Demo")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))

# ========== Pickers Panel =============
top_panel = ttk.Frame(mainframe, padding="3 3 12 12")
top_panel.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

demo_picker_vscroll = ttk.Scrollbar(top_panel)
demo_picker_hscroll = ttk.Scrollbar(top_panel, orient='horizontal')
demo_picker_vscroll.grid(column=1, row=1, sticky=(N, W, S))
demo_picker_hscroll.grid(column=0, row=2, sticky=(N, W, E))
demo_picker_label = ttk.Label(top_panel, text="Table:", anchor=CENTER)
demo_picker_label.grid(column=0, row=0, sticky=(W, E))
demo_picker = treectrl.MultiListbox(top_panel, yscrollcommand=demo_picker_vscroll.set,
                                      xscrollcommand=demo_picker_hscroll.set)
demo_picker.focus_set()
demo_picker.configure(selectcmd=None, selectmode="single", width=300, height=200)
demo_picker.config(columns=("Column1", "Column2"))
demo_picker.column_configure(0, width=200)
demo_picker.grid(column=0, row=1, sticky=(W, E, S, N))
demo_picker_vscroll.config(command=demo_picker.yview)
demo_picker_hscroll.config(command=demo_picker.xview)

for i, row in enumerate(range(50)):
    demo_picker.insert(i, i, row)

root.mainloop()

Image of Tkinter Window displaying a list with 2 columns as defined by the code above

In my actual use case one of these columns could contain assorted strings not just numbers, others may be timestamps, so sorting them becomes very needed.

Further, I have a date column in one of my panels like this. Is there a particular datetime formatting that's still human readable but will obey sensible ordering? YYY-MM-DD HH:MM:SS should work for this right?

Lastly as a side question, can I make the text from these copy-pastable?

ch4rl1e97
  • 666
  • 1
  • 7
  • 24

1 Answers1

1

There is a good example of MultiListbox with column sorting in the demo folder of the source code of TkinterTreeCtrl. I have adapted this example, with integers in the first column and dates in the second.

It is based on the .sort method of the listbox

demo_picker.sort(column=nb, command=sort_fct, mode='increasing'/'decreasing')

where the documentation for the command option states

Use command as a comparison command. To compare two items, evaluate a command with the numerical ids of the two items passed as arguments. The comamnd should return an integer less than, equal to, or greater than zero if the first item is to be considered less than, equal to, or greater than the second, respectively.

Therefore sort_fct(item1, item2) has to retrieve the values for the items in the target column and convert them to an appropriate type to perform the comparison (e.g. int, datetime, ...)

from tkinter import *
from tkinter import ttk
import TkTreectrl as treectrl
from datetime import datetime, timedelta

root = Tk()
root.title("Columns Demo")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))

# ========== Pickers Panel =============
top_panel = ttk.Frame(mainframe, padding="3 3 12 12")
top_panel.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

demo_picker_vscroll = ttk.Scrollbar(top_panel)
demo_picker_hscroll = ttk.Scrollbar(top_panel, orient='horizontal')
demo_picker_vscroll.grid(column=1, row=1, sticky=(N, W, S))
demo_picker_hscroll.grid(column=0, row=2, sticky=(N, W, E))
demo_picker_label = ttk.Label(top_panel, text="Table:", anchor=CENTER)
demo_picker_label.grid(column=0, row=0, sticky=(W, E))
demo_picker = treectrl.MultiListbox(top_panel, yscrollcommand=demo_picker_vscroll.set,
                                      xscrollcommand=demo_picker_hscroll.set)
demo_picker.focus_set()
demo_picker.configure(selectcmd=None, selectmode="single", width=300, height=200)
demo_picker.config(columns=("Column1", "Column2"))
demo_picker.column_configure(0, width=200)
demo_picker.grid(column=0, row=1, sticky=(W, E, S, N))
demo_picker_vscroll.config(command=demo_picker.yview)
demo_picker_hscroll.config(command=demo_picker.xview)

now = datetime.now()
for i, row in enumerate(range(50)):
    demo_picker.insert(i, row, (now - timedelta(i)).strftime("%H:%M %d-%m-%Y"))


# add arrow icons to the column headers first
demo_picker.column_configure(demo_picker.column(0), arrow='down', arrowgravity='right')
demo_picker.column_configure(demo_picker.column(1), arrow='down', arrowgravity='right')

# set sortorder flags indicating the sorting order for every column
sortorder_flags = {0: 'increasing', 1: 'increasing'}

# custom sorting functions
def sort_0(item1, item2):
    i1, i2 = demo_picker.index(item=item1), demo_picker.index(item=item2)
    a = int(demo_picker.get(i1)[0][0])
    b = int(demo_picker.get(i2)[0][0])

    if b > a:
        return -1
    elif b < a:
        return 1
    else:
        return 0

def sort_1(item1, item2):
    i1, i2 = demo_picker.index(item=item1), demo_picker.index(item=item2)
    a = datetime.strptime(demo_picker.get(i1)[0][1], "%H:%M %d-%m-%Y")
    b = datetime.strptime(demo_picker.get(i2)[0][1], "%H:%M %d-%m-%Y")

    if b > a:
        return -1
    elif b < a:
        return 1
    else:
        return 0

# now create a common sort command for all columns
def sort_list(event):
    # do the sorting
    if event.column == 0:
        demo_picker.sort(column=0, command=sort_0, mode=sortorder_flags[0])
    else:
        demo_picker.sort(column=1, command=sort_1, mode=sortorder_flags[1])
    # switch the sortorder flag and turn the arrow icons upside down
    if sortorder_flags[event.column] == 'increasing':
        demo_picker.column_configure(demo_picker.column(event.column), arrow='up')
        sortorder_flags[event.column] = 'decreasing'
    else:
        demo_picker.column_configure(demo_picker.column(event.column), arrow='down')
        sortorder_flags[event.column] = 'increasing'

# finally register the sort command
demo_picker.notify_install('<Header-invoke>')
demo_picker.notify_bind('<Header-invoke>', sort_list)

root.mainloop()
    
j_4321
  • 15,431
  • 3
  • 34
  • 61
  • Brilliant thank you!! I've applied this successfully to my case. Only issue is it hangs for quite some time on lengthy lists. Not the end of the world though – ch4rl1e97 May 25 '21 at 14:09
  • Just a thought on speed for long lists. If I format my datetime strings as year-month-day hours:minutes:seconds, I could just rely on string sorts instead of the converting 100,000 datetime strings back into datetime objects? Alternatively could I have a "hidden" column that contains the unix timestamp floats and when clicking the human-readable date column it actually calls for data from the hidden column? – ch4rl1e97 May 25 '21 at 14:27
  • 1
    @ch4rl1e97 I have not seen an equivalent of `displaycolumns` option of the `ttk.Treeview` for the `MultiListbox` so I am not sure it is possible to have a hidden column, so using a "year-month-day hours:minutes:seconds" format and the default sorting should work fine. – j_4321 May 25 '21 at 15:47
  • No worries! This does make it rather hard to sort very long lists in reasonable time unfortunately. Might it be more worth while to sort the data more "natively" using builtin `sort()` and then re-load the data into the `Multilistbox`? perhaps like this https://stackoverflow.com/a/6618543/9311137. I'll give it a go later and see if I get an improvement. Might also be worth trying with numpy arrays – ch4rl1e97 May 27 '21 at 11:25