How to disable specific index/es or item/s in combobox? - python

I have a combobox and I need to disable some items/indexes. Here is my code.
months = ['january','feruary'.....'december']
cbMonth=ttk.Combobox(frame,values=months)
cbMonth.set(0)
cbMonth.place(relx=xDistance,rely=yDistance,relwidth=xWidth,relheight=yHeight)
cbMonth.update()
Let's say I want to disable or grayed out months of january,march and december or index 5,9,12. How to do it in Python. Something like;
cbMonth.Index[1].Enabled=false or
cbMonth.cget('january').Disable

There isn't a way to disable items in Combobox. But, if you still want to do so you will have to create a custom Combobox.
Here is a demo on how to create a custom Combobox. (you may add custom functionality if you want to).
import tkinter as tk
class CustomComboBox(tk.Frame):
def __init__(self, parent, values=[], validate='none', validatecommand='none',*args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.disabled = []
self.values = values
self.listbox = None
self.scrollbar = None
self.entry = tk.Entry(self, borderwidth=0, background="white",relief="groove")
self.entry.configure(validate=validate, validatecommand=validatecommand)
self.entry.pack(expand=True, fill='both', side='left')
self.entry.bind('<FocusIn>', self._removeListView)
self.arrowButton = tk.Label(self, text='↓', background="white", width=2)
self.arrowButton.pack(side='right', fill='y')
self.arrowButton.bind('<1>', self._listView)
self.previousIndex = 0
self.arrowButton.bind('<Enter>', self._changeBackground)
self.arrowButton.bind('<Leave>', self._changeBackground)
self.bind('<Configure>', self._updatePos)
def _changeBackground(self, event):
if str(event.type)=='Enter':
self.arrowButton.config(bg='#b3d2ff', border=1)
self.entry.config(border=1)
elif str(event.type) == 'Leave':
self.arrowButton.config(bg='white', border=0)
self.entry.config(border=0)
def _listView(self, event=None):
if self.listbox is None:
self.frame = tk.Frame(background=self['bg'] ,width=self.entry['width']+2)
self.frame.lift()
self.frame.place(x=self.winfo_x(), y=self.winfo_y()+self.winfo_height()-2, width=self.winfo_width())
self.listbox = tk.Listbox(self.frame, width=self.entry['width'], border=1, relief="groove", activestyle='none')
self.listbox.focus()
self._addValues()
self.listbox.bind('<<ListboxSelect>>', self._checkDisabled)
self.listbox.bind("<Motion>", self._highlight)
self.listbox.bind("<Leave>", self._removeHighlight)
self.listbox.pack(side='left', expand=True, fill='both')
self._updateListHeight()
else:
self._removeListView()
def _updatePos(self, event):
if self.listbox:
self.frame.place_configure(x=self.winfo_x(), y=self.winfo_y()+self.winfo_height()-2, width=self.winfo_width())
def _updateListHeight(self):
if self.listbox:
if self.listbox.size()>5:
self.listbox.configure(height=5)
if self.scrollbar is None:
self.scrollbar = tk.Scrollbar(self.frame, command = self.listbox.yview)
self.scrollbar.pack(side='right', fill="y")
self.listbox.config(yscrollcommand = self.scrollbar.set)
else:
self.listbox.configure(height=len(self.values))
if self.scrollbar:
self.scrollbar.destroy()
self.scrollbar = None
def _removeListView(self, event=None):
if self.listbox:
self.frame.destroy()
self.listbox = None
self.scrollbar = None
def _addValues(self, remove=(False, 0)):
if self.listbox:
if remove[0]:
self.listbox.delete(remove[1])
else:
for index, element in enumerate(self.values):
self.listbox.insert(index, element)
self._updateDisabled()
self._updateListHeight()
def _highlight(self, event):
self.listbox.itemconfig(self.previousIndex, background='white')
self.previousIndex = self.listbox.nearest(event.y)
if self.previousIndex not in self.disabled:
self.listbox.itemconfig(self.previousIndex, background='#b3d2ff')
def _removeHighlight(self, event):
self.listbox.itemconfig(self.previousIndex, background='white')
def _checkDisabled(self, event):
selection = self.listbox.curselection()[0]
if selection in self.disabled:
self.listbox.selection_clear(selection)
return
else:
self.entry.delete(0, 'end')
self.entry.insert(0, self.listbox.get(selection))
self.entry.focus_set()
self.event_generate('<<ComboboxSelected>>', when='tail')
def _updateDisabled(self, enable=(False, 0)):
if self.listbox:
for x in self.disabled:
self.listbox.itemconfig(x, fg="gray")
if enable[0]:
self.listbox.itemconfig(enable[1], fg="black")
def setValues(self, values): # adds values pass a list
self.values = values
self._addValues()
def removeValue(self, index): # removes values pass index
try:
self.values.pop(index)
self._addValues(remove=(True, index))
except IndexError:
pass
def disable(self, index:list): # disables the values and given indexes
if index not in self.disabled:
self.disabled.extend(index)
self._updateDisabled()
def enable(self, index:int): # enables the values at the given index
try:
self.disabled.remove(index)
self._updateDisabled((True, index))
except ValueError:
pass
def getCurrent(self): # returns current text in the entrybox
return self.entry.get()
def setDefault(self, index): # sets default value pass index
self.entry.insert(0, self.values[index])
(do note that this will not fully function like a Combobox).
Usage:
import tkinter as tk
from tkinter import ttk
from Combo import CustomComboBox
def truth(event):
return event.isalpha() or event==""
def fun(event):
print(monthchoosen.getCurrent())
monthchoosen.enable(2)
window = tk.Tk()
window.geometry('350x250')
ttk.Label(window, text = "Select the Month :",
font = ("Times New Roman", 10)).pack()
monthchoosen = CustomComboBox(window, validate='key', validatecommand=(window.register(truth), '%P'))
monthchoosen.values = ['January', 'Febuary', 'March', 'April', 'May', 'June', 'July']
monthchoosen.setDefault(1)
monthchoosen.bind('<<ComboboxSelected>>', fun)
monthchoosen.disable([2, 4, 3])
monthchoosen.pack(expand=True, fill='x')
window.mainloop()

You can change the values displayed (but not grayed) with postcommand:
def f():
co['values'] = ['c', 'd']
co = ttk.Combobox(values=['a', 'b'], postcommand=f)

Related

Changing one scale widget will change the secone one

I have two scale widgets in my code that are supposed to display values from a list. I want to be able to set individual values for both sliders. For example, for file 1 "2.4" and for file 2 "5.025". But currently I can only update the second (lower) slider without it triggering the first (upper) one. If I use the upper one, the value of the second one is also changed.
I'm stuck implementing the lambda function, so it would be create if some could give me a hint.
Thanks!
import tkinter as tk
class File_Selection():
def __init__(self, frame, text):
self.frame = frame
self.text = text
self.label_file = tk.Label(self.frame, text=text)
self.label_file.pack(side="left", anchor="s")
self.label_test = tk.Label(self.frame, text="| Select value: ")
self.label_test.pack(side="left", anchor="s")
self.scale = tk.Scale(self.frame, orient=tk.HORIZONTAL, from_=0)
self.scale.pack(side="left", anchor="s")
class View:
def __init__(self, view):
self.view = view
self.frame = tk.Frame(self.view)
self.frame.pack()
self.frame_row1 = tk.Frame(self.frame)
self.frame_row1.pack(side="top")
self.frame_row2 = tk.Frame(self.frame)
self.frame_row2.pack(side="top")
self.file_one = File_Selection(self.frame_row1, "File 1")
self.file_two = File_Selection(self.frame_row2, "File 2")
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
def run(self):
self.root.mainloop()
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)
if __name__ == "__main__":
c = Controller()
c.run()
The problem is that you have both scales set to active_scales. You can take advantage of the event and do away with active_scales.
You can get the widget associated with an event with event.widget. So, you can change the update_scales function from
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
to
def update_scale(self, event):
event.widget.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=event.widget: self.set_scale(value, event.widget))
Additionally, since self.values don't appear to change, it does not seem necessary to config each scale to change its bounds and resolution on each event. You can instead pull that out of the event and make you Controller class as follows.
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
self.view.file_one.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
self.view.file_two.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
def run(self):
self.root.mainloop()
def update_scale(self, event):
event.widget.config(command=lambda value=event.widget: self.set_scale(value, event.widget))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)

