Sort treeview columns

2

I have a thread in which I asked how to generate a treeview from a panda dataframe in python. The thread is this:

How to use Tkinter ProgressBar

My problem now is that I do not know how to implement a function that orders each column when clicking on its header. I used to generate the treeview in a different way and I used something like that to do what I'm talking about but now I can not adapt 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))




tv.heading('#1', text="text", command=lambda: treeview_sort_column(tv, '#1', False))
    
asked by Alfredo Lopez Rodes 13.09.2018 в 11:29
source

1 answer

0

The DataFrameTreeView class exposes the% internal% co through the instance attribute TreeView , which you can use to access the widget from the same class using tree_view as well as from the instance ( self.tree_view in this case from self.treeview.tree_view ). This is what you have to use instead of Application , it would be more appropriate to make the function an instance method of the class:

import functools
import tkinter as tk
from tkinter import ttk

import pandas as pd



class DataFrameTreeView(tk.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tree_view = None
        self.hscrollbar = None
        self.vscrollbar = None

    def load_table(self, df, columns=None, columns_headers=None, chunk_size=100):

        """
        Args:
            path: cadena -> ruta al fichero .xlsx
            columns: list -> columnas a mostrar en la tabla, si es None se,muestran todas
            columns_headers: list -> Nombres para las cabeceras de las columnas,
                                     si es None se usan las cabeceras del DataFrame

            chunk_size: int -> Número de filas creadas por iteración
        """ 

        if columns is not None:
            dif = set(columns) - set(df.columns)
            if dif:
                raise ValueError(f"Columns: {tuple(dif)} are not in DataFrame") 
        else:
            columns = df.columns

        if columns_headers is not None:
            if  len(columns_headers) != len(df.columns):
                raise ValueError("headers length not mismath columns number")
        else:
            columns_headers = columns
        tk_col_names =[f"#{name}" for name in columns_headers]

        # Treeview y barras
        if self.tree_view is not None:
            self.tree_view.destroy()
            self.hscrollbar.destroy()
            self.vscrollbar.destroy()

        self.tree_view = ttk.Treeview(self, columns=tk_col_names)
        self.vscrollbar = ttk.Scrollbar(self, orient='vertical', command = self.tree_view.yview)
        self.vscrollbar.pack(side='right', fill=tk.Y)
        self.hscrollbar = ttk.Scrollbar(self, orient='horizontal', command = self.tree_view.xview)
        self.hscrollbar.pack(side='bottom', fill=tk.X)
        self.tree_view.configure(yscrollcommand=self.vscrollbar.set)
        self.tree_view.configure(xscrollcommand=self.hscrollbar.set)

        # Configuar columnas y cabeceras
        for name, header in zip(tk_col_names, columns_headers):
            self.tree_view.column(name, anchor=tk.W)
            sort_f = functools.partial(self.treeview_sort_column, name, False)
            self.tree_view.heading(name, text=header, anchor=tk.W,
                                   command=sort_f)
        # Cargamos los items
        rows = df.shape[0]
        chunks = rows / chunk_size
        progress = 0
        step = 100 / chunks

        progress_bar = ttk.Progressbar(self, orient="horizontal",
                                        length=100, mode="determinate")
        progress_bar["value"] = progress 
        label = tk.Label(self, text="Cargando filas")
        label.place(relx=0.50, rely=0.45, anchor=tk.CENTER)
        progress_bar.place(relx=0.5, rely=0.5, relwidth=0.80,  anchor=tk.CENTER)

        for ind in df.index:
            values = [str(v) for v in df.loc[ind, columns].values]
            self.tree_view.insert("", tk.END, text=ind+1, values=values)
            if ind % chunk_size == 0:
                self.update_idletasks()
                progress += step
                progress_bar["value"] = progress 

        progress_bar["value"] = progress
        self.update_idletasks()

        progress_bar.destroy()
        label.destroy()
        self.tree_view.pack(expand=True, fill='both')


    def treeview_sort_column(self, col, reverse):
        l = [(self.tree_view.set(k, col), k) for k in self.tree_view.get_children('')]
        l.sort(reverse=reverse)

        for index, (_, k) in enumerate(l):
            self.tree_view.move(k, '', index)

        self.tree_view.heading(col,
            command=lambda: self.treeview_sort_column(col, not reverse)
            )

Keep in mind that the list that you get from tv and which we sort is always a list of strings . This implies that the ordering is lexicographic , which is not possibly what we want if the column contains scalars for example, since Treeview is ordered as [1, 5, 19, 255] . The simplest solution is to perform the appropriate casting depending on the type of the column when generating the list:

l = [(int(self.tree_view.set(k, col)), k)
         for k in self.tree_view.get_children('')]

This can be complicated to generalize and inefficient for columns with many elements. Since your data comes from a DataFrame, a much simpler and more efficient option is to store a copy of the data displayed in a DataFrame, if we make sure that each column has the appropriate type before sending it to the instance of [1, 19, 255, 5] it is trivial to use that DataFrame to order the column without the problem mentioned above and more efficiently:

import functools
import tkinter as tk
from tkinter import ttk

import pandas as pd



class DataFrameTreeView(tk.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tree_view = None
        self.hscrollbar = None
        self.vscrollbar = None
        self._data = None

    def load_table(self, df, columns=None, columns_headers=None, chunk_size=100):

        """
        Args:
            path: cadena -> ruta al fichero .xlsx
            columns: list -> columnas a mostrar en la tabla, si es None se,muestran todas
            columns_headers: list -> Nombres para las cabeceras de las columnas,
                                     si es None se usan las cabeceras del DataFrame

            chunk_size: int -> Número de filas creadas por iteración
        """ 

        if columns is not None:
            dif = set(columns) - set(df.columns)
            if dif:
                raise ValueError(f"Columns: {tuple(dif)} are not in DataFrame") 
        else:
            columns = df.columns

        if columns_headers is not None:
            if  len(columns_headers) != len(df.columns):
                raise ValueError("headers length not mismath columns number")
        else:
            columns_headers = columns
        tk_col_names =[f"#{name}" for name in columns_headers]

        # Treeview y barras
        if self.tree_view is not None:
            self.tree_view.destroy()
            self.hscrollbar.destroy()
            self.vscrollbar.destroy()

        self.tree_view = ttk.Treeview(self, columns=tk_col_names)
        self.vscrollbar = ttk.Scrollbar(self, orient='vertical', command = self.tree_view.yview)
        self.vscrollbar.pack(side='right', fill=tk.Y)
        self.hscrollbar = ttk.Scrollbar(self, orient='horizontal', command = self.tree_view.xview)
        self.hscrollbar.pack(side='bottom', fill=tk.X)
        self.tree_view.configure(yscrollcommand=self.vscrollbar.set)
        self.tree_view.configure(xscrollcommand=self.hscrollbar.set)

        # Configuar columnas y cabeceras
        for name, header, col in zip(tk_col_names, columns_headers,  columns):
            print(name)
            self.tree_view.column(name, anchor=tk.W)
            sort_f = functools.partial(self.treeview_sort_column, name, False)
            self.tree_view.heading(name, text=header, anchor=tk.W, command=sort_f)

        # Cargamos los items
        rows = df.shape[0]
        chunks = rows / chunk_size
        progress = 0
        step = 100 / chunks

        progress_bar = ttk.Progressbar(self, orient="horizontal",
                                        length=100, mode="determinate")
        progress_bar["value"] = progress 
        label = tk.Label(self, text="Cargando filas")
        label.place(relx=0.50, rely=0.45, anchor=tk.CENTER)
        progress_bar.place(relx=0.5, rely=0.5, relwidth=0.80,  anchor=tk.CENTER)

        for ind in df.index:
            values = [str(v) for v in df.loc[ind, columns].values]
            self.tree_view.insert("", tk.END, text=ind+1, values=values)
            if ind % chunk_size == 0:
                self.update_idletasks()
                progress += step
                progress_bar["value"] = progress 

        progress_bar["value"] = progress
        self.update_idletasks()

        progress_bar.destroy()
        label.destroy()
        self.tree_view.pack(expand=True, fill='both')

        self._data = df[columns].copy()
        # Creamos columna en dataframe local con los items
        self._data.columns = tk_col_names
        self._data["#iids"] = self.tree_view.get_children('')


    def treeview_sort_column(self, col, reverse):
        """
        Ordena el treeview al hacer click en la cabecera de cada columna
        de forma alterna en orden ascendente y descendente.

        """
        self._data.sort_values(by=col, inplace=True,  ascending=reverse)        
        for index, iid in enumerate(self._data["#iids"]):
            self.tree_view.move(iid, '', index+1)

        sort_f = functools.partial(self.treeview_sort_column, col, not reverse)
        self.tree_view.heading(col, command=sort_f)

The price to pay is the use of more memory obviously to have to keep this DataFrame in memory while the TreeView exists.

    
answered by 13.09.2018 / 15:14
source