Tkinter - Expanding frame after (interactively) adding elements to it - python-3.x

I have the following code:
from tkinter import *
DEF_CHANNELS = {'iris': (255, 0, 0), 'sclera': (0, 255, 0), 'pupil': (0, 0, 255)}
class GUI(Tk):
def __init__(self, init_source, init_target, *args, **kw):
super().__init__(*args, **kw)
self.frame = Frame(self, height=400, width=500)
self.frame.pack(fill=BOTH, expand=YES)
self.channel_frame = Frame(self.frame, height=200, width=500, pady=16)
self.channel_frame.grid(column=0, row=0, columnspan=2)
self.channel_label = Label(self.channel_frame, text="Channel")
self.channel_label.grid(column=0, row=0)
self.colour_label = Label(self.channel_frame, text="Colour")
self.colour_label.grid(column=1, row=0)
self.channel_frames = []
for channel, colour in DEF_CHANNELS.items():
self.add_channel_frame(channel, colour)
self.channel_button = Button(self.channel_frame, text="+", command=self.add_channel_frame)
self.channel_button.grid(column=0, row=len(self.channel_frames) + 1)
def add_channel_frame(self, def_channel="", def_colour=""):
pair_frame = ChannelColourFrame(self.channel_frame, def_channel=def_channel, def_colour=def_colour, height=100, width=500, pady=2)
pair_frame.grid(column=0, row=len(self.channel_frames) + 1, columnspan=2)
self.channel_frames.append(pair_frame)
class ChannelColourFrame(Frame):
def __init__(self, *args, def_channel="", def_colour="", **kw):
super().__init__(*args, **kw)
self.channel_txt = Entry(self, width=30)
self.channel_txt.insert(END, def_channel)
self.channel_txt.grid(column=0, row=0)
self.colour_txt = Entry(self, width=30)
self.colour_txt.insert(END, def_colour)
self.colour_txt.grid(column=1, row=0)
self.color_picker_button = Button(self, text="\u2712")
self.color_picker_button.grid(column=2, row=0)
self.remove_button = Button(self, text="-", command=self.remove)
self.remove_button.grid(column=3, row=0)
def remove(self):
self.master.master.master.channel_frames.remove(self)
self.destroy()
gui = GUI('', '')
gui.mainloop()
The idea is to have a Frame that starts with 3 default text Entry pairs, which a user can arbitrarily remove/add. For the most part it works fine, but with one big problem. The Frame (self.channel_frame) never expands past its initial height, which causes problems when more than the initial 3 Entry pairs appear on it.
How do I make the entire Frame fit to the Entry pairs every time one is removed/added?
As an additional question, \u2712 appears as a box on my button, but it's supposed to be the black nib symbol (✒). Why isn't the symbol showing up despite being part of unicode?

You aren't creating any new rows, so it's not going to grow. At the start, you create three channel frames, and they are put in rows 0, 1, and 2. You then add a "+" button in row 4.
When you click the "+" button, it adds a new row at len(self.channel_frames) + 1. Since len(self.channel_frames) is 3, it adds the new frame at row 4, which is on top of the "+" button. Thus, you aren't adding a new row.
If you move the "+" button out of the frame, or move it down each time you add a new row, your code works fine.
For example:
def add_channel_frame(self, def_channel="", def_colour=""):
pair_frame = ChannelColourFrame(self.channel_frame, def_channel=def_channel, def_colour=def_colour, height=100, width=500, pady=2)
pair_frame.grid(column=0, row=len(self.channel_frames) + 1, columnspan=2)
self.channel_frames.append(pair_frame)
self.channel_button.grid(column=0, row=len(self.channel_frames)+1)
As an additional question, \u2712 appears as a box on my button, but it's supposed to be the black nib symbol (✒). Why isn't the symbol showing up despite being part of unicode?
Probably because the font you're using doesn't have that symbol. Try using a different font.

Related

Tkinter Canvas scroll slow rendering