How to set background color of tk.simpledialog?

Is there a way to set the background color of a tkinter simpledialog window?
I am trying to implement a darkmode theme by changing background colors and am struggling with getting the simpledialog & filedialog windows to follow suit.
My current approach is to check right after creating a new toplevel, whether darkmode should apply, but I couldn't find any information in the Tkinter Dialogs documentation regarding options for the simpledialogs
top = tk.Toplevel()
if self.config["global"]["darkmode"] == "True":
top.configure(bg='black')
Simpledialog call:
cur_row = tk.simpledialog.askinteger("Row Selection", msg_string, parent = parent)
There isn't an existing argument, but I did some digging through the code for tkinter.simpledialog and it turns out that askinteger is based on the Toplevel class anyway so if you change tkinter/simpledialog.py to the code at the end of this answer, it adds a bg parameter to simpledialog that you can use as follows:
cur_row = tk.simpledialog.askinteger("Row Selection", msg_string, parent = parent, bg = "black")
I hope this helps :)
Here is the full code:
#
# An Introduction to Tkinter
#
# Copyright (c) 1997 by Fredrik Lundh
#
# This copyright applies to Dialog, askinteger, askfloat and asktring
#
# fredrik#pythonware.com
# http://www.pythonware.com
#
"""This modules handles dialog boxes.
It contains the following public symbols:
SimpleDialog -- A simple but flexible modal dialog box
Dialog -- a base class for dialogs
askinteger -- get an integer from the user
askfloat -- get a float from the user
askstring -- get a string from the user
"""
from tkinter import *
from tkinter import messagebox
import tkinter # used at _QueryDialog for tkinter._default_root
class SimpleDialog:
def __init__(self, master,
text='', buttons=[], default=None, cancel=None,
title=None, class_=None):
if class_:
self.root = Toplevel(master, class_=class_)
else:
self.root = Toplevel(master)
if title:
self.root.title(title)
self.root.iconname(title)
self.message = Message(self.root, text=text, aspect=400)
self.message.pack(expand=1, fill=BOTH)
self.frame = Frame(self.root)
self.frame.pack()
self.num = default
self.cancel = cancel
self.default = default
self.root.bind('<Return>', self.return_event)
for num in range(len(buttons)):
s = buttons[num]
b = Button(self.frame, text=s,
command=(lambda self=self, num=num: self.done(num)))
if num == default:
b.config(relief=RIDGE, borderwidth=8)
b.pack(side=LEFT, fill=BOTH, expand=1)
self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
self._set_transient(master)
def _set_transient(self, master, relx=0.5, rely=0.3):
widget = self.root
widget.withdraw() # Remain invisible while we figure out the geometry
widget.transient(master)
widget.update_idletasks() # Actualize geometry information
if master.winfo_ismapped():
m_width = master.winfo_width()
m_height = master.winfo_height()
m_x = master.winfo_rootx()
m_y = master.winfo_rooty()
else:
m_width = master.winfo_screenwidth()
m_height = master.winfo_screenheight()
m_x = m_y = 0
w_width = widget.winfo_reqwidth()
w_height = widget.winfo_reqheight()
x = m_x + (m_width - w_width) * relx
y = m_y + (m_height - w_height) * rely
if x+w_width > master.winfo_screenwidth():
x = master.winfo_screenwidth() - w_width
elif x < 0:
x = 0
if y+w_height > master.winfo_screenheight():
y = master.winfo_screenheight() - w_height
elif y < 0:
y = 0
widget.geometry("+%d+%d" % (x, y))
widget.deiconify() # Become visible at the desired location
def go(self):
self.root.wait_visibility()
self.root.grab_set()
self.root.mainloop()
self.root.destroy()
return self.num
def return_event(self, event):
if self.default is None:
self.root.bell()
else:
self.done(self.default)
def wm_delete_window(self):
if self.cancel is None:
self.root.bell()
else:
self.done(self.cancel)
def done(self, num):
self.num = num
self.root.quit()
class Dialog(Toplevel):
'''Class to open dialogs.
This class is intended as a base class for custom dialogs
'''
def __init__(self, parent, title = None, bg = None):
'''Initialize a dialog.
Arguments:
parent -- a parent window (the application window)
title -- the dialog title
'''
Toplevel.__init__(self, parent, bg = bg)
self.withdraw() # remain invisible for now
# If the master is not viewable, don't
# make the child transient, or else it
# would be opened withdrawn
if parent.winfo_viewable():
self.transient(parent)
if title:
self.title(title)
self.parent = parent
self.result = None
body = Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=5, pady=5)
self.buttonbox()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
if self.parent is not None:
self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
parent.winfo_rooty()+50))
self.deiconify() # become visible now
self.initial_focus.focus_set()
# wait for window to appear on screen before calling grab_set
self.wait_visibility()
self.grab_set()
self.wait_window(self)
def destroy(self):
'''Destroy the window'''
self.initial_focus = None
Toplevel.destroy(self)
#
# construction hooks
def body(self, master):
'''create dialog body.
return widget that should have initial focus.
This method should be overridden, and is called
by the __init__ method.
'''
pass
def buttonbox(self):
'''add standard button box.
override if you do not want the standard buttons
'''
box = Frame(self)
w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
w.pack(side=LEFT, padx=5, pady=5)
w = Button(box, text="Cancel", width=10, command=self.cancel)
w.pack(side=LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack()
#
# standard button semantics
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
try:
self.apply()
finally:
self.cancel()
def cancel(self, event=None):
# put focus back to the parent window
if self.parent is not None:
self.parent.focus_set()
self.destroy()
#
# command hooks
def validate(self):
'''validate the data
This method is called automatically to validate the data before the
dialog is destroyed. By default, it always validates OK.
'''
return 1 # override
def apply(self):
'''process the data
This method is called automatically to process the data, *after*
the dialog is destroyed. By default, it does nothing.
'''
pass # override
# --------------------------------------------------------------------
# convenience dialogues
class _QueryDialog(Dialog):
def __init__(self, title, prompt,
initialvalue=None,
minvalue = None, maxvalue = None,
parent = None, bg = None):
if not parent:
parent = tkinter._default_root
self.prompt = prompt
self.minvalue = minvalue
self.maxvalue = maxvalue
self.initialvalue = initialvalue
Dialog.__init__(self, parent, title, bg = bg)
def destroy(self):
self.entry = None
Dialog.destroy(self)
def body(self, master):
w = Label(master, text=self.prompt, justify=LEFT)
w.grid(row=0, padx=5, sticky=W)
self.entry = Entry(master, name="entry")
self.entry.grid(row=1, padx=5, sticky=W+E)
if self.initialvalue is not None:
self.entry.insert(0, self.initialvalue)
self.entry.select_range(0, END)
return self.entry
def validate(self):
try:
result = self.getresult()
except ValueError:
messagebox.showwarning(
"Illegal value",
self.errormessage + "\nPlease try again",
parent = self
)
return 0
if self.minvalue is not None and result < self.minvalue:
messagebox.showwarning(
"Too small",
"The allowed minimum value is %s. "
"Please try again." % self.minvalue,
parent = self
)
return 0
if self.maxvalue is not None and result > self.maxvalue:
messagebox.showwarning(
"Too large",
"The allowed maximum value is %s. "
"Please try again." % self.maxvalue,
parent = self
)
return 0
self.result = result
return 1
class _QueryInteger(_QueryDialog):
errormessage = "Not an integer."
def getresult(self):
return self.getint(self.entry.get())
def askinteger(title, prompt, **kw):
'''get an integer from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is an integer
'''
d = _QueryInteger(title, prompt, **kw)
return d.result
class _QueryFloat(_QueryDialog):
errormessage = "Not a floating point value."
def getresult(self):
return self.getdouble(self.entry.get())
def askfloat(title, prompt, **kw):
'''get a float from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is a float
'''
d = _QueryFloat(title, prompt, **kw)
return d.result
class _QueryString(_QueryDialog):
def __init__(self, *args, **kw):
if "show" in kw:
self.__show = kw["show"]
del kw["show"]
else:
self.__show = None
_QueryDialog.__init__(self, *args, **kw)
def body(self, master):
entry = _QueryDialog.body(self, master)
if self.__show is not None:
entry.configure(show=self.__show)
return entry
def getresult(self):
return self.entry.get()
def askstring(title, prompt, **kw):
'''get a string from the user
Arguments:
title -- the dialog title
prompt -- the label text
**kw -- see SimpleDialog class
Return value is a string
'''
d = _QueryString(title, prompt, **kw)
return d.result
if __name__ == '__main__':
def test():
root = Tk()
def doit(root=root):
d = SimpleDialog(root,
text="This is a test dialog. "
"Would this have been an actual dialog, "
"the buttons below would have been glowing "
"in soft pink light.\n"
"Do you believe this?",
buttons=["Yes", "No", "Cancel"],
default=0,
cancel=2,
title="Test Dialog")
print(d.go())
print(askinteger("Spam", "Egg count", initialvalue=12*12))
print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
maxvalue=100))
print(askstring("Spam", "Egg label"))
t = Button(root, text='Test', command=doit)
t.pack()
q = Button(root, text='Quit', command=t.quit)
q.pack()
t.mainloop()
test()
I just added by own buttonbox method, found the children and configured them.
class bug_dialog(tk.simpledialog.Dialog):
def buttonbox(self):
super().buttonbox()
for _ in self.children.values(): _.configure(bg='light green')
self.configure(bg='light green')

