Python3 Tkinter - problem with expanding one frame to fit root window - python-3.x

Hi there (this is my first question)
I am building an app with Tkinter as the GUI. I want multiple frames to expand to fill out the entire root window.
With the code below, I expected the bottom (green) frame to expand all the way up to the top (cyan) frame. Instead, it stays at the bottom, and there is a "frame-less" white area between the two frames.
screenshot of an actual result when code is run
This is the code, I am executing (methods that do not mess with frame layout has been shortened out):
class CreateWindow:
def __init__(self, master, screen):
self.master = master
self.master.geometry('300x400')
self.master.title("THE PROGRAM")
self.screen = screen
self.menu_bar = Menu(self.master)
self.setup_menu = Menu(self.menu_bar)
self.setup_bar()
self.main_menu = Menu(self.menu_bar)
self.main_bar()
self.diary_menu = Menu(self.menu_bar)
self.diary_bar()
self.master.config(menu=self.menu_bar)
# self.master.grid_columnconfigure(0, weight=1) # What is difference between these two and the two below?
# self.master.grid_rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
self.top_menu(self.master) # TODO: Make this menu actively do stuff
if self.screen == "setup":
setup = SetupScreen(self.master)
elif self.screen == "main":
setup = MainScreen(self.master)
elif self.screen == "diary":
setup = DiaryScreen(self.master)
else:
raise TypeError("wrong screen")
def setup_bar(self): ...
def main_bar(self): ...
def diary_bar(self): ...
def top_menu(self, window): # Defines top frame : placeholder for future menu
top = tk.Frame(window, bg='cyan', pady=5)
top.grid(row=0, sticky='new')
button = tk.Button(top, text="Setup", command=self.do_nothing)
button.grid(row=0, column=0)
button = tk.Button(top, text="Main", command=self.do_nothing)
button.grid(row=0, column=1)
button = tk.Button(top, text="Diary", command=self.do_nothing)
button.grid(row=0, column=2)
top.columnconfigure(0, weight=1)
top.columnconfigure(1, weight=1)
top.columnconfigure(2, weight=1)
def do_nothing(self): ...
def b_exit(self): ...
"""This class contains methods, that create and manage the setup screen.
I want the green frame to expand all the way up to the cyan (top menu) """
class SetupScreen(CreateWindow):
def __init__(self, master):
self.master = master
self.menu = tk.Frame(self.master, bg='green')
self.menu.grid(row=1, sticky='new')
self.menu.columnconfigure(0, weight=1) # Again, what is difference between 'grid_'or not?
self.menu.grid_rowconfigure(1, weight=1) #I have tried setting index to both 0 and 1, no difference
self.create_buttons()
def create_buttons(self): ...
def personal_details(self): ...
def start_new(self):
pass
if __name__ == "__main__":
files = FileHandler() #Class meant to be handling file operations - currently only sets a boolean to false, that makes the app start with setup screen
ap = files.active_program
print(ap)
root = tk.Tk()
if not files.active_program: #based on the boolean from FileHandler class, this starts the setup screen
top_menu = CreateWindow(root, "setup")
else:
top_menu = CreateWindow(root, "main")
root.mainloop()

It looks like you're trying to create a notebook widget with several tabs.
So I would suggest you use ttk.Notebook instead of re-inventing it yourself.

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()

TkInter - Can't Get Frames to work correctly and resize