The canvas Widget in Tkinter is very slow at drawing, causing a lot of distortion to the applications visuals when scrolling even when using limited widgets.
I have had a search around but only seem to have answers from people drawing multiple things to a canvas rather than the scrollbar effects.
Is there any issues with my code that would cause this issue or are there any methods to fix the draw times to be more visually smooth. In the application this is meant for each row is a different colour which can make it extremely ugly to look at and hard to find the data the user is looking for.
MVCE:
#python 3.8.6
from tkinter import *
import random
class test:
def __init__(self):
self.words = ["troop","relieve","exact","appeal","shortage","familiar","comfortable","sniff","mold","clay","rack","square","color","book","velvet","address","elaborate","grip","neutral","pupil"]
def scrollable_area2(self, holder):
base_frame = Frame(holder, padx=5, pady=5)
base_frame.pack(fill=BOTH, expand=1)
base_frame.rowconfigure(0, weight=0)
base_frame.columnconfigure(0, weight=1)
can = Canvas(base_frame, bg="white")
can.pack(side=LEFT, expand=1, fill=BOTH)
scrollArea = Frame(base_frame, bg="white", )
scrollArea.pack(side=LEFT, expand=1, fill=BOTH)
can.create_window(0, 0, window=scrollArea, anchor='nw')
Scroll = Scrollbar(base_frame, orient=VERTICAL)
Scroll.config(command=can.yview)
Scroll.pack(side=RIGHT, fill=Y)
can.config(yscrollcommand=Scroll.set)
scrollArea.bind("<Configure>", lambda e=Event(), c=can: self.update_scrollregion(e, c))
return scrollArea, can
def update_scrollregion(self, event, can):
if can.winfo_exists():
can.configure(scrollregion=can.bbox("all"))
def generate(self, count): #generates the rows
for i in range(int(count.get())):
row = Frame(self.holder)
row.pack(side=TOP)
for i in range(9):
a = Label(row, text=self.words[random.randint(0, len(self.words)-1)])
a.pack(side=LEFT)
b = Button(row, text=self.words[random.randint(0, len(self.words)-1)])
b.pack(side=LEFT)
def main(self):
opts = Frame(self.root)
opts.pack(side=TOP)
v= StringVar()
e = Entry(opts, textvariable=v)
e.pack(side=LEFT)
b=Button(opts, text="Run", command=lambda e=Event(), v=v:self.generate(v))
b.pack(side=LEFT)
main_frame=Frame(self.root)
main_frame.pack(side=TOP, fill=BOTH, expand=1)
self.holder, can = self.scrollable_area2(main_frame)
def run(self):
self.root = Tk()
self.main()
self.root.mainloop()
if __name__ == "__main__":
app = test()
app.run()
I have left a box where you can type the number of rows. I have tried from 30 rows to over 300 rows and although the initial render time changes the scroll issue is always the same.
NOTE: sorry about the weird way I am creating a scroll region, its from a more complex piece of code which I have modified to fit here if that ends up being a factor.
Since you are just creating a vertical stack of frames, it will likely be more efficient to use a text widget as the container rather than a canvas and embedded frame.
Here's a simple example that creates 1000 rows similar to how you're doing it with the canvas. On my OSX machine it performs much better than the canvas.
def scrollable_area2(self, parent):
base_frame = Frame(parent, padx=5, pady=5)
base_frame.pack(fill="both", expand=True)
holder = Text(base_frame)
vsb = Scrollbar(base_frame, orient="vertical", command=holder.yview)
holder.configure(yscrollcommand=vsb.set)
holder.pack(side="left", fill="both", expand=True)
vsb.pack(side="right", fill="y")
return holder
...
def generate(self, count): #generates the rows
for i in range(int(count.get())):
row = Frame(self.holder)
self.holder.window_create("end", window=row)
self.holder.insert("end", "\n")
...
def main(self):
...
self.holder = self.scrollable_area2(main_frame)
The above example keeps the inner frames, but you don't really need it. You can insert the text directly in the text widget, making the code even more efficient.
In a comment you said you aren't actually creating a stack of frames but rather a table of values. You can create a table in the text widget by using tabstops to create columns. By inserting text directly in the widget you're creating far fewer widgets which will definitely improve performance.
Here's an example using hard-coded the tabstops, but you could easily compute them based on the longest word in the list.
def scrollable_area2(self, parent):
base_frame = Frame(parent, padx=5, pady=5)
base_frame.pack(fill="both", expand=True)
self.holder = Text(base_frame, wrap="none", tabs=100)
vsb = Scrollbar(base_frame, orient="vertical", command=self.holder.yview)
self.holder.configure(yscrollcommand=vsb.set)
self.holder.pack(side="left", fill="both", expand=True)
vsb.pack(side="right", fill="y")
Your generate function then might look something like this:
def generate(self, count): #generates the rows
for i in range(int(count.get())):
for i in range(9):
text = "\t".join([random.choice(self.words) for x in range(9)])
self.holder.insert("end", text + "\t")
button = Button(self.holder, text=random.choice(self.words))
self.holder.window_create("end", window=button)
self.holder.insert("end", "\n")

TKInter - Confused about frames and scrolling

