Following is the smallest fully functional tkinter code I could write to demonstrate a problem I am having in a larger application. This code presents two frames - the left containing a listbox, the right containing a scrollable text widget. When the user selects a listbox item, the content of that item appears in the text widget. If you place your cursor in the text widget, all is well. You can add more text with no problem and/or use the delete key to delete text. But if you select any text in the text widget, the "ListboxSelect" function is called, and throws the error "IndexError: tuple index out of range". This makes no sense. Why would selecting text in the text widget call a function that is explicitly tied to the listbox widget?
import tkinter as tk
from tkinter import scrolledtext
root = tk.Tk()
root.geometry("400x200")
def listbox_selected(event):
w = event.widget
listbox_index = int(w.curselection()[0])
right_text.delete(1.0,tk.END)
right_text.insert(tk.END,left_listbox.get(listbox_index))
left_frame = tk.Frame(root,height=200,width=180,bg="lightblue")
left_frame.place(x=15,y=2)
# left frame contains listbox
left_listbox = tk.Listbox(left_frame)
left_listbox.bind("<<ListboxSelect>>",listbox_selected)
left_listbox.place(x=5,y=5)
for index in range(5):
left_listbox.insert(index,"This is item " + str(index))
right_frame = tk.Frame(root,height=200,width=180,bg="lightyellow")
right_frame.place(x=200,y=5)
# right frame contains scrollable text widget
right_text = tk.scrolledtext.ScrolledText(right_frame,width=18,
height=10)
right_text.place(x=5,y=5)
root.mainloop()
It is because when selecting text inside Text widget will deselect the selected item in Listbox which triggers the <<ListboxSelect>> event.
The deselection in the Listbox can be disabled by setting exportselection=0:
left_listbox = tk.Listbox(left_frame, exportselection=0)
Another way is to check whether there is selected item inside listbox_selected():
def listbox_selected(event):
w = event.widget
selection = w.curselection()
# check whether there is item selected
if selection:
listbox_index = int(selection[0])
right_text.delete(1.0,tk.END)
right_text.insert(tk.END,left_listbox.get(listbox_index))
Related
I have two tkinter.Listboxes. The idea is to show one of the Listboxes, and when an item is selected, the other Listbox is shown. To do that I use grid_forget. This is the code:
import tkinter as tk
root = tk.Tk()
listbox1 = tk.Listbox(root)
listbox1.grid(row=0, column=0)
listbox1.insert(tk.END, "Item 1.1")
listbox1.insert(tk.END, "Item 1.2")
def change_listbox(event):
print(listbox1.curselection())
listbox1.grid_forget()
listbox2.grid(row=0, column=0)
listbox1.bind("<<ListboxSelect>>", change_listbox)
listbox2 = tk.Listbox(root)
listbox2.insert(tk.END, "Item 2.1")
listbox2.insert(tk.END, "Item 2.2")
root.mainloop()
When I select an item from listbox1, the listbox2 is show, but when I select an item of the listbox2, change_listbox is called again (only one time). You can check this by the print I added.
However, if I change grid_forget by destroy the issue is solved, but I don't want to destroy listbox1.
It is because when item in listbox2 is selected, the selected item in listbox1 will be deselected due to exportselection option is set to True by default. So it will trigger the bound event <<ListboxSelect>> on listbox1. Just set exportselection=False (or 0) in both listboxes will fix it.
Since the calls are
(0,)
(or (1,)) when initially selecting the item from the listbox, followed by
()
(i.e. "nothing selected") it looks like the second invocation occurs when the item gets unselected from list 1 (since you're selecting an item from list 2).
You can verify this by laying out both listboxes side by side (so remove the forget stuff):
listbox1.grid(row=0, column=0)
listbox2.grid(row=0, column=1)
When you select an item in the second listbox, the selection in the first listbox is unselected.
If you don't care about that unselection, well... don't:
def change_listbox(event):
sel = listbox1.curselection()
if not sel: # Nothing selected, whatever
return
print("Selected:", sel)
# show list 2 or something...
I am placing labels on a Tab in Tkinter with a for loop. How can I identify in the event handler which label was clicked (or its loop index)? I guess it is functionally similar to a ListBox but without the formatting restrictions. I might want to put the labels in a circle or place them diagonally. I tried finding the coordinates of the label but these are available only if the tab is the first one visible or the tab is redrawn when made active. Also the x, y passed in the event handler is the x, y within the label which does not help to identify the label.
I could copy the label code about 10 times and and have about 10 event handlers. This would work but this is no longer 1970!
Perhaps I could bind a handler to the tab canvas and identify the label from its coordinates. The label would need to be on the first tab or the tab drawn when active.
Perhaps I could create a different event handler for each label by holding the event handlers in an array. I would need an event handler for each label. The code would need to change if the number of labels changed.
I am currently trying a label with ''. Would using buttons with command be easier?
What simple part of Python am I missing? I cannot be the first person to need this! Any help or advice would be appreciated.
You can save a reference to the label text for each label widget in a dict.
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry('+800+50')
notebook = ttk.Notebook(root, width=300, height=200, padding=[10,10,10,10])
notebook.pack()
tab_one = tk.Frame(notebook, bg='wheat')
notebook.add(tab_one, text='Cheese', padding=[10,10,10,10])
tab_two = tk.Frame(notebook, bg='mint cream')
notebook.add(tab_two, text='Misc.', padding=[10,10,10,10])
def clicked(event):
print('Clicked:', name_dict[event.widget])
# Populating Cheese tab with clickable Labels
name_list = ['Cheddar', 'Ilchester', 'Limburger']
name_dict = {}
for index, name in enumerate(name_list):
a = tk.Label(tab_one, width=10, text=name, bg='peru')
a.grid(row=index, column=0, padx=5, pady=5)
name_dict[a] = name # Save references in a dict
a.bind('<Button-1>', clicked)
tk.Label(tab_two, text='Just some text...', bg='powder blue').pack(anchor='nw')
root.mainloop()
Is this what you had in mind?
When you bind events, the function receives an object that includes a reference to the widget that received the event. In the following example, notice how it uses event.widget to refer to the widget that was clicked on.
import tkinter as tk
def update_label(event):
event.widget.configure(text="You clicked me")
root = tk.Tk()
for i in range(10):
l = tk.Label(root, text="Click me", width=20)
l.pack()
l.bind("<1>", update_label)
root.mainloop()
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()
I have a function which adds a text widget to the frame dynamically when a user clicks a button. And then different text is inserted into each of the text widget. These text widgets have been stored in a list because I want to scroll all the text box by calling the .see method.
I have a function defined which auto-scrolls all the text widget to a specific position which is obtained using the text.search() method.
the text.search() methods returns the index for the search-term from the text widget no. 1 of the frame, and then it auto-scrolls all the text widget.
Question
How to search for a specific term individually in all text_widgets and obtain their indexes and use their indexes to scroll their respective text boxes?
Similar code
#Initializing an array at the top of your code:
widgets = []
#Next, add each text widget to the array:
for i in range(10):
text1 = tk.Text(...)
widgets.append(text1)
#Next, define a function that calls the see method on all of the widgets:
def autoscroll(pos):
for widget in widgets:
widget.see(pos)
#Finally, adjust your binding to call this new method:
pos_start = text1.search(anyword, '1.0', "end")
text1.tag_bind(tag, '<Button-1>', lambda e, index=pos_start: autoscroll(index))
This is relatively simple to do.
I don't understand, however, why you are performing the search before the activate the auto-scroll functionality. All you need to do is cycle through the list widgets and determine where the text appears whenever you want to start the autoscroll, the below script will perform what I believe to be the desired behaviour:
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.texts = [tk.Text(self.root) for i in range(3)]
for i in self.texts:
i.pack(side="left")
tk.Button(self.root, text="Find 'foobar'", command=self.find).pack()
def find(self):
for i in self.texts:
if i.search("foobar", "1.0", "end") != "":
i.see(i.search("foobar", "1.0", "end"))
root = tk.Tk()
App(root)
root.mainloop()
However, this does not take in to account multiple results for the search term within the same Text widget or even in multiple Text widgets.
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()