Deselecting from a listbox in Tkinter - python

I'm just wondering how I can deselect from a list box in thinter. Whenever I click on something in a list box, it gets highlighted and it gets underlined, but when I click off of the screen towards the side, the list box selection stays highlighted. Even when I click a button, the selection still stays underlined. For ex: in the example code below, I can't click off of the list box selection after clicking on one of them.
from tkinter import *
def Add():
listbox.insert(END, textVar.get())
root = Tk()
textVar = StringVar()
entry = Entry(root, textvariable = textVar)
add = Button(root, text="add", command = Add)
frame = Frame(root, height=100, width=100, bg="blue")
listbox = Listbox(root, height=5)
add.grid(row=0, column=0, sticky=W)
entry.grid(row=0, column=1, sticky=E)
listbox.grid(row=1, column=0)
frame.grid(row=1, column=1)
root.mainloop()

Yes, that's the normal behavior of the listbox. If you want to change that you could call the clear function every time the listbox left focus:
listbox.bind('<FocusOut>', lambda e: listbox.selection_clear(0, END))

Use the selectmode parameter on the Listbox widget.
You can click the selected item again and it will clear the selection.
See the effbot link:
http://effbot.org/tkinterbook/listbox.htm
listbox = Listbox(root, height=5, selectmode=MULTIPLE)

I have managed to create the functionality needed within the Listbox widget so that when a user clicks either back on the same item in the Listbox or elsewhere on screen the currently selected item is deselected. The solution came out to be quite simple.
Firsly I created a binding so that when the left mouse button is pressed anywhere on the window a function to deselect the list box is executed.
root.bind('<ButtonPress-1>', deselect_item)
I then created a variable to store the value of the last listbox item to be selected and initialised its value to None
previous_selected = None
Then I defined the function to deselect the listbox as follows. Firsly the new item (what item the user has just clicked on) is selected and compared to the previously selected item. If this is true then the user has clicked on an already highlighted item in the listbox and so the listbox's selection is cleared, removing the selected item. Finally, the function updates the previously selected box to the current selected box.
def deselect_item(event):
if listbox.curselection() == previous_selected:
listbox.selection_clear(0, tkinter.END)
previous_selected = listbox.curselection()
A full working example of this (in python 3.8.0) is shown below:
import tkinter
class App(tkinter.Tk):
def __init__(self):
tkinter.Tk.__init__(self)
self.previous_selected = None
self.listbox = tkinter.Listbox(self)
self.bind('<ButtonPress-1>', self.deselect_item)
self.listbox.insert(tkinter.END, 'Apple')
self.listbox.insert(tkinter.END, 'Orange')
self.listbox.insert(tkinter.END, 'Pear')
self.listbox.pack()
def deselect_item(self, event):
if self.listbox.curselection() == self.previous_selected:
self.listbox.selection_clear(0, tkinter.END)
self.previous_selected = self.listbox.curselection()
app = App()
app.mainloop()

Related

Messed Up Spacing After Button Click in Tkinter

I'm trying to create a GUI using Tkinter for a Pip-boy from Fallout 3. I'm running into a problem where after I click a button in one of the frames, the spacing between the buttons gets messed up. This spacing change happens for all but one of the buttons in the frame (the Lockpick one).
This is what I want the button spacing to look like (what it looks like before I click a button):
This is what happens after I click a button (in this case the Barter one)
Here is the code I am using:
from tkinter import *
# to read descriptions of each skill from a text file
with open("skills.txt") as f:
lines = f.readlines()
# function that updates the label with a different description when the corresponding button is clicked
def show_skill_desc(index):
desc['text'] = lines[index-1]
# makes the window and frame
root = Tk()
root.geometry("1024x600")
root.title("Skills")
frame = Frame(root)
frame.grid()
# creates the label
Label(root, text="Stats").grid(row=0, column=0)
# list of skills which will each have their separate labels
skills_list = ["Barter", "Big Guns", "Energy Weapons", "Explosives", "Lockpick", "Medicine",
"Melee Weapons", "Repair", "Science", "Small Guns", "Sneak", "Speech", "Unarmed"]
# placing the label in the frame
desc = Label(root, text="", padx=30, wraplength=600, justify=LEFT)
desc.grid(row=2, column=1)
# creates a button for each skill
button_list = []
for i in range(12):
button_list.append(Button(root, text=skills_list[i], width=40,
height=2, command=lambda c=i: show_skill_desc(button_list[c].grid_info()['row']), padx=0, pady=0))
button_list[i].grid(row=i+1, column=0)
root.mainloop()
The purpose of the GUI is to display the description of each skill, when the button for a skill is clicked.
Does anyone know why the spacing change happens? Thank you!

