find index of elements created with window_create inside tkinter Text - python

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

Related

Python/Tkinter: Unwanted interaction between two widgets

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

Tkinter: Adding and self-deleting buttons in list | Adding works, Deleting not

I am just in the middle of creating an entry form for a program, and it seems that I have got stuck with the logic on this one.
Basically I wanted to design a dropdwon-list, which adds words to an array and displays these words as little buttons beneath it. When you click the buttons they disappear again and remove themselfs from the array.
Simple enough, I thought. The adding worked fine so far. But removing not so much... There is a logic error with the button array and I can't seem to figure it out!
I extracted the code for reviewing,
any help is greatly appreciated!
Word adding Window
import tkinter as tk
from tkinter import ttk
def rel_add(*args):
rel_array.append(tkvar.get())
print(rel_array)
rel_buttons_update()
def del_button(i):
print(i)
del rel_array[i]
print(rel_array)
rel_buttons[i].grid_remove()
# del rel_buttons[i]
rel_buttons_update()
def rel_buttons_update():
for i, rel in enumerate(rel_array):
rel_buttons.append(tk.Button(rel_grid, text=rel, font="Helvetica 7", command=lambda c=i: del_button(c)))
rel_buttons[i].grid(column=i, row=0, sticky="nw")
rel_array = []
rel_buttons = []
win = tk.Tk()
tkvar = tk.StringVar(win) # Create a Tkinter variable
choices_words = ["oolbath", "pflanze", "haus", "wasser", "brimbambum"] # Create Variable List
tkvar.set('Related Words...') # set the default option
choices_words.sort() # Sort List
tk.Label(win, text="Related Words: ").grid(row=0,column=0, sticky="w")
rel = tk.OptionMenu(win, tkvar, *choices_words)
rel.grid(row=0,column=1, sticky="w")
# Callbuck Function for Dropdwon Menu
tkvar.trace("w", rel_add)
rel_grid = tk.Frame(win)
# Display the Buttons for the related Words
rel_grid.grid(row=1,column=1, sticky="w")
win.mainloop()
The main problem is that you keep recreating the same buttons over and over, so rel_buttons contains many more elements than you expect.
As a simple experiement, add a print statement to rel_buttons_update like this:
def rel_buttons_update():
for i, rel in enumerate(rel_array):
rel_buttons.append(ttk.Button(rel_grid, text=rel, command=lambda c=i: del_button(c)))
rel_buttons[i].grid(column=i, row=0, sticky="nw")
print('in update, rel_buttons is now', rel_buttons)
You'll notice that there is one button the first time you use the option menu, three buttons the second time, six buttons the third time, and so on.
You need to either only create new buttons, or delete all the old buttons before recreating them.

Display text on a Tkinter label

I would like to know how to display text or a number to an empty label, after a button is clicked. Let's pretend, I want to display the number "0" to an empty label after a button is clicked.
My questions in simplified form are:
How to set a numeric value of 0 to a button.
Once the numeric integer value of (0) is set, how do you display the result to an empty label after the button widget is clicked?
You need to bind a callback helper method which would be triggered when the button is clicked. Here's an MCVE:
import tkinter as tk
root = tk.Tk()
label = tk.Label(root)
label.pack()
button = tk.Button(
root,
text='Click Me!',
command=lambda: label.configure(text='beautiful spam')
)
button.pack()
root.mainloop()
...you'd do the same for 0 - the text doesn't matter, nor does it matter whether you're configuring an empty label, or a label which already has some text.

Deselecting from a listbox in Tkinter

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

How to get the value from tkinter toplevel in side a 0 attribute function?

How to get the value from tkinter toplevel in side a 0 attribute function?
def Update():
Up = Toplevel()
Up.title("Update")
taskLabel = Label(Up, text ="Update", font=('Times', 20)).grid(row=0)
var = IntVar()
radio = Radiobutton(Up, text="Fully", variable=var, value=1, command = taskUpdate).grid(row=2, sticky=W)
radio = Radiobutton(Up, text="Partly", variable=var, value=2, command = taskUpdate).grid(row=3, sticky=W)
radio = Radiobutton(Up, text="Un", variable=var, value=3, command = taskUpdate).grid(row=4, sticky=W)
note = EntryaUp, width=30, font=('Arial', 12,)).grid(row=6)
Button = Button(Up, text ="Task Complete and Send Invoice", command = taskUpdate).grid(row =7)
return (var, note)
def updateB ():
var, noteBox = Update()
a = (var.get())
b = (note.get())
This is a top level window in tkinter and I want to get the value 1,2,3 when I click the radio button and get the string value when I enter text in the entry and click the command button, but I just have no idea how can I do that. By the way, I don't think I can return the value at all, every time when its return it only return it initial value which is 0 and " ". I know how it works and can get the value if the radiobutton is not in the top level (or in a function). But I could not figure it out how can I do it if the struture is like this.
The short answer to your question is that you can use wait_window() to wait for the window to be dismissed, and then you can return whatever you want from your function. You would then hook up your "Task complete" button to simply destroy the window.
You have to be very careful here -- wait_window creates a new, nested event loop. Most often this is done in conjunction with a window grab to prevent events in other windows from being processed. When you do this, you are creating a modal dialog.
Here's a working example; I had to tweak your code a little, and I took the liberty of removing some unnecessary variables (such as references to the radiobuttons, since you never do anything with the references)
import Tkinter as tk # python 2.7
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
button = tk.Button(self, text="Open window", command=self.on_button)
button.pack()
def on_button(self):
var, note = Update()
a = var.get()
b = note.get()
print "a: '%s' b: '%s'" % (a,b)
def Update():
Up = tk.Toplevel()
Up.title("Update")
tk.Label(Up, text ="Update", font=('Times', 20)).grid(row=0)
var = tk.IntVar()
tk.Radiobutton(Up, text="Fully", variable=var, value=1).grid(row=2, sticky="w")
tk.Radiobutton(Up, text="Partly", variable=var, value=2).grid(row=3, sticky="w")
tk.Radiobutton(Up, text="Un", variable=var, value=3).grid(row=4, sticky="w")
evar = tk.StringVar()
note = tk.Entry(Up, width=30, font=('Arial', 12,), textvariable=evar)
note.grid(row=6)
Button = tk.Button(Up, text ="Task Complete and Send Invoice", command = Up.destroy).grid(row =7)
Up.wait_window()
print "done waiting..."
return (var, evar)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
To see why you must be careful when using this function, try the following scenarios:
click on the "Open Window" button, fill in the form and click on the "Task Complete" button. Notice that it probably works the way you expect (it prints to stdout, so make sure you're running from a console window)
click on the "Open Window" button, then click on it again. You now have two windows. Fill in the second window, click on "Task Complete" and notice it works like you expect. Now fill in the first window and click on "Task Complete" and it still works just fine. In both cases it prints the correct values to the screen when you click the button.
click on the "Open Window" button, then click on it again. You now have two windows. This time, fill in the first window and click "Task Complete". Notice that it does not print the results to the screen. This is because the nested event loop of the first window can't complete until the nested event loop of the third window completes. Fill in the form of this second window and click on "Task Complete" and you'll see that the two nested event loops unwind, but not in the order that you clicked on "Task Complete" but rather in the order that the windows were created.
This is why you need to be careful when using wait_window - you should prevent this recursive nesting of event loops or your GUI may not behave the way you expect.

Categories