How can I pass another argument when binding with tkinter? - python

I just forayed into tkinter for the first time and am running into some issues. I want to display several lists to users, and store their selections for each list in a dictionary (used to filter several columns of a dataframe later). Suppose, for instance, there are two lists: 1) One labeled "Brand", containing 'Brand X' and 'Brand Y' as options, 2) another "Customer Type", containing "New," "Existing," "All."
In sum, when all is said and done, if a user picks "Brand X", "New", and "All", then I'd get a dictionary back of {'Brand':['Brand X'],'Customer Type':['New','All']}. Getting one list is easy... but looping through the lists is presenting problems.
I have the below code so far:
from tkinter import *
from tkinter import ttk
class checkList(Frame):
def __init__(self, options, parent=None):
Frame.__init__(self, parent)
self.makeHeader()
self.options = options
self.pack(expand=YES, fill=BOTH, side=LEFT)
self.makeWidgets(self.options)
self.selections = []
def makeHeader(self):
header = ttk.Label(self,text='Please select options to limit on.')
header.pack(side=TOP)
self.header = header
def makeWidgets(self, options):
for key in self.options.keys():
lbl = ttk.Label(self, text=key)
lbl.pack(after=self.header)
listbox = Listbox(self, selectmode=MULTIPLE, exportselection=0)
listbox.pack(side=LEFT)
for item in self.options[key]:
listbox.insert(END, item)
listbox.bind('<<ListboxSelect>>', self.onselect)
self.listbox = listbox
def onselect(self, event):
selections = self.listbox.curselection()
selections = [int(x) for x in selections]
self.selections = [self.options[x] for x in selections]
if __name__ == '__main__':
options = {'Brand':['Brand','Brand Y'], 'Customer Type': ['All Buyers','New Buyers','Existing Buyers']}
checkList(options).mainloop()
Needless to say, the [self.options[x] for x in selections] works great with just one list, but since I have a dictionary, I really need [self.options[key][x] for x in selections]. However, I can't figure out how to pass the key at any given point in the loop. Is there a way to achieve what I'm trying to do?

The "magic" you're looking for to pass the key is simple because the tkinter objects are extensible. Here's your code working, I believe, the way you want:
from tkinter import *
from tkinter import ttk
class checkList(Frame):
def __init__(self, options, parent=None):
Frame.__init__(self, parent)
self.makeHeader()
self.options = options
self.pack(expand=YES, fill=BOTH, side=LEFT)
self.listboxes = [] # New
self.makeWidgets(self.options)
self.selections = {} # Changed
def makeHeader(self):
header = ttk.Label(self,text='Please select options to limit on.')
header.pack(side=TOP)
self.header = header
def makeWidgets(self, options):
for key in self.options.keys():
lbl = ttk.Label(self, text=key)
lbl.pack(after=self.header)
listbox = Listbox(self, selectmode=MULTIPLE, exportselection=0)
listbox.key = key # here's the magic you were asking about...
listbox.pack(side=LEFT)
self.listboxes.append(listbox) # New
for item in self.options[key]:
listbox.insert(END, item)
listbox.bind('<<ListboxSelect>>', self.onselect)
self.listbox = listbox
def onselect(self, event):
for lb in self.listboxes:
selections = lb.curselection()
selections = [int(x) for x in selections]
self.selections[lb.key] = [self.options[lb.key][x] for x in selections]
print(self.selections)
if __name__ == '__main__': # \/
options = {'Brand':['Brand X','Brand Y'], 'Customer Type': ['All Buyers','New Buyers','Existing Buyers']}
checkList(options).mainloop()

