tkinter allows us to create GUI applications in Python. My question is to create a responsive window that:
A column has a fixed width, but a flexible height.
When window's width increases, more columns are added.
When window's width decreases, columns are removed.
When window's height increases, columns become longer.
When window's height decreases, columns become shorter.
Each column has texts that moves to other columns depending on their sizes. For example:
If columns' heights increase, then more text is shown inside them. The 1st column will be taking more texts from the 2nd column, and the 2nd column will be taking texts from the 3rd (or the buffer).
My question is: how to achieve this effect with tkinter?
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()
Related
My application should look as follows:
If I click in BasicWindow at Button "Left" and click in NewWindow (should be some widget in seperate class) at Button "apply", in a cell of some grid (4 columns) below of Button "Left" the word hello should appear. If I repeat that n times, new words "Hello" should arranged in the grid below of the "Left" button n times.
If I click in BasicWindow at Button "Right" and click in NewWindow at Button "apply", in a cell of some grid (4 columns) below of Button "Left" the word hello should appear. If I repeat that n times, new words "Hello" should arranged in the grid below of the "Right" button n times.
This graphic illustrates, what the program should do:
I just tried to solve that with following code:
from tkinter import *
class NewWindow(Toplevel):
def __init__(self, master = None, apply=None):
super().__init__(master = master)
self.title('NewWindow')
self.master = master
self.words = 'Hello'
self.bt1 = Button(self, text="apply", command=self.bt_press)
self.bt1.grid(column=0, row=0)
self.apply = apply
def bt_press(self):
self.apply(self.words)
self.destroy()
window = Tk()
def new_Editor(key):
def make_label1(lbl_txt):
lbl = Label(window, text=lbl_txt)
lbl.grid(column=0,row=2)
def make_label2(lbl_txt):
lbl = Label(window, text=lbl_txt)
lbl.grid(column=2,row=2)
if key == 1:
a = NewWindow(window, make_label1)
else:
a = NewWindow(window, make_label1)
window.title("BasicWindow")
window.basic_bt_l = Button(window, text="Left", command=lambda: new_Editor(1))
window.basic_bt_l.grid(column=0, row=0, columnspan=2)
window.basic_bt_r = Button(window, text="Right", command=lambda: new_Editor(2))
window.basic_bt_r.grid(column=1, row=0)
window.mainloop()
My Programm looks like this:
For some reason the two buttons are not very good arranged and the format of the output isn't very good. How can I just define some well formatet grid between Left and Right button and two grids below of Left/Right button with the properties I descriped above?
You've specified columnspan=2 when adding root.basic_bt_l to the window, so it will occupy columns 0 and 1. You then try to put root.basic_bt_r in column 1, which will overlap the first button.
Sometimes these mistakes are easier to see if you group your grid statements together. Notice how the columnspan sticks out like a sore thumb here, and makes it easier to visualize the layout
root.basic_bt_l.grid(column=0, row=0, columnspan=2)
root.basic_bt_r.grid(column=1, row=0)
You need to remove columnspan=2 so that the buttons don't overlap.
As for adding new words on new rows, you will need to calculate which row to place the new label. In this specific case, since you are stacking the labels vertically and never remove them from the middle, you can use the grid_slaves method to tell you how many labels there are. You can add one to that number to get the first empty row for that column. You could also just keep a counter of how many labels you've added for each column.
You also need to make sure that you call make_label2 - you are calling make_label1 in both conditions of your if statement.
def new_Editor(key):
def make_label1(lbl_txt):
print("root.grid_slaves(0):", root.grid_slaves(column=0))
row = len(root.grid_slaves(column=0))+1
lbl = Label(root, text=lbl_txt)
lbl.grid(row=row, column=0)
def make_label2(lbl_txt):
print("root.grid_slaves(1):", root.grid_slaves(column=1))
row = len(root.grid_slaves(column=1))+1
lbl = Label(root, text=lbl_txt)
lbl.grid(row=row, column=1)
if key == 1:
a = NewWindow(root, make_label1)
else:
a = NewWindow(root, make_label2)
I have a function which adds a text widget to the frame dynamically when a user clicks a button. And then different text is inserted into each of the text widget. These text widgets have been stored in a list because I want to scroll all the text box by calling the .see method.
I have a function defined which auto-scrolls all the text widget to a specific position which is obtained using the text.search() method.
the text.search() methods returns the index for the search-term from the text widget no. 1 of the frame, and then it auto-scrolls all the text widget.
Question
How to search for a specific term individually in all text_widgets and obtain their indexes and use their indexes to scroll their respective text boxes?
Similar code
#Initializing an array at the top of your code:
widgets = []
#Next, add each text widget to the array:
for i in range(10):
text1 = tk.Text(...)
widgets.append(text1)
#Next, define a function that calls the see method on all of the widgets:
def autoscroll(pos):
for widget in widgets:
widget.see(pos)
#Finally, adjust your binding to call this new method:
pos_start = text1.search(anyword, '1.0', "end")
text1.tag_bind(tag, '<Button-1>', lambda e, index=pos_start: autoscroll(index))
This is relatively simple to do.
I don't understand, however, why you are performing the search before the activate the auto-scroll functionality. All you need to do is cycle through the list widgets and determine where the text appears whenever you want to start the autoscroll, the below script will perform what I believe to be the desired behaviour:
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.texts = [tk.Text(self.root) for i in range(3)]
for i in self.texts:
i.pack(side="left")
tk.Button(self.root, text="Find 'foobar'", command=self.find).pack()
def find(self):
for i in self.texts:
if i.search("foobar", "1.0", "end") != "":
i.see(i.search("foobar", "1.0", "end"))
root = tk.Tk()
App(root)
root.mainloop()
However, this does not take in to account multiple results for the search term within the same Text widget or even in multiple Text widgets.
Dears,
Although I want to make it simple, I'm still failing in creating a class of "windows" which by default will have a basic menu (not in code yet), a frame of 10 rows and 10 cls, and in the last cell of this frame (row = 9, col=9) a "Close" button.
Then, I could create several classes that will inherit from that one, and adding more widgets, commands, .. Well very basic
Yes but, although I gave weight to cells,..,... the button is still on the top left corner, and not the bottom right one. What did I miss when managing widgets with .grid()
Thks a lot
import tkinter as tk
from tkinter import ttk
class myWindows(tk.Tk):
def __init__(self,
pWtitle='',
pParent = '',
pIsOnTop = False,
pWidth=800,
pHeight=600,
pIsResizable=False,
pNbRows = 10,
pNbCols = 10):
tk.Tk.__init__(self)
tk.Tk.wm_title(self, pWtitle)
self.geometry('%sx%s' % (pWidth, pHeight))
if pIsResizable :
self.minsize(pWidth, pWidth)
rFrame =tk.Frame(self, borderwidth=1, relief="ridge")
#to make it simple by default, there will be a grid of 10rows and 10columns
for r in range(pNbRows) :
rFrame.grid_rowconfigure(r,weight=1)
for c in range(pNbCols) :
rFrame.grid_columnconfigure(c,weight=1)
rFrame.grid(row=0, column=0, sticky='ns')
#all windows will have a quit button on the bottom right corner (last cells)
#Need deduct 1 in the parameter as indexes start from 0
bt=ttk.Button(self, text='Close',command=self.quit)
bt.grid(row=pNbRows -1, column=pNbCols -1, sticky='se')
app = myWindows( pWtitle='MAIN')
app.mainloop()
You are configuring the weights inside of rFrame, but you are putting rFrame and the button directly in the root window. You have not configured weights for any rows or columns in the root window.
grid doesn't display rows and columns that don't contain anythng. Try, for example adding one placeholder label with empty picture (Label = (self, image = PhotoImage())) in every row and column of the grid until you populate it with real stuff. Source
http://effbot.org/tkinterbook/grid.htm
minsize= Defines the minimum size for the row. Note that if a row is
completely empty, it will not be displayed, even if this option is
set.
Finally, I came up with a solution :
import tkinter as tk
from tkinter import ttk
class myWindows(tk.Tk):
def __init__(self,
pWtitle='',
pParent = '',
pIsOnTop = False,
pWidth=800,
pHeight=600,
pIsResizable=False
):
tk.Tk.__init__(self)
tk.Tk.wm_title(self, pWtitle)
self.geometry('%sx%s' % (pWidth, pHeight))
if pIsResizable :
self.minsize(pWidth, pHeight)
#all windows will have a quit button on the bottom right corner (last cells)
#Need deduct 1 in the parameter as indexes start from 0
bt=ttk.Button(self, text='Close',command=self.quit)
bt.grid(row=1, column=0, sticky='se')
#to make it simple by default, there will be a container on top of button
rFrame =tk.Frame(self, borderwidth=1, bg = 'blue', relief="ridge")
rFrame.grid(row=0, column=0)
#give max weight to the first cell to
#make sure the container is filling up the empty space
#on top of the button(s)
self.grid_rowconfigure(0, weight = 1)
self.grid_rowconfigure(1, weight =0)
self.grid_columnconfigure(0, weight =1)
app = myWindows( pWtitle='MAIN')
app.mainloop()
I am quite new to Tkinter, but, nevertheless, I was asked to "create" a simple form where user could provide info about the status of their work (this is sort of a side project to my usual work).
Since I need to have quite a big number of text widget (where users are required to provide comments about status of documentation, or open issues and so far), I would like to have something "scrollable" (along the y-axis).
I browsed around looking for solutions and after some trial and error I found something that works quite fine.
Basically I create a canvas, and inside a canvas a have a scrollbar and a frame. Within the frame I have all the widgets that I need.
This is a snipet of the code (with just some of the actual widgets, in particular the text ones):
from Tkinter import *
## My frame for form
class simpleform_ap(Tk):
# constructor
def __init__(self,parent):
Tk.__init__(self,parent)
self.parent = parent
self.initialize()
#
def initialize(self):
#
self.grid_columnconfigure(0,weight=1)
self.grid_rowconfigure(0,weight=1)
#
self.canvas=Canvas(self.parent)
self.canvas.grid(row=0,column=0,sticky='nsew')
#
self.yscrollbar = Scrollbar(self,orient=VERTICAL)
self.yscrollbar.grid(column =4, sticky="ns")
#
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGTH,expand=FALSE)
#
self.canvas.config(yscrollcommand=self.yscrollbar.set)
self.canvas.pack(side=LEFT,expand=TRUE,fill=BOTH)
#
self.frame1 = Frame(self.canvas)
self.canvas.create_window(0,0,window=self.frame1,anchor='nw')
# Various Widget
# Block Part
# Label
self.labelVariableIP = StringVar() # Label variable
labelIP=Label(self.frame1,textvariable=self.labelVariableIP,
anchor="w",
fg="Black")
labelIP.grid(column=0,row=0,columnspan=1,sticky='EW')
self.labelVariableIP.set(u"IP: ")
# Entry: Single line of text!!!!
self.entryVariableIP =StringVar() # variable for entry field
self.entryIP =Entry(self.frame1,
textvariable=self.entryVariableIP,bg="White")
self.entryIP.grid(column = 1, row= 0, sticky='EW')
self.entryVariableIP.set(u"IP")
# Update Button or Enter
button1=Button(self.frame1, text=u"Update",
command=self.OnButtonClickIP)
button1.grid(column=2, row=0)
self.entryIP.bind("<Return>", self.OnPressEnterIP)
#...
# Other widget here
#
# Some Text
# Label
self.labelVariableText = StringVar() # Label variable
labelText=Label(self.frame1,textvariable=
self.labelVariableText,
anchor="nw",
fg="Black")
labelText.grid(column=0,row=curr_row,columnspan=1,sticky='EW')
self.labelVariableTexta.set(u"Insert some texts: ")
# Text
textState = TRUE
self.TextVar=StringVar()
self.mytext=Text(self.frame1,state=textState,
height = text_height, width = 10,
fg="black",bg="white")
#
self.mytext.grid(column=1, row=curr_row+4, columnspan=2, sticky='EW')
self.mytext.insert('1.0',"Insert your text")
#
# other text widget here
#
self.update()
self.geometry(self.geometry() )
self.frame1.update_idletasks()
self.canvas.config(scrollregion=(0,0,
self.frame1.winfo_width(),
self.frame1.winfo_height()))
#
def release_block(argv):
# Create Form
form = simpleform_ap(None)
form.title('Release Information')
#
form.mainloop()
#
if __name__ == "__main__":
release_block(sys.argv)
As I mentioned before, this scripts quite does the work, even if, it has a couple of small issue that are not "fundamental" but a little annoying.
When I launch it I got this (sorry for the bad screen-capture):
enter image description here
As it can be seen, it only shows up the first "column" of the grid, while I would like to have all them (in my case they should be 4)
To see all of the fields, I have to resize manually (with the mouse) the window.
What I would like to have is something like this (all 4 columns are there):
enter image description here
Moreover, the scrollbar does not extend all over the form, but it is just on the low, right corner of the windows.
While the latter issue (scrollbar) I can leave with it, the first one is a little more important, since I would like to have the final user to have a "picture" of what they should do without needing to resize the windows.
Does any have any idea on how I should proceed with this?
What am I missing?
Thanks in advance for your help
In the __init__ method of your class, you do not appear to have set the size of your main window. You should do that, or it will just set the window to a default size, which will only show whatever it can, and in your case, only 1 column. Therefore, in the __init__ method, try putting self.geometry(str(your_width) + "x" + str(your_height)) where your_width and your_height are whatever integers you choose that allow you to see what you need to in the window.
As for your scrollbar issue, all I had to do was change the way your scrollbar was added to the canvas to a .pack() and added the attributes fill = 'y' and side = RIGHT to it, like so:
self.yscrollbar.pack(side = 'right', fill = 'y')
Also, you don't need:
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGHT,expand=FALSE)
Just add the command option to the creation of the scrollbar, like so:
self.scrollbar = Scrollbar(self,orient=VERTICAL,command=self.canvas.yview)
In all, the following changes should make your code work as expected:
Add:
def __init__(self,parent):
Tk.__init__(self,parent)
self.parent = parent
self.initialize()
# Resize the window from the default size to make your widgets fit. Experiment to see what is best for you.
your_width = # An integer of your choosing
your_height = # An integer of your choosing
self.geometry(str(your_width) + "x" + str(your_height))
Add and Edit:
# Add `command=self.canvas.yview`
self.yscrollbar = Scrollbar(self,orient=VERTICAL,command=self.canvas.yview)
# Use `.pack` instead of `.grid`
self.yscrollbar.pack(side = 'right', fill = 'y')
Remove:
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGHT,expand=FALSE)
When I run the following code the created labels appear over top of the Entry boxes as if they are not being added to the same grid.
class Application(Frame):
def __init__(self,master):
super(Application,self).__init__(master)
self.grid()
self.new_intervals()
def new_intervals(self):
self.int_label = Label(text="Interval Name")
self.int_label.grid(row=0, column=0,sticky=W)
self.int_time_label = Label(text="Time (HH:MM:SS)")
self.int_time_label.grid(row=0, column=1,sticky=W)
self.box1 = Entry(self)
self.box1.grid(row=1,column=0,sticky=W)
self.box2 = Entry(self)
self.box2.grid(row=1,column=1,sticky=W)
self.box3 = Entry(self)
self.box3.grid(row=2,column=0,sticky=W)
self.box4 = Entry(self)
self.box4.grid(row=2,column=1,sticky=W)
root = Tk()
root.title("Interval Timer")
root.geometry("400x500")
app=Application(root)
root.mainloop()
I know that i can add these boxes in a loop, however, I can't get it to work without the loop at the moment
The application frame is in row 0, column 0 of the main window. That is the default when you don't specify anything. Also as a default, they appear in the middle
This frame has four entry widgets spread across two rows, making the frame grow to fit around those entry widgets
The "Interval Name" label is also being placed in row 0, column 0 of the main window, because that's what you explicitly tell it to do, and because its parent is the main window.
The "Time" label is also in row 0 of the main window because, again, it's parent is the main window
both of these labels are appearing in the vertical center of the row because that is the default behavior which you haven't overridden, which is why they appear on top of the entry widgets.
So, because the labels and the application frame are in the same row of the main window, and because the labels default to being in the vertical center, they appear to be in the middle of the entry widgets.
I assume you intended for the labels to be children of the frame, so you need to specify "self" as the first parameter when creating them:
self.int_label = Label(self, text="Interval Name")
...
self.int_time_label = Label(self, text="Time (HH:MM:SS)")
I also recommend grouping all of your grid statements for a particular master window together, so it's easier to see the organization of your widgets. In my experience this makes the code easier to read and easier to maintain.
For example:
self.int_label = Label(...)
self.int_time_label = Label(...)
self.box1 = Entry(...)
...
self.int_label.grid(...)
self.int_time_label.grid(...)
self.box1.grid(...)
...