Dynamically changing scrollregion of a canvas in Tkinter - python

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

Related

How to show and expand only one frame in tkinter frame swapping?

Both frames show when using pack(). The login frame is supposed to show first, and when register is clicked, the register email frame is supposed to show.
Works fine when using grid(), but the frame doesn't fill the container. I tried doing sticky on all four corners, but it only sticks to one corner.
The size of the container might not be the problem since I made sure it expanded to fill the 400x400 window, so it's just the frames themselves causing the issue
How do I fix this?
`
import tkinter as tk
import customtkinter as ctk
class Window(ctk.CTk):
def __init__(self, *args, **kwargs):
super().__init__()
self.geometry('400x400')
self.title('Music Mayhem')
self.resizable(False, False)
container = ctk.CTkFrame(master=self)
container.pack(side='top', fill='both', expand=True)
self.frames={}
for F in (LoginFrame, RegEmailFrame):
frame = F(container, self)
self.frames[F] = frame
#frame.grid(row=0, column=0, sticky= 'n'+'e'+'s'+'w')
frame.pack(expand= True, fill = 'both')
self.show_frame(LoginFrame)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
#Create a frame to place the logo in
class LogoFrame(ctk.CTkFrame):
def __init__(self, *args, header_name="Logo Frame", **kwargs):
super().__init__(*args, **kwargs)
self.logo = ctk.CTkLabel(master=self, text='PLACE LOGO HERE')
self.logo.grid(row=0, column=0)
#First Frame that will show when app is opened
class LoginFrame (tk.Frame):
def __init__(self,parent, controller, header_name="Login"):
tk.Frame.__init__(self, parent, bg='#363635')
self.logo = LogoFrame(self, header_name='Logo')
self.logo.grid(row=0, column=0)
self.emailEntry = ctk.CTkEntry(master=self,
placeholder_text = 'Email Address',
width=150,
height=35)
self.emailEntry.grid(row=1,column=0, pady = 5)
self.passwordEntry = ctk.CTkEntry(master=self,
placeholder_text = 'Password',
width = 150,
height =35)
self.passwordEntry.grid(row=2, column = 0, pady = 5)
self.loginBtn = ctk.CTkButton(master = self,
width = 60,
height = 30,
text = 'Login')
self.loginBtn.grid(row=3, column=0, pady=10)
self.regBtn = ctk.CTkButton(master=self,
width=60,
height=20,
text = 'Register',
fg_color = 'transparent',
command= lambda:
controller.show_frame(RegEmailFrame))
self.regBtn.grid(row=4, column=0, pady=5)
class RegEmailFrame(tk.Frame):
def __init__(self, parent, controller,header_name="Register Email"):
tk.Frame.__init__(self, parent, bg='#81817e')
self.logo = LogoFrame(self)
self.logo.grid(row=0, column=0)
self.emailLabel = ctk.CTkLabel(master=self,
text='Register Email:')
self.emailLabel.grid(row=1, column=0, pady=5)
self.emailEntry = ctk.CTkEntry(master=self,
placeholder_text = 'Email Address',
width=150,
height=35)
self.emailEntry.grid(row=2, column=0)
self.contBtn = ctk.CTkButton(master=self,
width=60,
height=30,
text = 'Continue')
self.contBtn.grid(row=3, column=0, pady=5)
self.backBtn = ctk.CTkButton(master=self,
width=60,
height=20,
text = 'Back',
fg_color = 'transparent',
command= lambda:
controller.show_frame(LoginFrame))
self.backBtn.grid(row=4, column=0, pady=5)
if __name__ == '__main__':
app = Window()
app.mainloop()
`
EDIT
I tried to implement forget(). Changes to parts of the code showed below:
def show_frame(self, cont, prev):
prev = self.frames[prev]
prev.forget()
frame = self.frames[cont]
frame.tkraise()
self.regBtn = ctk.CTkButton(master=self,
width=60,
height=20,
text = 'Register',
fg_color = 'transparent',
command= lambda:
controller.show_frame(RegEmailFrame, LoginFrame))
self.backBtn = ctk.CTkButton(master=self,
width=60,
height=20,
text = 'Back',
fg_color = 'transparent',
command= lambda:
controller.show_frame(LoginFrame, RegEmailFrame))
However, an error appears :
TypeError: Window.show_frame() missing 1 required positional argument: 'prev'
SOLUTION:
for F in (LoginFrame, RegEmailFrame):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky= 'n'+'e'+'s'+'w')
frame.grid_columnconfigure(0,weight=1)
frame.grid_rowconfigure(0,weight=1)

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 access individual Button tkinter?