With the code you posted, you only have access to the last ListBox created by makeWidgets in onselect.
With minimal changes:
from tkinter import *
from tkinter import ttk
class checkList(Frame):
def __init__(self, options, parent=None):
Frame.__init__(self, parent)
self.listboxes = []
self.selections = {}
self.makeHeader()
self.options = options
self.pack(expand=YES, fill=BOTH, side=LEFT)
self.makeWidgets(self.options)
def makeHeader(self):
header = ttk.Label(self,text='Please select options to limit on.')
header.pack(side=TOP)
self.header = header
def makeWidgets(self, options):
for key in self.options.keys():
lbl = ttk.Label(self, text=key)
lbl.pack(after=self.header)
listbox = Listbox(self, selectmode=MULTIPLE, exportselection=0)
listbox.pack(side=LEFT)
for item in self.options[key]:
listbox.insert(END, item)
listbox.bind('<<ListboxSelect>>', self.onselect)
self.listboxes.append(listbox)
def onselect(self, event):
for (option, options), listbox in zip(self.options.items(), self.listboxes):
self.selections[option] = [options[x] for x in map(int, listbox.curselection())]
print(self.selections)
if __name__ == '__main__':
options = {'Brand':['Brand','Brand Y'], 'Customer Type': ['All Buyers','New Buyers','Existing Buyers']}
checkList(options).mainloop()
This recreates selections every time either ListBox selection is modified. Alternatively, you could use event to determine which ListBox selection was modified and update the corresponding part of selections. This would require initializing selections, though.

Related

Python 3.x - using text string as variable name

I'm trying to avoid to multiply functions in code by using
def Return_Label(self,number)
with a parameter.
Any Idea how to use string in order to define variable name usable to .set value to StringVar()?
Example code below:
import tkinter as tk
from tkinter import *
class WINDOW():
def __init__(self):
self.Settings_Window()
def Settings_Window(self):
self.settings_window = tk.Tk()
self.settings_window.minsize(200,200)
self.entry = Entry(self.settings_window)
self.entry.pack()
self.entry2 = Entry(self.settings_window)
self.entry2.pack()
self.label1input = StringVar()
self.label = Label(self.settings_window,textvariable=self.label1input, bg='yellow')
self.label.pack(expand='yes',fill='x')
self.label2input = StringVar()
self.label2 = Label(self.settings_window, textvariable=self.label2input, bg='yellow')
self.label2.pack(expand='yes', fill='x')
self.button = Button(self.settings_window,text='SETUP1',command=self.Next)
self.button.pack()
self.button2 = Button(self.settings_window,text='SETUP2',command=self.Next2)
self.button2.pack()
self.settings_window.mainloop()
def Next(self):
self.number=1
self.Return_Label(self.number)
def Next2(self):
self.number=2
self.Return_Label(self.number)
def Return_Label(self,number):
self.entry_field_value = self.entry.get()
print(self.entry_field_value)
#self.label1input.set(self.entry_field_value)
setattr(self,'label'+str(number)+'input.set',self.entry_field_value)
window=WINDOW()
I prefer a list approach to managing multiple entry fields and updating values.
By using list you can use the index value to manage the labels as well :D.
See the below example of how you can use list to deal with all the values and updates.
import tkinter as tk
from tkinter import *
class Window(tk.Tk):
def __init__(self):
super().__init__()
self.minsize(200, 200)
self.entry_list = []
self.label_list = []
entry_count = 2
for i in range(entry_count):
self.entry_list.append(Entry(self))
self.entry_list[i].pack()
for i in range(entry_count):
self.label_list.append(Label(self,bg='yellow'))
self.label_list[i].pack(expand='yes', fill='x')
Button(self, text='SETUP', command=self.Return_Label).pack()
def Return_Label(self):
for ndex, lbl in enumerate(self.label_list):
lbl.config(text=self.entry_list[ndex].get())
if __name__ == '__main__':
Window().mainloop()
Create lists of objects rather than individual attributes for each object. For example,
import tkinter as tk
from tkinter import *
class Window:
def __init__(self):
self.settings_window()
def Settings_Window(self):
self.settings_window = tk.Tk()
self.settings_window.minsize(200,200)
self.entries = [
Entry(self.settings_window),
Entry(self.settings_window)
]
for e in self.entries:
e.pack()
self.labelinputs = [
StringVar(),
StringVar()
]
self.labels = [
Label(self.settings_window, textvariable=label, bg='yellow')
for label in self.labelinputs
]
for l in self.labels:
l.pack(expand='yes', fill='x')
self.buttons = [
Button(self.settings_window,text='SETUP1',command=lambda: self.return_label(0))
Button(self.settings_window,text='SETUP2',command=lambda: self.return_label(1))
]
for b in self.buttons:
b.pack()
self.settings_window.mainloop()
def return_label(self,number):
entry_field_value = self.entry.get()
self.labelsinput[number].set(entry_field_value)
window=WINDOW()
Dynamicly computing variable names should be avoided at all costs. They are difficult to do correctly, and it makes your code hard to understand, hard to maintain, and hard to debug.
Instead, store the widgets in a dictionary or list. For example:
def __init___(self):
...
self.vars = {}
...
self.vars[1] = StringVar()
self.vars[2] = StringVar()
...
def Return_Label(self,number):
self.entry_field_value = self.entry.get()
var = self.vars[number]
var.set(self.entry_field_value)
Though, you really don't need to use StringVar at all -- they usually just add extra overhead without providing any extra value. You can save the labels instead of the variables, and call configure on the labels
self.labels[1] = Label(...)
...
self.labels[number].configure(text=self.entry_field_value)

