2

I would like to change the foreground or background colour of a selected cell in tkinter.treeview. How can I do that?

This link showed the command to change the colour of all cells in a treeview but I could not get it to work for a single cell.

ttk.Style().configure("Treeview", background="#383838", 
 foreground="white", fieldbackground="red")

I had previously written a test code. Please use this code to derive your solution/advice. Thanks.

This link showed how tags may be used to change the colour of a row of data, i.e. a selected item, but not a cell.

Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • 1
    I think this would've been a much better question if you've provided the minimal code to produce an example treeview of whom's cell to be configured. – Nae Jan 20 '18 at 16:26
  • you cannot change the color of an individual cell. – Bryan Oakley Jan 20 '18 at 16:35
  • @Nae I wanted to avoid repeating the same code. One can simply add the command shown above to the last line of `def selectItem(self, event):` in the [test code](https://stackoverflow.com/questions/48268506/select-a-cell-in-tkinter-treeview-and-get-the-cell-data?noredirect=1#comment83523709_48268506) that I wrote previously. – Sun Bear Jan 20 '18 at 17:09
  • @BryanOakley Thanks. This means I may need to look at overlaying a tk.canvas over the treeview to change colour of cell font or background? – Sun Bear Jan 20 '18 at 17:14
  • If you've resolved your issue, please provide it as an answer. – Nae Jan 22 '18 at 14:02
  • If you've additional questions please ask them separately. – Nae Jan 22 '18 at 14:02

5 Answers5

7
  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()
Sun Bear
  • 7,594
  • 11
  • 56
  • 102
3

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')
Ron Norris
  • 2,642
  • 1
  • 9
  • 13
  • I could not find `tktable` and `ttkTable` in [Tk8.5](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html) and [Tk8.6](https://www.tcl.tk/man/tcl/TkCmd/contents.htm). `ImportError: No module named 'tkinter.tktable'`. Can you direct me to the documentation on this widget? – Sun Bear Jan 21 '18 at 08:53
  • I'm sorry I forgot that I had specifically downloaded this outside of pypy. It is copyrighted software, but license is fairly open. Read it. This is the link that will enable you to download it. It is merely a tktable.py file. So you should move/copy it to your tkinter folder on your system. It uses the Tk Widget to build the table, so it should be portable to all systems that tkinter supports https://sourceforge.net/p/tktable/wiki/Home. – Ron Norris Jan 21 '18 at 12:07
  • Thanks for introducing me to a new widget. I chose to remain using tk.Treeview and have uploaded a solution that meets my need. Sharing it with you in case it interest you. – Sun Bear Jan 22 '18 at 14:33
2

I believe you can obtain your desired behavior in a really simple way:

  1. Assign tags to your cells

In this question you can see both how to insert tags and how to change them

  1. Assign a color to each tag

Like suggested in this question

  1. Note that Tkinter is a little bit old and disbanded... you may have some hardships, so let me link this already:

Why my code below may not work properly

Note that you are asking about "how to change a selected cell", but from what you wrote I believe you are trying to change a selected row. My code below changes rows.

import tkinter as tk
from tkinter import ttk
from random import choice




colors = ["red", "green", "black", "blue", "white", "yellow", "orange", "pink", "grey", "purple", "brown"]
def recolor():
    for child in tree.get_children():
        picked = choice(colors)
        tree.item(child, tags=(picked), values=(picked))
    for color in colors:
        tree.tag_configure(color, background=color)
    tree.tag_configure("red", background="red")


root = tk.Tk()

tree=ttk.Treeview(root)


tree["columns"]=("one","two","three")
tree.column("#0", width=60, minwidth=30, stretch=tk.NO)
tree.column("one", width=120, minwidth=30, stretch=tk.NO)

tree.heading("#0",text="0",anchor=tk.W)
tree.heading("one", text="1",anchor=tk.W)

for i in range(10):
    tree.insert("", i, text="Elem"+str(i), values=("none"))

tree.pack(side=tk.TOP,fill=tk.X)


b = tk.Button(root, text="Change", command=recolor)
b.pack()


root.mainloop()

Result:

After the first click

After the second click

Federico Dorato
  • 710
  • 9
  • 27
  • It will be good to illustrate what you mean with a sample script so that others can compare your solution against existing solutions. – Sun Bear Apr 08 '20 at 08:48
  • @SunBear Here we go ;) Done! – Federico Dorato Apr 14 '20 at 10:19
  • @SunBear Oh man, I wrote this in my previous answer:"Note that you are asking about "how to change a selected cell", but from what you wrote I believe you are trying to change a selected row. My code below changes rows." **and this statement was totally wrong**. Man I did waste my time... – Federico Dorato Apr 14 '20 at 10:22
  • I appreciate your effort. Yes, the set objective is to change the color of a single selected cell in the Treeview widget. – Sun Bear Apr 14 '20 at 14:07
2

If anyone looking for an answer to change selected color for tkinter treeview, you can check below code.

style = ttk.Style()
# this is set background and foreground of the treeview
style.configure("Treeview",
                background="#E1E1E1",
                foreground="#000000",
                rowheight=25,
                fieldbackground="#E1E1E1")

# set backgound and foreground color when selected
style.map('Treeview', background=[('selected', '#BFBFBF'), foreground=[('selected', 'black')])
Durai
  • 505
  • 4
  • 12
0

I'm adding my solution based on the unique need and lack of support. I took @Sun Bear's answer and created an abstracted function that just colors the cell and nothing else.

def set_treeview_cell_color(self, treeview: ttk.Treeview, iid: str, colnum: int, text_color: str, bg_color: str) -> None:
        '''
        sets the color of a cell in the treeview using a canvas
        puts the canvas over top of the cell and matches the text location
        creates a collection of canvases so they can be managed later
        '''   
        #get the text of the cell
        cell_value = treeview.item(iid, "values")[colnum]

        #get the text anchor of the column (treeview sets anchors at the column level)
        x_padding = 4
        anchor = treeview.column(colnum, "anchor")

        #create the canvas
        canvas = tk.Canvas(master=treeview, background=bg_color, borderwidth=0, highlightthickness=0)
        canvas.text = canvas.create_text(0, 0, text=cell_value, fill=text_color, anchor=anchor)

        #add the canvas to the collection, make sure it exists first
        if not hasattr(treeview, "canvases"):
            treeview.canvases = []
        treeview.canvases.append(canvas)

        #get location, width, and height of specified treeview cell
        x, y, width, height = treeview.bbox(iid, colnum)  #not working because the tree isn't visible until after the canvas is placed
        
        #move and size the canvas text to match the location in the treeview cell
        text_y = height / 2
        #match the canvas text to the treeview cell text
        canvas.coords(canvas.text, x_padding, text_y)
        canvas.configure(width=width, height=height)
        #canvas.text.configure(textAlign=textAlign)
    
        #place the canvas in the treeview based on the cell location
        canvas.place(in_=treeview, x=x, y=y)

I'm not 100% happy with it yet, I'm trying to grab some of the padding and alignment from the treeview, to make it more automatic, but it's surprisingly hard to get the padding from Treeview libraries. I haven't been successful yet, so I'm using x_padding for now. I'm planning to write some code that'll handle east or right alignment and i'll come back and add that when I do.

turbonate
  • 159
  • 2
  • 13