Python Forum
[Tkinter] Scrollable buttons with an add/delete button
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Scrollable buttons with an add/delete button
#1
I am trying to make a GUI to manage the sensors I am going to be putting in my aquariums, terrariums, and vivariums. I am going to have a main menu screen which will have a tab where I can see sensor info. My problem is I can not seem to figure out a way to make my sensor "rows" scrollable. What I have in mind is this:

   

I have the two bottom buttons but I am stuck with the top canvas and not sure how to add my buttons. I am new to python and I have been trying to follow multiple tkinter guides online and have been looking around different forums but I can't seem to make them work with what I am trying to achieve.

This is my code:
import tkinter as tk
from tkinter import scrolledtext

class GUI(object):
    
    def __init__(self, root):
        
        self.root = root
        self.root.title('Sensors')
        
        windowWidth = 700
        windowHeight = 500
        
        # get the screen dimension
        screenWidth = root.winfo_screenwidth()   # Current monitor is 1920
        screenHeight = root.winfo_screenheight() # x 1080

        # find the center point
        centerX = int(screenWidth/2 - windowWidth / 2)
        centerY = int(screenHeight/2 - windowHeight / 2)

        # set the position of the window to the center of the screen
        root.geometry(f'{windowWidth}x{windowHeight}+{centerX}+{centerY}')
        root.resizable(False,False)
        
        ### On main menu
        self.sensors = tk.Button(root, text='Sensors',height=3, width=10, font=('Helvetica',15), borderwidth=5, command=self.Sensors)
        self.options = tk.Button(root, text='Options',height=3, width=10, font=('Helvetica',15), borderwidth=5, command=self.Options)
        
        
        ### In sensor menu
        self.addSensorBtn = tk.Button(root, text='Add Sensor')
        self.ReturnMenu = tk.Button(root, text="Return to Main Menu", font=('Helvetica'), command=self.MainMenu)
       
        self.frame = tk.Frame(root).grid(row=0,column=0)
        self.canvas = tk.Canvas(self.frame,bg='gray75')
        self.vsb = tk.Scrollbar(self.frame, orient='vertical', command=self.canvas.yview)
        
        self.MainMenu()
        
    def GridConfig(self):
        root.rowconfigure(0,weight=3)
        root.rowconfigure(1,weight=1)
        root.columnconfigure((0,2), weight=1)
        
    def MainMenu(self):
         self.GridConfig()
         self.RemoveAll()
         self.sensors.grid(column=0, row=1, sticky='NE')
         self.options.grid(column=2, row=1, sticky='NW')
         
    def Sensors(self) :
        self.RemoveAll()
        
        self.canvas.grid(column=0,row=0,columnspan=3,sticky='NEWS')
        self.vsb.grid(row=0, column=3, sticky='NSE')
        self.canvas.configure(yscrollcommand=self.vsb.set, scrollregion=self.canvas.bbox('all'))
        
        self.addSensorBtn.grid(column=2,row=1, sticky='NEWS')        
        self.ReturnMenu.grid(column=0, row=1,sticky='NEWS')
    
    def Options(self):
        self.RemoveAll()
        self.GridConfig()
        self.ReturnMenu.grid(column=1, row=1)
    
    def RemoveAll(self):
        self.vsb.grid_remove()
        self.canvas.grid_remove()
        self.addSensorBtn.grid_remove()
        self.sensors.grid_remove()
        self.options.grid_remove()
        self.ReturnMenu.grid_remove()
        
if __name__ == '__main__':
    root = tk.Tk()
    myGUI = GUI(root)
    root.mainloop()
Reply
#2
Since you can scroll a canvas the common way of doing this is make a canvas, put a frame on the canvas, add controls to the frame. You might want to take a look at tkScrolledFrame.

pypi.org/project/tkScrolledFrame/
Reply
#3
This is an example from my toolbox. It uses 10 Labels, but you can use the code for buttons also.

from tkinter import * 