Bind Tkinter Combobox to Entry change?

I have a ttk.Combobox that my users can select from a dropdown list of options, or manually type something in. I have it bound to Return, so that if my user presses return after making a change it will update, but if my user clicks in the box and accidentally types something else in, it will cause an error down the road. To be clear, I already have an event bound to a new selection, as well as pressing return.
I am asking if it is possible to check if the box value has been changed when focus leaves the box, and if so, then call a function? When I tried a FocusOut bind, everytime I click on one of the dropdowns it calls my function and doesn't let me select anything from the dropdown, so that isn't working.
selection.bind('<Return>', lambda event, entry=selection, row=row: update(
updated_entry=entry.get(), row=row, entry=entry))
selection.bind('<<ComboboxSelected>>', lambda event, entry=selection, row=row: update(
updated_entry=entry.get(), row=row, entry=entry))
edit: Here is a sample code. The way this is written, if the user selects an item from the dropdown, it updates the label. If the users types something in and presses Return, it updates the label. But if the user types something in, and clicks on the other dropdown, it does not update the label.
import tkinter as tk
from tkinter import ttk
def update(updated_entry, row, entry):
label = tk.Text(root, height=1, width=10)
label.insert(tk.END, updated_entry)
label.grid(row=row, column=2)
return 'break'
def gui(root):
root.geometry('300x150')
root.config(background='snow3')
for row in range(2):
options = ['test', 'test1', 'test2']
selection = tk.ttk.Combobox(root, value=options)
selection.bind('<Return>', lambda event, entry=selection, row=row: update(
updated_entry=entry.get(), row=row, entry=entry))
selection.bind('<<ComboboxSelected>>', lambda event, entry=selection, row=row: update(
updated_entry=entry.get(), row=row, entry=entry))
selection.grid(row=row, column=1)
label = tk.Text(root, height=1, width=10)
label.grid(row=row, column=2)
if __name__ == '__main__':
root = tk.Tk()
gui(root)
tk.mainloop()
ttk.Comboboxes are a subclass of Entry widgets, which means that you can add validation to them in the same manner as you would to their base class. Namely by using the validate= and validatecommand= options Entrys support.
The reason to do this is because "validation" will allow the contents of the associated Combobox to be checked when it loses focus—i.e. your stated goal. This should work fine in conjunction with the bound event-handling you already have. The following code, which is similar to your minimal reproducible example, illustrates how do to something like that.
Note: This approach would also allow doing some real validation of the values the user has entered to prevent problems later on if they're invalid.
import tkinter as tk
from tkinter import ttk
def update(updated_entry, entry):
''' Combobox change Callback. '''
entry.delete('1.0', tk.END)
entry.insert(tk.END, updated_entry)
def gui(root):
root.geometry('300x150')
root.config(background='snow3')
for row in range(2):
text = tk.Text(root, height=1, width=10) # Widget to be updated.
text.grid(row=row, column=2)
def check_okay(new_value, text=text):
update(new_value, text)
return True # Note: accepts anything.
combobox = ttk.Combobox(root, value=('test', 'test1', 'test2'),
validate='focusout',
validatecommand=(root.register(check_okay), '%P'))
combobox.grid(row=row, column=1)
combobox.bind('<Return>', lambda event, entry=combobox, text=text:
update(entry.get(), entry=text))
combobox.bind('<<ComboboxSelected>>', lambda event, entry=combobox, text=text:
update(entry.get(), entry=text))
if __name__ == '__main__':
root = tk.Tk()
gui(root)
tk.mainloop()

