How to change the foreground or background colour of a selected cell in tkinter treeview?

Solution 1:

  1. @BryanOkley shared that one cannot change the color of an individual cell in ttk.Treeview.
  2. So I explored using tk.Canvas() and tk.Canvas.create_text() to create the illusion of changing the color of a selected cell in a ttk.Treeview() widget. I was fortunate to come by j08lue/ttkcalendar.py which had the same objective and I adapted from it.
  3. My adapted script (with the relevant comments) is shown below. I hope it can help others thinking of doing the same.

Improvement needed: I have not figured out why my algorithm could not accurately overlay the Canvas Textbox over the values in the selected Treeview cells in the icon/tree column and the value columns. To that end, I resorted to using fudge values determined via trial & error. However, this is not ideal. Can someone share how I can achieve accurate alignment of the canvas_textbox overlay with the Treeview cell value without using a fudge value?

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkFont

class App(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        ttk.Frame.__init__(self, parent, *args, **kwargs)

        #1. Create Treeview with binding
        self.tree = ttk.Treeview(parent, columns=("size", "modified"))
        self.tree["columns"] = ("date", "time", "loc")

        self.tree.column("#0",   width=100, anchor='center')
        self.tree.column("date", width=100, anchor='center')
        self.tree.column("time", width=100, anchor='center')
        self.tree.column("loc",  width=100, anchor='center')

        self.tree.heading("#0",   text="Name")
        self.tree.heading("date", text="Date")
        self.tree.heading("time", text="Time")
        self.tree.heading("loc",  text="Location")

        self.tree.insert("","end", text = "Grace",
                         values = ("2010-09-23","03:44:53","Garden"))
        self.tree.insert("","end", text = "John" ,
                         values = ("2017-02-05","11:30:23","Airport"))
        self.tree.insert("","end", text = "Betty",
                         values = ("2014-06-25","18:00:00",""))

        self.tree.grid()
        self.tree.bind('<ButtonRelease-1>', self.selectItem)

        #2. Create a Canvas Overlay to show selected Treeview cell 
        sel_bg = '#ecffc4'
        sel_fg = '#05640e'
        self.setup_selection(sel_bg, sel_fg)


    def setup_selection(self, sel_bg, sel_fg):
        self._font = tkFont.Font()

        self._canvas = tk.Canvas(self.tree,
                                 background=sel_bg,
                                 borderwidth=0,
                                 highlightthickness=0)

        self._canvas.text = self._canvas.create_text(0, 0,
                                                     fill=sel_fg,
                                                     anchor='w')

    def selectItem(self, event):
        # Remove Canvas overlay from GUI
        self._canvas.place_forget()

        # Local Parameters
        x, y, widget = event.x, event.y, event.widget
        item = widget.item(widget.focus())
        itemText = item['text']
        itemValues = item['values']
        iid = widget.identify_row(y)
        column = event.widget.identify_column(x)
        print ('\n&&&&&&&& def selectItem(self, event):')
        print ('item = ', item)
        print ('itemText = ', itemText)
        print('itemValues = ',itemValues)
        print ('iid = ', iid)
        print ('column = ', column)

        #Leave method if mouse pointer clicks on Treeview area without data
        if not column or not iid:
            return

        #Leave method if selected item's value is empty
        if not len(itemValues): 
            return

        #Get value of selected Treeview cell
        if column == '#0':
            self.cell_value = itemText
        else:
            self.cell_value = itemValues[int(column[1]) - 1]
        print('column[1] = ',column[1])
        print('self.cell_value = ',self.cell_value)

        #Leave method if selected Treeview cell is empty
        if not self.cell_value: # date is empty
            return

        #Get the bounding box of selected cell, a tuple (x, y, w, h), where
        # x, y are coordinates of the upper left corner of that cell relative
        #      to the widget, and
        # w, h are width and height of the cell in pixels.
        # If the item is not visible, the method returns an empty string.
        bbox = widget.bbox(iid, column)
        print('bbox = ', bbox)
        if not bbox: # item is not visible
            return

        # Update and show selection in Canvas Overlay
        self.show_selection(widget, bbox, column)

        print('Selected Cell Value = ', self.cell_value)


    def show_selection(self, parent, bbox, column):
        """Configure canvas and canvas-textbox for a new selection."""
        print('@@@@ def show_selection(self, parent, bbox, column):')
        x, y, width, height = bbox
        fudgeTreeColumnx = 19 #Determined by trial & error
        fudgeColumnx = 15     #Determined by trial & error

        # Number of pixels of cell value in horizontal direction
        textw = self._font.measure(self.cell_value)
        print('textw = ',textw)

        # Make Canvas size to fit selected cell
        self._canvas.configure(width=width, height=height)

        # Position canvas-textbox in Canvas
        print('self._canvas.coords(self._canvas.text) = ',
              self._canvas.coords(self._canvas.text))
        if column == '#0':
            self._canvas.coords(self._canvas.text,
                                fudgeTreeColumnx,
                                height/2)
        else:
            self._canvas.coords(self._canvas.text,
                                (width-(textw-fudgeColumnx))/2.0,
                                height/2)

        # Update value of canvas-textbox with the value of the selected cell. 
        self._canvas.itemconfigure(self._canvas.text, text=self.cell_value)

        # Overlay Canvas over Treeview cell
        self._canvas.place(in_=parent, x=x, y=y)



if __name__ == "__main__":
    window = tk.Tk()
    app = App(window)
    window.mainloop()

Solution 2:

If you're getting ready to overlay widgets, you could have very custom requirements, or there may be a more suitable widget for your needs. If you're not bent on using the treeview, perhaps the table widget will provide what you want. You can control the individual cell contents, it allows user editing of the cells (by default) and you can control the 'active' cell attributes separately from other cells. Your data is placed in a table using this code.

import tkinter as tk
import tkinter.ttk as ttk
from tkinter.tktable import Table as ttkTable
from tkinter.tktable import ArrayVar

class App(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        ttk.Frame.__init__(self, parent, *args, **kwargs)
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)
        parent.grid_columnconfigure(0, weight=1)
        parent.grid_rowconfigure(0, weight=1)
        self.content = ArrayVar(parent)
        self.table = ttkTable(rows=4,  cols=4,  titlerows=1,
            titlecols=0,    roworigin=0,    colorigin=0,   anchor='w', 
            selecttype='cell',   rowstretch='none',  colstretch='unset',
            flashmode='off', ellipsis='...', ipadx=2,    colwidth=12,
            multiline=False, resizeborders='col',   selectmode='browse',
            cursor='arrow', insertwidth=2, variable=self.content,
            insertbackground='white'
        )
        self.table.tag_configure('title', relief='raised', anchor='center', bg='blue',
            fg='white', state='disabled'
        )
        self.table.tag_configure('active', bg='gray30', fg='white')

        c_headers = ["Name", "Date", "Time", "Loc"]
        for col, word in enumerate(c_headers, start=0):
            index = '0,' + str(col)
            self.table.set('col', index, word)

        self.table.width((0,1,2,3), (30,30,30,40))

        self.table.set('row','1,0', "John","2017-02-05","11:30:23","Airport")
        self.table.set('row','2,0', "Betty","2014-06-25","18:00:00","Orchard Road")

        self.table.grid(sticky='news')