tkinter ttk OptionMenu losing checkmark highlight when updating option list

When I update a ttk.OptionMenu widget using this example: https://stackoverflow.com/a/7403530 , I lose the check mark that showed up before when I selected an item if I was using the initial list of items.
How do I get back the checkmark for the selected item?
Init Code: self.om = ttk.OptionMenu(self, self.om_variable,'a', *['a','b','c'])
Before:
After Update:
Code here:
import tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.om_variable = tk.StringVar(self)
b1 = tk.Button(self, text="Colors", width=8, command=self.use_colors)
b2 = tk.Button(self, text="Sizes", width=8, command=self.use_sizes)
self.om = tk.OptionMenu(self, self.om_variable, ())
self.om.configure(width=20)
self.use_colors()
b1.pack(side="left")
b2.pack(side="left")
self.om.pack(side="left", fill="x", expand=True)
def _reset_option_menu(self, options, index=None):
'''reset the values in the option menu
if index is given, set the value of the menu to
the option at the given index
'''
menu = self.om["menu"]
menu.delete(0, "end")
for string in options:
menu.add_command(label=string,
command=lambda value=string:
self.om_variable.set(value))
if index is not None:
self.om_variable.set(options[index])
def use_colors(self):
'''Switch the option menu to display colors'''
self._reset_option_menu(["red","orange","green","blue"], 0)
def use_sizes(self):
'''Switch the option menu to display sizes'''
self._reset_option_menu(["x-small", "small", "medium", "large"], 0)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
The ttk version, which you used previously, adds the checkmark functionality by default and a check will appear on the selected item. However, when you manually add items, you'll need to use the method add_radiobutton instead of add_command. This is what enables the check mark (on both tk and ttk versions).
import tkinter.tkk as tkk
def __init__(self, *args, **kwargs):
...
self.om = ttk.OptionMenu(self, self.om_variable)
...
def _reset_option_menu(self, options, index=None):
...
menu.add_radiobutton(
label=string,
command=tk._setit(self.om_variable, string)
)
...

Disable the List box after double click action - Python