communication between tkinter toplevels

I'm working on a little project and made a little on-screen keyboard as a tkinter Toplevel
my application is buildt like this:
Root-Window (Tk-Widget)
input 1 (Entry-Widget)
input 2 (Entry-Widget)
input 3 (Text-Widget)
on_screen-keyboard (Toplevel-Widget)
the Toplevel-Widget contains Buttons, with callbacks that should enter text in the entries (just like keyboard-Buttons)
What I want is a communication between children of the keyboard/the keyboard and the last active input-Widget. My Problem is, that I don't know, how to say the keyboard, to which input-Widget it should send the message.
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.active_input = tk.Variable(value=None)
ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
self.text = tk.StringVar(value="")
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", lambda e: self.active_input.set(self.input1))
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", lambda e: self.active_input.set(self.input2))
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", lambda e: self.active_input.set(self.input3))
self.input1.pack()
self.input3.pack()
self.input2.pack()
class Keyboard(tk.Toplevel):
OPENED = False
NAME = "- Keyboard -"
NUM = [{"text":"1", "width":1},
{"text":"2", "width":1},
{"text":"3", "width":2}]
CHAR= [{"text":"A", "width":1},
{"text":"B", "width":1},
{"text":"C", "width":2}]
def __init__(self, master):
if not Keyboard.OPENED:
Keyboard.OPENED = True
print("keyboard opened!")
self.master = master
tk.Toplevel.__init__(self, master)
self.title(self.NAME)
self.protocol("WM_DELETE_WINDOW", self.close)
self.keyb_nb = ttk.Notebook(self)
self.keyb_nb.pack()
self.num_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.num_tab, Keyboard.NUM,2)
self.keyb_nb.add(self.num_tab, text="123")
self.char_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.char_tab, Keyboard.CHAR, 2)
self.keyb_nb.add(self.char_tab, text="ABC")
def createPad(self, master, pad:list, max_col):
self.co_count = 0
self.ro = 1
for button in pad:
button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=self.bclicked(button))
if self.co_count >= max_col:
self.ro = self.ro + 1
self.co_count = 0
button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
self.co_count = self.co_count+button["width"]
def bclicked(self, button:dict):
"""
reciver = self.master.active_input #I think the Problem here is, that the variable contains a string, not a widget
reciever.focus_force()
reciever.insert(index=tk.INSERT, string=button["text"])
"""
pass
def close(self):
Keyboard.OPENED = False
self.destroy()
print("keyboard closed!")
root = MainWindow()
root.mainloop()
Here the init of the Mainwindow and the bclicked of the Keyboard class are important...
the code is debug-ready
I would prefer a solution, similar to the communication in the internet (sender=button, receiver-id, message), but very welcome every working solution
btw: I'm also looking for a solution, how I don't have to force the input to focus and the Toplevel stays an the highest layer of the screen (that if I focus the Tk-Widget/one of the inputs/the button, the keyboard will stay in front of it)
SUMMARY: how do I find out, which of the 3 input-widgets was active at last, when the keyboard-toplevel has already the focus?
I may made more changes than needed, but mainly focus on the method keyboard_triger() and pass_key_to_master(), this two use the idea that the variable master implements, having access to call methods out of scope.
Olso the method set_focused_object() stores a reference to the last object beeng focused, note than it stores the widget and not the event, it's easyer than searching each time the object
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def keyboard_triger(self, key):
# to identify wath object is just use
# isinstance(self.active_input, ttk.Entry)
self.active_input.insert(tk.END, key)
def new_keyboard(self):
Keyboard(self)
def set_focused_object(self, event):
self.active_input = event.widget
def __init__(self):
tk.Tk.__init__(self)
self.active_input = None
ttk.Button(self, text="Show Keyboard", command=self.new_keyboard).pack()
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", self.set_focused_object)
self.input1.pack()
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", self.set_focused_object)
self.input2.pack()
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", self.set_focused_object)
self.input3.pack()
class Keyboard(tk.Toplevel):
def pass_key_to_master(self, key):
self.master.keyboard_triger(key)
def __init__(self, master):
tk.Toplevel.__init__(self, master)
self.master = master
self.title('Keyboard')
# this way of agruping keys stores the kwags
# of the drawing method
keys = {
'A': {'x': 0, 'y': 0},
'B': {'x': 20, 'y': 20},
'C': {'x': 50, 'y': 50}
}
# expected structure
# {string key: reference to the button}
self.buttons = {}
for i in keys:
self.buttons[i] = tk.Button( # i=i is required to make a instance
self, text=i, command=lambda i=i: self.pass_key_to_master(i)
)
self.buttons[i].place(**keys[i])
if __name__ == '__main__':
root = MainWindow()
root.mainloop()
Your code maybe could have a better construction.(But I didn't revise your code construction.)
Followed by your code,I use a global variable.And fix some errors in your code.And it could work normally in my computer.
import tkinter as tk
from tkinter import ttk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.active_input = tk.Variable(value=None)
ttk.Button(self, text="show Keyboard", command=lambda: Keyboard(self)).pack()
global focusedWidget
focusedWidget = None
self.text = tk.StringVar(value="")
self.input1 = ttk.Entry(self)
self.input1.bind("<FocusIn>", self.getFocusWidget)
self.input2 = ttk.Entry(self)
self.input2.bind("<FocusIn>", self.getFocusWidget)
self.input3 = tk.Text(self, height=3, width=15)
self.input3.bind("<FocusIn>", self.getFocusWidget)
self.input1.pack()
self.input3.pack()
self.input2.pack()
def getFocusWidget(self,event): # this function could be a static function
global focusedWidget
focusedWidget = event.widget
class Keyboard(tk.Toplevel):
OPENED = False
NAME = "- Keyboard -"
NUM = [{"text":"1", "width":1},
{"text":"2", "width":1},
{"text":"3", "width":2}]
CHAR= [{"text":"A", "width":1},
{"text":"B", "width":1},
{"text":"C", "width":2}]
def __init__(self, master):
if not Keyboard.OPENED:
Keyboard.OPENED = True
print("keyboard opened!")
self.master = master
tk.Toplevel.__init__(self, master)
self.title(self.NAME)
self.protocol("WM_DELETE_WINDOW", self.close)
self.keyb_nb = ttk.Notebook(self)
self.keyb_nb.pack()
self.num_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.num_tab, Keyboard.NUM,2)
self.keyb_nb.add(self.num_tab, text="123")
self.char_tab = ttk.Frame(self.keyb_nb)
self.createPad(self.char_tab, Keyboard.CHAR, 2)
self.keyb_nb.add(self.char_tab, text="ABC")
def createPad(self, master, pad:list, max_col):
self.co_count = 0
self.ro = 1
for button in pad:
button["id"] = ttk.Button(master, width=6*button["width"], text=button["text"], command=lambda button=button:self.bclicked(button)) # this lambda expression has some errors.
if self.co_count >= max_col:
self.ro = self.ro + 1
self.co_count = 0
button["id"].grid(row=self.ro, columnspan=button["width"], column=self.co_count)
self.co_count = self.co_count+button["width"]
def bclicked(self, button:dict):
global focusedWidget
"""
reciver = self.master.active_input #I think the Problem here is, that the variable contains a string, not a widget
reciever.focus_force()
reciever.insert(index=tk.INSERT, string=button["text"])
"""
if not focusedWidget: # If user hasn't click a entry or text widget.
print("Please select a entry or text")
return
if focusedWidget.widgetName=='ttk::entry': # use if statement to check the type of selected entry.
focusedWidget.insert(index=tk.INSERT,string=button["text"])
else:
focusedWidget.insert("end",button["text"])
def close(self):
Keyboard.OPENED = False
self.destroy()
print("keyboard closed!")
root = MainWindow()
root.mainloop()

