4

Is is possible to wrap text in row using tkinter treeview? I have tried to change row height but text wont wrap. basically i want to have multi line text in row.

This is the code:

from tkinter import*
from tkinter import ttk

myApp = Tk()


NewTree= ttk.Treeview(myApp, height=5)
NewTree['show'] = 'headings'
s = ttk.Style()
s.configure('Treeview', rowheight=40)

NewTree["columns"]=("1","2")

NewTree.column("1", width=60)
NewTree.column("2", width=60)

NewTree.heading("1", text="col a")
NewTree.heading("2", text="col b")

item = NewTree.insert("", "end", values=("i want to wrap this text","and this text"))

NewTree.grid(row=0,column=0)

myApp.mainloop()

This is the second version of code (when you can manually add items in treeview), It just have one big function before the code that I posted above. I tried to put different values on lenght but the result is the same (It wont wrap):

from tkinter import*
from tkinter import ttk
from tkinter import messagebox

import textwrap

def wrap(string, lenght=15):
    return '\n'.join(textwrap.wrap(string, lenght))

myApp = Tk()

def editact(event):

    def double(event): # funkcija koja kreira celiju kada se klikne dupli klik
        try:            
            if NewTree.identify_region(event.x, event.y) == 'cell':
                # the user clicked on a cell

                def ok(event):
                    """Change item value."""
                    NewTree.set(item, column, entry.get())
                    entry.destroy()

                column = NewTree.identify_column(event.x)  # identify column
                item = NewTree.identify_row(event.y)  # identify item
                x, y, width, height = NewTree.bbox(item, column) 
                value = NewTree.set(item, column)

            elif NewTree.identify_region(event.x, event.y) == 'heading': 
                    # the user clicked on a heading

                def ok(event):
                    """Change heading text."""
                    NewTree.heading(column, text=entry.get())
                    entry.destroy()

                column = NewTree.identify_column(event.x) # identify column
                # tree.bbox work only with items so we have to get the bbox of the heading differently
                x, y, width, _ = NewTree.bbox(NewTree.get_children('')[0], column) # get x and width (same as the one of any cell in the column)
                # get vertical coordinates (y1, y2)
                y2 = y
                # get bottom coordinate
                while NewTree.identify_region(event.x, y2) != 'heading':  
                    y2 -= 1
                # get top coordinate
                y1 = y2
                while NewTree.identify_region(event.x, y1) == 'heading':
                    y1 -= 1
                height = y2 - y1
                y = y1
                value = NewTree.heading(column, 'text')

            elif NewTree.identify_region(event.x, event.y) == 'nothing': 
                column = NewTree.identify_column(event.x) # identify column
                # check whether we are below the last row:
                x, y, width, height = NewTree.bbox(NewTree.get_children('')[-1], column)
                if event.y > y:

                    def ok(event):
                        """Change item value."""
                        # create item
                        item = NewTree.insert("", "end", values=("", ""))
                        NewTree.set(item, column, entry.get())
                        entry.destroy()

                    y += height
                    value = ""
                else:
                    return
            else:
                return

            # display the Entry   
            entry = ttk.Entry(NewTree)  # create edition entry
            entry.place(x=x, y=y, width=width, height=height, anchor='nw')  # display entry on top of cell
            entry.insert(0, value)  # put former value in entry
            entry.bind('<FocusOut>', ok)  #validate when you click on other cell

            entry.focus_set()

        except IndexError:

            Error=messagebox.showinfo("Error!","You have 0 rows. Please add a new row.")
            sys.exit() #za resavalje greske`
            pass

    NewTree.bind('<Double-Button-1>', double) #create new cell with double click


NewTree= ttk.Treeview(myApp, height=5)
NewTree['show'] = 'headings'

s = ttk.Style()
s.configure('Treeview', rowheight=60)

NewTree["columns"]=("1","2")

NewTree.column("1", width=100, anchor="center")
NewTree.column("2", width=100, anchor="w")

NewTree.heading("1", text="Col A")
NewTree.heading("2", text="Col B")


item = NewTree.insert("", "end", values=(wrap(""),wrap("")))
NewTree.item(item, tags=item)
NewTree.bind('<1>', editact)

NewTree.grid(row=0,column=0, columnspan=5, padx=5)

myApp.mainloop()
taga
  • 3,537
  • 13
  • 53
  • 119

2 Answers2

6