I am trying to code a tkinter application that has three frames - a top frame, where the user inputs some text, a dynamically constructed middle section where some pre-analysis is conducted on the text, and a bottom frame where, once the user has selected which option they want in the middle section, the output will be produced.
The problem is that, depending upon the input, there could be around 10-20 (and in the worst case 30) lines displayed and on a small monitor the output will disappear off the screen.
What I would like is for the top (input) and bottom (output) frames to be visible no matter how the screen is re-sized, and for the middle section to scroll (if required) and still allow the user to select their choice.
I am confused as to how to get the middle section to resize when the screen is resized, show a scrollbar if required, and still allow all of the content to be accessed.
I have created a cut-down version here (for simplicity, I have removed the processing methods and have instead created some fake output in a loop that resembles what the actual middle section would look like).
Please ignore the hideous colour-scheme - I was just trying to understand which frame went where (I will remove the colours as soon as I can!)
Thank you for any suggestions...
import tkinter as tk
from tkinter import scrolledtext
class MyApp(tk.Tk):
def __init__(self, title="Sample App", *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title(title)
self.configure(background="Gray")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Create the overall frame:
master_frame = tk.Frame(self, bg="Light Blue", bd=3, relief=tk.RIDGE)
master_frame.grid(sticky=tk.NSEW)
master_frame.rowconfigure([0, 2], minsize=90) # Set min size for top and bottom
master_frame.rowconfigure(1, weight=1) # Row 1 should adjust to window size
master_frame.columnconfigure(0, weight=1) # Column 0 should adjust to window size
# Create the frame to hold the input field and action button:
input_frame = tk.LabelFrame(master_frame, text="Input Section", bg="Green", bd=2, relief=tk.GROOVE)
input_frame.grid(row=0, column=0, padx = 5, pady = 5, sticky=tk.NSEW)
input_frame.columnconfigure(0, weight=1)
input_frame.rowconfigure(0, weight=1)
# Create a frame for the middle (processing) section.
middle_frame = tk.LabelFrame(master_frame, text = "Processing Section")
middle_frame.grid(row=1, column=0, padx=5, pady=5, sticky=tk.NSEW)
# Create the frame to hold the output:
output_frame = tk.LabelFrame(master_frame, text="Output Section", bg="Blue", bd=2, relief=tk.GROOVE)
output_frame.grid(row=2, column=0, columnspan=3, padx=5, pady=5, sticky=tk.NSEW)
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(0, weight=1)
# Add a canvas in the middle frame.
self.canvas = tk.Canvas(middle_frame, bg="Yellow")
self.canvas.grid(row=0, column=0)
# Create a vertical scrollbar linked to the canvas.
vsbar = tk.Scrollbar(middle_frame, orient=tk.VERTICAL, command=self.canvas.yview)
vsbar.grid(row=0, column=1, sticky=tk.NS)
self.canvas.configure(yscrollcommand=vsbar.set)
# Content for the input frame, (one label, one input box and one button).
tk.Label(input_frame,
text="Please type, or paste, the text to be analysed into this box:").grid(row=0, columnspan = 3, sticky=tk.NSEW)
self.input_box = scrolledtext.ScrolledText(input_frame, height=5, wrap=tk.WORD)
self.input_box.columnconfigure(0, weight=1)
self.input_box.grid(row=1, column=0, columnspan = 3, sticky=tk.NSEW)
tk.Button(input_frame,
text="Do it!",
command=self.draw_choices).grid(row=2, column=2, sticky=tk.E)
# Content for the output frame, (one text box only).
self.output_box = scrolledtext.ScrolledText(output_frame, width=40, height=5, wrap=tk.WORD)
self.output_box.grid(row=0, column=0, columnspan=3, sticky=tk.NSEW)
def draw_choices(self):
""" This method will dynamically create the content for the middle frame"""
self.option = tk.IntVar() # Variable used to hold user's choice
self.get_input_text()
for i in range(30):
tk.Radiobutton(self.canvas,
text=f"Option {i + 1}: ", variable=self.option,
value=i,
command=self.do_analysis
).grid(row=i, column=0, sticky=tk.W)
tk.Label(self.canvas,
text=f"If you pick Option {i + 1}, the output will look like this: {self.shortText}.",
anchor=tk.W
).grid(row=i, column=1, sticky=tk.W)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def get_input_text(self):
""" Will get the text from the input box and also create a shortened version to display on one line"""
screenWidth = 78
self.input_text = self.input_box.get(0.0, tk.END)
if len(self.input_text) > screenWidth:
self.shortText = self.input_text[:screenWidth]
else:
self.shortText = self.input_text[:]
self.shortText = self.shortText.replace('\n', ' ') # strip out carriage returns just in case
def do_analysis(self):
"""This will ultimately process and display the results"""
option = self.option.get() # Get option from radio button press
output_txt = f"You picked option {option + 1} and here is the output: \n{self.input_text}"
self.output_box.delete(0.0, tk.END)
self.output_box.insert(0.0, output_txt)
if __name__ == "__main__":
app = MyApp("My Simple Text Analysis Program")
app.mainloop()
I understand that you can't mix grid and pack geometries in the same container, and that a scrollbar must be attached to a canvas, and objects to be placed on that canvas must therefore be in yet another container so, attempting to follow Bryan's example, I created a minimal version of what I want - window with three sections - top, middle and bottom. The Top and bottom sections will contain a simple text field, the middle section will contain dynamic content and must be able to scroll as required.
Imports:
ScrollbarFrame
Extends class tk.Frame to support a scrollable Frame]
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("A simple GUI")
# Top frame
self.top_frame = tk.Frame(self, bg="LIGHT GREEN")
self.top_frame.pack(fill=tk.X)
tk.Label(self.top_frame, bg=self.top_frame.cget('bg'),
text="This is a label on the top frame")\
.grid(row=0, columnspan=3, sticky=tk.NSEW)
# Middle Frame
# Import from https://stackoverflow.com/a/62446457/7414759
# and don't change anything
sbf = ScrollbarFrame(self, bg="LIGHT BLUE")
sbf.pack(fill=tk.X, expand=True)
# self.middle_frame = tk.Frame(self, bg="LIGHT BLUE")
self.middle_frame = sbf.scrolled_frame
# Force scrolling by adding multiple Label
for _ in range(25):
tk.Label(self.middle_frame, bg=self.middle_frame.cget('bg'),
text="This is a label on the dynamic (middle) section")\
.grid()
# Bottom Frame
self.bottom_frame = tk.Frame(self, bg="WHITE")
self.bottom_frame.pack(fill=tk.X)
tk.Label(self.bottom_frame, bg=self.bottom_frame.cget('bg'),
text="This is a label on the bottom section")\
.grid(row=0, columnspan=3, sticky=tk.NSEW)
if __name__ == '__main__':
App().mainloop()

