Scrollable Frame does not resize properly using tkinter - python

Based on the example from Dynamically changing scrollregion of a canvas in Tkinter, I am trying to implement a Frame where you can add and delete entries in a scrollable Frame using tkinter. My Problem is that the Frame holding items does not resize after deleting entries. When adding entries, it resizes correctly. I call update_layout() in both cases:
from tkinter import *
class ScrollableContainer(Frame):
"""A scrollable container that can contain a number of messages"""
def __init__(self, master, **kwargs):
Frame.__init__(self, master, **kwargs) #holds canvas & scrollbars
# configure row and col to take additional space if there is some
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# create canvas
self.canv = Canvas(self, bd=0, highlightthickness=0)
# create scrollbars
self.hScroll = Scrollbar(self, orient='horizontal',
command=self.canv.xview)
self.hScroll.grid(row=1, column=0, sticky='we')
self.vScroll = Scrollbar(self, orient='vertical',
command=self.canv.yview)
self.vScroll.grid(row=0, column=1, sticky='ns')
# set postiotion of canvas in (self-)Frame
self.canv.grid(row=0, column=0, sticky='nsew')
self.canv.configure(xscrollcommand=self.hScroll.set,
yscrollcommand=self.vScroll.set)
# create frame to hold messages in canvas
self.frm = Frame(self.canv, bd=2, bg='gray') #holds messages
self.frm.grid_columnconfigure(0, weight=1)
# create empty tkinter widget (self.frm) on the canvas
self.canv.create_window(0, 0, window=self.frm, anchor='nw', tags='inner')
# update layout
self.update_layout()
# on change of size or location this event is fired. The event provides new width an height to callback function on_configure
self.canv.bind('<Configure>', self.on_configure)
self.widget_list = []
# update and resize layout
def update_layout(self):
print('update')
self.frm.update_idletasks()
self.canv.configure(scrollregion=self.canv.bbox('all'))
self.size = self.frm.grid_size()
# resize canvas and scroll region depending on content
def on_configure(self, event):
print('on_configure')
# get new size of canvas
w,h = event.width, event.height
# get size of frm required to display all content
natural = self.frm.winfo_reqwidth()
self.canv.itemconfigure('inner', width= w if w>natural else natural)
self.canv.configure(scrollregion=self.canv.bbox('all'))
# add new entry and update layout
def add_message(self, text):
print('add message')
# create var to represent states
int_var = IntVar()
cb = Checkbutton(self.frm, text=text, variable=int_var)
cb.grid(row=self.size[1], column=0, padx=1, pady=1, sticky='we')
self.widget_list.append(cb)
self.update_layout()
# delete all messages
def del_message(self):
print('del message')
for it in self.widget_list:
it.destroy()
self.update_layout()
root = Tk()
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
sc = ScrollableContainer(root, bd=2, bg='black')
sc.grid(row=0, column=0, sticky='nsew')
def new_message():
test = 'Something Profane'
sc.add_message(test)
def del_message():
sc.del_message()
b = Button(root, text='New Message', command=new_message)
b.grid(row=1, column=0, sticky='we')
del_b = Button(root, text='Del Message', command=del_message)
del_b.grid(row=2, column=0, sticky='we')
root.mainloop()