The following code uses the <B1-Motion> event to dynamically wrap the text (so resizing the columns is supported, and the text will "wrap" accordingly).

from tkinter import Tk, ttk
from tkinter.font import Font
from functools import partial

myApp = Tk()

NewTree= ttk.Treeview(myApp, height=5)
NewTree['show'] = 'headings'
s = ttk.Style()
s.configure('Treeview', rowheight=50)

NewTree["columns"]=("1","2")

NewTree.column("1", width=80, anchor="nw")
NewTree.column("2", width=80, anchor="nw")

NewTree.heading("1", text="col a")
NewTree.heading("2", text="col b")

item = NewTree.insert("", "end", values=("i want to wrap this text","and this text"))
print(item)

NewTree.grid(row=0,column=0)

def motion_handler(tree, event):
    f = Font(font='TkDefaultFont')
    # A helper function that will wrap a given value based on column width
    def adjust_newlines(val, width, pad=10):
        if not isinstance(val, str):
            return val
        else:
            words = val.split()
            lines = [[],]
            for word in words:
                line = lines[-1] + [word,]
                if f.measure(' '.join(line)) < (width - pad):
                    lines[-1].append(word)
                else:
                    lines[-1] = ' '.join(lines[-1])
                    lines.append([word,])

            if isinstance(lines[-1], list):
                lines[-1] = ' '.join(lines[-1])

            return '\n'.join(lines)

    if (event is None) or (tree.identify_region(event.x, event.y) == "separator"):
        # You may be able to use this to only adjust the two columns that you care about
        # print(tree.identify_column(event.x))

        col_widths = [tree.column(cid)['width'] for cid in tree['columns']]

        for iid in tree.get_children():
            new_vals = []
            for (v,w) in zip(tree.item(iid)['values'], col_widths):
                new_vals.append(adjust_newlines(v, w))
            tree.item(iid, values=new_vals)


NewTree.bind('<B1-Motion>', partial(motion_handler, NewTree))
motion_handler(NewTree, None)   # Perform initial wrapping

myApp.mainloop()

Separator shifted left Separator shifted right

Samuel Kazeem
  • 787
  • 1
  • 8
  • 15
jedwards
  • 29,432
  • 3
  • 65
  • 92
  • This is great, but this works only if you have string value in columns, if you have int or double, it does not work. Can you please try to make it work for int and double too? – taga Jul 02 '18 at 21:53
  • 1
    Good stuff, thanks for sharing. It looks like `col_widths = [tree.column(cid)['width'] for cid in NewTree['columns']]` in the `motion_handler` function should read `col_widths = [tree.column(cid)['width'] for cid in tree['columns']]`. – mikey Oct 02 '19 at 04:13
1

You can use the textwrap module to define this wrap function:

def wrap(string, lenght=8):
    return '\n'.join(textwrap.wrap(string, lenght))

Now, you can replace the item by:

item = NewTree.insert("", "end", values=(wrap("i want to wrap this text"),
                                         wrap("and this text")))

Full code

from tkinter import *
from tkinter import ttk

import textwrap


def wrap(string, lenght=8):
    return '\n'.join(textwrap.wrap(string, lenght))


myApp = Tk()

NewTree = ttk.Treeview(myApp, height=5)
NewTree['show'] = 'headings'
s = ttk.Style()
s.configure('Treeview', rowheight=40)

NewTree["columns"] = ("1", "2")

NewTree.column("1", width=60)
NewTree.column("2", width=60)

NewTree.heading("1", text="col a")
NewTree.heading("2", text="col b")

item = NewTree.insert("", "end", values=(wrap("i want to wrap this text"),
                                         wrap("and this text")))

NewTree.grid(row=0, column=0)

myApp.mainloop()

Output

Output

  • For some reason when I add your code, it does not work. It says `NameError: name 'textwrap' is not defined` Can you please post full code? – taga Jul 02 '18 at 09:19
  • I did it @taga, if you still have the error just run `pip3 install textwrap` in a Terminal. –  Jul 02 '18 at 09:21
  • 1
    What if the user resizes the columns, or the window? – jedwards Jul 02 '18 at 09:46
  • @Blincer Your code, works good. I made a treeview where you can manually add values to rows and columns (like real table), and in that case the code does not work. It does not show any errors , it just dont work. Is there any way to send you the example of the code so that you can see whats the problem? – taga Jul 02 '18 at 18:29
  • @Blincer I UPDATED the question, please check it – taga Jul 02 '18 at 20:41