Made the list box to display and able to parse the selected values from listbox with the help of few sample examples, but not able to disable after selecting
def createlistBox:
list1 = ['apple','mango']
listBOXName = tk.Listbox(root,selectmode = "SINGLE")
listBOXName.grid()
for j in range(len(list1)):
listBOXName.insert(tk.END,list1 [j])
listBoxName.bind("<Double-Button-1>", selectlist1)
def selectlist1(evnt):
lst = evnt.widget
index = int(lst.curselection()[0])
value = lst.get(index)
I want to disable the list when it calls selectlist1
You can use the w.configure(state=tk.DISABLED) to disable the listbox. Here w refers to the widget.
Test Script:
# tkinter modules
import tkinter as tk
class App(tk.Frame):
def __init__(self, parent=None, *args, **options):
# Initialise App Frame
tk.Frame.__init__(self, parent)
self.parent = parent
self.createlistBox()
def createlistBox(self):
list1 = ['apple','mango']
listBOXName = tk.Listbox(self, selectmode = "SINGLE")
listBOXName.grid()
for j in range(len(list1)):
listBOXName.insert(tk.END,list1 [j])
listBOXName.bind("<Double-Button-1>", self.selectlist1)
def selectlist1(self, evnt):
lst = evnt.widget
index = int(lst.curselection()[0])
value = lst.get(index)
lst.configure(state=tk.DISABLED) #Add this command after selection
if __name__ == '__main__':
root = tk.Tk()
root.geometry('400x350+300+300')
app = App(root)
app.grid(row=0, column=0, sticky='nsew')
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
root.mainloop()

Format individual cell/item rather than entire row in tkinter ttk treeview

