Tkinter Python managing scrollbar inside the listbox in a canvas - python

I got the code below which contain a listbox inside a canvas, I'm trying to place the scrollbar only inside the listbox but what I've got is the scrollbar always set on the whole form. Can you pls help me to put the scrollbar inside the listbox only and not on the whole canvas.
def __init__(self):
self.form = Tk()
self.form.title("Admin");
self.form.geometry('608x620+400+50')
self.form.option_add("*font",("Monotype Corsiva",13))
self.form.overrideredirect(True)
self.form.resizable(width=FALSE, height=FALSE)
self.canvas = Canvas(self.form)
self.canvas.pack(expand=YES,fill=BOTH)
self.photo = PhotoImage(file='src/back3.gif')
self.canvas.create_image(-7,-8,image=self.photo,anchor=NW)
self.scrollbar = tk.Scrollbar(self.canvas, orient="vertical")
self.lb = tk.Listbox(self.canvas, width=78, height=15,yscrollcommand=self.scrollbar.set)
self.lb.place(relx=0.04,rely=0.17)
self.scrollbar.config(command=self.lb.yview)
self.scrollbar.pack(side="right", fill="none")

I suggest you to create a Frame to wrap the Listbox and the Scrollbar. Here it is a class I wrote to do the same thing, it does not fit exactly with your code - I use grid() instead of pack() -, but you get the idea.
class ScrollableListbox(tk.Listbox):
def __init__(self, master, *arg, **key):
self.frame = tk.Frame(master)
self.yscroll = tk.Scrollbar(self.frame, orient=tk.VERTICAL)
tk.Listbox.__init__(self, self.frame, yscrollcommand=self.yscroll.set, *arg, **key)
self.yscroll['command'] = self.yview
def grid(self, *arg, **key):
self.frame.grid(*arg, **key)
tk.Listbox.grid(self, row=0, column=0, sticky='nswe')
self.yscroll.grid(row=0, column=1, sticky='ns')

Related

Scrollable Tkinter GUI with shiftable pods

I am trying to make a GUI such as this with pods, each containing their own elements such as text, images and buttons.
My goal is to make it so that the so called pods can be added to the GUI window (a scrolling capable window) at any point in the code and updated in the window shifting the previous pod to the right or down to the next row if the current row is full like the image below.
I have never messed with Tkinter before so I was wondering if anyone could help me with what steps I would need to take to make such a GUI.
Implement a class that inherits from the Frame class. You can then create as many instances of this class that you want. Since you want the pods to wrap, you can use a Text widget to hold the pods since it's the only scrollable widget that natively supports wrapping.
The "pod" class might look something like this:
class Pod(tk.Frame):
def __init__(self, parent, title, subtitle, image):
super().__init__(parent, bd=2, relief="groove")
if isinstance(image, tk.PhotoImage):
self.image = image
else:
self.image = tk.PhotoImage(file=image_path)
self.title = tk.Label(self, text=title)
self.image_label = tk.Label(self, image=self.image, bd=1, relief="solid")
self.subtitle = tk.Label(self, text=subtitle)
self.b1 = tk.Button(self, text="Button 1")
self.b2 = tk.Button(self, text="Button 2")
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure((0,1), weight=1)
self.title.grid(row=0, column=0, columnspan=2, sticky="ew")
self.image_label.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=8, pady=8)
self.subtitle.grid(row=2, column=0, columnspan=2, sticky="ew")
self.b1.grid(row=3, column=0)
self.b2.grid(row=3, column=1)
You can create another class to manage these objects. If you base it on a Text widget you get the wrapping behavior for free. Though, you could also base it on a Frame or Canvas and manage the wrapping yourself.
It might look something like this:
class PodManager(tk.Text):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.configure(state="disabled", wrap="char")
self.pods = []
def add(self, pod):
self.pods.append(pod)
self.configure(state="normal")
self.window_create("end", window=pod)
self.configure(state="disabled")
To tie it all together, create one PodManager class, then pass one or more instances of Pod to its add method:
import tkinter as tk
...
root = tk.Tk()
pm = PodManager(root)
vsb = tk.Scrollbar(root, orient="vertical", command=pm.yview)
pm.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
pm.pack(side="left", fill="both", expand=True)
for i in range(10):
image = tk.PhotoImage(width=200,height=100)
pod = Pod(pm, f"Title #{i+1}", "More Text", image)
pm.add(pod)
root.mainloop()

