Search code examples
pythonpython-3.xtkintertreeviewttk

Format individual cell/item rather than entire row in tkinter ttk treeview


I am currently using a function to display a pandas dataframe in a spreadsheet style format. I would like to be able to add some functionality to format individual cells of the treeview based on their content e.g. if they contain substring 'X' or if their value is higher than Y.

The update function currently implemented is as follows:

   def updateTree(self, dataframe):
    '''
    Updates the treeview with the data in the dataframe
    parameter
    '''
    #Remove any nan values which may have appeared in the dataframe parameter
    df = dataframe.replace(np.nan,'', regex=True)

    #Currently displayed data
    self.treesubsetdata = dataframe

    #Remove existing items
    for item in self.tree.get_children(): self.tree.delete(item)
    #Recreate from scratch the columns based on the passed dataframe
    self.tree.config(columns= [])
    self.tree.config(columns= list(dataframe.columns))

    #Ensure all columns are considered strings and write column headers
    for col in dataframe.columns:
        self.tree.heading(col,text=str(col))

    #Get number of rows and columns in the imported script
    self.rows,self.cols = dataframe.shape

    #Populate data in the treeview
    for row in dataframe.itertuples():
        self.tree.insert('', 'end',values = tuple(row[1:]))

    #Minimise first column
    self.tree.column('#0',width=0)
    self.tree.update()

Can anyone confirm that you can in fact edit an individual cell in a treview?

If yes are there any ideas as to how this could be implemented?


Solution

  • It's not possible to set the style of individual cells in Treeview; only entire rows can use the tag attribute.

    If you just want a table of values then I'd recommend just using ttk.Label widgets, which you can format in a huge number of ways. For example:

    import Tkinter as tk
    import ttk
    import pandas as pd
    from random import randrange
    
    PADDING = dict(padx=3, pady=3)
    class GridView(tk.Frame):
        def __init__(self, master=None, **kwargs):
            tk.Frame.__init__(self, master, **kwargs)
            self.labels = []
            style = ttk.Style()
            style.configure("red.TLabel", background='red')
            style.configure("green.TLabel", background='green')
            style.configure("header.TLabel", font = '-weight bold')
    
        def set(self, df):
            self.clear()
            for col, name in enumerate(df.columns):
                lbl = ttk.Label(self, text=name, style='header.TLabel')
                lbl.grid(row=0, column=col, **PADDING)
                self.labels.append(lbl)
    
            for row, values in enumerate(df.itertuples(), 1):
                for col, value in enumerate(values[1:]):
                    lbl = ttk.Label(self, text=value, style=self.get_style(value))
                    lbl.grid(row=row, column=col, **PADDING)
                    self.labels.append(lbl)
    
        @staticmethod
        def get_style(value):
            if value > 70:
                return "red.TLabel"
            elif value < 30:
                return "green.TLabel"
            else:
                return None
    
        def clear(self):
            for lbl in self.labels:
                lbl.grid_forget()
            self.labels = []
    
    class GUI(tk.Frame):
        def __init__(self, master=None, **kwargs):
            tk.Frame.__init__(self, master, **kwargs)
            self.table = GridView(self)
            self.table.pack()
            btn = ttk.Button(self, text="populate", command=self.populate)
            btn.pack()
            btn = ttk.Button(self, text="clear", command=self.table.clear)
            btn.pack()
    
        def populate(self):
            self.table.set(new_rand_df())
    
    def main():
        root = tk.Tk()
        win = GUI(root)
        win.pack()
        root.mainloop()
    
    def new_rand_df():
        width = 5
        height = 5
        return pd.DataFrame([[randrange(100) for _ in range(width)] for _ in range(height)], columns = list('abcdefghijklmnopqrstuvwxyz'[:width]))
    
    if __name__ == '__main__':
        main()