Tkinter how to catch this particular key event - python

Problem Description
I have an application in Tkinter that uses a Listbox that displays search results. When I press command + down arrow key, I am putting the focus from the search field to the first item in the Listbox. This is exactly how I want the behaviour but instead for just the down arrow.
However, I am already binding the down arrow to this Listbox by self.bind("<Down>", self.moveDown). I can not understand why command + down works while simply down (to which I literally bind'ed it to) does not. Specifically the result of pressing the down arrow is the following
While pressing command + down gives the intended result:
How can I let down behave just like command + down, and what is the reason why command is required at all?
Code snippets
def matches(fieldValue, acListEntry):
pattern = re.compile(re.escape(fieldValue) + '.*', re.IGNORECASE)
return re.match(pattern, acListEntry)
root = Tk()
img = ImageTk.PhotoImage(Image.open('imgs/giphy.gif'))
panel = Label(root, image=img)
panel.grid(row=1, column=0)
entry = AutocompleteEntry(autocompleteList, panel, root, matchesFunction=matches)
entry.grid(row=0, column=0)
root.mainloop()
With AutocompleteEntry being:
class AutocompleteEntry(Tkinter.Entry):
def __init__(self, autocompleteList, df, panel, rdi, *args, **kwargs):
self.df = df
self.product_row_lookup = {key:value for value, key in enumerate(autocompleteList)}
temp = df.columns.insert(0, 'Product_omschrijving')
temp = temp.insert(1, 'grams')
self.result_list = pd.DataFrame(columns=temp)
self.panel = panel
self.rdi = rdi
# self.bind('<Down>', self.handle_keyrelease)
# Listbox length
if 'listboxLength' in kwargs:
self.listboxLength = kwargs['listboxLength']
del kwargs['listboxLength']
else:
self.listboxLength = 8
# Custom matches function
if 'matchesFunction' in kwargs:
self.matchesFunction = kwargs['matchesFunction']
del kwargs['matchesFunction']
else:
def matches(fieldValue, acListEntry):
pattern = re.compile('.*' + re.escape(fieldValue) + '.*', re.IGNORECASE)
return re.match(pattern, acListEntry)
self.matchesFunction = matches
Entry.__init__(self, *args, **kwargs)
self.focus()
self.autocompleteList = autocompleteList
self.var = self["textvariable"]
if self.var == '':
self.var = self["textvariable"] = StringVar()
self.var.trace('w', self.changed)
self.bind("<Right>", self.selection)
self.bind("<Up>", self.moveUp)
self.bind("<Down>", self.moveDown)
self.bind("<Return>", self.selection)
self.listboxUp = False
self._digits = re.compile('\d')
def changed(self, name, index, mode):
if self.var.get() == '':
if self.listboxUp:
self.listbox.destroy()
self.listboxUp = False
else:
words = self.comparison()
if words:
if not self.listboxUp:
self.listbox = Listbox(width=self["width"], height=self.listboxLength)
self.listbox.bind("<Button-1>", self.selection)
self.listbox.bind("<Right>", self.selection)
self.listbox.bind("<Down>", self.moveDown)
self.listbox.bind("<Tab>", self.selection)
self.listbox.place(x=self.winfo_x(), y=self.winfo_y() + self.winfo_height())
self.listboxUp = True
self.listbox.delete(0, END)
for w in words:
self.listbox.insert(END, w)
else:
if self.listboxUp:
self.listbox.destroy()
self.listboxUp = False
else:
string = self.get()
if '.' in string:
write_to_file(self, string)
def contains_digits(self, d):
return bool(self._digits.search(d))
def selection(self, event):
if self.listboxUp:
string = self.listbox.get(ACTIVE)
self.var.set(string + ' ')
self.listbox.destroy()
self.listboxUp = False
self.icursor(END)
def moveDown(self, event):
self.focus()
if self.listboxUp:
if self.listbox.curselection() == ():
index = '0'
print "ok"
else:
index = self.listbox.curselection()[0]
print "blah"
if index != END:
self.listbox.selection_clear(first=index)
print "noo"
if index != '0':
index = str(int(index) + 1)
self.listbox.see(index) # Scroll!
self.listbox.selection_set(first=index)
self.listbox.activate(index)
else:
print "not up"
def comparison(self):
return [w for w in self.autocompleteList if self.matchesFunction(self.var.get(), w)]

Both command+down and down should produce the same output excepted that down also types question mark  onto the entry which made the last letter typed is the question mark box.
This is because pressing command, your computer checks the option menu to see if there's a shortcut with that key, if there isn't any, it will not do anything. While tkinter registered the down button as being pressed, so the event was triggered.
In contrast, With out pressing command, the Entry first displays the value of "down", which there isn't any, then executes the event binding, what you can do is, in the event, remove the last letter of the Entry. You can do so by self.delete(len(self.get())-1) in your event. Or add a return 'break' at the end of your event to prevent it from being typed.

Unfortunately, it's really hard to understand your real problem because you've posted too much unrelated code and not enough related code. It seems to me that what you're trying to accomplish is to let the user press the down or up arrow while an entry has focus, and have that cause the selection in a listbox to move down or up. Further, it seems that part of the problem is that you're seeing characters in the entry widget that you do not want to see when you press down or up.
If that is the problem, the solution is fairly simple. All you need to do is have your binding return the string "break" to prevent the default binding from being processed. It is the default binding that is inserting the character.
Here is an example. Run the example, and press up and down to move the selection of the listbox. I've left out all of the code related to autocomplete so you can focus on how the event binding works.
import Tkinter as tk
class Example(object):
def __init__(self):
self.root = tk.Tk()
self.entry = tk.Entry(self.root)
self.listbox = tk.Listbox(self.root, exportselection=False)
for i in range(30):
self.listbox.insert("end", "Item #%s" % i)
self.entry.pack(side="top", fill="x")
self.listbox.pack(side="top", fill="both", expand=True)
self.entry.bind("<Down>", self.handle_updown)
self.entry.bind("<Up>", self.handle_updown)
def start(self):
self.root.mainloop()
def handle_updown(self, event):
delta = -1 if event.keysym == "Up" else 1
curselection = self.listbox.curselection()
if len(curselection) == 0:
index = 0
else:
index = max(int(curselection[0]) + delta, 0)
self.listbox.selection_clear(0, "end")
self.listbox.selection_set(index, index)
return "break"
if __name__ == "__main__":
Example().start()
For a fairly thorough explanation of what happens when an event is triggered, see this answer: https://stackoverflow.com/a/11542200/7432

Again, leaving aside the autocomplete requirement, I came up with a solution that uses that standard available commands and events for Listbox and Scrollbar. <<ListboxSelect>> lets you capture changes in selection from any of the lists and align the others. In addition, the Scrollbar and Listbox callbacks are directed to a routing function that passes things on to all of the listboxes.
# updownmultilistbox.py
# 7/24/2020
#
# incorporates vsb to propagate scrolling across lists
#
import tkinter as tk
class Example(object):
def __init__(self):
self.root = tk.Tk()
self.listOfListboxes = []
# self.active_lb = None
self.vsb = tk.Scrollbar(orient='vertical', command=self.OnVsb)
self.vsb.pack(side='right', fill='y')
self.lb1 = tk.Listbox(self.root, exportselection=0,
selectmode= tk.SINGLE, yscrollcommand=self.vsb_set)
self.lb2 = tk.Listbox(self.root, exportselection=0,
selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
self.lb3 = tk.Listbox(self.root, exportselection=0,
selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
self.listOfListboxes.append(self.lb1)
self.listOfListboxes.append(self.lb2)
self.listOfListboxes.append(self.lb3)
for i in range(30):
self.lb1.insert("end", "lb1 Item #%s" % i)
self.lb2.insert("end", "lb2 Item #%s" % i)
self.lb3.insert("end", "lb3 Item #%s" % i)
self.lb1.pack(side="left", fill="both", expand=True)
self.lb2.pack(side="left", fill="both", expand=True)
self.lb3.pack(side="left", fill="both", expand=True)
for lb in self.listOfListboxes:
lb.bind('<<ListboxSelect>>', self.handle_select)
for lb in self.listOfListboxes:
lb.selection_set(0)
lb.activate(0)
self.listOfListboxes[0].focus_force()
def start(self):
self.root.title('updownmultilistbox')
self.root.mainloop()
def OnVsb(self, *args):
for lb in self.listOfListboxes:
lb.yview(*args)
def vsb_set(self, *args):
print ('vsb_set args: ', *args)
self.vsb.set(*args)
for lb in self.listOfListboxes:
lb.yview_moveto(args[0])
def handle_select(self, event):
# set evey list to the same selection
print ('select handler: ', event, event.widget.curselection())
# self.active_lb = event.widget
for lb in self.listOfListboxes:
if lb != event.widget:
lb.selection_clear(0, 'end') # have to avoid this for the current widget
lb.selection_set(event.widget.curselection())
lb.activate(event.widget.curselection())
if __name__ == "__main__":
Example().start()

Related

tkinter - joint focus of two foreign objects?

I'm trying to improve an implementation of Entry with autocomplete. Currently the code uses a StringVar with a trace to track changes and posts a Listbox placed right under the Entry. In order for the Listbox to draw properly over other widgets I had to set its master to be the root/toplevel window.
Now I wanted to make listbox close when the entry loses focus. The thing is, the entry loses focus when the user selects an item from the listbox as well.
FocusOut takes places first and this closes the listbox before the choice is even made.
Right now I have it solved by setting a delayed call (50ms) when FocusOut occurs but that solution isn't great.
Is there another way around this? maybe a way to make the listbox be considered 'a part of' the entry?
This is the code I'm currently using with the timed solution:
# Based on: http://code.activestate.com/recipes/580770-combobox-autocomplete/ by Miguel Martinez Lopez
# Modified to work with toplevel windows and some other adjustments
from tkinter import *
from tkinter.ttk import *
def autoscroll(sbar, first, last):
"""Hide and show scrollbar as needed."""
first, last = float(first), float(last)
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)
class Combobox_Autocomplete(Entry, object):
def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_width=None, listbox_height=7,
ignorecase_match=True, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
if hasattr(self, "autocomplete_function"):
if autocomplete_function is not None:
raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
else:
if autocomplete_function is not None:
self.autocomplete_function = autocomplete_function
else:
if list_of_items is None:
raise ValueError("If not guiven complete function, list_of_items can't be 'None'")
if not ignorecase_match:
if startswith_match:
def matches_function(entry_data, item):
return item.startswith(entry_data)
else:
def matches_function(entry_data, item):
return item in entry_data
self.autocomplete_function = lambda entry_data: [item for item in self.list_of_items if
matches_function(entry_data, item)]
else:
if startswith_match:
def matches_function(escaped_entry_data, item):
if re.match(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False
else:
def matches_function(escaped_entry_data, item):
if re.search(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False
def autocomplete_function(entry_data):
escaped_entry_data = re.escape(entry_data)
return [item for item in self.list_of_items if matches_function(escaped_entry_data, item)]
self.autocomplete_function = autocomplete_function
self._listbox_height = int(listbox_height)
self._listbox_width = listbox_width
self.list_of_items = list_of_items
self._use_vscrollbar = vscrollbar
self._use_hscrollbar = hscrollbar
kwargs.setdefault("background", "white")
if "textvariable" in kwargs:
self._entry_var = kwargs["textvariable"]
else:
self._entry_var = kwargs["textvariable"] = StringVar()
Entry.__init__(self, master, **kwargs)
self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
self._listbox = None
# self.bind("<Tab>", self._on_tab)
self.bind("<Control-space>",lambda event: self.post_listbox())
self.bind("<Up>", self._previous)
self.bind("<Down>", self._next)
self.bind('<Control-n>', self._next)
self.bind('<Control-p>', self._previous)
self.bind("<Return>", self._update_entry_from_listbox)
self.bind("<Escape>", lambda event: self.unpost_listbox())
# self.bind("<FocusOut>", lambda event: self._update_entry_from_listbox(event, False))
self.bind("<FocusOut>", lambda event: self.after(50, self._on_focus_out, event))
parent = self.master
while not isinstance(parent, (Toplevel, Tk)):
parent = parent.master
self.parent_window = parent
def _on_focus_out(self, event):
if self._listbox:
focused = self._listbox.focus_get()
if focused and self._listbox.winfo_name() in str(focused):
return
self.unpost_listbox()
# def _on_focus_out_list(self, event):
# focused = self.focus_get()
# if focused and str(focused).endswith(self.winfo_name()):
# return
# self.unpost_listbox()
# def _on_tab(self, event):
# self.post_listbox()
# return "break"
def _on_change_entry_var(self, name, index, mode):
entry_data = self._entry_var.get()
if entry_data == '':
self.unpost_listbox()
# self.focus()
else:
values = self.autocomplete_function(entry_data)
if values:
if self._listbox is None:
self._build_listbox(values)
else:
self._listbox.delete(0, END)
height = min(self._listbox_height, len(values))
self._listbox.configure(height=height)
for item in values:
self._listbox.insert(END, item)
else:
self.unpost_listbox()
self.focus()
def _build_listbox(self, values):
listbox_frame = Frame(self.parent_window, padding=0)
if self._listbox_width:
width = self._listbox_width
else:
width = self.winfo_width()
self._listbox = Listbox(listbox_frame, background="white", selectmode=SINGLE, activestyle="none",
exportselection=False)
self._listbox.grid(row=0, column=0, sticky='nsew', padx=0, pady=0)
self._listbox.bind("<ButtonRelease-1>", self._update_entry_from_listbox)
self._listbox.bind("<Return>", self._update_entry_from_listbox)
self._listbox.bind("<Escape>", lambda event: self.unpost_listbox())
# self._listbox.bind("<FocusOut>", lambda event: self.after(50, self._on_focus_out_list, event))
self._listbox.bind('<Control-n>', self._next)
self._listbox.bind('<Control-p>', self._previous)
if self._use_vscrollbar:
vbar = Scrollbar(listbox_frame, orient=VERTICAL, command=self._listbox.yview)
vbar.grid(row=0, column=1, sticky='ns')
self._listbox.configure(yscrollcommand=lambda f, l: autoscroll(vbar, f, l))
if self._use_hscrollbar:
hbar = Scrollbar(listbox_frame, orient=HORIZONTAL, command=self._listbox.xview)
hbar.grid(row=1, column=0, sticky='ew')
self._listbox.configure(xscrollcommand=lambda f, l: autoscroll(hbar, f, l))
listbox_frame.grid_columnconfigure(0, weight=1)
listbox_frame.grid_rowconfigure(0, weight=1)
listbox_frame.place(in_=self, x=0, y=self.winfo_height(), width=width)
height = min(self._listbox_height, len(values))
self._listbox.configure(height=height)
for item in values:
self._listbox.insert(END, item)
def post_listbox(self):
if self._listbox is not None: return
entry_data = self._entry_var.get()
if entry_data == '': return
values = self.autocomplete_function(entry_data)
if values:
self._build_listbox(values)
def unpost_listbox(self):
if self._listbox is not None:
self._listbox.master.destroy()
self._listbox = None
def get_value(self):
return self._entry_var.get()
def set_value(self, text, close_dialog=False):
self._set_var(text)
if close_dialog:
self.unpost_listbox()
self.icursor(END)
self.xview_moveto(1.0)
def _set_var(self, text):
self._entry_var.trace_vdelete("w", self._trace_id)
self._entry_var.set(text)
self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
def _update_entry_from_listbox(self, event, focus=True):
if self._listbox is not None:
current_selection = self._listbox.curselection()
if current_selection:
text = self._listbox.get(current_selection)
self._set_var(text)
self._listbox.master.destroy()
self._listbox = None
if focus:
self.focus()
self.icursor(END)
self.xview_moveto(1.0)
return "break"
def _previous(self, event):
if self._listbox is not None:
current_selection = self._listbox.curselection()
if len(current_selection) == 0:
self._listbox.selection_set(0)
self._listbox.activate(0)
else:
index = int(current_selection[0])
self._listbox.selection_clear(index)
if index == 0:
index = END
else:
index -= 1
self._listbox.see(index)
self._listbox.selection_set(first=index)
self._listbox.activate(index)
return "break"
def _next(self, event):
if self._listbox is not None:
current_selection = self._listbox.curselection()
if len(current_selection) == 0:
self._listbox.selection_set(0)
self._listbox.activate(0)
else:
index = int(current_selection[0])
self._listbox.selection_clear(index)
if index == self._listbox.size() - 1:
index = 0
else:
index += 1
self._listbox.see(index)
self._listbox.selection_set(index)
self._listbox.activate(index)
return "break"
if __name__ == '__main__':
list_of_items = ["Cordell Cannata", "Lacey Naples", "Zachery Manigault", "Regan Brunt", "Mario Hilgefort",
"Austin Phong", "Moises Saum", "Willy Neill", "Rosendo Sokoloff", "Salley Christenberry",
"Toby Schneller", "Angel Buchwald", "Nestor Criger", "Arie Jozwiak", "Nita Montelongo",
"Clemencia Okane", "Alison Scaggs", "Von Petrella", "Glennie Gurley", "Jamar Callender",
"Titus Wenrich", "Chadwick Liedtke", "Sharlene Yochum", "Leonida Mutchler", "Duane Pickett",
"Morton Brackins", "Ervin Trundy", "Antony Orwig", "Audrea Yutzy", "Michal Hepp",
"Annelle Hoadley", "Hank Wyman", "Mika Fernandez", "Elisa Legendre", "Sade Nicolson", "Jessie Yi",
"Forrest Mooneyhan", "Alvin Widell", "Lizette Ruppe", "Marguerita Pilarski", "Merna Argento",
"Jess Daquila", "Breann Bevans", "Melvin Guidry", "Jacelyn Vanleer", "Jerome Riendeau",
"Iraida Nyquist", "Micah Glantz", "Dorene Waldrip", "Fidel Garey", "Vertie Deady",
"Rosalinda Odegaard", "Chong Hayner", "Candida Palazzolo", "Bennie Faison", "Nova Bunkley",
"Francis Buckwalter", "Georgianne Espinal", "Karleen Dockins", "Hertha Lucus", "Ike Alberty",
"Deangelo Revelle", "Juli Gallup", "Wendie Eisner", "Khalilah Travers", "Rex Outman",
"Anabel King", "Lorelei Tardiff", "Pablo Berkey", "Mariel Tutino", "Leigh Marciano", "Ok Nadeau",
"Zachary Antrim", "Chun Matthew", "Golden Keniston", "Anthony Johson", "Rossana Ahlstrom",
"Amado Schluter", "Delila Lovelady", "Josef Belle", "Leif Negrete", "Alec Doss", "Darryl Stryker",
"Michael Cagley", "Sabina Alejo", "Delana Mewborn", "Aurelio Crouch", "Ashlie Shulman",
"Danielle Conlan", "Randal Donnell", "Rheba Anzalone", "Lilian Truax", "Weston Quarterman",
"Britt Brunt", "Leonie Corbett", "Monika Gamet", "Ingeborg Bello", "Angelique Zhang",
"Santiago Thibeau", "Eliseo Helmuth"]
root = Tk()
root.geometry("300x200")
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
frm_form = Frame(root)
frm_form.grid(row=0, column=0)
lbl_names = Label(frm_form, text='Name:')
lbl_names.grid(row=0, column=0, padx=5, pady=5)
entry_name = Combobox_Autocomplete(frm_form, list_of_items=list_of_items, startswith_match=False)
entry_name.grid(row=0, column=1, padx=5)
entry_name.focus()
lbl_names = Label(frm_form, text='Food:')
lbl_names.grid(row=1, column=0, padx=5, pady=5)
entry_food = Combobox_Autocomplete(frm_form, list_of_items=['Apples', 'Oranges', 'Avocados'], startswith_match=False)
entry_food.grid(row=1, column=1, padx=5)
root.mainloop()

Why is the tk key binding not working here?

Here is the code:
#!/usr/bin/env python3
################################################################
### Solve the unknown
################################################################
class App:
def __init__(self, root):
self.fields = ["A", "B", "AB"]
self.root = root
self.entries = []
self.numerics = []
self.makeRows()
Button(self.root, text="Run", command=self.run).grid(row=3, column=0)
Button(self.root, text="Reset", command=self.reset).grid(row=3, column=1)
root.bind("<Return>", self.run)
root.bind("<space>", self.reset)
def makeRows(self):
for i in range(len(self.fields)):
label = Label(self.root, text=self.fields[i], justify=LEFT)
entry = Entry(self.root)
label.grid(row=i, sticky=W)
entry.grid(row=i, column=1)
self.entries.append(entry)
def getValues(self):
try:
values = [e.get() for e in self.entries]
self.numerics = [float(v) if v!="" else v for v in values]
print(self.numerics)
except ValueError:
messagebox.showerror(title="Error", message="Input only numerical values!")
self.numerics = []
def checkEmpty(self, elem):
if elem == "":
return 1
else:
return 0
def checkEmptyInEntries(self):
if len(self.numerics) != 0:
entry_emptiness = [self.checkEmpty(v) for v in self.numerics]
if sum(entry_emptiness) != 1:
messagebox.showerror(title="Error", message="Leave one and only one entry empty!")
return None
return entry_emptiness
else:
return None
def run(self):
self.getValues()
emptiness = self.checkEmptyInEntries()
if emptiness == None:
return None
empty_index = emptiness.index(1)
if empty_index == 0:
self.entries[0].delete(0, END)
self.entries[0].insert(0, str(self.numerics[2] / self.numerics[1]))
elif empty_index == 1:
self.entries[1].delete(0, END)
self.entries[1].insert(0, str(self.numerics[2] / self.numerics[0]))
else:
self.entries[2].delete(0, END)
self.entries[2].insert(0, str(self.numerics[0] * self.numerics[1]))
def reset(self):
for entry in self.entries:
entry.delete(0, END)
from tkinter import *
root = Tk()
app = App(root)
root.mainloop()
The buttons work fine, but whenever I press enter, Python complains:
TypeError: run() takes 1 positional argument but 2 were given
Using bind to bind events to callbacks causes an event object to be sent to the callback every time the binding is invoked. You need to handle this extra argument. The easiest way to do this is to use a lambda function:
root.bind("<Return>", lambda _: self.run())
In the above code, the _ will be the event object.
Note that you could also change the definition of run to accept this object:
def run(self, event):
But I personally prefer the lambda because it makes it clearer that run does not use the event object.

i cant use the equals or the enter key on my keyboard to get an answer for anything [duplicate]

This question already has answers here:
python tkinter how to bind key to a button
(3 answers)
Closed 4 months ago.
ok i am using this code for educational purposes and if i go 45+54 and hit enter key i dont get any answer but if use the = on the screen not on my keyboard it works. i am so stuck and stressed out because i have done so much research but not able to find my answer. all i ask for the help is that am i missing a piece of code that is stopping me to use the equals key or the enter key on the numpad. please help here this is the code:
from tkinter import *
import tkinter
# Calculator is a class derived from Frame. Frames, being someone generic,
# make a nice base class for whatever you what to create.
class Calculator(Frame):
# Create and return a packed frame.
def frame(this, side):
w = Frame(this)
w.pack(side=side, expand=YES, fill=BOTH)
return w
# Create and return a button.
def button(this, root, side, text, command=None):
w = Button(root, text=text, command=command)
w.pack(side=side, expand=YES, fill=BOTH)
return w
# Enter a digit.
need_clr = False
def digit(self, digit):
if self.need_clr:
self.display.set('')
self.need_clr = False
self.display.set(self.display.get() + digit)
# Change sign.
def sign(self):
need_clr = False
cont = self.display.get()
if len(cont) > 0 and cont[0] == '-':
self.display.set(cont[1:])
else:
self.display.set('-' + cont)
# Decimal
def decimal(self):
self.need_clr = False
cont = self.display.get()
lastsp = cont.rfind(' ')
if lastsp == -1:
lastsp = 0
if cont.find('.',lastsp) == -1:
self.display.set(cont + '.')
# Push a function button.
def oper(self, op):
self.display.set(self.display.get() + ' ' + op + ' ')
self.need_clr = False
# Calculate the expressoin and set the result.
def calc(self):
try:
self.display.set(eval(self.display.get()))
self.need_clr = True
except:
showerror('Operation Error', 'Illegal Operation')
self.display.set('')
self.need_clr = False
def Enter(self):
self.display.set('Enter')
def keyPressed(self,event):
if event.keysym == 'Enter':
self.enter()
def __init__(self):
Frame.__init__(self)
self.option_add('*Font', 'Verdana 12 bold')
self.pack(expand=YES, fill=BOTH)
self.master.title('Simple Calculator')
# The StringVar() object holds the value of the Entry.
self.display = StringVar()
e = Entry(self, relief=SUNKEN, textvariable=self.display)
e.pack(side=TOP, expand=YES, fill=BOTH)
# This is a nice loop to produce the number buttons. The Lambda
# is an anonymous function.
for key in ("123", "456", "789"):
keyF = self.frame(TOP)
for char in key:
self.button(keyF, LEFT, char,
lambda c=char: self.digit(c))
keyF = self.frame(TOP)
self.button(keyF, LEFT, '-', self.sign)
self.button(keyF, LEFT, '0', lambda ch='0': self.digit(ch))
self.button(keyF, LEFT, '.', self.decimal)
# The frame is used to hold the operator buttons.
opsF = self.frame(TOP)
for char in "+-*/=":
if char == '=':
btn = self.button(opsF, LEFT, char, self.calc)
else:
btn = self.button(opsF, LEFT, char,
lambda w=self, s=char: w.oper(s))
# Clear button.
clearF = self.frame(BOTTOM)
self.Enter_button = self.button(clearF, LEFT, 'Enter', self.Enter)
self.bind_all('<Key>', self.keyPressed)
# Make a new function for the - sign. Maybe for . as well. Add event
# bindings for digits to call the button functions.
# This allows the file to be used either as a module or an independent
# program.
if __name__ == '__main__':
Calculator().mainloop()
To call a the same function as a button press on a keypress you have to bind keypress to you GUI.
Example
try:
# for Python2
from Tkinter import *
except ImportError:
# for Python3
from tkinter import *
class GUI(Frame):
# Create and return a packed frame.
def frame(self, side):
w = Frame(self)
w.pack(side=side, expand=YES, fill=BOTH)
return w
# Create and return a button.
def button(self, root, side, text, command=None):
w = Button(root, text=text, command=command)
w.pack(side=side, expand=YES, fill=BOTH)
return w
def hello(self):
self.display.set('hello')
def keyPressed(self,event):
#test to see whether enetr is pressed
if event.keysym == 'Return':
self.hello()
def __init__(self):
Frame.__init__(self)
self.option_add('*Font', 'Verdana 12 bold')
self.pack(expand=YES, fill=BOTH)
self.master.title('Simple GUI')
# The StringVar() object holds the value of the Entry.
self.display = StringVar()
e = Entry(self, relief=SUNKEN, textvariable=self.display)
e.pack(side=TOP, expand=YES, fill=BOTH)
# Clear button.
clearF = self.frame(BOTTOM)
self.hello_button = self.button(clearF, LEFT, 'Hello', self.hello)
#bind keypresses
self.bind_all('<Key>', self.keyPressed)
if __name__ == '__main__':
GUI().mainloop()
for this specific example the folowing code shoulod work:
try:
# for Python2
from Tkinter import *
except ImportError:
# for Python3
from tkinter import *
# Calculator is a class derived from Frame. Frames, being someone generic,
# make a nice base class for whatever you what to create.
# Calculator is a class derived from Frame. Frames, being someone generic,
# make a nice base class for whatever you what to create.
class Calculator(Frame):
# Create and return a packed frame.
def frame(this, side):
w = Frame(this)
w.pack(side=side, expand=YES, fill=BOTH)
return w
def keyPressed(self,event):
#test to see whether enetr is pressed
if event.keysym == 'Return':
self.calc()
# Create and return a button.
def button(this, root, side, text, command=None):
w = Button(root, text=text, command=command)
w.pack(side=side, expand=YES, fill=BOTH)
return w
# Enter a digit.
need_clr = False
def digit(self, digit):
if self.need_clr:
self.display.set('')
self.need_clr = False
self.display.set(self.display.get() + digit)
# Change sign.
def sign(self):
need_clr = False
cont = self.display.get()
if len(cont) > 0 and cont[0] == '-':
self.display.set(cont[1:])
else:
self.display.set('-' + cont)
# Decimal
def decimal(self):
self.need_clr = False
cont = self.display.get()
lastsp = cont.rfind(' ')
if lastsp == -1:
lastsp = 0
if cont.find('.',lastsp) == -1:
self.display.set(cont + '.')
# Push a function button.
def oper(self, op):
self.display.set(self.display.get() + ' ' + op + ' ')
self.need_clr = False
# Calculate the expressoin and set the result.
def calc(self):
try:
self.display.set(eval(self.display.get()))
self.need_clr = True
except:
showerror('Operation Error', 'Illegal Operation')
self.display.set('')
self.need_clr = False
def __init__(self):
Frame.__init__(self)
self.option_add('*Font', 'Verdana 12 bold')
self.pack(expand=YES, fill=BOTH)
self.master.title('Simple Calculator')
# The StringVar() object holds the value of the Entry.
self.display = StringVar()
e = Entry(self, relief=SUNKEN, textvariable=self.display)
e.pack(side=TOP, expand=YES, fill=BOTH)
# This is a nice loop to produce the number buttons. The Lambda
# is an anonymous function.
for key in ("123", "456", "789"):
keyF = self.frame(TOP)
for char in key:
self.button(keyF, LEFT, char,
lambda c=char: self.digit(c))
keyF = self.frame(TOP)
self.button(keyF, LEFT, '-', self.sign)
self.button(keyF, LEFT, '0', lambda ch='0': self.digit(ch))
self.button(keyF, LEFT, '.', self.decimal)
# The frame is used to hold the operator buttons.
opsF = self.frame(TOP)
for char in "+-*/=":
if char == '=':
btn = self.button(opsF, LEFT, char, self.calc)
else:
btn = self.button(opsF, LEFT, char,
lambda w=self, s=char: w.oper(s))
# Clear button.
clearF = self.frame(BOTTOM)
self.button(clearF, LEFT, 'Clr', lambda w=self.display: w.set(''))
self.bind_all('<Key>', self.keyPressed)
# Make a new function for the - sign. Maybe for . as well. Add event
# bindings for digits to call the button functions.
# This allows the file to be used either as a module or an independent
# program.
if __name__ == '__main__':
Calculator().mainloop()

Having multiple-page GUI in Python with several buttons

I am currently trying to implement a GUI for a task at work. I have found some open source code (see below) where we make a GUI having 3 pages where one can go between the pages using next/previous buttons. If you run the code you'll see what each button and so on is indended to do.
However, when you run the code and click the "count++" button, the overall count increases by one and not the count for the individual page (e.g. being on page 1 and clicking the count++ 4 times, still makes the count for page 2 or 3, 4 as well, and not zero). The same problem occurs when I try to update the text in each of the textboxes on each page (supposed to be number of clicks), as it won't update. I am not sure how to actually get to the textframe for each individual page.
Any suggestions on where to go from here? In the long run I would like to have scroll-down menus where the selections will be put onto each individual text-frame.
Thanks,
import ttk
from Tkinter import *
import tkMessageBox
class Wizard(object, ttk.Notebook):
def __init__(self, master=None, **kw):
npages = kw.pop('npages', 3)
kw['style'] = 'Wizard.TNotebook'
ttk.Style(master).layout('Wizard.TNotebook.Tab', '')
ttk.Notebook.__init__(self, master, **kw)
self._children = {}
self.click_count = 0
self.txt_var = "Default"
for page in range(npages):
self.add_empty_page()
self.current = 0
self._wizard_buttons()
def _wizard_buttons(self):
"""Place wizard buttons in the pages."""
for indx, child in self._children.iteritems():
btnframe = ttk.Frame(child)
btnframe.pack(side='left', fill='x', padx=6, pady=4)
txtframe = ttk.Frame(child)
txtframe.pack(side='right', fill='x', padx=6, pady=4)
nextbtn = ttk.Button(btnframe, text="Next", command=self.next_page)
nextbtn.pack(side='top', padx=6)
countbtn = ttk.Button(txtframe, text="Count++..", command=self.update_click)
countbtn.grid(column=0,row=0)
txtBox = Text(txtframe,width = 50, height = 20, wrap = WORD)
txtBox.grid(column=1,row=0)
txtBox.insert(0.0, self.txt_var)
rstbtn = ttk.Button(btnframe, text="Reset count!", command=self.reset_count)
rstbtn.pack(side='top', padx=6)
if indx != 0:
prevbtn = ttk.Button(btnframe, text="Previous",
command=self.prev_page)
prevbtn.pack(side='right', anchor='e', padx=6)
if indx == len(self._children) - 1:
nextbtn.configure(text="Finish", command=self.close)
def next_page(self):
self.current += 1
def prev_page(self):
self.current -= 1
def close(self):
self.master.destroy()
def add_empty_page(self):
child = ttk.Frame(self)
self._children[len(self._children)] = child
self.add(child)
def add_page_body(self, body):
body.pack(side='top', fill='both', padx=6, pady=12)
def page_container(self, page_num):
if page_num in self._children:
return self._children[page_num]
else:
raise KeyError("Invalid page: %s" % page_num)
def _get_current(self):
return self._current
def _set_current(self, curr):
if curr not in self._children:
raise KeyError("Invalid page: %s" % curr)
self._current = curr
self.select(self._children[self._current])
current = property(_get_current, _set_current)
def update_click(self):
self.click_count += 1
message = "You have clicked %s times now!" % str(self.click_count)
tkMessageBox.showinfo("monkeybar", message)
self.txt_var = "Number of clicks: %s" % str(self.click_count) #this will not change the text in the textbox!
def reset_count(self):
message = "Count is now 0."
#ctypes.windll.user32.MessageBoxA(0, message, "monkeybar", 1)
tkMessageBox.showinfo("monkeybar", message)
self.click_count = 0
def combine_funcs(*funcs):
def combined_func(*args, **kwargs):
for f in funcs:
f(*args, **kwargs)
return combined_func
def demo():
root = Tk()
nbrpages = 7
wizard = Wizard(npages=nbrpages)
wizard.master.minsize(400, 350)
wizard.master.title("test of GUI")
pages = range(nbrpages)
for p in pages:
pages[p] = ttk.Label(wizard.page_container(p), text='Page %s'%str(p+1))
wizard.add_page_body(pages[p])
wizard.pack(fill='both', expand=True)
root.mainloop()
if __name__ == "__main__":
demo()
Your update_click method operate on the attribute click_count of your wizard. If you want different count, you could either create a class for your pages and thus, each object would manage its own count, or manage several counter, for instance in a list, the same way you handle your _children list.
For the former case, you can create a page class inheriting ttk.Frame with the body of your _wizard_buttons loop as constructor. In the later case, you could try something like this
class Wizard(object, ttk.Notebook):
def __init__(self, master=None, **kw):
[...]
#replace self.click_count = 0 with
self.click_counters = [0 for i in range(npages)]
def update_click(self):
self.click_counters[self.current] += 1
# and so on...
Regarding the Text widget update, you can not handle it though a variable, it works with Entry(one line text field) but not on Text (multiline, rich text field). If you continue with Text, the usual recipe to what you seems to want is
text.delete(1.0, END)
text.insert(END, content)

How to make ttk.Treeview's rows editable?

Is there any way to use ttk Treeview with editable rows?
I mean it should work more like a table. For example on double click on the item make the #0 column 'editable'.
If this isn't possible, any way to allow mouse selecting on the item would be just fine. I haven't found any mention of this in tkdocs or other documents.
After long research I haven't found such feature so I guess there's any. Tk is very simple interface, which allows programmer to build 'high-level' features from the basics. So my desired behaviour this way.
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
# self.destroyPopups()
# what row and column was clicked on
rowid = self._tree.identify_row(event.y)
column = self._tree.identify_column(event.x)
# get column position info
x,y,width,height = self._tree.bbox(rowid, column)
# y-axis offset
# pady = height // 2
pady = 0
# place Entry popup properly
text = self._tree.item(rowid, 'text')
self.entryPopup = EntryPopup(self._tree, rowid, text)
self.entryPopup.place( x=0, y=y+pady, anchor=W, relwidth=1)
This is method within a class which composes ttk.Treeview as self._tree
And EntryPopup is then very simple sub-class of Entry:
class EntryPopup(Entry):
def __init__(self, parent, iid, text, **kw):
''' If relwidth is set, then width is ignored '''
super().__init__(parent, **kw)
self.tv = parent
self.iid = iid
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
self.tv.item(self.iid, text=self.get())
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
You could also pop up a tool window with the editable fields listed with Entries to update the values. This example has a treeview with three columns, and does not use subclasses.
Bind your double click to this:
def OnDoubleClick(self, treeView):
# First check if a blank space was selected
entryIndex = treeView.focus()
if '' == entryIndex: return
# Set up window
win = Toplevel()
win.title("Edit Entry")
win.attributes("-toolwindow", True)
####
# Set up the window's other attributes and geometry
####
# Grab the entry's values
for child in treeView.get_children():
if child == entryIndex:
values = treeView.item(child)["values"]
break
col1Lbl = Label(win, text = "Value 1: ")
col1Ent = Entry(win)
col1Ent.insert(0, values[0]) # Default is column 1's current value
col1Lbl.grid(row = 0, column = 0)
col1Ent.grid(row = 0, column = 1)
col2Lbl = Label(win, text = "Value 2: ")
col2Ent = Entry(win)
col2Ent.insert(0, values[1]) # Default is column 2's current value
col2Lbl.grid(row = 0, column = 2)
col2Ent.grid(row = 0, column = 3)
col3Lbl = Label(win, text = "Value 3: ")
col3Ent = Entry(win)
col3Ent.insert(0, values[2]) # Default is column 3's current value
col3Lbl.grid(row = 0, column = 4)
col3Ent.grid(row = 0, column = 5)
def UpdateThenDestroy():
if ConfirmEntry(treeView, col1Ent.get(), col2Ent.get(), col3Ent.get()):
win.destroy()
okButt = Button(win, text = "Ok")
okButt.bind("<Button-1>", lambda e: UpdateThenDestroy())
okButt.grid(row = 1, column = 4)
canButt = Button(win, text = "Cancel")
canButt.bind("<Button-1>", lambda c: win.destroy())
canButt.grid(row = 1, column = 5)
Then confirm the changes:
def ConfirmEntry(self, treeView, entry1, entry2, entry3):
####
# Whatever validation you need
####
# Grab the current index in the tree
currInd = treeView.index(treeView.focus())
# Remove it from the tree
DeleteCurrentEntry(treeView)
# Put it back in with the upated values
treeView.insert('', currInd, values = (entry1, entry2, entry3))
return True
Here's how to delete an entry:
def DeleteCurrentEntry(self, treeView):
curr = treeView.focus()
if '' == curr: return
treeView.delete(curr)
I have tried #dakov solution but it did not work for me since my treeView has multiple columns and for few more reasons. I made some changes that enhanced it so here is my version
class Tableview(ttk.Treeview):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tv.bind("<Double-1>", lambda event: self.onDoubleClick(event))
def onDoubleClick(self, event):
''' Executed, when a row is double-clicked. Opens
read-only EntryPopup above the item's column, so it is possible
to select text '''
# close previous popups
try: # in case there was no previous popup
self.entryPopup.destroy()
except AttributeError:
pass
# what row and column was clicked on
rowid = self.identify_row(event.y)
column = self.identify_column(event.x)
# handle exception when header is double click
if not rowid:
return
# get column position info
x,y,width,height = self.bbox(rowid, column)
# y-axis offset
pady = height // 2
# place Entry popup properly
text = self.item(rowid, 'values')[int(column[1:])-1]
self.entryPopup = EntryPopup(self, rowid, int(column[1:])-1, text)
self.entryPopup.place(x=x, y=y+pady, width=width, height=height, anchor='w')
The EntryPopup class
class EntryPopup(ttk.Entry):
def __init__(self, parent, iid, column, text, **kw):
ttk.Style().configure('pad.TEntry', padding='1 1 1 1')
super().__init__(parent, style='pad.TEntry', **kw)
self.tv = parent
self.iid = iid
self.column = column
self.insert(0, text)
# self['state'] = 'readonly'
# self['readonlybackground'] = 'white'
# self['selectbackground'] = '#1BA1E2'
self['exportselection'] = False
self.focus_force()
self.select_all()
self.bind("<Return>", self.on_return)
self.bind("<Control-a>", self.select_all)
self.bind("<Escape>", lambda *ignore: self.destroy())
def on_return(self, event):
rowid = self.tv.focus()
vals = self.tv.item(rowid, 'values')
vals = list(vals)
vals[self.column] = self.get()
self.tv.item(rowid, values=vals)
self.destroy()
def select_all(self, *ignore):
''' Set selection on the whole text '''
self.selection_range(0, 'end')
# returns 'break' to interrupt default key-bindings
return 'break'
from tkinter import ttk
from tkinter import *
root = Tk()
columns = ("Items", "Values")
Treeview = ttk.Treeview(root, height=18, show="headings", columns=columns) #
Treeview.column("Items", width=200, anchor='center')
Treeview.column("Values", width=200, anchor='center')
Treeview.heading("Items", text="Items")
Treeview.heading("Values", text="Values")
Treeview.pack(side=LEFT, fill=BOTH)
name = ['Item1', 'Item2', 'Item3']
ipcode = ['10', '25', '163']
for i in range(min(len(name), len(ipcode))):
Treeview.insert('', i, values=(name[i], ipcode[i]))
def treeview_sort_column(tv, col, reverse):
l = [(tv.set(k, col), k) for k in tv.get_children('')]
l.sort(reverse=reverse)
for index, (val, k) in enumerate(l):
tv.move(k, '', index)
tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))
def set_cell_value(event):
for item in Treeview.selection():
item_text = Treeview.item(item, "values")
column = Treeview.identify_column(event.x)
row = Treeview.identify_row(event.y)
cn = int(str(column).replace('#', ''))
rn = int(str(row).replace('I', ''))
entryedit = Text(root, width=10 + (cn - 1) * 16, height=1)
entryedit.place(x=16 + (cn - 1) * 130, y=6 + rn * 20)
def saveedit():
Treeview.set(item, column=column, value=entryedit.get(0.0, "end"))
entryedit.destroy()
okb.destroy()
okb = ttk.Button(root, text='OK', width=4, command=saveedit)
okb.place(x=90 + (cn - 1) * 242, y=2 + rn * 20)
def newrow():
name.append('to be named')
ipcode.append('value')
Treeview.insert('', len(name) - 1, values=(name[len(name) - 1], ipcode[len(name) - 1]))
Treeview.update()
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
newb.update()
Treeview.bind('<Double-1>', set_cell_value)
newb = ttk.Button(root, text='new item', width=20, command=newrow)
newb.place(x=120, y=(len(name) - 1) * 20 + 45)
for col in columns:
Treeview.heading(col, text=col, command=lambda _col=col: treeview_sort_column(Treeview, _col, False))
root.mainloop()
After so much research while doing my project got this code, it helped me a lot.
Double click on the element you want to edit, make the required change and click 'OK' button
I think this is what exactly you wanted
#python #tkinter #treeview #editablerow
New row
Editable row
This is just for creating a tree for the specified path that is set in the constructor. you can bind your event to your item on that tree. The event function is left in a way that the item could be used in many ways. In this case, it will show the name of the item when double clicked on it. Hope this helps somebody.
import ttk
from Tkinter import*
import os*
class Tree(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
path = "/home/...."
self.initUI(path)
def initUI(self, path):
self.parent.title("Tree")
self.tree = ttk.Treeview(self.parent)
self.tree.bind("<Double-1>", self.itemEvent)
yScr = ttk.Scrollbar(self.tree, orient = "vertical", command = self.tree.yview)
xScr = ttk.Scrollbar(self.tree, orient = "horizontal", command = self.tree.xview)
self.tree.configure(yscroll = yScr.set, xScroll = xScr.set)
self.tree.heading("#0", text = "My Tree", anchor = 'w')
yScr.pack(side = RIGHT, fill = Y)
pathy = os.path.abspath(path)
rootNode = self.tree.insert('', 'end', text = pathy, open = True)
self.createTree(rootNode, pathy)
self.tree.pack(side = LEFT, fill = BOTH, expand = 1, padx = 2, pady = 2)
self.pack(fill= BOTH, expand = 1)
def createTree(self, parent, path)
for p in os.listdir(path)
pathy = os.path.join(path, p)
isdir = os.path.isdir(pathy)
oid = self.tree.insert(parent, 'end' text = p, open = False)
if isdir:
self.createTree(oid, pathy)
def itemEvent(self, event):
item = self.tree.selection()[0] # now you got the item on that tree
print "you clicked on", self.tree.item(item,"text")
def main():
root = Tk.Tk()
app = Tree(root)
root.mainloop()
if __name__ == '__main__'
main()
You should not do this manually
there are ready to use pack that have this Feature and many more such as
tkintertable
it have some insane features
there is also pygubu-editable-treeview
if you are intrested in pygubu,
as for the the reason you shouldnt code your own ,
in order to do a good treeview you will need to build more Feature that make your gui easier to use
however such Feature takes hundred lines of code to create.(takes a long time to get right)
unless you are making a custom TREE-View-widget,it doesnot worth the effort.
I don't know about making the row editable, but to capture clicking on a row, you use the <<TreeviewSelect>> virtual event. This gets bound to a routine with the bind() method, then you use the selection() method to get the ids of the items selected.
These are snippets from an existing program, but show the basic sequence of calls:
# in Treeview setup routine
self.tview.tree.bind("<<TreeviewSelect>>", self.TableItemClick)
# in TableItemClick()
selitems = self.tview.tree.selection()
if selitems:
selitem = selitems[0]
text = self.tview.tree.item(selitem, "text") # get value in col #0

Categories

Resources