tkinter frame inside of canvas not expanding to fill area

I have a scrollable frame class that I borrowed from some code I found, and I'm having trouble adjusting it to fit my needs. It was managed by .pack(), but I needed to use .grid(), so I simply packed a frame (self.region) into it so I could grid my widgets inside of that. However, the widgets inside of this frame won't expand to meet the edges of the container and I'm not sure why. Theres a lot of issues similar to mine out there, but none of the solutions seemed to help. I tried using .grid_columnconfigure, .columnconfigure(), and .bind("Configure") all to no avail. Does anyone have any suggestions to get the widgets in my scrollable region to expand east and west to fill the window?
import tkinter as tk
from tkinter import ttk
class ScrollableFrame(ttk.Frame):
"""taken from https://blog.tecladocode.com/tkinter-scrollable-frames/ and modified to
allow for the use of grid inside self.region
Class that allows for the creation of a frame that is scrollable"""
def __init__(self, container, *args, **kwargs):
super().__init__(container, *args, **kwargs)
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
canvas.rowconfigure(0, weight=1)
canvas.columnconfigure(0, weight=1)
scrollbar.pack(side="right", fill="y")
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
self.scrollable_frame.rowconfigure(0, weight=1)
self.scrollable_frame.columnconfigure(0, weight=1)
self.region=ttk.Frame(self.scrollable_frame)
self.region.pack(fill='both', expand=1)
self.region.grid_rowconfigure(0, weight=1)
self.region.grid_columnconfigure(0, weight=1)
class OtherWindow():
"""data collector object and window"""
def __init__(self, window):
self.window=tk.Toplevel(window)
self.window.grid_columnconfigure(0, weight=1)
self.window.grid_columnconfigure(1, weight=1)
self.window.grid_columnconfigure(2, weight=1)
self.window.grid_columnconfigure(3, weight=1)
self.window.grid_rowconfigure(3, weight=1)
self.lbl1=ttk.Label(self.window, text="this is a sort of long label")
self.lbl1.grid(row=0, column=0, columnspan=2)
self.lbl2=ttk.Label(self.window, text="this is another label")
self.lbl2.grid(row=0, column=2)
self.lbl3=ttk.Label(self.window, text="Other information: blah blah blah blah")
self.lbl3.grid(row=0, column=3)
self.directions=ttk.Label(self.window, text='These are instructions that are kind of long and take'+\
'up about this much space if I had to guess so random text random text random text', wraplength=700)
self.directions.grid(row=1, column=0, columnspan=4)
self.scrolly=ScrollableFrame(self.window)
self.scrolly.grid(row=2, column=0, columnspan=4,sticky='nsew')
self.frame=self.scrolly.region
self.fillScrollRegion()
self.continueBtn=ttk.Button(self.window, text="Do Something", command=self.do)
self.continueBtn.grid(row=3, column=0, columnspan=4, sticky='nsew')
def fillScrollRegion(self):
"""fills scrollable region with label widgets"""
for i in range(15):
for j in range(5):
lbl=ttk.Label(self.frame, text="Sample text"+str(i)+' '+str(j))
lbl.grid(row=i, column=j, sticky='nsew')
def do(self):
pass
root=tk.Tk()
app=OtherWindow(root)
root.mainloop()
The problem is that the scrollframe container Frame is not filling the Canvas horizontally. Instead of bothering to fix some copy/paste, example scrollframe, and explain it, I'll just give you my scrollframe. It is substantially more robust than the one you are using, and the problem you are having doesn't exist with it. I've already plugged it into a version of your script below.
The solution to your scrollframe's issue is found in my on_canvas_configure method. It simply tells the container frame to be the same width as the canvas, on canvas <Configure> events.
import tkinter as tk, tkinter.ttk as ttk
from typing import Iterable
class ScrollFrame(tk.Frame):
def __init__(self, master, scrollspeed=5, r=0, c=0, rspan=1, cspan=1, grid={}, **kwargs):
tk.Frame.__init__(self, master, **{'width':400, 'height':300, **kwargs})
#__GRID
self.grid(**{'row':r, 'column':c, 'rowspan':rspan, 'columnspan':cspan, 'sticky':'nswe', **grid})
#allow user to set width and/or height
if {'width', 'height'} & {*kwargs}:
self.grid_propagate(0)
#give this widget weight on the master grid
self.master.grid_rowconfigure(r, weight=1)
self.master.grid_columnconfigure(c, weight=1)
#give self.frame weight on this grid
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
#_WIDGETS
self.canvas = tk.Canvas(self, bd=0, bg=self['bg'], highlightthickness=0, yscrollincrement=scrollspeed)
self.canvas.grid(row=0, column=0, sticky='nswe')
self.frame = tk.Frame(self.canvas, **kwargs)
self.frame_id = self.canvas.create_window((0, 0), window=self.frame, anchor="nw")
vsb = tk.Scrollbar(self, orient="vertical")
vsb.grid(row=0, column=1, sticky='ns')
vsb.configure(command=self.canvas.yview)
#attach scrollbar to canvas
self.canvas.configure(yscrollcommand=vsb.set)
#_BINDS
#canvas resize
self.canvas.bind("<Configure>", self.on_canvas_configure)
#frame resize
self.frame.bind("<Configure>", self.on_frame_configure)
#scroll wheel
self.canvas.bind_all('<MouseWheel>', self.on_mousewheel)
#makes frame width match canvas width
def on_canvas_configure(self, event):
self.canvas.itemconfig(self.frame_id, width=event.width)
#when frame dimensions change pass the area to the canvas scroll region
def on_frame_configure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
#add scrollwheel feature
def on_mousewheel(self, event):
self.canvas.yview_scroll(int(-event.delta / abs(event.delta)), 'units')
#configure self.frame row(s)
def rowcfg(self, index, **options):
index = index if isinstance(index, Iterable) else [index]
for i in index:
self.frame.grid_rowconfigure(i, **options)
#so this can be used inline
return self
#configure self.frame column(s)
def colcfg(self, index, **options):
index = index if isinstance(index, Iterable) else [index]
for i in index:
self.frame.grid_columnconfigure(i, **options)
#so this can be used inline
return self
class AuxiliaryWindow(tk.Toplevel):
def __init__(self, master, **kwargs):
tk.Toplevel.__init__(self, master, **kwargs)
self.geometry('600x300+600+200')
self.attributes('-topmost', True)
self.title('This Is Another Title') #:D
#if you reconsider things, you can accomplish more with less
labels = ["this is a sort of long label",
"this is another label",
"Other information: blah blah blah blah"]
for i, text in enumerate(labels):
ttk.Label(self, text=text).grid(row=0, column=i)
self.grid_columnconfigure(i, weight=1)
#doing it this way the text will always fit the display as long as you give it enough height to work with
instr = tk.Text(self, height=3, wrap='word', bg='gray94', font='Arial 8 bold', bd=0, relief='flat')
instr.insert('1.0', ' '.join(['instructions']*20))
instr.grid(row=1, columnspan=3, sticky='nswe')
#instantiate the scrollframe, configure the first 5 columns and return the frame. it's inline mania! :p
self.scrollframe = ScrollFrame(self, 10, 2, 0, cspan=3).colcfg(range(5), weight=1).frame
self.fillScrollRegion()
#why store a reference to this? Do you intend to change/delete it later?
ttk.Button(self, text="Do Something", command=self.do).grid(row=3, columnspan=3, sticky='ew')
def fillScrollRegion(self):
"""fills scrollable region with label widgets"""
r, c = 30, 5 #math is our friend
for i in range(r*c):
ttk.Label(self.scrollframe, text=f"row_{i%r} col_{i//r}").grid(row=i%r, column=i//r, sticky='nsew')
def do(self):
pass
class Root(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.geometry('+550+150')
self.title('This Is A Title Probably Or Something') #:D
aux = AuxiliaryWindow(self)
self.mainloop()
Root() if __name__ == "__main__" else None

how do i put dynamically added entry fields in a set frame or canvas that does not resize

i have dynamically addable and delete able entry fields that i want to set inside a frame or canvas inside of a main frame but when i try the frame dissappears or dynamically grows with the entry fields. i want the canvas to use the scrollbar if entry fields exceed the window size.
from tkinter import *
import tkinter as tk
class Demo2:
def __init__(self, master):
global rows
self.master = master
self.frame = tk.Frame(self.master)
master.title("test")
self.frame.pack()
addboxButton = Button(self.frame, text='<Add Time Input>', fg="Red", command=self.addBox)
addboxButton.pack()
this is where my buttons are added and deleted.
def addBox(self):
def delete():
delboxButton.grid_remove()
ent1.delete(0,END)
ent2.delete(0,END)
ent1.grid_remove()
ent2.grid_remove()
root = self.frame
frame=Frame(root,width=900,height=900)
frame.pack()
canvas=Canvas(frame,bg='#FFFFFF',width=700,height=300,scrollregion=(0,0,700,300))
vbar=Scrollbar(frame,orient=VERTICAL)
vbar.pack(side=RIGHT,fill=Y)
vbar.config(command=canvas.yview)
canvas.config(width=700,height=300)
canvas.config(yscrollcommand=vbar.set)
canvas.pack(side=LEFT,expand=TRUE,fill=BOTH)
I am trying to figure out now how to make the first set of entry start out on the screen when its opened. and bind the add call to an action.
i = 0
ent1 = Entry(canvas)
ent1.grid(row=i, column=0,sticky="nsew")
i += 1
i = 0
ent2 = Entry(canvas)
ent2.grid(row=i, column=1,sticky="nsew")
i += 1
delboxButton = Button(canvas, text='delete', fg="Red", command=delete)
delboxButton.grid(row=0 ,column=2)
root = tk.Tk()
root.title("test Complete")
root.geometry("500x500")
app = Demo2(root)
root.mainloop()
The normal way this is tackled is to create a single frame and add it to the canvas with the canvas create_window method. Then, you can put whatever you want in the frame using pack, place or grid.
For a description of the technique see Adding a scrollbar to a group of widgets in Tkinter
Here's an example illustrating how the technique works for widgets created by a button. I didn't include the delete functionality or the ability for everything to resize properly to keep the example short, but you seem to have a pretty good idea of how to make the delete function work and I don't know exactly what sort of resize behavior you want.
import tkinter as tk
class Demo2:
def __init__(self, master):
self.master = master
self.entries = []
self.canvas = tk.Canvas(master, width=400, height=200)
self.vsb = tk.Scrollbar(master, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.add_button = tk.Button(master, text="Add", command=self.add)
self.container = tk.Frame()
self.canvas.create_window(0, 0, anchor="nw", window=self.container)
self.add_button.pack(side="top")
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
# start with 3 entry widgets
self.add()
self.add()
self.add()
def add(self):
entry = tk.Entry(self.container)
entry.pack(side="top", fill="x")
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
self.entries.append(entry)
root = tk.Tk()
demo = Demo2(root)
root.mainloop()

How to make reusable scrollbars in Tkinter?

I'd like to create an easy way to add scrollbars to any frame I like. So far, only one works. What's wrong with this script? What's a proper way to do this? I still only have a faint grasp of all the concepts hidden in this, sorry.
import Tkinter as tk
def data(parent):
for i in range(50):
tk.Label(parent,text=i).grid(row=i,column=0)
tk.Label(parent,text="my text"+str(i)).grid(row=i,column=1)
tk.Label(parent,text="..........").grid(row=i,column=2)
class ScrollBar():
#def __init__(self, tk):
# self.canvas = tk.Canvas()
def myfunction(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"), width=200, height=200)
print("orig: ",self.canvas)
def makeScrollBar(self, tk, parent):
self.outerframe = tk.Frame(parent, relief="groove", width=50, height=100, bd=1)
self.outerframe.pack()
self.canvas = tk.Canvas(self.outerframe)
self.innerframe = tk.Frame(self.canvas)
self.myscrollbar = tk.Scrollbar(self.outerframe, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.myscrollbar.set)
self.myscrollbar.pack(side="right",fill="y")
self.canvas.pack(side="left")
self.canvas.create_window((0,0), window=self.innerframe, anchor="nw")
self.innerframe.bind("<Configure>", self.myfunction)
print("orig: ",self.canvas)
return self.innerframe
root = tk.Tk()
root.wm_geometry("800x600+100+100")
scrollbargenerator = ScrollBar()
b = scrollbargenerator.makeScrollBar(tk, root)
c = scrollbargenerator.makeScrollBar(tk, root)
data(b)
data(c)
root.mainloop()
First, you manage to create your scrollbars. If you scroll your mouse over the first scrollbar it will actually scroll the canvas. The thumb does not appear (or do not move) because the scrollregion is never set for this canvas.
In fact, your use of classes and objects is broken. Your Scrollbar class is instantiated once and this only instance update its fields each time makeScrollBar is called. Thus in myfunction callback, self.canvas always refer to the lastly created canvas.
You can easily fix your code my using different scrollbar generators
scrollbargenerator = ScrollBar()
b = scrollbargenerator.makeScrollBar(tk, root)
scrollbargenerator = ScrollBar()
c = scrollbargenerator.makeScrollBar(tk, root)
or capturing the canvas in a closure
def myfunction(self, canvas):
canvas.configure(scrollregion=canvas.bbox("all"), width=200, height=200)
def makeScrollBar(self, tk, parent):
#(...)
self.innerframe.bind("<Configure>", (lambda canvas: (lambda event: self.myfunction(canvas)))(self.canvas))
or relying on the information already present in event
#staticmethod
def myfunction(event):
event.widget.master.configure(scrollregion=event.widget.master.bbox("all"), width=200, height=200)

scrollable listbox within a grid using tkinter

I'm new to this place and tkinter. I am stuck at making a scrollable listbox or canvas. I have tried both widgets. Within this listbox or canvas, I have several entry and label widgets. The origin point is R0,C0. I used row/columnconfigure to stretch the listbox or canvas.
In the main window, I had 4 buttons on row four to column four (0,4->4,4). I placed the scrollbar on column 5. I attempted to use the grid method. The issue I am having is making the scrollbar functional.
Note: Turning the mainframe into a class is only one of the ways I have tried. Packing the scrollbar on the right has worked, with the listbox/canvas packed on the left. However, the listbox/canvas widget that the scrollbar is commanded to does not scroll the listbox/canvas. Also, adding many entry boxes does not cause the listbox/canvas to scroll. Help please.
from tkinter import *
from tkinter.ttk import *
Style().configure("B.TFrame", relief="flat",
background="blue")
Style().configure("R.TFrame", relief="flat",
background="red")
Style().configure("R.TLabel", background="red")
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master, style="B.TFrame")
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
top=self.winfo_toplevel()
self.menuBar = Menu(top)
top["menu"] = self.menuBar
self.subMenu = Menu(self.menuBar, tearoff=0)
self.subMenu2 = Menu(self.menuBar, tearoff=0)
self.menuBar.add_cascade(label="File", menu=self.subMenu)
self.menuBar.add_cascade(label="About", menu=self.subMenu2)
self.subMenu.add_command(label="Open")
self.subMenu.add_command(label="Save")
self.subMenu.add_command(label="Exit")
self.subMenu2.add_command(label="About")
self.subMenu2.add_command(label="Help")
self.data = Listbox (self, bg='red')
scrollbar = Scrollbar(self.data, orient=VERTICAL)
self.add = Button(self, text="")
self.remove = Button(self, text="")
self.run = Button(self, text="")
self.stop = Button(self, text="")
self.data.grid (row=0, column=0, rowspan=4, columnspan=4, sticky=N+E+S+W)
self.data.columnconfigure(1, weight=1)
self.data.columnconfigure(3, weight=1)
self.add.grid(row=4,column=0,sticky=EW)
self.remove.grid(row=4,column=1,sticky=EW)
self.run.grid(row=4,column=2,sticky=EW)
self.stop.grid(row=4,column=3,sticky=EW)
scrollbar.grid(column=5, sticky=N+S)
Without any content in the listbox, there's nothing to scroll...
This seems to work though (shortened the example a bit). See also the example at the scrollbar documentation.
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
self.data = Listbox(self, bg='red')
self.scrollbar = Scrollbar(self.data, orient=VERTICAL)
self.data.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.data.yview)
for i in range(1000):
self.data.insert(END, str(i))
self.run = Button(self, text="run")
self.stop = Button(self, text="stop")
self.data.grid(row=0, column=0, rowspan=4,
columnspan=2, sticky=N+E+S+W)
self.data.columnconfigure(0, weight=1)
self.run.grid(row=4,column=0,sticky=EW)
self.stop.grid(row=4,column=1,sticky=EW)
self.scrollbar.grid(column=2, sticky=N+S)
a = Application()
a.mainframe()
a.mainloop()
You must define the command attribute to the scrollbar, and you must supply the yscrollcommand attribute to the listbox. These two attributes work together to make something scrollable.
The yscrollcommand option tells the listbox "when you are scrolled in the Y direction, call this command. This is usually the set method of a scrollbar, so that when the user scrolls via arrow keys, the scrollbar gets updated.
The command attribute of a scorllbar says "when the user moves you, call this command". This is usually the yview or xview method of a widget, which causes the widget to change its view parameters in the Y or X direction.
In your case, after creating the widgets you would do this:
self.data.config(yscrollcommand=self.scrollbar.set)
scrollbar.config(command=self.data.yview)
This thread is old but in case somebody else falls across it as I did, it needs a few precisions.
Junuxx's answer doesnt work as is, not only because there is an indentation problem due to difficulties in seizing code here (from "self.run" which is part of the "mainframe" function) but because it seems necessary to put the listbox and the scrollbar in their own frame.
Here is a working code for Python 2 and 3 :
#!/usr/bin/env python2
try:
# for Python2
from Tkinter import *
except ImportError:
# for Python3
from tkinter import *
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.grid(sticky=N+S+E+W)
self.mainframe()
def mainframe(self):
frame = Frame(self)
scrollbar = Scrollbar(frame, orient=VERTICAL)
data = Listbox(frame, yscrollcommand=scrollbar.set,
bg='red')
scrollbar.config(command=data.yview)
scrollbar.pack(side=RIGHT, fill=Y)
data.pack(side=LEFT, fill=BOTH, expand=1)
for i in range(1000):
data.insert(END, str(i))
self.run = Button(self, text="run")
self.stop = Button(self, text="stop")
frame.grid(row=0, column=0, rowspan=4,
columnspan=2, sticky=N+E+S+W)
frame.columnconfigure(0, weight=1)
self.run.grid(row=4,column=0,sticky=EW)
self.stop.grid(row=4,column=1,sticky=EW)
a = Application()
a.mainframe()
a.mainloop()
You may find further information here : https://www.effbot.org/tkinterbook/listbox.htm.
Hope this helps.

Categories

Resources