I was working on something similar so I took my code and merged it with yours for the answer.
Here is a scrollingFrame class that will add scrollbars and remove them whenever the box is resized. Then there is a second class for your message list that will tell the scrollingFrame to readjust itself as necessary whenever items are added/deleted.
class scrollingFrame(Frame):
def __init__(self, parentObject, background):
Frame.__init__(self, parentObject, background = background)
self.canvas = Canvas(self, borderwidth=0, background = background, highlightthickness=0)
self.frame = Frame(self.canvas, background = background)
self.vsb = Scrollbar(self, orient="vertical", command=self.canvas.yview, background=background)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.grid(row=0, column=1, sticky=N+S)
self.hsb = Scrollbar(self, orient="horizontal", command=self.canvas.xview, background=background)
self.canvas.configure(xscrollcommand=self.hsb.set)
self.hsb.grid(row=1, column=0, sticky=E+W)
self.canvas.grid(row=0, column=0, sticky=N+S+E+W)
self.window = self.canvas.create_window(0,0, window=self.frame, anchor="nw", tags="self.frame")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.frame.bind("<Configure>", self.onFrameConfigure)
self.canvas.bind("<Configure>", self.onCanvasConfigure)
def onFrameConfigure(self, event):
#Reset the scroll region to encompass the inner frame
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def onCanvasConfigure(self, event):
#Resize the inner frame to match the canvas
minWidth = self.frame.winfo_reqwidth()
minHeight = self.frame.winfo_reqheight()
if self.winfo_width() >= minWidth:
newWidth = self.winfo_width()
#Hide the scrollbar when not needed
self.hsb.grid_remove()
else:
newWidth = minWidth
#Show the scrollbar when needed
self.hsb.grid()
if self.winfo_height() >= minHeight:
newHeight = self.winfo_height()
#Hide the scrollbar when not needed
self.vsb.grid_remove()
else:
newHeight = minHeight
#Show the scrollbar when needed
self.vsb.grid()
self.canvas.itemconfig(self.window, width=newWidth, height=newHeight)
class messageList(object):
def __init__(self, scrollFrame, innerFrame):
self.widget_list = []
self.innerFrame = innerFrame
self.scrollFrame = scrollFrame
# Keep a dummy empty row if the list is empty
self.placeholder = Label(self.innerFrame, text=" ")
self.placeholder.grid(row=0, column=0)
# add new entry and update layout
def add_message(self, text):
print('add message')
self.placeholder.grid_remove()
# create var to represent states
int_var = IntVar()
cb = Checkbutton(self.innerFrame, text=text, variable=int_var)
cb.grid(row=self.innerFrame.grid_size()[1], column=0, padx=1, pady=1, sticky='we')
self.widget_list.append(cb)
self.innerFrame.update_idletasks()
self.scrollFrame.onCanvasConfigure(None)
# delete all messages
def del_message(self):
print('del message')
for it in self.widget_list:
it.destroy()
self.placeholder.grid()
self.innerFrame.update_idletasks()
self.scrollFrame.onCanvasConfigure(None)
deviceBkgColor = "#FFFFFF"
root = Tk() # Makes the window
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
root.wm_title("Title") # Makes the title that will appear in the top left
root.config(background = deviceBkgColor)
myFrame = scrollingFrame(root, background = deviceBkgColor)
myFrame.grid(row=0, column=0, sticky=N+S+E+W)
msgList = messageList(myFrame, myFrame.frame)
def new_message():
test = 'Something Profane'
msgList.add_message(test)
def del_message():
msgList.del_message()
b = Button(root, text='New Message', command=new_message)
b.grid(row=1, column=0, sticky='we')
del_b = Button(root, text='Del Message', command=del_message)
del_b.grid(row=2, column=0, sticky='we')
root.mainloop() #start monitoring and updating the GUI

Related

python tkinter scrollable frame scroll with mousewheel