find index of elements created with window_create inside tkinter Text

I used window_create to create interactive buttons inside a Text element. The buttons represent random or static values and I want to be able to compile the contents of the text element and replace the buttons with their respective values. However, I cannot find where any of the buttons are.
I've tried self.text.get("1.0",tk.END), but it only returns the text, not including the button elements
the button elements are created like so:
btn_text = tk.StringVar()
value = StaticValue('static', btn_text, self.custom_val_veiwer, idx)
button = tk.Button(self.text,
textvariable=btn_text, command=lambda v=value:
self.veiw_custom_val(None, val=v))
btn_text.set('static')
self.custom_vals.append(value)
self.text.window_create(tk.INSERT, window=button)
edit:
if you want to recreate the problem use this:
import tkinter as tk
root = tk.Tk()
text = tk.Text(root)
text.pack()
text.insert(tk.END, 'before button')
button = tk.Button(text, text='button')
text.window_create(tk.END, window=button)
text.insert(tk.END, 'after button')
print(text.get("1.0",tk.END))
root.mainloop()
notice how the button appears in the text field, but it is not printed out
(the output is before buttonafter button I want someting like before button<button>after button or a function that would tell me there is a button at row x at index y)
There's nothing that gives you exactly what you want, but it just takes a couple lines of code to get the index of the clicked button.
What I would do is have the button pass a reference to itself as an argument to the command. That requires making the button in two steps, since you can't reference the button before it is created.
button = tk.Button(text, text="button")
button.configure(command=lambda button=button: handle_click(button))
In the function called by the button, you can use the text widget dump command to get a list of all windows, and from that you can find the index of the button. The dump command will return a list of tuples. Each tuple has a key (in this case, "window"), the window name, and the index of the window. You can iterate over the result of that command to find the index of the button which was passed to the function.
def handle_click(button):
for (key, name, index) in text.dump("1.0", "end", window=True):
if name == str(button):
print("you clicked on the button at index {}".format(index))
break
Example
Here is a contrived example that adds several buttons. Clicking on the button will display the index of that button in a label. Notice how it will continue to work even if you manually edit the text widget to change the index of the button.
import tkinter as tk
root = tk.Tk()
text = tk.Text(root)
label = tk.Label(root)
label.pack(side="top", fill="x")
text.pack(side="top", fill="both", expand=True)
def handle_click(button):
for (key, name, index) in text.dump("1.0", "end", window=True):
if name == str(button):
label.configure(text="You clicked on the button at {}".format(index))
break
for word in ("one", "two", "three", "four", "five"):
text.insert("end", word + "\n")
button = tk.Button(text, text="click me")
button.configure(command=lambda button=button: handle_click(button))
text.window_create("insert-1c", window=button)
tk.mainloop()

Python 2.7: Widget Doesn't Destroy When Radiobutton is Clicked More Than Once