Goal : dynamically create label and button (represents a task) and when I click this button it should destroy the label and button.
Problem : Unable to access buttons individually
Here is my code :
class Gui:
# constructor
def __init__(self, root):
self.root = root
self.root.title("TASKS")
self.root.geometry("300x700")
self.root.resizable(width=False, height=False)
self.clock = Label(self.root, fg="blue")
self.clock.grid(row = 0,column = 0,padx = 80,pady = 40)
self.update_clock()
self.new_button = Button(self.root, text="New",command = self.newwindow).grid(row = 0, column =1)
self.r =0
# clock
def update_clock(self):
now = strftime("%H:%M:%S")
self.clock.configure(text=now)
self.clock.after(1000, self.update_clock)
# label creator
def label(self, txt):
self.l = Label(self.root, text=txt, fg="red",pady =15)
self.l.grid(row = self.r, column =0)
# button creator
def donebutton(self):
self.b = Button(self.root, text="Done",command = lambda : self.del_task())
self.b.grid(row = self.r,column = 1)
# create a task
def task(self,txt):
self.r +=1
self.label(txt)
self.donebutton()
# delete task
def del_task(self):
self.l.destroy()
self.b.destroy()
# display gui method
def display(self):
self.root.mainloop()
# new window
def newwindow(self):
self.newwindow = Toplevel(self.root)
self.newwindow.title("NEW TASK")
self.newwindow.geometry("300x200")
self.newwindow.resizable(width=False, height=False)
Label(self.newwindow,text="Task").grid()
self.t1 = Text(self.newwindow,height = 2,width = 36)
self.t2 = Text(self.newwindow,height = 2,width = 10)
self.t1.grid()
Label(self.newwindow, text="Time").grid()
self.t2.grid()
self.c_b=Button(self.newwindow,text = "CREATE",command = lambda : self.task(self.t1.get("1.0",END)))
self.c_b.grid()
if __name__ == '__main__':
a = Gui(Tk())
a.display()
Requesting help with the code and I do not mind changing the whole code.
You keep overwriting the button. You said you don't care if the code is written completely different so, I changed a bunch.
Windows, Clock and Tasks are separated into classes
no targetable references to windows or buttons are held
the command for the "New Button" creates the NewTasks window
the command for the "Done Button" destroys its parent
the command for the "Create Button" creates a Task in the main window
a scrollable frame was added so tasks can never vertically overflow the main window
start, pause, done and remove Buttons were included
a scrollable output panel was included
ability to reposition tasks in the display is included
task labels can be clicked to show their content in the output panel
start, pause and done report to the output, and done includes elapsed time
once a task is started it cannot be removed
a task can only be "done" if it is running
a modicum of widget/grid formatting was applied to stop the display from jumping around as tasks were created/removed
I got bored and built your app ... probably.
from tkinter import Tk, Button, Label, Toplevel, Text, Frame, Canvas, Scrollbar
from time import strftime, time
class Task(Frame):
def __init__(self, master, text, output, move, **kwargs):
Frame.__init__(self, master, **kwargs)
self.grid_columnconfigure(0, weight=1)
self.stored = []
self.starttime = 0
self.send_output= output
self.lbl = Label(self, text=text, height=1, anchor='nw', fg='blue', font='calibri 14')
self.lbl.grid(row=0, column=0, sticky='nswe')
self.lbl.bind('<1>', self.output)
font = 'consolas 10 bold'
self.b1 = Button(self, font=font, text=chr(9654), command=self.start)
self.b2 = Button(self, font=font, text=chr(10073)+chr(10073), state='disabled', command=self.pause)
self.b3 = Button(self, font=font, text=chr(10006), state='disabled', command=self.done)
self.b4 = Button(self, font=font, text=chr(9866), command=self.destroy)
self.b5 = Button(self, font=font, text=chr(9650), command=lambda: move(self, -1))
self.b6 = Button(self, font=font, text=chr(9660), command=lambda: move(self, 1))
self.b1.grid(row=0, column=1) #start
self.b2.grid(row=0, column=2) #pause
self.b3.grid(row=0, column=3) #done
self.b4.grid(row=0, column=4) #remove
self.b5.grid(row=0, column=5) #move up
self.b6.grid(row=0, column=6) #move down
def start(self):
self.b1['state'] = 'disabled'
self.b2['state'] = 'normal'
self.b3['state'] = 'normal'
self.b4['state'] = 'disabled'
self.starttime = time()
self.send_output(f"{self.lbl['text']}", f"{strftime('%I:%M:%S')} STARTED: ")
def pause(self):
self.b1['state'] = 'normal'
self.b2['state'] = 'disabled'
self.b3['state'] = 'disabled'
self.stored.append(time() - self.starttime)
self.send_output(f"{self.lbl['text']}", f"{strftime('%I:%M:%S')} PAUSED: ")
def done(self):
self.stored.append(time() - self.starttime)
t = sum(self.stored)
self.send_output(f"{self.lbl['text']}\telapsed time: {self.etime(t)}\n", f"{strftime('%I:%M:%S')} FINISHED: ")
self.destroy()
def etime(self, s):
h = int(s//3600)
s -= 3600*h
m = int(s//60)
s -= 60*m
return f'{h:02}:{m:02}:{int(s):02}'
def output(self, event):
self.send_output(self.lbl['text'], 'Task: ')
class NewTasks(Toplevel):
WIDTH = 416
HEIGHT = 50
def __init__(self, master, slave, output, move, **kwargs):
Toplevel.__init__(self, master, **kwargs)
self.title("New Task")
self.geometry(f'{NewTasks.WIDTH}x{NewTasks.HEIGHT}')
self.resizable(width=False, height=False)
Label(self, text="Task").grid(row=0, column=0)
txt = Text(self, height=2, width=36, font='consolas 12')
txt.grid(row=0, column=1)
Button(self, text="CREATE", command=lambda: self.create(slave, output, move, txt)).grid(row=0, column=2, sticky='e', padx=4, pady=12)
def create(self, target, output, move, txt):
t = Task(target.frame, txt.get("1.0",'end'), output, move)
t.grid(column=0, sticky='nswe')
target.update(t)
class ScrollFrame(Canvas):
def __init__(self, master, **kwargs):
Canvas.__init__(self, master, **kwargs)
vsb = Scrollbar(self, orient='vertical', command=self.yview)
vsb.pack(side='right', fill='y')
self.configure(yscrollcommand=vsb.set)
self.frame = Frame(self, height=0)
self.frame.grid_columnconfigure(0, weight=1)
self.frame.bind('<Configure>', lambda e:self.configure(scrollregion=self.bbox("all")))
self.create_window((0,0), width=App.WIDTH-20, window=self.frame, anchor="nw")
self.movelist = []
def update(self, target):
self.movelist.append(target)
def move_item(self, elem, dir=1):
c = self.frame.winfo_children()
i = self.movelist.index(elem)
if i+dir in range(0, len(self.movelist)):
e = self.movelist.pop(i)
self.movelist.insert(i+dir, e)
for n in range(len(self.movelist)):
while n < len(self.movelist) and self.movelist[n] not in c:
self.movelist.pop(n)
if n < len(self.movelist):
self.movelist[n].grid(row=n, column=0, sticky='nswe')
continue
break
class Clock(Label):
def __init__(self, master, **kwargs):
Label.__init__(self, master, **kwargs)
self.update()
def update(self):
self['text'] = strftime('%I:%M:%S')
self.after(1000, self.update)
class App(Tk):
WIDTH = 600
HEIGHT = 447
def __init__(self, **kwargs):
Tk.__init__(self, **kwargs)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
Clock(self, fg="blue", font='calibri 18').grid(row=0, column=0, ipady=10, sticky='nswe')
sf = ScrollFrame(self, highlightthickness=0)
sf.grid(row=1, column=0, columnspan=3, sticky='nswe')
command = lambda: NewTasks(self, sf, self.output, sf.move_item)
Button(self, text="New", font='calibri 12', command=command).grid(row=0, column=1, columnspan=2)
self.out = Text(self, height=8, font="calibri 14")
self.out.grid(row=2, column=0, columnspan=2)
self.out.tag_configure("bold", font="calibri 12 bold")
vsb = Scrollbar(self, orient='vertical', command=self.out.yview)
vsb.grid(row=2, column=2, sticky='ns')
self.out.configure(yscrollcommand=vsb.set)
def output(self, text, btext=''):
self.out.insert('end', btext, 'bold')
self.out.insert('end', text)
if __name__ == '__main__':
app = App()
app.title("Task Scheduler")
app.geometry(f'{App.WIDTH}x{App.HEIGHT}')
app.resizable(width=False, height=False)
app.mainloop()

the tkinter widgets in my classes are not displaying

I'm trying to write a code that contains multiple pages and can be switched to when a button is clicked on. it worked initially but my widgets are not displaying, and there is neither a warning or an error message. Secondly, what is the difference between using tk and tk.TK?
from tkinter import *
import tkinter as tk
class moreTab(tk.Tk):
def __init__(self):
Tk.__init__(self)
self.geometry("1200x600")
container = Frame(self, bg='#c9e3c1')
container.pack(side = "top", fill = 'both', expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
self.frames = {}
for q in (pageone, widget):
frame = q(container,self)
self.frames[q] = frame
frame.place(x= 0,y = 0)
self.raise_frame(pageone)
def raise_frame(self,cont):
frame = self.frames[cont]
frame.tkraise()
class widget(Frame):
def __init__(self, master, control):
Frame.__init__(self, master)
lab = tk.Label(self, text="main page")
lab.place(x = 10, y = 40)
but = tk.Button(self, text='visit start page', command=lambda:
control.raise_frame(pageone))
but.place(x = 10, y = 70)
class pageone(Frame):
def __init__(self, master, control):
Frame.__init__(self,master)
lab = Label(self, text = 'welcome to Game Analysis')
lab.place(x = 10, y = 10)
but = Button(self, text = "Start", command = lambda:
control.raise_frame(widget))
but.place(x = 10, y = 20)
app = moreTab()
app.mainloop()
It turns the issue was that you were using place(). Use the grid geometry manager. Using both import tkinter as tk and from tkinter import * is meaningless. Use one and be consistent. If you use the latter, you have everything available, hence you will write, say Button(...). But if you use the former, you will have to refer each widget like tk.Button(...).
import tkinter as tk
class moreTab(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry("1200x600")
container = tk.Frame(self, bg='#c9e3c1')
container.pack(side = "top", fill = 'both', expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
self.frames = {}
for q in (pageone, widget):
frame = q(container, self)
self.frames[q] = frame
frame.grid(row=0, column=0, sticky='nsew')
self.raise_frame(pageone)
def raise_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
class widget(tk.Frame):
def __init__(self, master, control):
tk.Frame.__init__(self, master)
lab = tk.Label(self, text="main page")
lab.grid(row=0, column=0, padx=10, pady=10)
but = tk.Button(self, text='visit start page', command=lambda: control.raise_frame(pageone))
but.grid(row=1, column=0, padx=10, pady=10)
class pageone(tk.Frame):
def __init__(self, master, control):
tk.Frame.__init__(self, master)
lab = tk.Label(self, text = 'welcome to Game Analysis')
lab.grid(row=0, column=0, padx=10, pady=10)
but = tk.Button(self, text = "Start", command = lambda: control.raise_frame(widget))
but.grid(row=1, column=0, padx=10, pady=10)
app = moreTab()
app.mainloop()

Scrollable Frame does not resize properly using tkinter

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

Categories

Resources