I have created a scrollable frame with tkinter and would like to use the mousewheel for the scrolling, I am also switching frames as pages. Everything works as expected for page 2 however the page does not scroll with the mousewheel on page 1, the scrollbar itself works and the mousewheel event is being triggered its just not scrolling.
Here is an example of what I have so far:-
import tkinter as tk
class ScrollableFrame(tk.Frame):
def __init__(self, container, *args, **kwargs):
super().__init__(container, *args, **kwargs)
self.canvas = tk.Canvas(self)
scrollbar = tk.Scrollbar(self, command=self.canvas.yview)
self.scrollable_frame = tk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")),
)
self.scrollable_frame.bind_all("<MouseWheel>", self._on_mousewheel)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=scrollbar.set)
self.canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _on_mousewheel(self, event):
caller = event.widget
if "scrollableframe" in str(caller):
if event.delta == -120:
self.canvas.yview_scroll(2, "units")
if event.delta == 120:
self.canvas.yview_scroll(-2, "units")
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("tkinter scrollable frame example.py")
self.geometry("700x450")
# set grid layout 1x2
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# create navigation frame
self.navigation_frame = tk.Frame(self)
self.navigation_frame.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
# create navigation buttons
self.page1_button = tk.Button(
self.navigation_frame,
text="Page 1",
command=self.page1_button_event,
)
self.page1_button.grid(row=0, column=0, sticky="ew")
self.page2_button = tk.Button(
self.navigation_frame,
text="Page 2",
command=self.page2_button_event,
)
self.page2_button.grid(row=1, column=0, sticky="ew")
# create page1 frame
self.page1_frame = ScrollableFrame(self)
self.page1_frame.grid_columnconfigure(0, weight=1)
# create page1 content
self.page1_frame_label = tk.Label(
self.page1_frame.scrollable_frame, text="Page 1"
)
self.page1_frame_label.grid(row=0, column=0, padx=20, pady=10)
for i in range(50):
tk.Label(
self.page1_frame.scrollable_frame, text=str("Filler number " + str(i))
).grid(row=i + 1, column=0)
# create page2 frame
self.page2_frame = ScrollableFrame(self)
self.page2_frame.grid_columnconfigure(1, weight=1)
# create page2 content
self.page2_frame_label = tk.Label(
self.page2_frame.scrollable_frame, text="Page 2"
)
self.page2_frame_label.grid(row=0, column=0, padx=20, pady=10)
for i in range(50):
tk.Label(
self.page2_frame.scrollable_frame, text=str("Filler number " + str(i))
).grid(row=i + 1, column=0)
# show default frame
self.show_frame("page1")
# show selected frame
def show_frame(self, name):
self.page1_button.configure(
background=("red") if name == "page1" else "#F0F0F0"
)
self.page2_button.configure(
background=("red") if name == "page2" else "#F0F0F0"
)
if name == "page1":
self.page1_frame.grid(row=0, column=1, padx=0, pady=0, sticky="nsew")
else:
self.page1_frame.grid_forget()
if name == "page2":
self.page2_frame.grid(row=0, column=1, sticky="nsew")
else:
self.page2_frame.grid_forget()
def page1_button_event(self):
self.show_frame("page1")
def page2_button_event(self):
self.show_frame("page2")
if __name__ == "__main__":
app = App()
app.mainloop()
Any ideas where I have gone wrong?
The issue is caused by using .bind_all() which will make the final callback to be self.page2_frame._on_mousewheel(). Therefore self.canvas inside the function will refer to the canvas of self.page2_frame, that is why canvas in self.page1_frame cannot be scrolled by mouse wheel.
One of the solution is to find the correct canvas using the event.widget (i.e. the caller in your code):
def find_canvas(self, widget):
while not isinstance(widget, tk.Canvas):
widget = widget.master
return widget
def _on_mousewheel(self, event):
caller = event.widget
if "scrollableframe" in str(caller) and "canvas" in str(caller):
self.find_canvas(caller).yview_scroll(event.delta//-60, "units")

How to create a TKinter scrollable frame with constant new labels being added

I'm trying to create a GUI for a sort of chat client. I'm fetching the chat with async and I am using multiprocessing to pipe the data to TKinter. That all works fine, but the problem I am currently having is when the new messages come in, they get added to the canvas and I can't scroll and the canvas is horizontally small so I would only be able to see the first two letters of the message anyways. I am like completely new to Python GUI so this is probably a really simple solution.
Here's the code that I have right now:
import tkinter as tk
class App(tk.Tk):
def __init__(self, pipe, *args, **kw):
super().__init__(*args, **kw)
self.app_pipe, _ = pipe
self.check_interval = 250
global message_queue
message_queue = []
self.configure(bg='gray')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
frame_main = tk.Frame(self, bg='gray')
frame_main.grid(sticky='news')
frame_main.columnconfigure(0, weight=1)
label1 = tk.Label(frame_main, text='Channel')
label1.grid(row=0, column=0, sticky='nw')
frame_canvas = tk.Frame(frame_main, bg='red')
frame_canvas.grid(row=1, column=0, sticky='nw')
canvas = tk.Canvas(frame_canvas, bg='yellow')
canvas.grid(row=0, column=0)
vsb = tk.Scrollbar(canvas, orient='vertical', command=canvas.yview)
vsb.grid(row=0, column=1, sticky='ns')
canvas.configure(yscrollcommand=vsb.set)
global names_frame
names_frame = tk.Frame(canvas)
canvas.create_window((0, 0), window=names_frame, anchor='nw')
frame_canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))
self.bind('<<newMessage>>', self.add_message)
self.after(self.check_interval, self.check)
def check(self):
while self.app_pipe.poll():
msg = self.app_pipe.recv()
if len(message_queue) < 200:
message_queue.append(msg)
elif len(message_queue) == 199:
message_queue.pop(0)
message_queue.append(msg)
self.event_generate('<<newMessage>>', when='tail')
self.after(self.check_interval, self.check)
def add_message(self, *args):
if message_queue[-1]['command'] == 'message':
message = message_queue[-1]['data']
print(message.author.name, len(message_queue))
name = tk.Label(names_frame, text=message.author.name)
name.grid(row=len(message_queue)-1, column=0)
This is what the GUI ends up looking like:
There are two issues:
You have created the scrollbar vsb as a child of canvas. It should be a child of frame_canvas instead
You need to update the scrollregion of canvas whenever names_frame is resized, not frame_canvas
class App(tk.Tk):
def __init__(self, pipe, *args, **kw):
super().__init__(*args, **kw)
self.app_pipe, _ = pipe
self.check_interval = 250
global message_queue
message_queue = []
self.configure(bg='gray')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
frame_main = tk.Frame(self, bg='gray')
frame_main.grid(sticky='news')
frame_main.columnconfigure(0, weight=1)
label1 = tk.Label(frame_main, text='Channel')
label1.grid(row=0, column=0, sticky='nw')
frame_canvas = tk.Frame(frame_main, bg='red')
frame_canvas.grid(row=1, column=0, sticky='nw')
canvas = tk.Canvas(frame_canvas, bg='yellow')
canvas.grid(row=0, column=0)
# should create 'vsb' as child of 'frame_canvas' instead
vsb = tk.Scrollbar(frame_canvas, orient='vertical', command=canvas.yview)
vsb.grid(row=0, column=1, sticky='ns')
canvas.configure(yscrollcommand=vsb.set)
global names_frame
names_frame = tk.Frame(canvas)
canvas.create_window((0, 0), window=names_frame, anchor='nw')
# should bind '<Configure>' on 'names_frame' instead
names_frame.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))
self.bind('<<newMessage>>', self.add_message)
self.after(self.check_interval, self.check)
...
Note that it is better to make message_queue and names_frame instance variables instead of global variables.