My code shows FunctionAllocation label and two radio buttons, and once one radio button is clicked, it shows Subject ID label and its entry bar. Once return key is clicked, all widgets are destroyed and new message appears.
Everything works smoothly if radio button is clicked once. However, if I click a radio button and then click another radio button, Subject ID label and its entry bar do not disappear despite .destroy() command.
How do I make sure the widgets disappear no matter which & how many times the radio buttons are pressed?
Thank you so much in advance!
from Tkinter import *
class Application(Frame):
#Initial Settings
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.radioButtons()
#Place Initial Buttons
def radioButtons(self):
#Variable to tie two radio buttons
self.tieVar1 = StringVar()
#Text before FA buttons
self.buttonLabel1 = Label(root, text="Function Allocation:")
self.buttonLabel1.place(relx=0.35, rely=0.3, anchor=CENTER)
#Two Radio FA buttons
self.radio1 = Radiobutton(text = "FA_F", variable=self.tieVar1, value="FA1", command=lambda: self.addSubject())
self.radio1.place(relx=0.5, rely=0.3, anchor=CENTER)
self.radio2 = Radiobutton(text = "FA_I", variable=self.tieVar1, value="FA2", command=lambda: self.addSubject())
self.radio2.place(relx=0.6, rely=0.3, anchor=CENTER)
def addSubject(self):
#Text before ID entry bar
self.buttonLabel2 = Label(root, text="Subject ID:")
self.buttonLabel2.place(relx=0.35, rely=0.6, anchor=CENTER)
#ID entry bar
self.myEntry = Entry()
self.myEntry.place(relx=0.5, rely=0.6, anchor=CENTER)
self.contents = StringVar()
self.contents.set("Sample Text")
self.myEntry["textvariable"] = self.contents
self.myEntry.bind('<Key-Return>', self.reset_contents)
#Action when return key pressed after typing subject ID
def reset_contents(self, event):
#Delete all
self.buttonLabel1.destroy()
self.buttonLabel2.destroy()
self.radio1.destroy()
self.radio2.destroy()
self.myEntry.destroy()
#Setting up new window
self.setActions()
def setActions(self):
Label(text="Done!", font=("Times", 10, "bold")).place(relx=0.5, rely=0.5, anchor=CENTER)
#Final settings to keep window open
root = Tk()
root.geometry("1000x400")
app = Application(master=root)
app.mainloop()
You are making a new Label and Entry every time you click that button. However your destroy only destroys the last one created. The easiest fix is to simply check if the Label has already been created:
def addSubject(self):
if hasattr(self, 'buttonLabel2'):
return # abort this method if the Label is already created
# rest of your method
Unrelated, but if you want your Radiobuttons to start in a blank state rather than a tristate, you need to initialize the StringVar like this:
self.tieVar1 = StringVar(value='Novel')

Tkinter - How would I go about resetting list of objects?

Yesterday I asked this question Creating elements by loop Tkinter to find out how to dynamically create some bullet points. Now I'm looking to add a clear button so when pressed, will reset the entire form. I have tried setting the list back to [] but it didn't work.
edit - So basically when I press reset I'd like it to look exactly like it did when the form was loaded.
The buttons are removed with the destroy method:
for button in self.button:
button.destroy()
import Tkinter as tk
class ButtonBlock(object):
def __init__(self, master):
self.master = master
self.button = []
self.button_val = tk.IntVar()
entry = tk.Entry()
entry.grid(row=0, column=0)
entry.bind('<Return>', self.onEnter)
entry.focus()
clear_button = tk.Button(master, text='Clear', command=self.onClear)
clear_button.grid(row=0, column=1)
def onClear(self):
for button in self.button:
button.destroy()
def onEnter(self, event):
entry = event.widget
num = int(entry.get())
self.onClear()
for i in range(1, num+1):
self.button.append(tk.Radiobutton(
self.master, text=str(i), variable=self.button_val, value=i,
command=self.onSelect))
self.button[-1].grid(sticky='WENS', row=i, column=0, padx=1, pady=1)
def onSelect(self):
print(self.button_val.get())
if __name__ == '__main__':
root = tk.Tk()
ButtonBlock(root)
root.mainloop()
Setting the list back (i.e. using self.button = []) just clears the data stored in the button variable. That action alone is not connected to the user interface (UI). You have to explicitly remove the widget objects which were created (by the onEnter method).
So the clearing feature you are looking for should be feasible by extending the answer from your previous question. Add an onClear method to the ButtonBlock class so that when your "Clear" control (i.e. using a button widget) is selected its callback function calls ButtonBlock.onClear(), similar to how your Entry widget invokes the onEnter method.
EDIT: See unutbu's answer to this question. When selected, the clear_button control calls ButtonBlock.onClear(). The for loop in onClear gets a reference to each button ojbect from the button list and calls the object's destroy method, which removes it from the UI.

Categories