I had a similar need and cobbled this control widget CheckListBox it is based off of numerous web examples. Had I known I would have shared this code I would have kept the references.
Note I cobbled it together I did not originate the major sections like the ScrolledWindow class.
Main features,
Checks [0,1,-1]
0 unchecked, 1 checked , -1 no check image
Options Item image
Appdata on items:
Any id text like tooltext
CheckListBox Demo Image
import os, sys
import tkinter as tk
from tkinter import ttk
from PIL import Image,ImageTk
class ScrolledWindow(tk.Frame):
def __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs):
self.up = False
super().__init__(parent, *args, **kwargs)
self.parent = parent
# creating a scrollbars
self.xscrlbr = ttk.Scrollbar(self.parent, orient = 'horizontal')
self.xscrlbr.grid(column = 0, row = 1, sticky = 'ew', columnspan = 2)
self.yscrlbr = ttk.Scrollbar(self.parent)
self.yscrlbr.grid(column = 1, row = 0, sticky = 'ns') # ***** Grid
# creating a canvas
self.canv = tk.Canvas(self.parent , border=1 , borderwidth=2)
# placing a canvas into frame
self.canv.grid(column = 0, row = 0, sticky = 'nsew') # ***** Grid
# accociating scrollbar comands to canvas scroling
self.xscrlbr.config(command = self.canv.xview)
self.yscrlbr.config(command = self.canv.yview)
# creating a frame to inserto to canvas
self.scrollwindow = ttk.Frame(self.parent, width=canv_w, height=canv_h)
self.scrollwindow.grid(column = 0, row = 0, sticky = 'nsew') # grid
self.canv.create_window(0, 0, window = self.scrollwindow, anchor = 'nw')
self.canv.config(xscrollcommand = self.xscrlbr.set,
yscrollcommand = self.yscrlbr.set,
scrollregion = (0, 0, 100, 100) # scrollregion = (0, 0, 100, 100)
)
self.yscrlbr.lift(self.scrollwindow)
self.xscrlbr.lift(self.scrollwindow)
self.scrollwindow.bind('<Configure>', self._configure_window)
self.scrollwindow.bind('<Enter>', self._bound_to_mousewheel)
self.scrollwindow.bind('<Leave>', self._unbound_to_mousewheel)
def _bound_to_mousewheel(self, event):
self.canv.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbound_to_mousewheel(self, event):
self.canv.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event):
self.canv.yview_scroll(int(-1*(event.delta/120)), "units")
def _configure_window(self, event):
# update the scrollbars to match the size of the inner frame
size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight())
self.canv.config(scrollregion='0 0 %s %s' % size)
if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
# update the canvas's width to fit the inner frame
self.canv.config(width = self.scrollwindow.winfo_reqwidth())
if self.up == True and self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
#if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
# update the canvas's width to fit the inner frame
self.canv.config(height = self.scrollwindow.winfo_reqheight())
class CheckListBox( tk.Frame ):
def __init__(self, parent, **kw):
# x = kw.pop( 'x' ) if 'x' in kw else 300
# y = kw.pop( 'y' ) if 'y' in kw else 300
#tk.Toplevel.__init__(self, self.parent, **kw)
#self.geometry(f'{width}x{height}+{x}+{y}')
if 'master' in kw and parent is None:
parent = kw.pop('master')
self.parent = parent
tk.Frame.__init__(self, parent, **kw )
self.height = kw.pop( 'height') if 'height' in kw else 250
self.width = kw.pop( 'width') if 'width' in kw else 550
self.win = ScrolledWindow( self , self.width , self.height)
self.tframe= self.win.scrollwindow
#self.tframe.pack_propagate(False)
pngd = os.path.dirname(os.path.realpath(__file__)) + "/_common/images/"
self.checked = checkedImg = tk.PhotoImage( file= pngd+"Checked_18.png" )
self.unchecked = uncheckedImg = tk.PhotoImage( file= pngd+"CheckedNot_18.png" )
#canv.create_image(0, 0, image=photoImg)
self.items=[]
self.win.canv.config(height = self.height)
self.win.up=False
def getvalue( self, values , matchstr , default ):
it = matchstr.strip().lower() # case insensitive match
for idx in range( 1, len(values) ): # skip the first entry that is the widget id
try: # do not let an execption stop us
p = values[ idx ].split( '=')
if p[0].strip().lower() == it: # case insensitive match
return p[1]
except: pass;
return default # Return the default
def setvalue( self, values , matchstr , data=None ):
it = matchstr.strip().lower() # case insensitive match
for idx in range( 1, len(values ) ): # skip the first entry that is the widget id
try:
p = values[ idx ].split( '=')
if p[0].strip().lower() == it:
if data == None or data =="": # empty data indicates deletion request
return values.remove( values[ idx ] ) # remove the entry in the list
values[idx] = matchstr + '=' + str(data) # Since it exists update the data
return
except: pass;
values.append( matchstr + '=' + str(data) ) # New data so append it
return
def OnItemDouble( self, ev, idx ) :
values = self.items[ idx ] # Get the values assigned to the item at [ idx ]
b = int( self.getvalue( values, 'chk',0 ) ) # Get the chk value set it to 0 if not set
b = 0 if b == 1 else 1 # Toggle the value and then get the image
self.setvalue( values, 'chk' , b ) # Save its new state
img = self.checked if b == 1 else self.unchecked
textctl=ev.widget # textctl=values[0] either will work
textctl.config(state='normal') # Set state to normale so we can overwrite
textctl.delete("1.0", "1.1") # Delete the line data including image
textctl.image_create( "1.0", image=img) # Add the checkmark image first
textctl.config( state='disabled') # Then set the state to readonly again
#textctl.config( state='disabled', bg='blue') # Then set the state to readonly again
print( values )
def append( self , txt, **kw ):
values=[]
bchk=int( kw.pop( 'chk')) if 'chk' in kw else -1
appdata = kw.pop( 'appdata') if 'appdata' in kw else []
pic = kw.pop( 'image') if 'image' in kw else None
textctl = tk.Text( self.tframe,height=1,width=self.width, **kw)
textctl.grid( column=0,row= len( self.items),sticky = 'nsew') # +++ nsew???
values.append( textctl ) # The text control is always first
self.setvalue( values , 'chk' , str(bchk) )
self.setvalue( values , 'image' , pic )
self.setvalue( values , 'text' , txt )
if bchk>=0:
img = self.checked if bchk==1 else self.unchecked
textctl.image_create( "end", image=img)
if pic : textctl.image_create( "end", image=pic)
textctl.insert("end", txt )
values += appdata
self.items.append( values )
idx = len( self.items ) -1
textctl.config(state=tk.DISABLED, highlightthickness = 0, borderwidth=1)
textctl.bind("<Double-Button-1>", lambda ev=None, x=idx: self.OnItemDouble( ev, x) )
return idx
def insert( self, idx, txt , **kw ):
values=[]
if idx < 0 : return -1
if idx > len( self.items ): return self.append( txt,**kw)
bchk=int( kw.pop( 'chk')) if 'chk' in kw else -1
appdata = kw.pop( 'appdata') if 'appdata' in kw else []
pic = kw.pop( 'image') if 'image' in kw else None
textctl = tk.Text( self.tframe,height=1,width=self.width, **kw)
values.append( textctl ) # The text control is always first
self.setvalue( values , 'chk' , str(bchk) )
self.setvalue( values , 'image' , pic )
self.setvalue( values , 'text' , txt )
self.items.insert( idx, values )
for i in range( 0 , len( self.items )):
self.items[i][0].grid_forget()
for i in range( 0, len( self.items )):
values = self.items[i]
textctl = values[0]
textctl.config(state='normal') # To change the text and images
textctl.grid( column=0,row=i,sticky='nsew')
if i==idx:
textctl.delete('1.0','end')
bchk = int( self.getvalue( values ,'chk',0))
if bchk >= 0:
img = self.checked if bchk == 1 else self.unchecked
textctl.image_create( "end", image=img)
pic = self.getvalue( values ,'image',"")
if pic !="None" : textctl.image_create( "end", image=pic)
txt = self.getvalue( values, 'text' ,"")
textctl.insert( "end", txt )
textctl.config(state=tk.DISABLED, highlightthickness = 0, borderwidth=1)
textctl.bind("<Double-Button-1>", lambda ev=None, x=i: self.OnItemDouble( ev, x) )
return len( self.items )
def curselection( self ):
sellist = []
for idx in range( 0, len( self.items ) ):
values = self.items[ idx ]
if int( self.getvalue( values, 'chk', '0') ) > 0:
sellist.append( idx )
return sellist
def get( self, idx, match='text' , default='N/A'):
sellist = []
if idx in range( 0, len( self.items )) :
values = self.items[ idx ]
return self.getvalue( values, match,default)
return default
def delete( self, idx ):
if idx < 0 or idx > len( self.items ): return -1
ctl=self.items[idx][0]
ctl.grid_forget() # forget it existed
ctl=None # delete the existing text control
return len( self.items )
if __name__ == "__main__":
def getsel( ctl ):
lst = ctl.curselection( )
for i in lst:
print( "Got ", ctl.get( i ) , ctl.get( i , 'altdata') ,ctl.get(i,'chk') )
# create a root window.
top = tk.Tk()
top.geometry("+300+300")
top.title("Check Listbox Demo")
topframe = tk.Frame( top)
topframe.pack()
label = tk.Label( topframe, width=10,text="Double click to toggle check.")
label.pack( side='top',fill='x')
pngd = os.path.dirname(os.path.realpath(__file__)) + "/_common/images/"
fileImg = tk.PhotoImage( file= pngd+"list_16.png" )
folderImg = tk.PhotoImage( file= pngd+"network_folder_18.png" )
leftframe = tk.Frame( topframe)
leftframe.pack(side='left')
# create listbox object
example = CheckListBox(leftframe,height=60,width=20)
example.pack(side="left", fill="both", expand=True)
for i in range( 0,10):
data = []
example.setvalue( data , "altData","Some data here."+str( i +1) )
example.setvalue( data ,"tip", "Item Tool Tip here")
pic = folderImg if i%2==0 else fileImg
example.append( "First text " + str( i +1) , bg='lightgray' , chk=i%2 , image=pic, appdata=data)
rightframe = tk.Frame( topframe)
rightframe.pack(side='left')
example2 = CheckListBox(rightframe ,height=100,width=30 )
example2.pack(side="left", fill="both", expand=True)
for i in range( 0,10):
if i%2==0:
example2.append( "Second text " + str( i + 1 ) , chk=1 , bg='#C0F862')
else:
example2.append( "Second text " + str( i + 1 ) , chk=1 , bg='#7FF1C0')
testframe = tk.Frame( top )
testframe.pack()
example3 = CheckListBox(testframe,height=80,width=30 )
example3.pack()
for i in range( 0,10):
if i%2==0:
example3.append( "Third text " + str( i + 1 ) , image=folderImg )
else:
example3.append( "Third text " + str( i +1), chk=1 , image=fileImg)
#example.insert( 11, "New First text 11" , chk=1 )
example.delete( 3)
example.insert( 3, "New Four " , chk=1 )
#example.insert( 0, "New First text 1" , chk=1 )
button = tk.Button( text="print selected in first", command=lambda x=example : getsel(x) ).pack()
top.mainloop()