TkInter's frames are driving me crazy. My goal is to have an options frame where I can select some options, then press "Archive" and the TkInter window changes to showing the output from the rest of my script.
I cannot get this to size correctly - there appears to be some additional frame taking up space in the window.
import string
from tkinter import *
import tkinter as tk
import threading
def main(argv):
print("In Main")
for arg in argv:
print(arg)
class TextOut(tk.Text):
def write(self, s):
self.insert(tk.CURRENT, s)
self.see(tk.END)
def flush(self):
pass
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, frameClass):
# make new frame - for archive output
self._frame = frameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class Mainframe(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self._frame = OptionsFrame(self)
self._frame.pack(expand=True)
def change(self, newFrameClass):
# make new frame - for archive output
self._frame = newFrameClass(self)
self._frame.pack(fill="both", expand=True)
return self._frame
class OptionsFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test")
master.geometry("325x180")
self.selectedProject = None
self.initUI(master)
def initUI(self, master):
frame1 = Frame(master)
frame1.pack(fill=BOTH, expand=True)
self.label1 = Label(frame1, text="Select Project to Archive, then click Archive")
self.projectListbox = tk.Listbox(frame1, width=60, height=100)
self.projectListbox.bind("<<ProjectSelected>>", self.changeProject)
# create a vertical scrollbar for the listbox to the right of the listbox
self.yscroll = tk.Scrollbar(self.projectListbox,command=self.projectListbox.yview,orient=tk.VERTICAL)
self.projectListbox.configure(yscrollcommand=self.yscroll.set)
# Archive button
self.archiveBtn=tk.Button(frame1,text="Archive",command=self.ArchiveButtonClick)
# Do layout
self.label1.pack()
self.projectListbox.pack(fill="both", expand=True)
self.yscroll.pack(side="right", fill="y")
self.archiveBtn.pack(side="bottom", pady=10, expand=False)
choices = ["test 1", "test 2", "test 3", "test 4", "test 5", "test 6"]
# load listbox with sorted data
for item in choices:
self.projectListbox.insert(tk.END, item)
def getSelectedProject(self):
# get selected line index
index = self.projectListbox.curselection()[0]
# get the line's text
return self.projectListbox.get(index)
# on change dropdown value
def changeProject(self,*args):
self.selectedProject = self.getSelectedProject()
def ArchiveButtonClick(self):
# Switch to second frame - for running the archive
self.changeProject(None)
# Hide existing controls
self.label1.pack_forget()
self.projectListbox.pack_forget()
self.yscroll.pack_forget()
self.archiveBtn.pack_forget()
newFrame = self.master.change(ArchivingOutputFrame)
newFrame.args = [ "-n", self.selectedProject]
newFrame.start()
# Frame shown while archive task is running
class ArchivingOutputFrame(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
master.title("Test Frame 2")
master.geometry("1000x600")
# Set up for standard output in window
self.var = tk.StringVar(self)
lbl = tk.Label(self, textvariable=self.var)
#lbl.grid(row=0, column=0)
lbl.pack(anchor="nw")
def start(self):
t = threading.Thread(target=self.process)
t.start()
def process(self):
main(self.args)
if __name__=="__main__":
# If command line options passed - skip the UI
if len(sys.argv) > 1:
main(sys.argv[1:])
else:
app=Mainframe()
text = TextOut(app)
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
Here is what I get in the UI; note this is showing the UI hierachy from Microsoft's Spy++ - there is a frame I didn't create (at least I don't think I did) that is at the bottom of the window and taking up half of the UI area; this is the yellow highlight. My options pane is thus squeezed into the top half.
Resize also doesn't work - if I resize the window, I get this:
When I click the button and the code to remove the options frame and put in the frame that is capturing stdout/stderr from the main script runs, I get this:
Now the extra space appears to be at the top!
Thanks for any ideas - I know I could switch to using the "Grid" UI layout engine, but this seems so simple - I'm not doing anything sophisticated here that shouldn't work with pack.
That was a lot of complicated code. It would be easier to debug if you provide a Minimal, Complete, and Verifiable example.
However; the bottom Frame is the TextOut() widget that you pack after Mainframe():
if __name__=="__main__":
app = Mainframe()
text = TextOut(app) # This one
sys.stdout = text
sys.stderr = text
text.pack(expand=True, fill=tk.BOTH)
app.mainloop()
You'll have an easier time debugging if you give each widget a bg colour and then give them all some padding so you can easier identify which widget is inside which widget.

Accessing Children in LabelFrames using Tkinter and Python 3

I am working with tkinter and have set up a bare bones application of my project. My objective is to retrieve a value from tk.Entry() which lies within a tk.LabelFrame() (in this code referenced by the groupbox variable). The button finds the groupbox, and the code passes the compiler, too. I guess my question is: How do I access Widgets and their values in a LabelFrame?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.master.title("Application Title")
# Introduce LabelFrame
self.groupbox = tk.LabelFrame(self, text="Parameters")
self.groupbox.grid(row=0, column=1, padx=5, pady=5)
# Test Label & Entry Widget
label = tk.Label(self.groupbox, text="label=")
label.grid(row=0, column=0, sticky="W")
entry = tk.Entry(self.groupbox)
entry.insert(0, default_value)
entry.grid(row = 0, column=1)
# Compile Button
button = tk.Button(self.groupbox, text="Compile", command=self.compile)
button.grid(row=1, column=1)
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.groupbox.entry.get(1))
if __name__ == '__main__':
figure = Application()
figure.pack()
figure.mainloop()
I am doing this because I want to perform some calculations based on the tk.Entry() values triggered by a button click which is contained in the same LabelFrame() as suggested by the code snippet above (in the original code there are a lot more widgets but that's essentially the gist of my current problem).
Change entry to self.entry.
class Application(tk.Frame):
def __init__(self, master=None):
....
self.entry = tk.Entry(self.groupbox)
self.entry.insert(0, "default_value")
self.entry.grid(row = 0, column=1)
...
# Retrieve first Value (second Widget) from LabelFrame
def compile(self):
print(self.entry.get())

How to open a second ptinker Window separately rather than as a tab

I am trying to open a second window in tkinter but it always appears as a tab rather than separately. Code showing the problem is as follows:
import tkinter as tk
class MainWindow:
def __init__(self, master):
self.master = master
frame = tk.Frame(self.master)
button = tk.Button(frame, text = 'New Window', width = 25, command = self.new_window)
button.pack()
frame.pack()
def new_window(self):
newWindow = tk.Toplevel(self.master)
SecondWindow(newWindow)
class SecondWindow:
def __init__(self, master):
frame = tk.Frame(master)
quitButton = tk.Label(frame, text = 'Second Window')
quitButton.pack()
frame.pack()
def main():
root = tk.Tk()
app = MainWindow(root)
root.mainloop()
if __name__ == '__main__':
main()
When I run this I get the following output:
Clicking the button gives:
(Ignoring the secondary issue of the size) if we expand it we get the following:
I can get a separate window by dragging the tab. How to I code this so that I get the window displaying as a separate window when I click the button ?

Resources