I am currently using a function to display a pandas dataframe in a spreadsheet style format. I would like to be able to add some functionality to format individual cells of the treeview based on their content e.g. if they contain substring 'X' or if their value is higher than Y.
The update function currently implemented is as follows:
def updateTree(self, dataframe):
'''
Updates the treeview with the data in the dataframe
parameter
'''
#Remove any nan values which may have appeared in the dataframe parameter
df = dataframe.replace(np.nan,'', regex=True)
#Currently displayed data
self.treesubsetdata = dataframe
#Remove existing items
for item in self.tree.get_children(): self.tree.delete(item)
#Recreate from scratch the columns based on the passed dataframe
self.tree.config(columns= [])
self.tree.config(columns= list(dataframe.columns))
#Ensure all columns are considered strings and write column headers
for col in dataframe.columns:
self.tree.heading(col,text=str(col))
#Get number of rows and columns in the imported script
self.rows,self.cols = dataframe.shape
#Populate data in the treeview
for row in dataframe.itertuples():
self.tree.insert('', 'end',values = tuple(row[1:]))
#Minimise first column
self.tree.column('#0',width=0)
self.tree.update()
Can anyone confirm that you can in fact edit an individual cell in a treview?
If yes are there any ideas as to how this could be implemented?
It's not possible to set the style of individual cells in Treeview; only entire rows can use the tag attribute.
If you just want a table of values then I'd recommend just using ttk.Label widgets, which you can format in a huge number of ways. For example:
import Tkinter as tk
import ttk
import pandas as pd
from random import randrange
PADDING = dict(padx=3, pady=3)
class GridView(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.labels = []
style = ttk.Style()
style.configure("red.TLabel", background='red')
style.configure("green.TLabel", background='green')
style.configure("header.TLabel", font = '-weight bold')
def set(self, df):
self.clear()
for col, name in enumerate(df.columns):
lbl = ttk.Label(self, text=name, style='header.TLabel')
lbl.grid(row=0, column=col, **PADDING)
self.labels.append(lbl)
for row, values in enumerate(df.itertuples(), 1):
for col, value in enumerate(values[1:]):
lbl = ttk.Label(self, text=value, style=self.get_style(value))
lbl.grid(row=row, column=col, **PADDING)
self.labels.append(lbl)
#staticmethod
def get_style(value):
if value > 70:
return "red.TLabel"
elif value < 30:
return "green.TLabel"
else:
return None
def clear(self):
for lbl in self.labels:
lbl.grid_forget()
self.labels = []
class GUI(tk.Frame):
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.table = GridView(self)
self.table.pack()
btn = ttk.Button(self, text="populate", command=self.populate)
btn.pack()
btn = ttk.Button(self, text="clear", command=self.table.clear)
btn.pack()
def populate(self):
self.table.set(new_rand_df())
def main():
root = tk.Tk()
win = GUI(root)
win.pack()
root.mainloop()
def new_rand_df():
width = 5
height = 5
return pd.DataFrame([[randrange(100) for _ in range(width)] for _ in range(height)], columns = list('abcdefghijklmnopqrstuvwxyz'[:width]))
if __name__ == '__main__':
main()
this is possible using a custom cellrenderer in Gtk3, e.g.:
import gi
gi.require_version( 'Gtk', '3.0' )
from gi.repository import Gtk, GObject
class My_CellRendererText( Gtk.CellRendererText ):
def __init__( self ):
super().__init__()
def do_render( self, cr, widget, background_area, cell_area, flags ):
cell_text = self.props.text
self.props.underline = ( "X" in cell_text )
self.props.weight = 700 if ( cell_text.isdigit() and int( cell_text ) > 3 ) else 400
return Gtk.CellRendererText.do_render( self, cr, widget, background_area, cell_area, flags )
GObject.type_register( My_CellRendererText )

displaying corresponding message on label in tkinter

--Edit: my currently attempt, pretty ugly
import tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.labelLists = []
self.labelBLists = []
self.Label1 = tk.Label(self,text=str(1),bg="red")
self.Label1.pack()
self.Label1.bind("<Enter>", self.on_enter1)
self.Label1.bind("<Leave>", self.on_leave1)
self.Labela = tk.Label(self,text="",bg="blue")
self.Labela.pack()
self.Label2 = tk.Label(self,text=str(2),bg="red")
self.Label2.pack()
self.Label2.bind("<Enter>", self.on_enter2)
self.Label2.bind("<Leave>", self.on_leave2)
self.Labelb = tk.Label(self,text="",bg="blue")
self.Labelb.pack()
self.Label3 = tk.Label(self,text=str(3),bg="red")
self.Label3.pack()
self.Label3.bind("<Enter>", self.on_enter3)
self.Label3.bind("<Leave>", self.on_leave3)
self.Labelc = tk.Label(self,text="",bg="blue")
self.Labelc.pack()
def on_enter1(self, event):
self.Labela.config(text=self.Label1.cget("text"))
def on_leave1(self, enter):
self.Labela.config(text="")
def on_enter2(self, event):
self.Labelb.config(text=self.Label2.cget("text"))
def on_leave2(self, enter):
self.Labelb.config(text="")
def on_enter3(self, event):
self.Labelc.config(text=self.Label3.cget("text"))
def on_leave3(self, enter):
self.Labelc.config(text="")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()
I want to create a group of label, say L1, L2, L3, and each one has a corresponding label La, Lb, Lc. What I want to do is when I hover over L1, La display the translation of word on L1. Currently I'm looking at Display message when going over something with mouse cursor in Python and Python Tkinter: addressing Label widget created by for loop, but neither of them addresses binding corresponding method. Is there a way that I can achieve this without creating three pairs of different methods?
Thanks!
Store each set of labels in a list. Then you can go through them together, along with a translation dictionary, and connect the secondary labels (that display a translation) to the primary labels (that respond to user input). This allows you to create a single enter method and a single leave method, using event.widget to access the widget that triggered the event.
import tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.translations = {'a':'A', 'b':'B', 'c':'C'}
self.labelLists = [tk.Label(self,text=str(x),bg="red") for x in range(1,4)]
self.labelBLists = [tk.Label(self,text="",bg="blue") for x in range(3)]
for x,y,tr in zip(self.labelLists, self.labelBLists, sorted(self.translations)):
x.bind('<Enter>', self.enter)
x.bind('<Leave>', self.leave)
x.connected = y
x.key = tr
x.pack()
y.pack()
def enter(self, event):
widget = event.widget
widget.connected.config(text=self.translations[widget.key])
def leave(self, event):
widget = event.widget
widget.connected.config(text='')
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()

Categories

Resources