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.
Related
tkinter allows us to create GUI applications in Python. My question is to create a responsive window that:
A column has a fixed width, but a flexible height.
When window's width increases, more columns are added.
When window's width decreases, columns are removed.
When window's height increases, columns become longer.
When window's height decreases, columns become shorter.
Each column has texts that moves to other columns depending on their sizes. For example:
If columns' heights increase, then more text is shown inside them. The 1st column will be taking more texts from the 2nd column, and the 2nd column will be taking texts from the 3rd (or the buffer).
My question is: how to achieve this effect with tkinter?
There is no built-in widget implementing this kind of column feature. Tkinter does not have a "column" layout manager that behaves this way either. It is therefore necessary to create a custom widget.
My solution is to create the columns with Text widgets in a container Frame.
You set the desired column width (in character) and then update the number of columns when the window is resized with a binding to <Configure> (see the first part of .resize() method in the example below). The Text widgets are displayed with .grid(row=0, column=<column number>, sticky="ns") on a single row and adapt to the row height thanks to the option sticky="ns".
To split the content between the different columns, I use the peer feature of the Text widget. The leftmost column is the main Text widget and I create peer widgets that have the same content for the other columns (see the first part of .resize() method in the example below). This way all the columns have the same content but the part of it which is displayed can be changed independently. To do so, I use .yview_moveto(<column number>/<total number of columns>) to display the proper part of the content in each column. However, for this to work when the content is shorter than the available display space, I need to pad the content with newlines to get a nice column display (see the second part of .resize() method in the example below).
Here is the code:
import tkinter as tk
class MulticolumnText(tk.Frame):
def __init__(self, master=None, **text_kw):
tk.Frame.__init__(self, master, class_="MulticolumnText")
# text widget options
self._text_kw = text_kw
self._text_kw.setdefault("wrap", "word")
self._text_kw.setdefault("state", tk.DISABLED) # as far as I understood you only want to display text, not allow for user input
self._text_kw.setdefault("width", 30)
# create main text widget
txt = tk.Text(self, **self._text_kw)
txt.grid(row=0, column=0, sticky="ns") # make the Text widget adapt to the row height
# disable mouse scrolling
# Improvement idea: bind instead a custom scrolling function to sync scrolling of the columns)
txt.bind("<4>", lambda event: "break")
txt.bind("<5>", lambda event: "break")
txt.bind("<MouseWheel>", lambda event: "break")
self.columns = [txt] # list containing the text widgets for each column
# make row 0 expand to fill the frame vertically
self.grid_rowconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
def __getattr__(self, name): # access directly the main text widget methods
return getattr(self.columns[0], name)
def delete(self, *args): # like Text.delete()
self.columns[0].configure(state=tk.NORMAL)
self.columns[0].delete(*args)
self.columns[0].configure(state=tk.DISABLED)
def insert(self, *args): # like Text.insert()
self.columns[0].configure(state=tk.NORMAL)
self.columns[0].insert(*args)
self.columns[0].configure(state=tk.DISABLED)
def resize(self, event):
# 1. update the number of columns given the new width
ncol = max(event.width // self.columns[0].winfo_width(), 1)
i = len(self.columns)
while i < ncol: # create extra columns to fill the window
txt = tk.Text(self)
txt.destroy()
# make the new widget a peer widget of the leftmost column
self.columns[0].peer_create(txt, **self._text_kw)
txt.grid(row=0, column=i, sticky="ns")
txt.bind("<4>", lambda event: "break")
txt.bind("<5>", lambda event: "break")
txt.bind("<MouseWheel>", lambda event: "break")
self.columns.append(txt)
i += 1
while i > ncol:
self.columns[-1].destroy()
del self.columns[-1]
i -= 1
# 2. update the view
index = self.search(r"[^\s]", "end", backwards=True, regexp=True)
if index: # remove trailling newlines
self.delete(f"{index}+1c", "end")
frac = 1/len(self.columns)
# pad content with newlines to be able to nicely split the text between columns
# otherwise the view cannot be adjusted to get the desired display
while self.columns[0].yview()[1] > frac:
self.insert("end", "\n")
# adjust the view to see the relevant part of the text in each column
for i, txt in enumerate(self.columns):
txt.yview_moveto(i*frac)
root = tk.Tk()
im = tk.PhotoImage(width=100, height=100, master=root)
im.put(" ".join(["{ " + "#ccc "*100 + "}"]*100))
txt = MulticolumnText(root, width=20, relief="flat")
txt.pack(fill="both", expand=True)
txt.update_idletasks()
txt.tag_configure("title", justify="center", font="Arial 14 bold")
txt.insert("1.0", "Title", "title")
txt.insert("end", "\n" + "\n".join(map(str, range(20))))
txt.insert("10.0", "\n")
txt.image_create("10.0", image=im)
root.mainloop()
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))
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'm trying to learn tkinter and I wanted to write a simple rock paper scissors game, where there is a window with 3 buttons and one text widget.
I'd like to be able to press any of the buttons and for the message to appear in the text field, then click a different button, the text field to clear and display a new message associated with the second button and so on.
From the tutorials I've watched, I know that I can pass the function housing text widget as an argument in button command parameter.I know I could make 3 functions with a text field, one for each button (displaying one at a time) but that's probably not the correct way. Here's what I have so far:
import tkinter as tk
root = tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
def Rock():
rockText = "Paper!"
return rockText
def Paper():
paperText = "Scissors!"
return paperText
def Scissors():
scissorsText = "Rock!"
return scissorsText
def display():
textDisplay = tk.Text(master = root, height = 10, width = 50)
textDisplay.grid(row = 1, columnspan = 5)
textDisplay.insert(tk.END, Rock())
buttonRock = tk.Button(text = "Rock", command = display).grid(row = 0, column = 1, padx = 10)
buttonPaper = tk.Button(text = "Paper").grid(row = 0, column = 2, padx = 10)
buttonScissors = tk.Button(text = "Scissors").grid(row = 0, column = 3, padx = 10)
root.mainloop()
Any help will be appreciated.
Edit: Second thought - I can imagine I'm complicating this for myself by trying to force the game to work this way. With the random module I'd be able to get away with one function for the computer choice with a list and saving the random pick in a parameter, then returning the value into the display function.
So if I got this right you just want to make a button click change the text in the Text-widget. For that you have two easy and quite similar options. First would be to define 3 functions, as you did, and let them change the text directly. The second option would be to make one function which changes the text according to whats given. Note that in the second case we will have to use lambda which works quite well in smaller projects but decreases the efficiency of your programs when they get bigger.
First option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=self.Rock).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=self.Paper).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=self.Scissors).grid(row=0,column=3,padx=10)
root.mainloop()
def Rock(self):
text="Paper!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Paper(self):
text="Scissors!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Scissors(self):
text="Rock!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Second option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=lambda: self.updateText('Paper!')).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=lambda: self.updateText('Scissors!')).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=lambda: self.updateText('Rock!')).grid(row=0,column=3,padx=10)
root.mainloop()
def updateText(self,text):
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Some little side notes from me here:
If you use grid, pack or place right on the widget itself you wont assign the widget to a variable but the return of the grid, pack or place function which is None. So rather first assign the widget to an variable and then use a geometry manager on it like I did for the Text-widget.
You don't have to extra set the title with the title function afterwards. You can set it with the className-argument in Tk.
If you're working with tkinter its fine to do it functionally but rather use a class to build up GUIs.
When creating new widgets always be sure to pass them the variable for the root window first. They will get it themselves too if you don't do that but that needs more unnecessary background activity and if you have more than one Tk-window open it will automatically chooses one which may not be the one you want it to take.
And one small tip in the end: If you want to learn more about all the tkinter widgets try http://effbot.org/tkinterbook/tkinter-index.htm#class-reference.
I hope its helpfull. Have fun programming!
EDIT:
I just saw your edit with the random module. In this case I would recommend the second option. Just remove the text-argument from updateText and replace lambda: self.updateText(...) with self.updateText(). In updateText itself you add that random of list thing you mentioned. :D
I am trying to set the text of an Entry widget using a button in a GUI using the tkinter module.
This GUI is to help me classify thousands of words into five categories. Each of the categories has a button. I was hoping that using a button would significantly speed me up and I want to double check the words every time otherwise I would just use the button and have the GUI process the current word and bring the next word.
The command buttons for some reason are not behaving like I want them to. This is an example:
import tkinter as tk
from tkinter import ttk
win = tk.Tk()
v = tk.StringVar()
def setText(word):
v.set(word)
a = ttk.Button(win, text="plant", command=setText("plant"))
a.pack()
b = ttk.Button(win, text="animal", command=setText("animal"))
b.pack()
c = ttk.Entry(win, textvariable=v)
c.pack()
win.mainloop()
So far, when I am able to compile, the click does nothing.
You might want to use insert method. You can find the documentation for the Tkinter Entry Widget here.
This script inserts a text into Entry. The inserted text can be changed in command parameter of the Button.
from tkinter import *
def set_text(text):
e.delete(0,END)
e.insert(0,text)
return
win = Tk()
e = Entry(win,width=10)
e.pack()
b1 = Button(win,text="animal",command=lambda:set_text("animal"))
b1.pack()
b2 = Button(win,text="plant",command=lambda:set_text("plant"))
b2.pack()
win.mainloop()
If you use a "text variable" tk.StringVar(), you can just set() that.
No need to use the Entry delete and insert. Moreover, those functions don't work when the Entry is disabled or readonly! The text variable method, however, does work under those conditions as well.
import Tkinter as tk
...
entry_text = tk.StringVar()
entry = tk.Entry( master, textvariable=entry_text )
entry_text.set( "Hello World" )
You can choose between the following two methods to set the text of an Entry widget. For the examples, assume imported library import tkinter as tk and root window root = tk.Tk().
Method A: Use delete and insert
Widget Entry provides methods delete and insert which can be used to set its text to a new value. First, you'll have to remove any former, old text from Entry with delete which needs the positions where to start and end the deletion. Since we want to remove the full old text, we start at 0 and end at wherever the end currently is. We can access that value via END. Afterwards the Entry is empty and we can insert new_text at position 0.
entry = tk.Entry(root)
new_text = "Example text"
entry.delete(0, tk.END)
entry.insert(0, new_text)
Method B: Use StringVar
You have to create a new StringVar object called entry_text in the example. Also, your Entry widget has to be created with keyword argument textvariable. Afterwards, every time you change entry_text with set, the text will automatically show up in the Entry widget.
entry_text = tk.StringVar()
entry = tk.Entry(root, textvariable=entry_text)
new_text = "Example text"
entry_text.set(new_text)
Complete working example which contains both methods to set the text via Button:
This window
is generated by the following complete working example:
import tkinter as tk
def button_1_click():
# define new text (you can modify this to your needs!)
new_text = "Button 1 clicked!"
# delete content from position 0 to end
entry.delete(0, tk.END)
# insert new_text at position 0
entry.insert(0, new_text)
def button_2_click():
# define new text (you can modify this to your needs!)
new_text = "Button 2 clicked!"
# set connected text variable to new_text
entry_text.set(new_text)
root = tk.Tk()
entry_text = tk.StringVar()
entry = tk.Entry(root, textvariable=entry_text)
button_1 = tk.Button(root, text="Button 1", command=button_1_click)
button_2 = tk.Button(root, text="Button 2", command=button_2_click)
entry.pack(side=tk.TOP)
button_1.pack(side=tk.LEFT)
button_2.pack(side=tk.LEFT)
root.mainloop()
Your problem is that when you do this:
a = Button(win, text="plant", command=setText("plant"))
it tries to evaluate what to set for the command. So when instantiating the Button object, it actually calls setText("plant"). This is wrong, because you don't want to call the setText method yet. Then it takes the return value of this call (which is None), and sets that to the command of the button. That's why clicking the button does nothing, because there is no command set for it.
If you do as Milan Skála suggested and use a lambda expression instead, then your code will work (assuming you fix the indentation and the parentheses).
Instead of command=setText("plant"), which actually calls the function, you can set command=lambda:setText("plant") which specifies something which will call the function later, when you want to call it.
If you don't like lambdas, another (slightly more cumbersome) way would be to define a pair of functions to do what you want:
def set_to_plant():
set_text("plant")
def set_to_animal():
set_text("animal")
and then you can use command=set_to_plant and command=set_to_animal - these will evaluate to the corresponding functions, but are definitely not the same as command=set_to_plant() which would of course evaluate to None again.
One way would be to inherit a new class,EntryWithSet, and defining set method that makes use of delete and insert methods of the Entry class objects:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
class EntryWithSet(tk.Entry):
"""
A subclass to Entry that has a set method for setting its text to
a given string, much like a Variable class.
"""
def __init__(self, master, *args, **kwargs):
tk.Entry.__init__(self, master, *args, **kwargs)
def set(self, text_string):
"""
Sets the object's text to text_string.
"""
self.delete('0', 'end')
self.insert('0', text_string)
def on_button_click():
import random, string
rand_str = ''.join(random.choice(string.ascii_letters) for _ in range(19))
entry.set(rand_str)
if __name__ == '__main__':
root = tk.Tk()
entry = EntryWithSet(root)
entry.pack()
tk.Button(root, text="Set", command=on_button_click).pack()
tk.mainloop()
e= StringVar()
def fileDialog():
filename = filedialog.askopenfilename(initialdir = "/",title = "Select A
File",filetype = (("jpeg","*.jpg"),("png","*.png"),("All Files","*.*")))
e.set(filename)
la = Entry(self,textvariable = e,width = 30).place(x=230,y=330)
butt=Button(self,text="Browse",width=7,command=fileDialog).place(x=430,y=328)