class ScrolledCanvas():
    def __init__(self, parent, color='brown'):
        canv = Canvas(parent, bg=color, relief=SUNKEN)
        canv.config(width=300, height=200)                

        ##---------- scrollregion has to be larger than canvas size
        ##           otherwise it just stays in the visible canvas
        canv.config(scrollregion=(0,0,300, 1000))         
        canv.config(highlightthickness=0)                 

        ybar = Scrollbar(parent)
        ybar.config(command=canv.yview)                   
        ## connect the two widgets together
        canv.config(yscrollcommand=ybar.set)              
        ybar.pack(side=RIGHT, fill=Y)                     
        canv.pack(side=LEFT, expand=YES, fill=BOTH)       

        for ctr in range(10):
            frm = Frame(parent,width=960, height=100,bg="#cfcfcf",bd=2)
            frm.config(relief=SUNKEN)
            Label(frm, text="Frame #"+str(ctr+1)).grid()
            canv.create_window(10,10+(100*ctr),anchor=NW, window=frm)

if __name__ == '__main__':
   root=Tk()
   ScrolledCanvas(root)
   root.mainloop() 
Reply
#4
I'd suggest getting a copy of John Shipman's reference manual, it's old, but still the most comprehensive work on tkinter.
You can get a copy here:

Try running the code below which is very old, but surprisingly still functional

look at lines 70-82 Note how sccrollbar is linked into the canvas
Notice also, the link to canvas on line 80.

Try running the code.
"""
cscroll.py

This demonstration script creates a simple canvas that can be
scrolled in two dimensions.

June 17, 2005
"""

import tkinter as Tk
import template as A



def i2c (i):
    return (str(i) + 'c')

class Box:
    def __init__(self, master, cvs, i, j):
        self.master = master
        self.cvs = cvs
        x = i*3 - 10
        y = j*3 - 10
        self.text = '%d,%d' % (i, j)
        self.id_box = self.cvs.create_rectangle(i2c(x), i2c(y), i2c(x+2), i2c(y+2), outline='black',
                                                        fill=self.master.bg, tags='box')
        self.id_text = self.cvs.create_text(i2c(x+1), i2c(y+1), text= self.text, anchor=Tk.CENTER, tags='text')
        self.cvs.tag_bind(self.id_box, '<Enter>', self.on_enter)
        self.cvs.tag_bind(self.id_box, '<Leave>', self.on_leave)
        self.cvs.tag_bind(self.id_text, '<Enter>', self.on_enter)
        self.cvs.tag_bind(self.id_box, '<1>', self.on_click)
        self.cvs.tag_bind(self.id_text, '<1>', self.on_click)


    def on_enter(self, event):
        self.cvs.itemconfigure(self.id_box, fill='SeaGreen1')

    def on_leave(self, event):
        self.cvs.itemconfigure(self.id_box, fill=self.master.bg)

    def on_click(self, event):
        self.master.echo.set(self.text)


        
