Using callback function in tkinter entry through buttons and StringVar - python-3.x

I am using tkinter to browse json files and pass them into an Entry with the intention of using them later on.
However, I would like each json file passed to be processed in some way using a callback function that gets the entry's text. Because other buttons are also changing this entry (clear button, etc.) I want it to be 'readonly' so that the user cannot mess up the format with the keyboard. StringVar conveniently solves this without having to change the state of the widget in order to change it.
I'm trying to just print the content of the entry every time it changes (json_entry_callback) but my code seems to print it only when the UI initializes. Here's my app:
import customtkinter as ctk
class AppConfigurationGui(ctk.CTk):
ctk.set_appearance_mode('dark')
ctk.set_default_color_theme('dark-blue')
window_width = 800
window_height = 480
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.geometry('{}x{}'.format(self.window_width, self.window_height))
self.resizable(False, False)
# Create JSON Loader widgets
self.json_frame = ctk.CTkFrame(self, width=200, corner_radius=0, fg_color='transparent')
self.json_frame.grid(row=0, column=1, sticky="nsew")
self.json_top_label = ctk.CTkLabel(self.json_frame,
text='JSON File Loader',
font=ctk.CTkFont(size=13, weight='bold'))
self.json_top_label.grid(row=0, column=0, columnspan=3, padx=20, pady=(10, 0), sticky='wn')
self.json_browse_button = ctk.CTkButton(self.json_frame,
text='Browse',
width=5,
command=self.browse_for_json)
self.json_browse_button.grid(row=1, column=0, padx=(20, 0), pady=(5, 10))
self.json_entry_sv = ctk.StringVar(self.json_frame, value='... search for a json file')
self.json_entry = ctk.CTkEntry(self.json_frame,
textvariable=self.json_entry_sv,
validate='focus',
validatecommand=self.json_entry_callback,
state='readonly',
width=200,
height=8,
font=ctk.CTkFont(size=13, slant='italic'))
self.json_entry.grid(row=1, column=1, padx=10, pady=(5, 10))
self.json_clear_button = ctk.CTkButton(self.json_frame,
text='Clear',
width=25,
height=25,
anchor='center',
command=self.clear_json_entry)
self.json_clear_button.grid(row=1, column=2, padx=0, pady=(5, 10))
def write_in_json_entry(self, text: str):
self.json_entry_sv.set(text)
def browse_for_json(self):
filename = 'test.json'
self.write_in_json_entry(filename)
def clear_json_entry(self):
self.write_in_json_entry('... search for a json file')
def json_entry_callback(self):
print(self.json_entry_sv.get())
if __name__ == "__main__":
configurator = AppConfigurationGui()
configurator.mainloop()
As soon as the code runs, '... search for a json file' is printed but when I click 'Browse' and the entry text changes, 'test.json' is not printed, as if the callback function is not called at all. I suspect this has to do with the validation condition not being met, since I am changing the StringVar and not using the entry directly.
So basically my questions are:
Why is the content of the entry printed in initialization but not in runtime?
What is the correct way to run the callback function while keeping the StringVar? (Let me know if there is a better alternative)
(Side question) Why is the 'Clear' button not centered within the row like the other widgets? :D
Thanks in advance

Related

How can I set the default container in a decorator class for tkinter.Frame?

