There's no simple solution when using a text widget, for the very reasons you include in your post. I think a simpler solution would be to use a grid of labels. The only real trick is that you need to add a binding that sets the wraplength
attribute of each label so that text on the label wraps when it is too long to fit.
Here's an example of creating a small grid of labels:
import tkinter as tk
data = (
("aaa", "bbb", "ccc"),
("ddd", "eee", "an example of a long value that wraps"),
("ggg", "hhh", "iii"),
)
class LabelGrid(tk.Frame):
def __init__(self, parent, data):
super().__init__(parent)
# this assumes only the third column should grow or shrink
# when the containing frame grows or shrinks.
self.grid_columnconfigure(2, weight=1)
for row_number, row_data in enumerate(data):
for column_number, column_data in enumerate(row_data):
label = tk.Label(self, width=10, text=column_data, justify="left", anchor="nw")
label.grid(row=row_number, column=column_number, sticky="nsew")
label.bind("<Configure>", self._reset_wraplength)
def _reset_wraplength(self, event):
event.widget.configure(wraplength = event.widget.winfo_width()-2)
root = tk.Tk()
lg = LabelGrid(root, data)
lg.pack(fill="both", expand=True, padx=2, pady=2)
root.mainloop()

If you need to be able to scroll the list, you can put this frame inside a canvas since the canvas supports scrolling. The following example shows how to do that, based on a technique seen here: Adding a scrollbar to a group of widgets in Tkinter
import tkinter as tk
data = (
("aaa", "bbb", "ccc"),
("ddd", "eee", "an example of a long value that wraps"),
("ggg", "hhh", "iii"),
)
class LabelGrid(tk.Frame):
def __init__(self, parent, data):
super().__init__(parent, bg="pink")
# this assumes only the third column should grow or shrink
# when the containing frame grows or shrinks.
self.grid_columnconfigure(2, weight=1)
for row_number, row_data in enumerate(data):
for column_number, column_data in enumerate(row_data):
label = tk.Label(self, width=10, text=column_data, justify="left", anchor="nw")
label.grid(row=row_number, column=column_number, sticky="nsew")
label.bind("<Configure>", self._reset_wraplength)
def _reset_wraplength(self, event):
event.widget.configure(wraplength = event.widget.winfo_width()-2)
class ScrollableFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent, bg="bisque")
self.canvas = tk.Canvas(self, borderwidth=0)
self.frame = None
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=None, anchor="nw",tags=("inner_frame",))
self.bind("<Configure>", self._reset_width)
def set_frame(self, frame):
self.frame = frame
self.canvas.itemconfigure("inner_frame", window=frame)
self.frame.lift(self.canvas)
self.frame.bind("<Configure>", self._reset_scrollregion)
def _reset_width(self, event):
self.canvas.itemconfigure("inner_frame", width=event.width-20)
def _reset_scrollregion(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
root = tk.Tk()
sf = ScrollableFrame(root)
sf.pack(fill="both", expand=True)
lg = LabelGrid(sf, data)
sf.set_frame(lg)
root.mainloop()