Python / tkinter: horizontal scrollbar not working properly

I have a problem using a horizontal tk.Scrollbar on a tk.Text widget. It is displayed too small for the region it is supposed to scroll - if I move it to the left, it reaches the end of the scrollable region before the small bar touches the right side, as you'd expect. Also, if I let it go it visually jumps back to its original position at the start, the scroll state is not altered though.
What's weird is that I'm using the exact same syntax for the vertical scrollbar and it works flawlessly.
I included how I'm (ab)using the tk.Text-widget, which is tk.Label-widgets embedded into the tk.Text-widget via the window_create method.
Screenshots:
Code:
import tkinter as tk
class app(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.frame = tk.Frame(self)
self.frame.grid(row=0, column=0, sticky='NW')
# widget definitions
cellYScrollbar = tk.Scrollbar(self, orient="vertical")
cellXScrollbar = tk.Scrollbar(self, orient="horizontal")
full_width = 30
self.cell = tk.Text(
self, yscrollcommand=cellYScrollbar.set, xscrollcommand=cellXScrollbar.set,
width=full_width, height=full_width / 2, wrap="none",
cursor="arrow"
)
cellYScrollbar.config(command=self.cell.yview)
cellXScrollbar.config(command=self.cell.xview)
# widget gridding
self.cell.grid(row=0, column=0, sticky='NW', padx=[5, 0], pady=[5, 0])
cellXScrollbar.grid(row=1, column=0, columnspan=2, sticky='NEW', padx=[5, 0], pady=[0, 5])
cellYScrollbar.grid(row=0, column=1, sticky='NSW', padx=[0, 5], pady=[5, 0])
tiles = []
for i in range(0, 9):
for j in range(0, 9):
test = tk.Label(self.cell, text='TEST'+str(i)+str(j), width=7, height=3)
tiles.append(test)
self.cell.window_create("end", window=test, padx=1, pady=1)
self.cell.insert("end", "\n")
for tile in tiles:
tile.bind("<MouseWheel>", lambda event: self.cell.yview_scroll(int(-1 * (event.delta / 120)), "units"))
self.cell.config(state='disabled')
root = app()
root.mainloop()
EDIT: fixed the code, .set was missing in xscrollcommand=cellXScrollbar.set, thank you to acw1668!

Positioning different sets of entries next to each other in python tkinter

I use a Tkinter frame in python, and on one of the pages I need to display 2 sets of entries, one next to the other, where the number of entries is equal to the number in range (in the full program is a changing variable, so each time the number of entries changes). I use the for loop to do it.
However, when I try to pack the entries into the frame, the 2 sets of 3 entries are shown in one single column of 6 entries, instead of showing 2 columns with 3 rows of entries each.
If I adjust the packs to the left and right sides of the frame, each set of entries then is shown in 1 row, and has 3 columns, which is not needed.
When I use .place or .grid instead of .pack, then for each set only one single entry is shown (I guess all 3 entries are just placed in a single defined location ex. (x = 550, y = 80), so that 3 entries "overlap" into one)
I guess I need to write a more sophisticated "for loop" function and use .grid or .place positioning, so that all 3 entries will be displayed in a column one after the other.
Or I'm also thinking that using .pack and inserting the entries into a new frame inside the first frame, and then position these 2 frames one next to another might work. But again, I tried to create an extra frame inside the first page, and it didn't work.
Any observations and tips would be highly appreciated!
Here is the full code, so you might try playing around with it and see the whole picture. (sorry for a mess in imports, I also have to sort it out)
P.S. It is a part of a bigger code, where I need to use more then 1 page, so this code is the smallest that works - if I there would be only a single frame in the program, I would have no problem arranging the entries as I need. The problem is that I don't know how to arrange the entries is this particular structure.
import tkinter as tk
from tkinter import font as tkfont
import traceback
from tkinter import messagebox
from pandastable.core import Table
from pandastable.data import TableModel
from tkinter import *
from tkinter import ttk
import tkinter as Tkinter
import tkinter.ttk as ttk
LARGE_FONT= ("Verdana", 12)
class MyTable(Table):
"""
Custom table class inherits from Table.
You can then override required methods
"""
def __init__(self, parent=None, **kwargs):
Table.__init__(self, parent, **kwargs)
return
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
# the container is where we'll stack a bunch of frames
# on top of each other, then the one we want visible
# will be raised above the others
self.geometry('800x600+200+100')
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageTwo):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button2 = tk.Button(self, text="Go to Page Two",
command=lambda: controller.show_frame("PageTwo"))
button2.place(x = 20, y = 50)
entries = [Entry(self, font =('Calibri', 7 )) for _ in range(3)]
for entry in entries:
#entry.place(x = 400, y = 80)
entry.pack()
entries_date = [Entry(self, font =('Calibri', 7 )) for _ in range(3)]
for entry in entries_date:
#entry.place(x = 550, y = 80)
entry.pack()
class PageTwo(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 2", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=lambda: controller.show_frame("StartPage"))
button.pack()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
If you're trying to create rows and columns, there are two generally accepted ways to do it:
make each row a frame, and pack the entries along a side
use grid to create rows and columns.
The first is best if your columns don't need to line up, the second is best if your columns do need to line up. Though, you also have to take into consideration what else you're putting in the frame -- you can't use both grid and pack on widgets that have the same master.
Note: If you're creating identical widgets in each row, then as a side effect the columns will line up even when you use pack.
Example using pack:
entries = []
entry_frame = Frame(self)
entry_frame.pack(side="top", fill="x")
for column in range(3):
entry = Entry(entry_frame, font=('Calibri', 7))
entry.pack(side="left", fill="both", expand=True)
entries.append(entry)
entries_date = []
entry_date_frame = Frame(self)
entry_date_frame.pack(side="top", fill="x")
for column in range(3):
entry = Entry(entry_date_frame, font=('Calibri', 7))
entry.pack(side="left", fill="both", expand=True)
entries_date.append(entry)
Example using grid:
entries = []
for column in range(3):
entry = Entry(self, font=('Calibri', 7))
entry.grid(row=0, column=column, sticky="ew")
entries.append(entry)
entries_date = []
for column in range(3):
entry = Entry(self, font=('Calibri', 7))
entry.grid(row=1, column=column, sticky="ew")
entries_date.append(entry)

Tkinter How To Make Button In Middle of Window

I want to make two buttons that are in the middle of the page - meaning one on top of the other with the same x-coordinates but the y-coordinates would have to be just outside the centre so that it would look proportionally correct
Code: [ This is what I have got so far ]
from tkinter import *
class Window:
def __init__(self, master):
self.signup_button = Button(master, text="Sign Up", width=6,height=2)
self.login_button = Button(master, text="Login", width=6,height=2)
self.signup_button.grid()
self.login_button.grid()
root=Tk()
run=Window(root)
root.mainloop()
Also, How do they position everything in such an organised manner:
not sure what exactly do you expect as result, but you can try to adjust padx and pady values:
self.signup_button.grid(row = 0, column = 0, padx=5, pady=5)
self.login_button.grid(row = 0, column = 1, padx=5, pady=5)

Resources