class Demo(A.Demo):
    """  a demo class """

    def __init__(self, cmain):
        A.Demo.__init__(self, cmain, __file__)
        self.ini_frame()

    def ini_demo_called(self):
        """ This method should be defined"""
        self.ini_demo_called_0()
        self.ini_frame()

    def ini_frame(self):
        self.demo_main_frame.master.title("Form Demonstration")
        self.demo_main_frame.master.minsize(width=400, height=170) 
        self.demo_main_frame.master.geometry("+50+50")
        A.Label(self.demo_frame, text=
        "This window displays a canvas widget that can be scrolled "
        "either using the scrollbars or by dragging with button 2 in the canvas.  "
        "If you click button 1 on one of the rectangles, its indices will be printed on the lavel."
        , width=40,  wraplength='9c')

        fa = Tk.Frame(self.demo_frame)
        fa.pack(fill=Tk.BOTH, expand=1, padx=1, pady=1)
        self.cvs = Tk.Canvas(fa, scrollregion=("-11c", "-11c",  "50c",  "20c"), 
                                   relief=Tk.SUNKEN, borderwidth=2)  
        self.cvs.grid(row=0, column=0, sticky= Tk.N+Tk.E+Tk.W+Tk.S)
        
        xscroll = Tk.Scrollbar(fa, orient=Tk.HORIZONTAL, command=self.cvs.xview)
        xscroll.grid(row=1, column=0, sticky=Tk.E+Tk.W)

        yscroll = Tk.Scrollbar(fa, orient=Tk.VERTICAL, command=self.cvs.yview)
        yscroll.grid(row=0, column=1, sticky=Tk.N+Tk.S)
        
        self.cvs.config(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
        fa.grid_rowconfigure(0, weight=1, minsize=0)     
        fa.grid_columnconfigure(0, weight=1, minsize=0)  

        self.bg = self.cvs.cget('bg')
        self.echo = Tk.StringVar()

        for i in range(25):
            for j in range(10):
                Box(self, self.cvs, i, j)

        self.cvs.bind('<Button2-Motion>', self.on_motion)
        self.cvs.bind('<2>', self.on_press2)
        

        label = Tk.Label(self.demo_frame, textvariable=self.echo, width=12, relief=Tk.SUNKEN, borderwidth=2)
        label.pack(padx=10, pady=20)

        
        
    def on_press2(self, event):
        self.cvs.scan_mark(event.x, event.y)

    def on_motion(self, event):
        self.cvs.scan_dragto(event.x, event.y)




##------------------------------------------------------------
def demo(*av):
    """ function called by `index.py'"""
    d = Demo(False)
    d.demo_window.focus_set()

if __name__ == '__main__':
    d = Demo(True)
    d.demo_main_frame.mainloop()
EDIT June16 5:39 EDT

I just realized you also need template.py which I have added below:
template.py
"""
This is a template for demo codes of Tkinter.
"""

import sys
import string
import tkinter as Tk
#from ScrolledText import ScrolledText
from tkinter.scrolledtext import ScrolledText

## functions -------------------------------------

def read_contents(fname):
    """ read contens of `fname' """
    f = file(fname)
    str = f.read()
    f.close()
    return str


def newlist(n):
    ls = []
    for i in range(n):
        ls.append(None)
    return ls

def i_range(val, min, max):
    if(min and val < min):
        return(min)
    elif(max and val > max):
        return(max)
    else:
        return(val)

def bottom_slide(str, dx):
    ls0 = str.split('+')
    ls1 = ls0[0].split('x')
    return ('+%d+%d' % (int(ls0[1]) + dx, int(ls1[1]) + int(ls0[2]) + 50))


def left_slide(str):  
    ls0 = str.split('+')
    ls1 = ls0[0].split('x')
    return ('500x600+%d+%s' % (int(ls1[0]) + int(ls0[1]) + 50, ls0[2]))


def str_same_p(str0, str1):
    ls0=string.split(str0)
    ls1=string.split(str1)
    for s0, s1 in zip(ls0, ls1):
        if s0!=s1:
            return False
    else:
        return True


## classes ----------------------------------------



class ShowVars:
    """ a function class to show variables' value in a separated window """
    
    def __init__(self, demo_window, dx, *vars):
        self.demo_window = demo_window
        self.vars = vars
        self.toplevel = None
        self.dx = dx

    def __call__(self, *av):
        if self.toplevel:
            self.toplevel.focus_set()
        else:
            self.toplevel=Tk.Toplevel(self.demo_window)
            self.toplevel.title('Variable values')
            self.toplevel.geometry(bottom_slide( self.demo_window.winfo_geometry(), self.dx))
            frame = Tk.Frame(self.toplevel)
            frame.pack(fill=Tk.BOTH, expand=1)
            l0 = Tk.Label(frame, text='Variable values: ', font=('Helvetica', '14'))
            l0.pack(padx=10, pady=10)
            f= Tk.Frame(frame)
            for i, (label, var) in enumerate(self.vars):
                l1 = Tk.Label(f, text=label, justify=Tk.LEFT, anchor=Tk.W, width=15)
                l1.grid(row=i, column=0, sticky=Tk.W)
                l2 = Tk.Label(f, textvariable=var, justify=Tk.LEFT, anchor=Tk.W, width=20)
                l2.grid(row=i, column=1, sticky=Tk.W)
            f.pack(fill=Tk.BOTH, anchor=Tk.W, padx=20, pady=20)
            b=Tk.Button(frame, text='OK', command=self.destroy_window)
            b.pack(side=Tk.BOTTOM, padx=10, pady=10)
#            self.demo_window.focus_set()

    def destroy_window(self):
        if self.toplevel:
            self.toplevel.destroy()
            self.toplevel= None



class ButtonFrame(Tk.Frame):
    """ This is a Frame of two common buttons; dismess and (see code or return demo) """

    def __init__(self, master, b0_text, b0_command,  b1_text,  b1_command):
        Tk.Frame.__init__(self, master, height=35)
        b0 = Tk.Button(self, text=b0_text, width=10, command=b0_command)
        b1 = Tk.Button(self, text=b1_text, width=10, command=b1_command)
        b0.pack(side=Tk.LEFT,  padx=30, pady=5)
        b1.pack(side=Tk.LEFT,  padx=30, pady=5)



class Label(Tk.Label):
    """ a label class for the demo """
    
    def __init__(self, master, **key):   #justify=Tk.LEFT, font=("Helvetica", "12")
        key['justify'] = Tk.LEFT
        key['font'] = ("Helvetica", "12")
        Tk.Label.__init__(self, master, **key)
        self.pack(fill=Tk.X, padx=5, pady=5)



class Demo:
    """  A class defining demo window and source code window. """

    demo_window = None
    demo_main_frame = None
    demo_label = None
    demo_frame = None
    code_window = None
    
    def __init__(self, cmain, fname):
        self.fname = fname.split('.').pop(0) + '.py'
        self.cmain = cmain
        if cmain:
            self.demo_main_frame=Tk.Frame()
        else:
            self.demo_window = Tk.Toplevel()
            self.demo_main_frame=Tk.Frame(self.demo_window)

        self.ini_demo()


    def ini_demo(self):
        self.demo_frame = Tk.Frame(self.demo_main_frame)
        self.demo_buttons = ButtonFrame(self.demo_frame, "Dismiss", self.demo_destroy, "Show Code", self.show_code)
        self.demo_main_frame.pack(fill=Tk.BOTH, expand=1, padx=3, pady=3)
        self.demo_frame.pack(side=Tk.BOTTOM, fill=Tk.BOTH, expand=1)
        self.demo_buttons.pack(side=Tk.BOTTOM, expand=0, pady=5)


    def ini_demo_called_0(self):
        self.demo_window = Tk.Toplevel()
        self.demo_main_frame=Tk.Frame(self.demo_window)
        self.ini_demo()



    def ini_demo_called(self):
        pass



    def show_code(self):
        if not self.code_window:
            self.code_window = Tk.Toplevel()
            self.code_window.geometry(left_slide(self.demo_main_frame.master.winfo_geometry()))
            self.code_window.title(self.fname)
            self.code_frame = Tk.Frame(self.code_window)
            self.code_frame.pack(fill=Tk.BOTH, expand=1)
            self.scrolled_text = ScrolledText(self.code_frame, wrap=Tk.WORD)
            self.scrolled_text.pack(fill=Tk.BOTH, expand=1)
            self.content0 = read_contents(self.fname)
            self.scrolled_text.insert(Tk.END, self.content0)
            self.code_buttons = ButtonFrame(self.code_frame, "Dismiss", self.code_destroy, "Return Demo", self.return_demo)
            self.code_buttons.pack()
        self.code_window.focus_set()

    def code_destroy(self):
        self.code_window.destroy()
        self.code_window = None

    def demo_destroy(self):
        if self.cmain:
            sys.exit()
        else:
            self.demo_window.destroy()
            self.demo_window = None

    def return_demo(self):
        if self.cmain:
            self.demo_main_frame.focus_set()
        else:
            content = self.scrolled_text.get('1.0', Tk.END)
            if str_same_p(self.content0, content):
                if self.demo_window:
                    self.demo_window.focus_set()
                else:
                    self.ini_demo_called()
            else:
                if self.demo_window:
                    self.demo_window.destroy()
                f=file('temp.py', 'w')
                f.write(content)
                f.close()
                mod = __import__('temp')
                reload(mod)
                self.code_window.destroy()
                d = mod.Demo(False)
                d.demo_window.focus_set()
                d.demo_window.master.after(20, d.show_code)
            

##----------------------------------------------------
if __name__ == '__main__':
    class De (Demo):
        def __init__(self):
            Demo.__init__(self, True, __file__)
            Label(self.demo_frame, text="test test test")
            
    a = De()
    a.demo_main_frame.mainloop()
Gribouillis likes this post
Reply
#5
My attempt. Much room for improvements though. The scrollbar doesn't seem to want to work as intended.(The area between arrows should shrink with more data introduced but, doesn't). There does not appear to be any bounds on the scrolling buttons.
The scroll should stop at the button 0 and ending button but doesn't.

