Reputation: 444
tkinter allows us to create GUI applications in Python. My question is to create a responsive window that:
Each column has texts that moves to other columns depending on their sizes. For example:
My question is: how to achieve this effect with tkinter?
Upvotes: 0
Views: 1272
Reputation: 16169
There is no built-in widget implementing this kind of column feature. Tkinter does not have a "column" layout manager that behaves this way either. It is therefore necessary to create a custom widget.
My solution is to create the columns with Text
widgets in a container Frame
.
You set the desired column width (in character) and then update the number of columns when the window is resized with a binding to <Configure>
(see the first part of .resize()
method in the example below). The Text
widgets are displayed with .grid(row=0, column=<column number>, sticky="ns")
on a single row and adapt to the row height thanks to the option sticky="ns"
.
To split the content between the different columns, I use the peer feature of the Text
widget. The leftmost column is the main Text
widget and I create peer widgets that have the same content for the other columns (see the first part of .resize()
method in the example below). This way all the columns have the same content but the part of it which is displayed can be changed independently. To do so, I use .yview_moveto(<column number>/<total number of columns>)
to display the proper part of the content in each column. However, for this to work when the content is shorter than the available display space, I need to pad the content with newlines to get a nice column display (see the second part of .resize()
method in the example below).
Here is the code:
import tkinter as tk
class MulticolumnText(tk.Frame):
def __init__(self, master=None, **text_kw):
tk.Frame.__init__(self, master, class_="MulticolumnText")
# text widget options
self._text_kw = text_kw
self._text_kw.setdefault("wrap", "word")
self._text_kw.setdefault("state", tk.DISABLED) # as far as I understood you only want to display text, not allow for user input
self._text_kw.setdefault("width", 30)
# create main text widget
txt = tk.Text(self, **self._text_kw)
txt.grid(row=0, column=0, sticky="ns") # make the Text widget adapt to the row height
# disable mouse scrolling
# Improvement idea: bind instead a custom scrolling function to sync scrolling of the columns)
txt.bind("<4>", lambda event: "break")
txt.bind("<5>", lambda event: "break")
txt.bind("<MouseWheel>", lambda event: "break")
self.columns = [txt] # list containing the text widgets for each column
# make row 0 expand to fill the frame vertically
self.grid_rowconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
def __getattr__(self, name): # access directly the main text widget methods
return getattr(self.columns[0], name)
def delete(self, *args): # like Text.delete()
self.columns[0].configure(state=tk.NORMAL)
self.columns[0].delete(*args)
self.columns[0].configure(state=tk.DISABLED)
def insert(self, *args): # like Text.insert()
self.columns[0].configure(state=tk.NORMAL)
self.columns[0].insert(*args)
self.columns[0].configure(state=tk.DISABLED)
def resize(self, event):
# 1. update the number of columns given the new width
ncol = max(event.width // self.columns[0].winfo_width(), 1)
i = len(self.columns)
while i < ncol: # create extra columns to fill the window
txt = tk.Text(self)
txt.destroy()
# make the new widget a peer widget of the leftmost column
self.columns[0].peer_create(txt, **self._text_kw)
txt.grid(row=0, column=i, sticky="ns")
txt.bind("<4>", lambda event: "break")
txt.bind("<5>", lambda event: "break")
txt.bind("<MouseWheel>", lambda event: "break")
self.columns.append(txt)
i += 1
while i > ncol:
self.columns[-1].destroy()
del self.columns[-1]
i -= 1
# 2. update the view
index = self.search(r"[^\s]", "end", backwards=True, regexp=True)
if index: # remove trailling newlines
self.delete(f"{index}+1c", "end")
frac = 1/len(self.columns)
# pad content with newlines to be able to nicely split the text between columns
# otherwise the view cannot be adjusted to get the desired display
while self.columns[0].yview()[1] > frac:
self.insert("end", "\n")
# adjust the view to see the relevant part of the text in each column
for i, txt in enumerate(self.columns):
txt.yview_moveto(i*frac)
root = tk.Tk()
im = tk.PhotoImage(width=100, height=100, master=root)
im.put(" ".join(["{ " + "#ccc "*100 + "}"]*100))
txt = MulticolumnText(root, width=20, relief="flat")
txt.pack(fill="both", expand=True)
txt.update_idletasks()
txt.tag_configure("title", justify="center", font="Arial 14 bold")
txt.insert("1.0", "Title", "title")
txt.insert("end", "\n" + "\n".join(map(str, range(20))))
txt.insert("10.0", "\n")
txt.image_create("10.0", image=im)
root.mainloop()
Upvotes: 0