Toykinter Listbox
Oct 26, 2020 5:14:38 GMT -8
Post by Uncle Buddy on Oct 26, 2020 5:14:38 GMT -8
import tkinter as tk
import widgets as wdg
import styles as st
import utes
import files
formats = st.make_formats_dict()
ST = st.ThemeStyles()
class Listbox(wdg.FrameHilited4):
'''
Tkinter Listbox and Text widget don't interact well. I
don't know why. When using a Tkinter Listbox to select
among subtopics for displaying notes in a Text widget,
it works but then to select text in the text widget you
have to click several times. For example, the first
double-click (meant to select a word) sends the cursor to
the end of the text. This happens with the code stripped
all the way down till there's nothing left but a tk.Listbox
and a tk.Text. This class replaces the tk.Listbox for a Text
widget that selects a single note display topic from a list.
The scrollbar has some redundant features since you can arrow
through the list and if list item doesn't fit horizontally, a
tooltip tells the user what the hidden text says. So a scrollbar
is still needed because, if the list is long, and scrollbar=False,
there's no scrollbar slider to tell how long the list is. But
if you use the auto-hiding scrollbar wdg.AutoScrollbar instead of
tk.Scrollbar, scrollbar=False has no effect (the scrollbar
is there if needed and not there if not needed). But to get rid
of the horizontal scrollbar, I've decided to use the autohiding
scrollbar for the vertical scrollbar only and use tk.Scrollbar
for the horizontal scrollbar so I can use scrollbar=False to turn
it off. Kinda silly features roulette but not ready to slash out
extraneous code till I've used this in practice and tested it more.
Currently this only lets you select one listbox item at a time, but
in other ways it's at least as practical as the tkinter Listbox. I
used the same method names so Tkinter developers could switch
easily to this one.
'''
def __init__(
self,
master,
items,
view_height=400,
view_width=150,
scrollbar=False,
**options):
wdg.FrameHilited4.__init__(self, master, **options)
self.master = master
self.items = items
self.view_height = view_height
self.view_width = view_width
self.scrollbar = scrollbar
self.master.grid_columnconfigure(0, weight=1)
self.master.grid_rowconfigure(0, weight=1)
self.config(takefocus=1)
self.bind('<FocusIn>', self.expand_on_focus)
self.bind('<FocusOut>', self.shrink_on_unfocus)
self.bind('<KeyPress>', self.highlight_first_last)
self.old_row = 0
self.make_widgets()
self.list_height = self.listbox_content.winfo_reqheight()
def make_widgets(self):
self.listbox_canvas = wdg.CanvasHilited(
self,
width=self.view_width,
height=self.view_height)
xsb = tk.Scrollbar(
self,
width=16,
orient="horizontal",
command=self.listbox_canvas.xview)
self.ysb = wdg.AutoScrollbar(
self,
width=16,
orient="vertical",
command=self.listbox_canvas.yview)
self.listbox_canvas.configure(
yscrollcommand=self.ysb.set, xscrollcommand=xsb.set)
self.listbox_canvas.configure(scrollregion=(0,0,300,1500))
xsb.grid(row=1, column=0, sticky="ew")
self.ysb.grid(row=0, column=1, sticky="ns")
xsb.grid_remove()
self.ysb.grid_remove()
if self.scrollbar is True:
xsb.grid()
self.ysb.grid()
self.listbox_canvas.grid(column=0, row=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.listbox_content = wdg.FrameHilited3(self.listbox_canvas)
self.listbox_content.grid_columnconfigure(0, weight=1, minsize=self.view_width)
# Currently there can be no other event callbacks
# triggered by FocusIn or this virtual event won't work.
self.listbox_content.event_add('<<ListboxSelected>>', '<FocusIn>')
self.make_listbox_content()
self.listbox_canvas.create_window(
0, 0, anchor='nw', window=self.listbox_content)
self.resize_scrollbar()
def make_listbox_content(self):
for child in self.listbox_content.winfo_children():
child.destroy()
g = 0
for item in self.items:
lab = wdg.LabelHilited(
self.listbox_content,
text=item,
anchor='w',
takefocus=0)
lab.grid(column=0, row=g, sticky='ew', padx=3)
lab.bind('<FocusOut>', self.unhighlight)
lab.bind('<Button-1>', self.select_item)
lab.bind('<Tab>', self.unfocus_listbox)
lab.bind('<KeyPress>', self.traverse_on_arrow)
if lab.winfo_reqwidth() > self.view_width:
utes.create_tooltip(lab, item)
g += 1
h = g
def get(self, idx):
items = self.listbox_content.winfo_children()
got = items[idx].cget('text')
return got
def delete(self, idx):
items = self.items
del items[idx]
self.items = items
self.make_listbox_content()
def selection_set(self, idx):
items = self.listbox_content.winfo_children()
items[idx].config(bg=formats['bg'])
def selection_clear(self):
for child in self.listbox_content.winfo_children():
child.config(bg=formats['highlight_bg'])
def size(self):
items_qty = len(self.items)
return items_qty
def insert(self, idx, stg):
'''
Create a new listbox item (stg) and insert it
into the list of items at the index referenced
by before_idx.
'''
items = self.items
new_item = stg
items.insert(idx, new_item)
self.items = items
self.make_listbox_content()
def curselection(self):
'''
Later versions should make it possible to select
more than one item but it's not needed right now.
'''
selected = None
items = self.listbox_content.winfo_children()
highlighted = st.formats['bg']
for child in items:
if child.cget('bg') == highlighted:
selected = items.index(child)
break
return selected
def highlight_first_last(self, evt):
if evt.keysym not in ('Down', 'Up'):
return
items = []
for child in self.listbox_content.winfo_children():
items.append(child)
len_items = len(items)
first = items[0]
last = items[len_items-1]
if evt.keysym == 'Down':
first.config(bg=formats['bg'])
first.focus_set()
self.listbox_canvas.yview_moveto(0.0)
elif evt.keysym == 'Up':
last.config(bg=formats['bg'])
last.focus_set()
self.listbox_canvas.yview_moveto(1.0)
def expand_on_focus(self, evt):
self.config(bd=4)
for child in self.listbox_content.winfo_children():
child.config(takefocus=1)
def shrink_on_unfocus(self, evt):
self.config(bd=2)
def unfocus_listbox(self, evt):
for child in self.listbox_content.winfo_children():
child.config(takefocus=0)
def resize_scrollbar(self):
self.update_idletasks()
self.listbox_canvas.config(
scrollregion=self.listbox_canvas.bbox("all"))
def select_item(self, evt, next_item=None, prev_item=None):
for widg in self.listbox_content.winfo_children():
widg.config(bg=formats['highlight_bg'])
if evt.type == '4':
selected_item = evt.widget
elif evt.type == '2' and evt.keysym == 'Down':
selected_item = next_item
elif evt.type == '2' and evt.keysym == 'Up':
selected_item = prev_item
selected_item.config(bg=formats['bg'])
widget_ht = int(self.list_height / len(self.items))
widget_pos_in_screen = selected_item.winfo_rooty()
widget_pos_in_list = selected_item.winfo_y()
window_top = self.winfo_rooty()
window_bottom = window_top + self.view_height
window_ratio = self.view_height / self.list_height
list_ratio = widget_pos_in_list / self.list_height
widget_ratio = widget_ht / self.list_height
up_ratio = list_ratio - window_ratio + widget_ratio
if widget_pos_in_screen > window_bottom - 0.75 * widget_ht:
self.listbox_canvas.yview_moveto(float(list_ratio))
elif widget_pos_in_screen < window_top:
self.listbox_canvas.yview_moveto(float(up_ratio))
selected_item.focus_set()
def unhighlight(self, evt):
evt.widget.config(bg=formats['highlight_bg'])
def traverse_on_arrow(self, evt):
if evt.keysym not in ('Up', 'Down'):
return
len_items = len(self.items)
widg_ht = int(
self.listbox_content.winfo_reqheight()/len_items)
self.trigger_down = self.view_height - widg_ht * 3
self.trigger_up = self.view_height - widg_ht * 2
self.update_idletasks()
items = self.listbox_content.winfo_children()
next_item = evt.widget.tk_focusNext()
prev_item = evt.widget.tk_focusPrev()
rel_ht = evt.widget.winfo_y()
if evt.keysym == 'Down':
if next_item in items:
self.select_item(evt, next_item=next_item)
else:
next_item = items[0]
next_item.focus_set()
next_item.config(bg=formats['bg'])
self.listbox_canvas.yview_moveto(0.0)
elif evt.keysym == 'Up':
if prev_item in items:
self.select_item(evt, prev_item=prev_item)
else:
prev_item = items[len_items-1]
prev_item.focus_set()
prev_item.config(bg=formats['bg'])
self.listbox_canvas.yview_moveto(1.0)
if __name__ == '__main__':
items = [
'red', 'pink', 'purple', 'magenta',
'green', 'brown', 'gray', 'black',
'yellow', 'white', 'chartreuse', 'aqua',
'beige', 'tan', 'violet', 'teal',
'ivory', 'rose', 'silver', 'gold',
]
# items = ['Maecenas lorem ipsem corvallis', 'quis', 'elit', 'eleifen', 'lobortis', 'turpis', 'iaculi', 'odio', 'Phasellus', 'congue', 'urna', 'sit', 'amet', 'posuere', 'luctus', 'mauris', 'risus', 'tincidunt', 'sapie', 'vulputate', 'scelerisqu', 'ipsum', 'libero', 'at', 'neque', 'Nunc', 'accumsan', 'pellentesque', 'nulla', 'a', 'ultricies', 'ex', 'convallis', 'sit', 'ame', 'Etiam', 'ut', 'sollicitudi', 'felis ', 'sit', 'amet', 'dictum', 'lacus', 'Mauris', 'sed', 'mattis', 'diam', 'Pellentesque', 'eu', 'malesuada', 'ipsu', 'vitae', 'sagittis', 'nisl', 'Morbi', 'a', 'mi', 'vitae', 'nunc', 'varius', 'ullamcorper', 'in', 'ut', 'urna', 'Maecenas', 'auctor', 'ultrices', 'orc', 'Donec', 'facilisis', 'tortor', 'pellentesque', 'venenati', 'Curabitur', 'pulvina', 'bibendum', 'se', 'id', 'eleifend', 'lorem', 'sodales', 'ne', 'Mauri', 'eget', 'scelerisque', 'liber', 'Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'eli', 'Integer', 'vel', 'tellus', 'ne', 'orci', 'finibus', 'ornar', 'Praesent', 'pellentesque', 'aliquet', 'augue', 'nec', 'feugiat', 'augue', 'pos']
def test_curselection():
selected = lb.curselection()
print('264 selected is', selected)
def test_insert():
new_item = c.get()
lb.insert(3, new_item)
def test_size():
qty = lb.size()
print('qty is', qty)
def test_selection_clear():
lb.selection_clear()
def test_selection_set():
lb.selection_set(8)
def test_delete():
lb.delete(6)
def test_get():
idx = 4
got = lb.get(idx)
print('got is', got)
root = tk.Tk()
root.geometry('1200x600+900+100')
left = wdg.Frame(root)
left.grid(column=0, row=0)
right = wdg.Frame(root)
right.grid(column=1, row=0)
lb = Listbox(
left,
items,
view_height=400,
view_width=150,
scrollbar=False)
lb.grid(column=0, row=0, padx=24, pady=24)
tx = wdg.Text(right)
tx.grid(column=1, row=0)
test_box = wdg.Frame(root)
test_box.grid(column=0, row=1, columnspan=2)
b = wdg.Button(
test_box, text='Test Curselection', command=test_curselection)
b.grid(column=0, row=0)
c = wdg.Entry(test_box)
c.grid(column=1, row=0)
d = wdg.Button(test_box, text='Test Insert', command=test_insert)
d.grid(column=2, row=0)
e = wdg.Button(test_box, text='Test Size', command=test_size)
e.grid(column=3, row=0)
f = wdg.Button(
test_box, text='Test Selection_clear', command=test_selection_clear)
f.grid(column=0, row=1)
g = wdg.Button(
test_box, text='Test Selection_set', command=test_selection_set)
g.grid(column=1, row=1)
h = wdg.Button(test_box, text='Test Delete', command=test_delete)
h.grid(column=2, row=1)
i = wdg.Button(test_box, text='Test Get', command=test_get)
i.grid(column=3, row=1)
ST.config_generic(root)
root.mainloop()