import tkinter as tk

class MyCanvas(tk.Canvas):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.container = tk.Frame(parent)
        self.container.grid(column=0, row=0, sticky='news')
        self.container.grid_columnconfigure(0, weight=3)

        self.canvas = tk.Canvas(self.container, bg='ivory')
        self.scrollbar = tk.Scrollbar(self.container, orient='vertical')

        self.canvas['yscrollcommand'] = self.scrollbar.set
        self.canvas['scrollregion'] = self.canvas.bbox('all')
        self.scrollbar['command'] = self.canvas.yview

        self.scrollbar.grid(column=1, row=0, sticky='ns')
        self.canvas.grid(column=0, row=0, sticky='news')

        self.frame = tk.Frame(self.canvas, bg='ivory')
        self.frame.grid(column=0, row=0, sticky='news')

        self.canvas.create_window(50, 10, window=self.frame, anchor='n')



class MyButton(tk.Button):
    def __init__(self, parent, text, col, row, command=None, *args, **kwargs):
        self.parent = parent
        super().__init__(*args, **kwargs)
        self.button = tk.Button(parent)
        self.button['text'] = text
        self.button['command'] = command
        self.button.grid(column=col, row=row, sticky='new', pady=4, padx=2)


root = tk.Tk()
root.geometry('+250+250')
canvas = MyCanvas(root)
col = 0
row = 0
for i in range(100):
    MyButton(canvas.frame, f'Button {i}', col, row)
    if col >= 4:
        row += 1
        col = 0
    else:
        col += 1
