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.