tkinter Multilistbox custom event binding problem

So my project is about showing information from a database in conjunction with images.
The idea is the following:
I have a database that describes images. One table has general information, like which color is contained in which image and another table has more granular information, like where that color can be found, etc.
When I launch a search, I want two windows to open (ClassTwo and ClassThree).
Instead of the first Window that only contains a Button to launch the search, my full application contains fields that are used to filter a database request. When I launch that search I want e.g. all images with the color green in them, so ClassTwo would list all images with that color, along with additional metadata.
ClassThree would then list all areas that contain the same color, but with a bit more detail, like position in the image and size, etc.
Upon clicking on either of the MultiListbox, I want to open an ImageViewer that shows the image.
In case of ClassThree, I would also directly highlight the area that I clicked on, so both classes would have functions bound to the MultiListbox.
My problem is with the binding of the Listboxes, that does not work properly. When I use e.g. image_parts_info_lb.bind() the function is not triggered at all. When i use image_parts_info_lb.bind_all() only the function of the last MultiListbox is triggered.
You can find the original Python2 version of the MultiListbox in the comment of the class.
Here is my code
import tkinter as tk
class MultiListbox(tk.Frame):
#original Python2 code here:
#https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s05.html
def __init__(self, master, lists):
tk.Frame.__init__(self, master)
self.lists = []
print(lists)
for l,w in lists:
frame = tk.Frame(self)
frame.pack(side='left', expand='yes', fill='both')
tk.Label(frame, text=l, borderwidth=1, relief='raised').pack(fill='x')
lb = tk.Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
relief='flat', exportselection=False, height=16)
lb.pack(expand='yes', fill='both')
self.lists.append(lb)
#commented out functions that were not necessary, as suggested in the comments
#lb.bind('<B1-Motion>', self._select)
lb.bind('<<ListboxSelect>>', self._select)
#lb.bind('<Leave>', lambda e: 'break')
lb.bind('<MouseWheel>', lambda e, s=self: s._scroll_mouse(e))
#lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
frame = tk.Frame(self)
frame.pack(side='left', fill='y')
tk.Label(frame, borderwidth=1, relief='raised').pack(fill='x')
sb = tk.Scrollbar(frame, orient='vertical', command=self._scroll)
sb.pack(expand='yes', fill='y')
self.lists[0]['yscrollcommand']=sb.set
def _select(self, event):
w = event.widget
curselection = w.curselection()
if curselection:
self.selection_clear(0, self.size())
self.selection_set(curselection[0])
def _button2(self, x, y):
for l in self.lists:
l.scan_mark(x, y)
return 'break'
def _b2motion(self, x, y):
for l in self.lists: l.scan_dragto(x, y)
return 'break'
def _scroll(self, *args):
for l in self.lists:
l.yview(*args)
return 'break'
def _scroll_mouse(self, event):
for l in self.lists:
l.yview_scroll(int(-1*(event.delta/120)), 'units')
return 'break'
def curselection(self):
return self.lists[0].curselection()
def delete(self, first, last=None):
for l in self.lists:
l.delete(first, last)
def get(self, first, last=None):
result = []
for l in self.lists:
result.append(l.get(first,last))
if last: return apply(map, [None] + result)
return result
def index(self, index):
self.lists[0].index(index)
def insert(self, index, *elements):
for e in elements:
for i, l in enumerate(self.lists):
l.insert(index, e[i])
def size(self):
return self.lists[0].size()
def see(self, index):
for l in self.lists:
l.see(index)
def selection_anchor(self, index):
for l in self.lists:
l.selection_anchor(index)
def selection_clear(self, first, last=None):
for l in self.lists:
l.selection_clear(first, last)
def selection_includes(self, index):
return self.lists[0].selection_includes(index)
def selection_set(self, first, last=None):
for l in self.lists:
l.selection_set(first, last)
class ClassOne:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.create_elements()
self.frame.pack()
def create_elements(self):
search_button = tk.Button(self.frame, text = "Launch searches", command = \
self.call_other_classes)
search_button.grid(row = 2, column = 0, padx = 20, pady = 10)
exit_button = tk.Button(self.frame, text = "Exit", command = self.master.quit)
exit_button.grid(row = 2, column = 1, padx = 20, pady = 10)
def call_other_classes(self):
self.classes_list = []
self.classes_list.append(ClassTwo(self.master))
self.classes_list.append(ClassThree(self.master))
class ClassTwo:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(tk.Toplevel(self.master))
self.frame.pack()
self.create_elements()
self.fill_image_listbox()
def create_elements(self):
#placement of the custom MultiListbox
self.available_images_lb = MultiListbox(self.frame, (('stuff1', 0), ('stuff1', 0), \
('stuff1', 0), ('stuff1', 0), ('stuff1', 0) ))
self.available_images_lb.grid(row = 1, column = 1)
self.available_images_lb.bind('<<ListboxSelect>>', self.print_stuff_two)
#Button
exit_button = tk.Button(self.frame, text = "Quit", command = self.frame.quit)
exit_button.grid(row = 2, column = 1, padx = 20, pady = 10)
def fill_image_listbox(self):
image_info = [5*['ABCD'],5*['EFGH'],5*['JKLM'],5*['NOPQ'],5*['RSTU'], 5*['VWXY']]
for item in image_info:
self.available_images_lb.insert('end', item)
def print_stuff_two(self, event):
print('Class Two active')
class ClassThree:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(tk.Toplevel(self.master))
self.create_elements()
self.frame.pack()
self.fill_imageparts_listbox()
def create_elements(self):
self.image_parts_info_lb = MultiListbox(self.frame, (('stuff1', 0), ('stuff1', 0), \
('stuff1', 0), ('stuff1', 0), ('stuff1', 0) ))
self.image_parts_info_lb.grid(row = 1, column = 1)
self.image_parts_info_lb.bind('<<ListboxSelect>>', self.print_stuff_three)
def fill_imageparts_listbox(self):
self.image_parts_info_lb.delete(0, 'end')
image_part_info = [5*['1234'],5*['5678'],5*['91011']]
for item in image_part_info:
self.image_parts_info_lb.insert('end', item)
def print_stuff_three(self, event):
print('Class Three active')
def main():
root = tk.Tk()
root.title('Image Viewer')
root.geometry('500x150+300+300')
my_class_one = ClassOne(root)
root.mainloop()
if __name__ == "__main__":
main()
Instead of the printing functions I would launch a simple Image Viewer and use Pillow to highlight areas in the image. That functionality is implemented and works well, only the function binding to the custom Listbox is not working as it should.
I am open to any input. Also, if you have any recommendations for coding patterns, feel free to let me know.
Question: MultiListbox custom event binding problem, click on any of the list elements, trigger the print_stuff_x function
Implement a custom event '<<MultiListboxSelect>>' which get fired on processed a '<<ListboxSelect>>' event and .bind(... at it.
Reference:
Tkinter.Widget.event_generate-method
event generate
Generates a window event and arranges for it to be processed just as if it had come from the window system. If the -when option is specified then it determines when the event is processed.
Crore Point:
self.event_generate('<<MultiListboxSelect>>', when='tail')
class MultiListbox(tk.Frame):
def __init__(self, master, lists):
...
for l,w in lists:
...
lb = tk.Listbox(frame, ...
lb.bind('<<ListboxSelect>>', self._select)
def _select(self, event):
w = event.widget
curselection = w.curselection()
if curselection:
self.event_generate('<<MultiListboxSelect>>', when='tail')
...
class ClassTwo:
...
def create_elements(self):
self.available_images_lb = MultiListbox(self.frame, ...
...
self.available_images_lb.bind('<<MultiListboxSelect>>', self.print_stuff_two)
...
class ClassThree:
...
def create_elements(self):
self.image_parts_info_lb = MultiListbox(self.frame, ...
...
self.image_parts_info_lb.bind('<<MultiListboxSelect>>', self.print_stuff_three)

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()

Categories

Resources