root.mainloop()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#6
I've gathered quite a lot of information pertaining to Tkinter and this link (below) shows a very simple example of the Scrollbar Widget, which would be my base for trying to do what you're doing. It could very well be that you've tried this and for whatever reason find it unsuitable. If so, please ignore this.

https://www.pythontutorial.net/tkinter/t...scrollbar/
Sig:
>>> import this

The UNIX philosophy: "Do one thing, and do it well."

"The danger of computers becoming like humans is not as great as the danger of humans becoming like computers." :~ Konrad Zuse

"Everything should be made as simple as possible, but not simpler." :~ Albert Einstein
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyQt] PyQt5 drawing on scrollable area HeinKurz 3 1,329 Mar-28-2023, 12:58 PM
Last Post: Axel_Erfurt
  [Tkinter] How to create scrollable frame mandiatutti 7 4,440 Aug-07-2021, 03:34 PM
Last Post: deanhystad
Question [Tkinter] Scrollable Treeview: change behavior of Prior/Next keys? RockDoctor 2 3,211 Apr-10-2021, 05:40 PM
Last Post: RockDoctor
  Scrollable big image in a window (TKinter) Prospekteur 3 4,474 Sep-14-2020, 03:06 AM
Last Post: Larz60+
  [Tkinter] Command button, then more command buttons Heyjoe 4 2,898 Aug-08-2020, 11:28 AM
Last Post: Yoriz
  [PySimpleGui] How to alter mouse click button of a standard submit button? skyerosebud 3 5,012 Jul-21-2019, 06:02 PM
Last Post: FullOfHelp
  [Tkinter] Fixate graphs on scrollable canvas janema 6 5,426 Apr-12-2019, 03:57 PM
Last Post: Larz60+
  [Tkinter] How to get & delete details from each input by clicking a button Vicolas 6 3,849 Feb-01-2019, 11:00 AM
Last Post: Vicolas
  Buttons not appearing, first time button making admiral_hawk 5 3,391 Dec-31-2018, 03:26 AM
Last Post: admiral_hawk
  [Tkinter] ListBox not contained or scrollable kellykimble 6 5,179 Apr-05-2018, 11:59 AM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020