I would like to create a contractible panel in a GUI, using the Python package tkinter.
My idea is to create a decorator for the tkinter.Frameclass, adding a nested frame and a "vertical button" which toggles the nested frame.
Sketch: (Edit: The gray box should say Parent of contractible panel)
I got it to toggle just fine, using the nested frame's grid_remove to hide it and then move the button to the left column (otherwise occupied by the frame).
Now I want to be able to use it like any other tkinter.Frame, but let it target the nested frame. Almost acting like a proxy for the nested frame. For example, adding a tkinter.Label (the green Child component in the sketch) to the decorator should add the label to the nested frame component (light yellow tk.Frame in the sketch) not the decorator itself (strong yellow ContractiblePanel in the sketch).
Minimal example: (omitting the toggling stuff and any "formatting"):
(Here's a published (runnable) Repl project)
import tkinter
class ContractiblePanel(tkinter.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self._panel = tkinter.Frame(self)
self._toggle = tkinter.Button(self, text='<', command=self._toggle_panel)
self.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
def _toggle_panel(self):
# ...
if __name__ == '__main__':
root = tkinter.Tk()
root.geometry('128x128')
contractible_panel = ContractiblePanel(root)
Forwarding configuration calls is just overriding the config method I guess?
class ContractiblePanel(tkinter.Frame):
# ...
def config(self, **kwargs):
self._panel.config(**kwargs)
# ...
contractible_panel.config(background='blue')
But I would like to be able to add a child component into the nested panel frame by
label_in_panel = tkinter.Label(contractible_panel, text='yadayada')
How do I get the ContractiblePanel object to act like a proxy to its member _panel, when adding child components?
What other methods/use cases should I consider? I am quite new to tkinter and thus expect the current implementation to break some common practices when developing tkinter GUIs.
This is an interesting question. Unfortunately, tkinter really isn't designed to support what you want. I think it would be less complicated to simply expose the inner frame and add widgets to it.
That being said, I'll present one possible solution. It's not implemented as a python decorator, but rather a custom class.
The difficulty is that you want the instance of the custom class to represent the outer frame in one context (for example, when packing it in your UI) and the inner frame in another context (when adding child widgets to it)
The following solution solves this by making the instance be the inner frame, and then overriding pack,place, and grid so that they operates on the outer frame. This works fine, with an important exception: you cannot use this class directly inside a notebook or embedded in a text widget or canvas.
I've used colors and borders so it's easy to see the individual components, but you can remove the colors in production code, obviously. Also, I used a label instead of a button since I created the screenshot on OSX where the background color of a button can't be changed.
import tkinter as tk
class ContractiblePanel(tk.Frame):
def __init__(self, parent, **kwargs):
self._frame = tk.Frame(parent, **kwargs)
super().__init__(self._frame, bd=2, relief="solid", bg="#EFE4B0")
self._button = tk.Label(
self._frame, text="<", bg="#00A2E8", bd=2,
relief="solid", font=("Helvetica", 20), width=4
)
self._frame.grid_rowconfigure(0, weight=1)
self._frame.grid_columnconfigure(0, weight=1)
self._button.grid(row=0, column=1, sticky="ns", padx=4, pady=4)
super().grid(row=0, column=0, sticky="nsew", padx=4, pady=4)
self._button.bind("<1>", lambda event: self.toggle())
def collapse(self):
super().grid_remove()
self._button.configure(text=">")
def expand(self):
super().grid()
self._button.configure(text="<")
def toggle(self):
self.collapse() if self.winfo_viewable() else self.expand()
def pack(self, **kwargs):
# override to call pack in the private frame
self._frame.pack(**kwargs)
def grid(self, **kwargs):
# override to call grid in the private frame
self._frame.grid(**kwargs)
def place(self, **kwargs):
# override to call place in the private frame
self._frame.place(**kwargs)
root = tk.Tk()
root.geometry("400x300")
cp = ContractiblePanel(root, bg="yellow", bd=2, relief="raised")
cp.pack(side="left", fill="y", padx=10, pady=10)
label = tk.Label(cp, text="Child component", background="#22B14C", height=3, bd=2, relief="solid")
label.pack(side="top", expand=True, padx=20, pady=20)
root.mainloop()
First of all it is kinda gross to use this code and it's very confusing. So I'm really not sure if you really want to take this route. However, it is possible to achieve it.
The basic idea is to have a wrapper and to pretend the wrapper is the actual object you can lie with __str__ and __repr__ about what the class really is. That is not what a proxy means.
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
You can do even more confusing things by delegate the lookup-chain to the _wrapped_frame inside the WrapperClass this enables you to call on the instance of WrapperFrame() methods like pack or every other method. It kinda works similar for inheritance with the difference that by referring to the object, you will point to different one.
I don't recommend using this code by the way.
import tkinter as tk
NONE = object()
#use an object here that there will no mistake
class WrapperClass:
def __init__(self, master=None, **kwargs):
self._wrapped_frame = tk.Frame(master, **kwargs)
self._panel = tk.Frame(self._wrapped_frame)
self._toggle = tk.Button(self._wrapped_frame, text='<', command=self._toggle_panel)
self._wrapped_frame.grid(row=0, column=0, sticky='nsw')
self._panel.grid(row=0, column=0, sticky='nsw')
self._toggle.grid(row=0, column=1, sticky='nsw')
return None
def _toggle_panel(self):
print('toggle')
def __str__(self):
return self._panel._w
__repr__ = __str__
def __getattr__(self, name):
#when wrapper class has no attr name
#delegate the lookup chain to self.frame
inreturn = getattr(self._wrapped_frame, name, NONE)
if inreturn is NONE:
super().__getattribute__(name)
return inreturn
root = tk.Tk()
wrapped_frame = WrapperClass(root, bg='red', width=200, height=200)
root.mainloop()

TKInter - Confused about frames and scrolling

I am trying to code a tkinter application that has three frames - a top frame, where the user inputs some text, a dynamically constructed middle section where some pre-analysis is conducted on the text, and a bottom frame where, once the user has selected which option they want in the middle section, the output will be produced.
The problem is that, depending upon the input, there could be around 10-20 (and in the worst case 30) lines displayed and on a small monitor the output will disappear off the screen.
What I would like is for the top (input) and bottom (output) frames to be visible no matter how the screen is re-sized, and for the middle section to scroll (if required) and still allow the user to select their choice.
I am confused as to how to get the middle section to resize when the screen is resized, show a scrollbar if required, and still allow all of the content to be accessed.
I have created a cut-down version here (for simplicity, I have removed the processing methods and have instead created some fake output in a loop that resembles what the actual middle section would look like).
Please ignore the hideous colour-scheme - I was just trying to understand which frame went where (I will remove the colours as soon as I can!)
Thank you for any suggestions...
import tkinter as tk
from tkinter import scrolledtext
class MyApp(tk.Tk):
def __init__(self, title="Sample App", *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title(title)
self.configure(background="Gray")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Create the overall frame:
master_frame = tk.Frame(self, bg="Light Blue", bd=3, relief=tk.RIDGE)
master_frame.grid(sticky=tk.NSEW)
master_frame.rowconfigure([0, 2], minsize=90) # Set min size for top and bottom
master_frame.rowconfigure(1, weight=1) # Row 1 should adjust to window size
master_frame.columnconfigure(0, weight=1) # Column 0 should adjust to window size
# Create the frame to hold the input field and action button:
input_frame = tk.LabelFrame(master_frame, text="Input Section", bg="Green", bd=2, relief=tk.GROOVE)
input_frame.grid(row=0, column=0, padx = 5, pady = 5, sticky=tk.NSEW)
input_frame.columnconfigure(0, weight=1)
input_frame.rowconfigure(0, weight=1)
# Create a frame for the middle (processing) section.
middle_frame = tk.LabelFrame(master_frame, text = "Processing Section")
middle_frame.grid(row=1, column=0, padx=5, pady=5, sticky=tk.NSEW)
# Create the frame to hold the output:
output_frame = tk.LabelFrame(master_frame, text="Output Section", bg="Blue", bd=2, relief=tk.GROOVE)
output_frame.grid(row=2, column=0, columnspan=3, padx=5, pady=5, sticky=tk.NSEW)
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(0, weight=1)
# Add a canvas in the middle frame.
self.canvas = tk.Canvas(middle_frame, bg="Yellow")
self.canvas.grid(row=0, column=0)
# Create a vertical scrollbar linked to the canvas.
vsbar = tk.Scrollbar(middle_frame, orient=tk.VERTICAL, command=self.canvas.yview)
vsbar.grid(row=0, column=1, sticky=tk.NS)
self.canvas.configure(yscrollcommand=vsbar.set)
# Content for the input frame, (one label, one input box and one button).
tk.Label(input_frame,
text="Please type, or paste, the text to be analysed into this box:").grid(row=0, columnspan = 3, sticky=tk.NSEW)
self.input_box = scrolledtext.ScrolledText(input_frame, height=5, wrap=tk.WORD)
self.input_box.columnconfigure(0, weight=1)
self.input_box.grid(row=1, column=0, columnspan = 3, sticky=tk.NSEW)
tk.Button(input_frame,
text="Do it!",
command=self.draw_choices).grid(row=2, column=2, sticky=tk.E)
# Content for the output frame, (one text box only).
self.output_box = scrolledtext.ScrolledText(output_frame, width=40, height=5, wrap=tk.WORD)
self.output_box.grid(row=0, column=0, columnspan=3, sticky=tk.NSEW)
def draw_choices(self):
""" This method will dynamically create the content for the middle frame"""
self.option = tk.IntVar() # Variable used to hold user's choice
self.get_input_text()
for i in range(30):
tk.Radiobutton(self.canvas,
text=f"Option {i + 1}: ", variable=self.option,
value=i,
command=self.do_analysis
).grid(row=i, column=0, sticky=tk.W)
tk.Label(self.canvas,
text=f"If you pick Option {i + 1}, the output will look like this: {self.shortText}.",
anchor=tk.W
).grid(row=i, column=1, sticky=tk.W)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def get_input_text(self):
""" Will get the text from the input box and also create a shortened version to display on one line"""
screenWidth = 78
self.input_text = self.input_box.get(0.0, tk.END)
if len(self.input_text) > screenWidth:
self.shortText = self.input_text[:screenWidth]
else:
self.shortText = self.input_text[:]
self.shortText = self.shortText.replace('\n', ' ') # strip out carriage returns just in case
def do_analysis(self):
"""This will ultimately process and display the results"""
option = self.option.get() # Get option from radio button press
output_txt = f"You picked option {option + 1} and here is the output: \n{self.input_text}"
self.output_box.delete(0.0, tk.END)
self.output_box.insert(0.0, output_txt)
if __name__ == "__main__":
app = MyApp("My Simple Text Analysis Program")
app.mainloop()
I understand that you can't mix grid and pack geometries in the same container, and that a scrollbar must be attached to a canvas, and objects to be placed on that canvas must therefore be in yet another container so, attempting to follow Bryan's example, I created a minimal version of what I want - window with three sections - top, middle and bottom. The Top and bottom sections will contain a simple text field, the middle section will contain dynamic content and must be able to scroll as required.
Imports:
ScrollbarFrame
Extends class tk.Frame to support a scrollable Frame]
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("A simple GUI")
# Top frame
self.top_frame = tk.Frame(self, bg="LIGHT GREEN")
self.top_frame.pack(fill=tk.X)
tk.Label(self.top_frame, bg=self.top_frame.cget('bg'),
text="This is a label on the top frame")\
.grid(row=0, columnspan=3, sticky=tk.NSEW)
# Middle Frame
# Import from https://stackoverflow.com/a/62446457/7414759
# and don't change anything
sbf = ScrollbarFrame(self, bg="LIGHT BLUE")
sbf.pack(fill=tk.X, expand=True)
# self.middle_frame = tk.Frame(self, bg="LIGHT BLUE")
self.middle_frame = sbf.scrolled_frame
# Force scrolling by adding multiple Label
for _ in range(25):
tk.Label(self.middle_frame, bg=self.middle_frame.cget('bg'),
text="This is a label on the dynamic (middle) section")\
.grid()
# Bottom Frame
self.bottom_frame = tk.Frame(self, bg="WHITE")
self.bottom_frame.pack(fill=tk.X)
tk.Label(self.bottom_frame, bg=self.bottom_frame.cget('bg'),
text="This is a label on the bottom section")\
.grid(row=0, columnspan=3, sticky=tk.NSEW)
if __name__ == '__main__':
App().mainloop()

Fix _tkinter.TclError: no events specified in binding

Got this code from somewhere online. I looked at the other SO answer but it didn't work for me. What should I fix this error
Question: How to fix "- _tkinter.TclError: no events specified in binding"
import tkinter as tk
fields = ['Email', 'Password', 'School']
def fetch(entries):
for entry in entries:
field = entry[0]
text = entry[1].get()
print('%s: "%s"' % (field, text))
def makeform(root, fields):
entries = []
for field in fields:
row = tk.Frame(root)
lab = tk.Label(row, width=15, text=field, anchor='w')
ent = tk.Entry(row)
row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
lab.pack(side=tk.LEFT)
ent.pack(side=tk.RIGHT, expand=tk.YES, fill=tk.X)
entries.append((field, ent))
return entries
def getCreds():
root = tk.Tk()
ents = makeform(root, fields)
root.bind('', (lambda events=ents: fetch(e)))
b1 = tk.Button(root, text='Show',
command=(lambda events=ents: fetch(e)))
b1.pack(side=tk.LEFT, padx=5, pady=5)
b2 = tk.Button(root, text='Quit', command=root.quit)
b2.pack(side=tk.LEFT, padx=5, pady=5)
root.mainloop()
getCreds()
_tkinter.TclError: no events specified in binding
This should be relatively obvious. If you look at any of the many many examples of bind() on SO or google you will find that the first argument always has something specific in it and never an empty string.
Take some time to read up on tkinter-events-and-bindings.
There are 2 problems with you root.bind() 1st any time you click anywhere on the screen it will call the function. This is likely not what you want.
The 2nd problem both with your bind and the button command is your lambda. events=ents: fetch(e) you define your list of entries as events but then pass e to the function. So you have to correct that.
Personally I would create the list in the same place you define the root as well as define the root in the global name space. This will allow us to avoid the lambda as well.
import tkinter as tk
fields = ['Email', 'Password', 'School']
def fetch(_=None):
for ndex, entry in enumerate(entries):
print('{}: {}'.format(fields[ndex], entry.get()))
root = tk.Tk()
root.config(background='gray')
entries = []
for ndex, field in enumerate(fields):
tk.Label(root, width=15, text=field, anchor='w').grid(row=ndex, column=0, sticky='ew')
entries.append(tk.Entry(root))
entries[-1].grid(row=ndex, column=1, sticky='ew')
# The problem with root.bind is that it will constantly be calling the function anywhere you click on root.
root.bind('<Button-1>', fetch)
tk.Button(root, text='Show', command=fetch).grid(row=len(fields)+1, column=1, sticky='ew')
tk.Button(root, text='Quit', command=root.quit).grid(row=len(fields)+2, column=1, sticky='ew')
root.mainloop()

Passing OptionMenu into a callback (or retrieving a reference to the used widget)

I'm working on a (toplevel in a) GUI that consists of an array of 8 OptionMenus, each of them containing the same option list. Currently, Im building these widgets using a for-loop, and I save references in a dictionary. All OptionMenus link to the same (lambda) callback function.
To stay practical: the items in the option list represent a sequence of processing steps, and the user can alter the order of processes.
A change in one of the lists will result in one process being executed twice, and one process not at all. However, I want each item to occur only once. Hence, each user input should be accompanied by a second OptionMenu alteration.
For example: initial order 1-2-3 --> user changes the second process: 1-3-3, which autocorrects to: 1-3-2, where each process is again executed only once.
To my understanding, I can only get this to work if I have a reference to the OptionMenu that was just altered (from within the callback function). I was looking into passing the widget into the callback. The sample code is an attempt to implement the second suggested method, but the result is not what I would have expected.
The thing is that the OptionMenu widget seems to behave somewhat differently from other widgets. The OptionMenu does not allow for a re-defintion of the command function. No matter what input I pass along with the command function, the callback only seems to retrieve the OptionMenu selection, which is insufficient information for me to determine my process order.
Suggestions would be much apreciated!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
self.active_procs = ['proc 1','proc 2','proc 3','proc 4',
'proc 5','proc 6','proc 7','proc 8']
itemnr, widgets = dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
itemnr[name_construct] = tk.StringVar(root)
itemnr[name_construct].set(self.active_procs[index])
widgets[name_construct] = tk.OptionMenu(self, itemnr[name_construct], *self.active_procs,
command=lambda widget=name_construct:
self.order_change(widget))
widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,widget):
print(widget)
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()
The OptionMenu will pass the new value to the callback, so you don't have to do anything to get the new value. That's why your widget value isn't the value of name_construct -- the value that is passed in is overwriting the default value that you're supplying in the lambda.
To remedy this you simply need to add another argument so that you can pass the value of name_construct to the callback to go along with the value which is automatically sent.
It would look something like this:
widgets[name_construct] = tk.OptionMenu(..., command=lambda value, widget=name_construct: self.order_change(value, widget))
...
def order_change(self, value, widget):
print(value, widget)
Note: the OptionMenu isn't actually a tkinter widget. It's just a convenience function that creates a standard Menubutton with an associated Menu. It then creates one item on the menu for each option, and ties it all together with a StringVar.
You can get the exact same behavior yourself fairly easily. Doing so would make it possible to change what each item in the menu does when selected.
For those interested, below you can find an example code of how I got the widget behaviour I wanted. I took Bryan's advice to replace the OptionMenu for a Menubutton/Menu combination. I also made use of this post to find duplicate entries in my process order list.
Any thoughts or suggestions on how to implement this in a cleaner or shorter way, or how to get the same functionality with a different interface (e.g. drag and drop), are ofcourse welcome!
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
self.create_widgets()
def create_widgets(self):
# Assisting text
l1 = tk.Label(self, text = "Data in", font=(None, 15))
l1.grid(row=0, column=2)
l2 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l2.grid(row=1, column=2)
l3 = tk.Label(self, text = "Data out", font=(None, 15))
l3.grid(row=11, column=2)
l4 = tk.Label(self, text = u'\N{BLACK DOWN-POINTING TRIANGLE}', font=(None, 15))
l4.grid(row=10, column=2)
# Process list
self.active_procs = ['proc a','proc b','proc c','proc d',
'proc e','proc f','proc g','proc h']
self.the_value, self.widgets, self.topmenu = dict(), dict(), dict()
for index in range(8):
name_construct = 'nr' + str(index)
self.the_value[name_construct] = tk.StringVar(root)
self.the_value[name_construct].set(self.active_procs[index])
self.widgets[name_construct] = tk.Menubutton(self, textvariable=
self.the_value[name_construct],
indicatoron=True)
self.topmenu[name_construct] = tk.Menu(self.widgets[name_construct],
tearoff=False)
self.widgets[name_construct].configure(menu=self.topmenu[name_construct])
for proc in self.active_procs:
self.topmenu[name_construct].add_radiobutton(label=proc, variable=
self.the_value[name_construct],
command=lambda proc=proc,
widget=name_construct:
self.order_change(proc,widget))
self.widgets[name_construct].grid(row=index+2, column=2, columnspan=2,
sticky="nwse", padx=10, pady=10)
def order_change(self,proc,widget):
# Get the index of the last changed Menubutton
index_user_change = list(self.widgets.keys()).index(widget)
procs_order = [] # Current order from widgets
for index in range(8):
name_construct = 'nr' + str(index)
procs_order.append(self.widgets[name_construct].cget("text"))
# 1 change may lead to 1 double and 1 missing process
doubles = self.list_duplicates_of(procs_order,proc)
if len(doubles) == 2: # If double processes are present...
doubles.remove(index_user_change) # ...remove user input, change the other
missing_proc = str(set(self.active_procs)^set(procs_order)).strip('{"\'}')
index_change_along = int(doubles[0])
# Update references
self.active_procs[index_user_change] = proc
self.active_procs[index_change_along] = missing_proc
# Update widgets
name_c2 = 'nr'+str(index_change_along)
self.the_value[name_c2].set(self.active_procs[index_change_along])
self.widgets[name_c2].configure(text=missing_proc)
def list_duplicates_of(self,seq,item):
start_at = -1
locs = []
while True:
try:
loc = seq.index(item,start_at+1)
except ValueError:
break
else:
locs.append(loc)
start_at = loc
return locs
root = tk.Tk()
root.title("OptionMenu test")
app = Application(master=root)
root.mainloop()

How to display text of listbox items in the canvas with Python3x tkiner?

I am trying to display the text of the selected item of a listbox into the canvas. When I bind the listbox to a helper event handler, it throws Attribute Error: CLASS object has no attribute "HELPER EVENT HANDLER".
What I want is as follows:
1) When double clicking an item in the listbox to the left, its text should be displayed on the canvas. This particular line of code is causing all the troulbe to me.
lstbox.bind("<Double-Button-1>", self.OnDouble)
Could you please help me fixing this error?
2) I believe that there must be a way to make the lines' height on the listbox larger than they appear in my application. However, I don't know how to do it. I tried providing several options but these options are not recognized by tkinter. Could you please suggest to me how to do it?
Here is the code:
import tkinter as tk
languages = ['Mandarin', 'English', 'French']
class LanguageFamilies(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
canv = tk.Canvas(self, width=675, height=530, bg="white", relief="sunken")
canv.config(scrollregion=(0,0,300,650), highlightthickness=0)
canv.pack(side="right", expand=True, fill="both")
# Create scroll bar
sbar = tk.Scrollbar(self)
canv.config(yscrollcommand=sbar.set)
sbar.config(command=canv.yview)
sbar.pack(side="right", fill="both")
# Create Scroll List
lstbox = tk.Listbox(self, width=240, height=530, relief="sunken", font="Courier")
lst_scrollbar = tk.Scrollbar(self)
lstbox.config(yscrollcommand=lst_scrollbar.set)
lst_scrollbar.config(command=lstbox.yview)
lstbox.pack(side="left", expand=True, fill="both")
lst_scrollbar.pack(side="right", fill="both")
lstbox.bind("<Double-Button-1>", self.OnDouble) # THIS IS THE LINE CAUSING THE ERROR
# Add items to the lstbox
i = 0
while i < len(languages):
lstbox.insert(i, languages[i])
i += 1
# Create a text inside canvas
canv_id = canv.create_text(50, 50, font="Times 14", anchor="nw")
msg = "This is a text inside canvas."
canv.insert(canv_id, "end", msg)
#Binding Handler
def OnDouble(self, event):
self.widget = event.widget
selection = self.widget.curselection()
content = self.widget.get(selection[0])
print("You selected", content)
if __name__ == "__main__":
root = tk.Tk()
root.geometry("930x530")
root.title("Language Families")
LanguageFamilies(root).pack(fill="both", expand=True)
root.mainloop()
And this is the error message:
Traceback (most recent call last):
File "C:/Python33/new_stack_overflow.py", line 43, in <module>
LanguageFamilies(root).pack(fill="both", expand=True)
File "C:/Python33/new_stack_overflow.py", line 23, in __init__
lstbox.bind("<Double-Button-1>", self.OnDouble)
AttributeError: 'LanguageFamilies' object has no attribute 'OnDouble'
Your help is highly appreciated!
the problem is that the def for OnDouble is defined inside __init__, making it not a class method but a method inside the scope of __init__. You need to remove one level of indentation for OnDouble.
HERE IS THE COMPLETE AND CORRECT ANSWER! BELOW IS STAGES OF SOLVING THE PROBLEM.
Bryan's suggestions of decreasing the indentation of the OnDouble event handler by one level were the first steps to the solution.
The crucial thing I learnt from this experience is that printing the text of a listbox on the canvas directly is not the right option. Rather, the best option is to put a text widget on the canvas so that manipulating text matters will be much easier. I came up to this conclusion after watching a nice video tutorial here. As such, a text widget was defined ontop of the canvas. An if condition was used to control what to print on the text-canvas and how many times it should be printed - (in this case, only onece exactly the same as an electronic dictionary - which is my application).
Here is the COMPLETE WORKING code:
import tkinter as tk
languages = ['Mandarin', 'English', 'French']
global counter
counter = 1
class LanguageFamilies(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
global canv, canv_id # IN ORDER TO MAKE THEM ACCESSIBLE TO OnDouble EVENT HANDLER
canv = tk.Canvas(self, width=675, height=530, bg="white", relief="sunken")
canv.config(scrollregion=(0,0,300,650), highlightthickness=0)
canv.pack(side="right", expand=True, fill="both")
# Create scroll bar
sbar = tk.Scrollbar(self)
canv.config(yscrollcommand=sbar.set)
sbar.config(command=canv.yview)
sbar.pack(side="right", fill="both")
# Create Scroll List
lstbox = tk.Listbox(self, width=240, height=530, relief="sunken", font="Courier")
lst_scrollbar = tk.Scrollbar(self)
lstbox.config(yscrollcommand=lst_scrollbar.set)
lst_scrollbar.config(command=lstbox.yview)
lstbox.pack(side="left", expand=True, fill="both")
lst_scrollbar.pack(side="right", fill="both")
lstbox.bind("<Double-Button-1>", self.OnDouble)
# Add items to the lstbox
i = 0
while i < len(languages):
lstbox.insert(i, languages[i])
i += 1
# Create a text inside canvas
global textmatter
textmatter = tk.Text(canv) # THE TEXT SHOULD BE PRINTED ON THE CANVAS
textmatter.insert("end", "Double-click on a language on the left to view information.")
textmatter.pack()
#Binding Handler, ONE INDENTATION LEVEL IS REMOVED AS PER BRYAN'S SUGGESTION
def OnDouble(self, event):
global counter
self.widget = event.widget
selection = self.widget.curselection()
content = self.widget.get(selection[0])
if counter == 1:
textmatter.delete(1.0, "end") # CLEAR THE TEXT WHICH IS DISPLAYED AT FIRST RUNNING
textmatter.insert("end", content)
counter += 1 #ONE ITEM HAS BEEN SELECTED AND PRINTED SUCCESSFULLY
elif counter > 1:
textmatter.delete(1.0, "end")
textmatter.insert("end", content)
counter = 1 # RESET THE COUNTER SO THAT NEXT SELECTED ITEM DISPLAYS PROPERLY
if __name__ == "__main__":
root = tk.Tk()
root.geometry("930x530")
root.title("Language Families")
LanguageFamilies(root).pack(fill="both", expand=True)
root.mainloop()
That is it. I hope this post will be of help to you!
SALAM,
Mohammed

Resources