How to configure row/column sizes in grid managed frames in a python tkinter class project

I am creating a GUI in python using Tkinter and classes and having trouble resizing specific rows in the grid packager.
The code I have attached works but I would like to control the row widths so that the top 2 frames are static in size and the bottom resizes with the window. I have tried many things whilst referencing different solutions but nothing works.
# !/usr/bin/python3
# Use Tkinter for python 2, tkinter for python 3
import tkinter as tk
# Main Window
class WinMain(tk.Frame):
def __init__(self, master):
# parameters that you want to send through the Frame class.
tk.Frame.__init__(self, master)
# reference to the master widget, which is the tk window
self.master = master
# with that, we want to then run init_window, which doesn't yet exist
self.init_window()
def init_window(self):
# changing the title of our master widget
self.master.title("GUI Tester")
# Create menu for window
self.createMenu()
# Create Gui for window
self.createGui()
# Update Gui every second after 1 second
self.after(1000, self.upDateGui)
def createMenu(self):
# Initialise drop-down menu
menu = tk.Menu(self.master)
self.master.config(menu=menu)
# Add drop-down menu for Options
options = tk.Menu(menu, tearoff=False)
menu.add_cascade(label="Options", menu=options)
options.add_command(label="Open...", command=self.menuOptionsOpen)
options.add_separator()
options.add_command(label="Close", command=self.menuOptionsClose)
# Add drop-down menu for Help
help = tk.Menu(menu, tearoff=False)
menu.add_cascade(label="Help", menu=help)
help.add_command(label="About...", command=self.menuHelpAbout)
def createGui(self):
# Define GUI using Grid to place widgets
# Size window to its minimum set to 2/3 of the screen resolution
top = self.winfo_toplevel()
screen_width = top.winfo_screenwidth()
screen_height = top.winfo_screenheight()
screen_resolution = str(int(screen_width * 2 / 3)) + 'x' + str(int(screen_height * 2 / 3))
top.geometry(screen_resolution)
top.minsize(int(screen_width * 1 / 3), int(screen_height * 1 / 3))
# ------------------------------------------------
# create all frames
# ------------------------------------------------
self.c = self.master
self.c.frameTop = tk.LabelFrame(self.c, text="Top", width=5, height=5, padx=5, pady=5)
self.c.frameMiddle = tk.LabelFrame(self.c, text="Middle", width=5, height=5, padx=5, pady=5)
self.c.frameBottom = tk.LabelFrame(self.c, text="Bottom", width=5, height=5, padx=5, pady=5)
self.c.frameRight = tk.Frame(self.c, borderwidth=5, relief=tk.GROOVE)
# ------------------------------------------------
# Create widgets for frameTop
# ------------------------------------------------
# Text Box
self.c.frameTop.textBox = tk.Text(self.c.frameTop, borderwidth=3, relief=tk.SUNKEN)
self.c.frameTop.textBox.config(font=("consolas", 12), undo=True, wrap='none')
# Text Box Scroll Bars
self.c.frameTop.textBoxYScroll = tk.Scrollbar(self.c.frameTop, orient=tk.VERTICAL,
command=self.c.frameTop.textBox.yview)
self.c.frameTop.textBox['yscrollcommand'] = self.c.frameTop.textBoxYScroll.set
self.c.frameTop.textBoxXScroll = tk.Scrollbar(self.c.frameTop, orient=tk.HORIZONTAL,
command=self.c.frameTop.textBox.xview)
self.c.frameTop.textBox['xscrollcommand'] = self.c.frameTop.textBoxXScroll.set
# ------------------------------------------------
# Create widgets for frameMiddle
# ------------------------------------------------
# Text Box
self.c.frameMiddle.textBox = tk.Text(self.c.frameMiddle, borderwidth=3, relief=tk.SUNKEN)
self.c.frameMiddle.textBox.config(font=("consolas", 12), undo=True, wrap='none')
# Text Box Scroll Bars
self.c.frameMiddle.textBoxYScroll = tk.Scrollbar(self.c.frameMiddle, orient=tk.VERTICAL,
command=self.c.frameMiddle.textBox.yview)
self.c.frameMiddle.textBox['yscrollcommand'] = self.c.frameMiddle.textBoxYScroll.set
self.c.frameMiddle.textBoxXScroll = tk.Scrollbar(self.c.frameMiddle, orient=tk.HORIZONTAL,
command=self.c.frameMiddle.textBox.xview)
self.c.frameMiddle.textBox['xscrollcommand'] = self.c.frameMiddle.textBoxXScroll.set
# ------------------------------------------------
# Create widgets for frameBottom
# ------------------------------------------------
# Text Box
self.c.frameBottom.textBox = tk.Text(self.c.frameBottom, borderwidth=3, relief=tk.SUNKEN)
self.c.frameBottom.textBox.config(font=("consolas", 12), undo=True, wrap='none')
# Text Box Scroll Bars
self.c.frameBottom.textBoxYScroll = tk.Scrollbar(self.c.frameBottom, orient=tk.VERTICAL,
command=self.c.frameBottom.textBox.yview)
self.c.frameBottom.textBox['yscrollcommand'] = self.c.frameBottom.textBoxYScroll.set
self.c.frameBottom.textBoxXScroll = tk.Scrollbar(self.c.frameBottom, orient=tk.HORIZONTAL,
command=self.c.frameBottom.textBox.xview)
self.c.frameBottom.textBox['xscrollcommand'] = self.c.frameBottom.textBoxXScroll.set
# ------------------------------------------------
# Create widgets for frameRight
# ------------------------------------------------
self.c.frameRight.btnStatus = tk.Button(self.c.frameRight, text='Status Window', command=self.launchWinStatus)
self.c.frameRight.btnSpare1 = tk.Button(self.c.frameRight, text='Send Middle', command=self.btnSpare1)
self.c.frameRight.btnSpare2 = tk.Button(self.c.frameRight, text='Spare 2', command=self.btnSpare2)
# ------------------------------------------------
# ------------------------------------------------
# Layout widgets in frameTop
# ------------------------------------------------
self.c.frameTop.grid_columnconfigure(0, weight=1)
self.c.frameTop.grid_columnconfigure(1, weight=0)
self.c.frameTop.grid_rowconfigure(0, weight=1)
self.c.frameTop.grid_rowconfigure(1, weight=0)
self.c.frameTop.textBox.grid(row=0, column=0, sticky="nsew")
self.c.frameTop.textBoxYScroll.grid(row=0, column=1, sticky="nsew")
self.c.frameTop.textBoxXScroll.grid(row=1, column=0, sticky="nsew")
# ------------------------------------------------
# Layout widgets in frameMiddle
# ------------------------------------------------
self.c.frameMiddle.grid_columnconfigure(0, weight=1)
self.c.frameMiddle.grid_columnconfigure(1, weight=0)
self.c.frameMiddle.grid_rowconfigure(0, weight=1)
self.c.frameMiddle.grid_rowconfigure(1, weight=0)
self.c.frameMiddle.textBox.grid(row=0, column=0, sticky="nsew")
self.c.frameMiddle.textBoxYScroll.grid(row=0, column=1, sticky="nsew")
self.c.frameMiddle.textBoxXScroll.grid(row=1, column=0, sticky="nsew")
# ------------------------------------------------
# Layout widgets in frameBottom
# ------------------------------------------------
self.c.frameBottom.grid_columnconfigure(0, weight=1)
self.c.frameBottom.grid_columnconfigure(1, weight=0)
self.c.frameBottom.grid_rowconfigure(0, weight=1)
self.c.frameBottom.grid_rowconfigure(1, weight=0)
self.c.frameBottom.textBox.grid(row=0, column=0, sticky="nsew")
self.c.frameBottom.textBoxYScroll.grid(row=0, column=1, sticky="nsew")
self.c.frameBottom.textBoxXScroll.grid(row=1, column=0, sticky="nsew")
# ------------------------------------------------
# Layout widgets in frameRight
# ------------------------------------------------
self.c.frameRight.grid_columnconfigure(0, weight=0)
self.c.frameRight.grid_rowconfigure(0, weight=0)
self.c.frameRight.btnStatus.grid(row=0, column=0, sticky="nsew")
self.c.frameRight.btnSpare1.grid(row=2, column=0, sticky="nsew")
self.c.frameRight.btnSpare2.grid(row=3, column=0, sticky="nsew")
# ------------------------------------------------
# Layout frames in top container
# ------------------------------------------------
numOfCols = 2
numOfRows = 6
self.c.grid_columnconfigure(0, weight=1, pad=3)
self.c.grid_columnconfigure(1, weight=0, pad=3)
self.c.grid_rowconfigure(0, weight=1, pad=3)
self.c.grid_rowconfigure(1, weight=1, pad=3)
self.c.grid_rowconfigure(2, weight=1, pad=3)
self.c.grid_rowconfigure(3, weight=1, pad=3)
self.c.grid_rowconfigure(4, weight=1, pad=3)
self.c.grid_rowconfigure(5, weight=1, pad=3)
frameTopRowCol = [0, 0]
frameTopSpan = [2, 1]
frameMiddleRowCol = [frameTopRowCol[0] + frameTopSpan[0],
frameTopRowCol[1]]
frameMiddleSpanRC = [2, 1]
frameBottomRowCol = [frameMiddleRowCol[0] + frameMiddleSpanRC[0],
frameMiddleRowCol[1]]
frameBottomSpanRC = [2, 1]
frameRightRowCol = [frameTopRowCol[0],
frameTopRowCol[1] + frameTopSpan[1]]
frameRightSpanRC = [numOfRows, 1]
self.c.frameTop.grid(row=frameTopRowCol[0], column=frameTopRowCol[1],
rowspan=frameTopSpan[0], columnspan=frameTopSpan[1], sticky="nsew")
self.c.frameMiddle.grid(row=frameMiddleRowCol[0], column=frameMiddleRowCol[1],
rowspan=frameMiddleSpanRC[0], columnspan=frameMiddleSpanRC[1], sticky="nsew")
self.c.frameBottom.grid(row=frameBottomRowCol[0], column=frameBottomRowCol[1],
rowspan=frameBottomSpanRC[0], columnspan=frameBottomSpanRC[1], sticky="nsew")
self.c.frameRight.grid(row=frameRightRowCol[0], column=frameRightRowCol[1],
rowspan=frameRightSpanRC[0], columnspan=frameRightSpanRC[1], sticky="nsew")
# ------------------------------------------------
# Layout top container in window
# ------------------------------------------------
self.grid_columnconfigure(0, weight=1, pad=3)
self.grid_rowconfigure(1, weight=1, pad=3)
# ------------------------------------------------
# Layout window
# ------------------------------------------------
top.grid_columnconfigure(0, weight=1, pad=3)
top.grid_rowconfigure(1, weight=1, pad=3)
def menuOptionsOpen(self):
print("menuOptionsOpen")
def menuOptionsClose(self):
print("menuOptionsClose")
def menuHelpAbout(self):
print("menuHelpAbout")
def launchWinStatus(self):
print ("launchWinStatus")
def btnSpare1(self):
print("btnSpare1")
def btnSpare2(self):
print("btnSpare2")
def upDateGui(self):
print("upDateGui")
# Perform update every x milliseconds
self.after(1000, self.upDateGui)
# Main
def main():
root = tk.Tk()
app = WinMain(root)
root.mainloop()
# Launch Main
if __name__ == '__main__':
main()
I have a section at line 174 where I can change rowconfigure weights and this has peculiar effects that I can't explain. It should be able to fix the height of some rows while other rows are allowed to grow/shrink with the window down to a minimum size.
There are a few things:
There is a lot of code not relevant to the problem.
You have constructed a confusing widget hierarchy. The class WinMain()
is instantiated with root as master. While WinMain() inherits Frame(),
you never put any widgets inside self. Then you create the name top = self.winfo_toplevel() which is the running Tk() instance = root = master = self.master = self.c.
Your use of notation is confusing;
self.c.frameTop.textBox = tk.Text(self.c.frameTop, ...
Why save reference to the Text widget in the textBox attribute of frameTop?
Finally you configure grid in self (Frame which does not contain anything)
and root.
I think much of your problems are because you have confused the names of things. I'm providing an example below with only the necessary code to get the correct behaviour. My use of bg colors is to ease identification of the different widgets.
import tkinter as tk
# Main Window
class WinMain(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.master.config(bg='tan')
master.geometry('600x400+800+50')
self.pack(padx=10, pady=10, expand=True, fill='both')
self.columnconfigure(0, weight=1)
self.rowconfigure(2, weight=1) # Expand cols 0 & 2 with resize
# Top
frameTop = tk.LabelFrame(self, text="Top", bg='peru')
frameTop.grid(row=0, column=0, sticky='nsew')
frameTop.columnconfigure(0, weight=1) # Expand frame with resize
frameTop.rowconfigure(0, weight=1)
Top_Box = tk.Text(frameTop, borderwidth=3, relief=tk.SUNKEN, width=20, height=2)
Top_Box.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Middle
frameMiddle = tk.LabelFrame(self, text="Middle", padx=5, pady=5, bg='tomato')
frameMiddle.grid(row=1, column=0, sticky='nsew')
frameMiddle.columnconfigure(0, weight=1) # Expand frame with resize
frameMiddle.rowconfigure(0, weight=1)
Middle_Box = tk.Text(frameMiddle, borderwidth=3, relief=tk.SUNKEN, width=20, height=2)
Middle_Box.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Bottom
frameBottom = tk.LabelFrame(self, text="Bottom", padx=5, pady=5, bg='khaki')
frameBottom.grid(row=2, column=0, sticky='nsew')
frameBottom.columnconfigure(0, weight=1) # Expand frame with resize
frameBottom.rowconfigure(0, weight=1)
Bottom_Box = tk.Text(frameBottom, borderwidth=3, relief=tk.SUNKEN, width=20, height=2)
Bottom_Box.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Right
frameRight = tk.Frame(self, borderwidth=5, relief=tk.GROOVE, bg='thistle')
frameRight.grid(row=0, column=1, rowspan=3, sticky='nsew')
btnStatus = tk.Button(frameRight, text='Status Window',)
btnSpare1 = tk.Button(frameRight, text='Send Middle')
btnSpare2 = tk.Button(frameRight, text='Spare 2')
btnStatus.grid(row=0, column=0, sticky="ew")
btnSpare1.grid(row=1, column=0, sticky="ew")
btnSpare2.grid(row=2, column=0, sticky="ew")
if __name__ == '__main__':
root = tk.Tk()
app = WinMain(root)
root.mainloop()

Scrolling and correct scaling with Tkinter

I need this simple form to:
1) correctly expand the fields when the window size is adjusted and
2) correctly scroll the list of fields.
I've tried every way I can think of and only 1 of the 2 above conditions are ever true. This code expands properly but does not scroll. Without frame2, and adding the fields to frame or canvas the opposite is true.
class test(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.initialize()
def makeform(self, root, fields):
i = 0
for field in fields:
Label(root, text=field + ": ", anchor=W).grid(row=i)
entry = Entry(root)
entry.grid(row=i, column=1, sticky=E+W)
entries[field] = entry
i += 1
def initialize(self):
frame = Frame(self)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
frame.grid(sticky=N+S+E+W)
canvas = Canvas(frame, width=900, height=800, bg='pink')
canvas.grid(row=0, column=0, sticky=N+S+E+W)
canvas.columnconfigure(0,weight=1)
canvas.rowconfigure(0,weight=1)
frame2 = Frame(canvas)
frame2.grid(row=0, column=0, sticky=N+S+E+W)
frame2.columnconfigure(1, weight=1)
vscrollbar = Scrollbar(frame2,orient=VERTICAL)
vscrollbar.grid(row=0, column=2, sticky=N+S)
vscrollbar.config(command=canvas.yview)
canvas.configure(yscrollcommand=vscrollbar.set)
self.grid_rowconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
names = {'a','long','list','of','names','here'}
self.makeform(frame2, names)
Button(self, text='Quit', command=self.quit).grid(row=1, column=0, sticky=W, pady=4)
canvas.create_window(0, 0)
canvas.config(scrollregion=canvas.bbox(ALL))
self.grid()
if __name__ == "__main__":
entries = {}
app = test(None)
app.title('Hi ')
app.mainloop()
Update
Integrating Bryan's example below, this works for scrolling but does not expand the fields when the window is resized. I tried adding weight=1 to the second column of the frame but it does not help. How do I prevent the frame from shrinking?
class test(Tkinter.Frame):
def __init__(self,parent):
Tkinter.Frame.__init__(self, root)
self.canvas = Tkinter.Canvas(root, borderwidth=0, background="#ffffff")
self.frame = Tkinter.Frame(self.canvas, background="#ffffff")
self.vsb = Tkinter.Scrollbar(root, 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((0,0), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def makeform(self, fields):
i = 0
for field in fields:
Label(self.frame, text=field + ": ", anchor=W).grid(row=i)
entry = Entry(self.frame)
entry.grid(row=i, column=1, sticky=E+W)
entries[field] = entry
i += 1
Button(self.frame, text='Quit', command=self.quit).grid(row=i, column=0, sticky=W, pady=4)
def populate(self):
names = {'a','long','list','of','names','here'}
self.makeform(names)
self.frame.columnconfigure(1, weight=1)
self.frame.grid_columnconfigure(1, weight=1)
if __name__ == "__main__":
entries = {}
root=Tk()
test(root).pack(side="top", fill="both", expand=True)
root.mainloop()
There are at least three problems in your code.
First, the frame to be scrolled must be a part of the canvas. You can't use pack or grid to place it in the canvas, you must use create_window. You're calling create_window but you aren't telling it what window to add.
Second, the scrollbars are children of the frame, but I'm assuming those are the scrollbars you want to scroll the canvas. The need to be outside of the inner frame, and outside of the canvas.
Third, you need to set up a binding to the canvas's <Configure> event so that you can resize the inner frame and recompute the scrollregion of the canvas.
A complete working example of a scrollable frame is in this answer: https://stackoverflow.com/a/3092341/7432

Dynamically changing scrollregion of a canvas in Tkinter

So I've got a custom widget that inherits from frame and contains a canvas and a scrollbar, and a custom widget that also inherits from frame that I want to dynamically add to the canvas, resizing the scrollregion as necessary. Here's my code:
class MessageItem(Frame):
"""A message to be contained inside a scrollableContainer"""
def __init__(self, message, **kwds):
Frame.__init__(self, **kwds)
self.text = Label(self, text = message)
self.text.grid(column = 0, row = 0, sticky = N+S+E+W)
class scrollableContainer(Frame):
"""A scrollable container that can contain a number of messages"""
def initContents(self):
"""Initializes a scrollbar, and a canvas that will contain all the items"""
#the canvas that will contain all our items
self.canv = Canvas(self)
self.canv.grid(column = 0, row = 0, sticky = N+S+W)
#force Tkinter to draw the canvas
self.canv.update_idletasks()
#use the values from the canvas being drawn to determine the size of the scroll region
#note that currently, since the canvas contains nothing, the scroll region will be the same as
#the size of the canvas
geometry = self.canv.winfo_geometry()
xsize, ysize, xpos, ypos = parse_geometry_string(geometry)
self.canv['scrollregion'] = (0, 0, xsize, ysize)
#the scrollbar for that canvas
self.vscroll = Scrollbar(self, orient = VERTICAL, command = self.canv.yview )
self.vscroll.grid(column = 1, row = 0, sticky = N+S+E)
self.canv["yscrollcommand"] = self.vscroll.set
def __init__(self, **kwds):
Frame.__init__(self, **kwds)
#initialize the widget's contents
self.grid(sticky = N+S+E+W)
self.pack()
self.initContents()
#initialize the list of contents so we can append to it
self.contents = []
def addMessage(self, message):
#Add the message to the list of contents
self.contents.append(MessageItem(message))
#Add the message to the grid
self.contents[(len(self.contents) - 1)].grid(column = 0, row = (len(self.contents) - 1))
#set the new scrollable region for the canvas
scrollregionlist = self.canv['scrollregion'].split()
oldRegion = int(scrollregionlist[3])
newRegion = oldRegion + parse_geometry_string(self.contents[
(len(self.contents) - 1)].winfo_geometry())[3]
self.canv['scrollregion'] = (int(scrollregionlist[0]), int(scrollregionlist[1]),
int(scrollregionlist[2]), newRegion)
The problem I'm experiencing is that self.canv['scrollregion'] appears to disappear outside of init. In the addMessage method, in the line:
scrollregionlist = self.canv['scrollregion'].split()
The scrollregion property on self.canv returns an empty string, which I can verify by putting a
print self.canv['scrollregion']
immediately before that line
You sure a Text widget wouldn't suffice here?
anyway,
from Tkinter import *
class MessageItem(Frame):
"""A message to be contained inside a scrollableContainer"""
def __init__(self, master, message, **kwds):
Frame.__init__(self, master, **kwds)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.text = Label(self, text=message, anchor='w', bg='gold')
self.text.grid(row=0, column=0, sticky='nsew')
class scrollableContainer(Frame):
"""A scrollable container that can contain a number of messages"""
def __init__(self, master, **kwargs):
Frame.__init__(self, master, **kwargs) #holds canvas & scrollbars
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canv = Canvas(self, bd=0, highlightthickness=0)
self.hScroll = Scrollbar(self, orient='horizontal',
command=self.canv.xview)
self.hScroll.grid(row=1, column=0, sticky='we')
self.vScroll = Scrollbar(self, orient='vertical',
command=self.canv.yview)
self.vScroll.grid(row=0, column=1, sticky='ns')
self.canv.grid(row=0, column=0, sticky='nsew')
self.canv.configure(xscrollcommand=self.hScroll.set,
yscrollcommand=self.vScroll.set)
self.frm = Frame(self.canv, bd=2, bg='green') #holds messages
self.frm.grid_columnconfigure(0, weight=1)
self.canv.create_window(0, 0, window=self.frm, anchor='nw', tags='inner')
self.messages = []
for i in range(20):
m = MessageItem(self.frm, 'Something Profound', bd=2, bg='black')
m.grid(row=i, column=0, sticky='nsew', padx=2, pady=2)
self.messages.append(m)
self.update_layout()
self.canv.bind('<Configure>', self.on_configure)
def update_layout(self):
self.frm.update_idletasks()
self.canv.configure(scrollregion=self.canv.bbox('all'))
self.canv.yview('moveto','1.0')
self.size = self.frm.grid_size()
def on_configure(self, event):
w,h = event.width, event.height
natural = self.frm.winfo_reqwidth()
self.canv.itemconfigure('inner', width= w if w>natural else natural)
self.canv.configure(scrollregion=self.canv.bbox('all'))
def add_message(self, message):
m = MessageItem(self.frm, message, bd=2, bg='red')
m.grid(row=self.size[1], column=0, padx=2, pady=2, sticky='we')
self.messages.append(m)
self.update_layout()
root = Tk()
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
sc = scrollableContainer(root, bd=2, bg='black')
sc.grid(row=0, column=0, sticky='nsew')
def new_message():
test = 'Something Profane'
sc.add_message(test)
b = Button(root, text='New Message', command=new_message)
b.grid(row=1, column=0, sticky='we')
root.mainloop()

Categories

Resources