Question
How can I insert text into a Tkinter Textbox? I am trying to create a word processor which inserts lists at the current position.
What I have tried so far
I have tried to use the CURRENT argument, but this is unreliable.
def listcmd(self): #THIS HAS BUGS!!! FIX IT SOON
number = self.listentry.get()
number = int(number)
listINT = 1
for x in xrange(number):
self.write.insert(CURRENT, "%s:" % (str(listINT) )) #This is used for the number
self.write.insert(CURRENT, "\n") #This inserts the newline
listINT += 1
You need to use the index "insert" or Tkinter.INSERT. That always refers to the insertion cursor.
Related
I am working on a wordle game and have entries that will take in a single character before moving on to the next entry. As of now, the game is working, but it does not allow for backspace if a mistake was made. I am trying to incorporate that.
def pause(event, i):
p = StringVar()
window.bind(event, lambda e : p.set(inner[i].get()))
window.wait_variable(p)
i = 0
while i < 30:
inner[i].focus_set()
pause("<KeyRelease>", i)
if inner[i].get() == "":
i -= 1
inner[i].delete(0, END)
word = word[:-1]
continue
word += inner[i].get()
inner is a list of all the entries. I wasn't sure if there was a better way to get the exact keyboard input (I tried to download and import keyboard, but that was not working), so I was able to determine that if inner[i].get() == "" that means that the input was a backspace. I wanted to increment i by negative one if this was the case so that the entry in focus would be the previous entry. I would then want to delete what was in that entry and insert a new character by continuing through the loop. However, the entry is not being deleted at all.
I need to get only the latest input from my text widget, and then append that character to a list.
I am using
Text.get(1.0,'end-1c')
, and it does not work because the loop constantly gets all the input, instead of only getting the latest input when there is a new latest input.
def main_screen():
start_time=time.time()
tk=Tk()
tk.title('Typing Test')
tk.geometry('800x500')
main_title=Label(tk,text='1 Minute Test',font=('Times New Roman',36))
main_title.pack(pady=5)
directions=Label(tk,text='Start Typing',font=('Times New Roman',14))
directions.pack()
base_text=Label(tk,text=randomizer(),bg='#E0E0EE',font=('Arial',14),wraplength=700,justify=LEFT)
base_text.pack(pady=10)
text_area=Text(tk,font=('Arial',14),width=63,height=7,wrap='word')
text_area.pack()
tk.update()
#WPM Calculation
target_text=randomizer()
typed_text=[]
wpm=0
errors=0
while True:
tk.update()
time_elapsed=max(time.time()-start_time,1)
wpm=round((len(typed_text)/60)/5)
if time_elapsed>=60:
break
#Problem Section
key=text_area.get(1.0,'end-1c')
typed_text.append(key)
for x in typed_text:
if x != target_text:
errors += 1
Alternatively, I tried using a variable in place of the 1.0 in .get, that would increase by one with each iteration of the loop. Next, I tried a try/except command, and put the #Problem Section into a function. I tried calling that function by binding the text area to
'<Key>'
'<KeyPress>'
'<KeyRelease>'
None of these attempts work. I used a print statement to see what those variables are with each iteration of the loop, and using the first method, it just keeps making a longer and longer string that repeats constantly, instead of updating with each new character. Trying the other ways I just got nothing, no output, but no error either. I am completely stuck, and don't know what else to try.
You can bind the text_area with a <KeyPress> event, but you need to pass the list typed_text as an argument so you can append the presses.
So you should do something like this:
text_area.bind("<KeyPress>", lambda _: getKey(_, typed_text))
while True:
tk.update()
time_elapsed = max(time.time() - start_time, 1)
wpm = round((len(typed_text) / 60) / 5)
if time_elapsed >= 60:
break
# Problem Section
for x in typed_text:
if x != target_text:
errors += 1
def getKey(event, list):
list.append(event.char)
print(list)
The text widget supports something called a "mark", which is like a bookmark. You can put a mark anywhere in the text and use it just like a normal index.
Assuming that data is only ever appended to the end of the widget, the simplest solution is to fetch a block of data and then move the mark to the end of the text that you fetched. The next time you fetch data, start at that mark.
Marks have something called "gravity" that defines which character the mark sticks to. For example, if the gravity is "left" and you set it to character "2.2", the mark will always stay adjacent to the character at index "2.2". If the gravity is "right", it will be stuck at the character following index "2.2" (eg: "2.3" or "3.0")
Here's a contrived example that will print only the latest additions to a text widget every five seconds, by tracking the last position that was used to fetch the data.
import tkinter as tk
def get_new_text():
data = text.get("last", "end-1c")
print(f"new data: >>>{data}<<<")
text.mark_set("last", "end-1c")
root.after(5000, get_new_text)
root = tk.Tk()
text = tk.Text(root, wrap="word")
text.pack(fill="both", expand=True)
text.mark_set("last", "1.0")
text.mark_gravity("last", "left")
root.after(5000, get_new_text)
root.mainloop()
I have a working Tkinter.Listbox object, but I want to set it up so that its elements can have carriage returns without having to somehow set up multiple linked items.
For instance, if I want to generate a selection pane with items that look like this..
# Here are four elements for the selector Listbox..
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element\n spanning two lines!')
# This generates and displays the selector window..
tk_selector = SingleSelect(lb_items, "TEST SELECTOR")
tk_selector.run_selector()
..it would be great if I could get the output to look like this mockup..
..instead of what it actually generates, which is this..
Listboxes seem to ignore '\n' and triple-quote strings with line-returns entirely; if \n is used, neither the characters nor the line break appears.
Is it possible to have individual, selectable Listbox elements that appear with line breaks?
I would also be satisfied with a word-wrap option, but after some looking, I couldn't find any such option in Listbox or Tk in general.
I could probably fake the effect by making multiline strings into multiple elements then setting it up to return the whole line if any of them are called, but it feels like an ordeal for something that could have a simple solution.
Like Bryan Oakley said, there is no native support in Listbox for carriage returns, so I tried building the 'fake' version I mentioned in the question, and it turns out that it isn't really that hard.
My solution is to parse each 'raw' string before inserting them into the Listbox, breaking strings into individual lines using splitlines, recording the number of lines and which indices in the Listbox's element roll correspond to which unbroken input string, and then selecting all the parts whenever the Listbox's selection changes using Listbox.bind('<<ListboxSelect>>', self._reselection_fxn).
There's an abridged and annotated sample below, or you can see my complete, working, even-more-heavily annotated code here.
class Multiline_Single_Selector(object):
## Go ahead and choose a better class name than this, too. :/
def __init__(self, itemlist, ..):
# ..
lb_splitlines = self._parse_strings(itemlist)
# ^ splits the raw strings and records their indices.
# returns the split strings as a list of Listbox elements.
self.my_Listbox.insert(0, *lb_splitlines)
# ^ put the converted strings into the Listbox..
self.my_Listbox.bind('<<ListboxSelect>>', self._reselect)
# ^ Whenever the Listbox selection is modifed, it triggers the
# <<ListboxSelect>> event. Bind _reselect to it to determine
# which lines ought to be highlighted when the selection updates.
# ..
def _parse_strings(self, string_list):
'''Accepts a list of strings and breaks each string into a series of lines,
logs the sets, and stores them in the item_roster and string_register attributes.
Returns the split strings to be inserted into a Listbox.'''
self.index_sets = index_sets = []
# ^ Each element in this list is a tuple containing the first and last
# Listbox element indices for a set of lines.
self.string_register = register = {}
# ^ A dict with a whole string element keyed to the index of the its
# first element in the Listbox.
all_lines = []
# ^ A list of every Listbox element. When a string is broken into lines,
# the lines go in here.
line_number = 0
for item in string_list:
lines = item.splitlines()
all_lines.extend(lines) # add the divided string to the string stack
register[line_number] = item
# ^ Saves this item keyed to the first Listbox element it's associated
# with. If the item is selected when the Listbox closes, the original
# (whole) string is found and returned based on this index number.
qty = len(lines)
if qty == 1: # single line item..
index_sets.append((line_number, line_number))
else: # multiple lines in this item..
element_range = line_number, line_number + qty - 1
# ^ the range of Listbox indices..
index_sets.extend([element_range] * qty)
# ^ ..one for each element in the Listbox.
line_number += qty # increment the line number.
return all_lines
def _reselect(self, event=None):
"Called whenever the Listbox's selection changes."
selection = self.my_Listbox.curselection() # Get the new selection data.
if not selection: # if there is nothing selected, do nothing.
return
lines_st, lines_ed = self.index_sets[selection[0]]
# ^ Get the string block associated with the current selection.
self.my_Listbox.selection_set(lines_st, lines_ed)
# ^ select all lines associated with the original string.
def _recall(self, event=None):
"Get the complete string for the currently selected item."
selection = self.my_Listbox.curselection()
if selection: # an item is selected!
return self.string_register[selection[0]]
return None # no item is selected.
If you adjust this code to match your own and drop it into an existing Listbox setup, it ought to let you simulate carriage returns. It's not the same as a native line-wrap or \n-parsing function directly in Listbox, but it does basically the same thing.
To get the whole string corresponding to the current selection, just bind _recall to whatever input or event you want to have return it. In this case, it returns None when no item is selected.
It's also kind of a lot of work considering the sophistication of the desired effect and it may not be appropriate to every situation. But at least you can do it.
It is not possible for a listbox item to be spread across more than one line or row.
This solution seems to me much easier:
Instead of trying:
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element\n spanning two lines!')
make two elements:
lb_items = ('mama', 'luigi', 'my birds', \
'this is a single element', 'spanning two lines!')
Is there an easier way to change the order of items in a tkinter listbox than deleting the values for specific key, then re-entering new info?
For example, I want to be able to re-arrange items in a listbox. If I want to swap the position of two, this is what I've done. It works, but I just want to see if there's a quicker way to do this.
def moveup(self,selection):
value1 = int(selection[0]) - 1 #value to be moved down one position
value2 = selection #value to be moved up one position
nameAbove = self.fileListSorted.get(value1) #name to be moved down
nameBelow = self.fileListSorted.get(value2) #name to be moved up
self.fileListSorted.delete(value1,value1)
self.fileListSorted.insert(value1,nameBelow)
self.fileListSorted.delete(value2,value2)
self.fileListSorted.insert(value2,nameAbove)
Is there an easier way to change the order of items in a tkinter listbox than deleting the values for specific key, then re-entering new info?
No. Deleting and re-inserting is the only way. If you just want to move a single item up by one you can do it with only one delete and insert, though.
def move_up(self, pos):
""" Moves the item at position pos up by one """
if pos == 0:
return
text = self.fileListSorted.get(pos)
self.fileListSorted.delete(pos)
self.fileListSorted.insert(pos-1, text)
To expand on Tim's answer, it is possible to do this for multiple items as well if you use the currentselection() function of the tkinter.listbox.
l = self.lstListBox
posList = l.curselection()
# exit if the list is empty
if not posList:
return
for pos in posList:
# skip if item is at the top
if pos == 0:
continue
text = l.get(pos)
l.delete(pos)
l.insert(pos-1, text)
This would move all selected items up 1 position. It could also be easily adapted to move the items down. You would have to check if the item was at the end of the list instead of the top, and then add 1 to the index instead of subtract. You would also want to reverse the list for the loop so that the changing indexes wouldn't mess up future moves in the set.
I have a method that is suppose to take a search parameter and remove everything from the list that does not meet the parameter. But when it runs it removes list items at almost random. I've debugged it and it correctly determines if an item needs to be removed but it doesn't remove the right one. I think it has something to do with when I remove one item it messes up the indexes of the rest of the list, which doesn't with with my method of tracking the index.
I posted the whole class but the relevant code is towards the bottom
class StudentFinderWindow(Tkinter.Toplevel):
def __init__(self):
Tkinter.Toplevel.__init__(self) # Create Window
##### window attributes
self.title('Edit Students') #sets window title
##### puts stuff into the window
# text
editStudentInfoLabel = Tkinter.Label(self,text='Select the student from the list below or search for one in the search box provided')
editStudentInfoLabel.grid(row=0, column=0)
# entry box
self.searchRepositoryEntry = Tkinter.Entry(self)
self.searchRepositoryEntry.grid(row=1, column=0)
# list box
self.searchResults = Tkinter.Listbox(self)
self.searchResults.grid(row=2, column=0)
# search results initial updater
self.getStudentList()
for student in self.studentList:
self.searchResults.insert(Tkinter.END, student)
##### event handler
self.searchRepositoryEntry.bind('<KeyRelease>', self.updateSearch)
This is the relevant code
def updateSearch(self, event):
parameters = self.searchRepositoryEntry.get()
int = 0
currentList = self.searchResults.get(0, Tkinter.END)
length = len(parameters)
print(parameters)
print(length)
for i in currentList:
if not i[0:length] == parameters:
self.searchResults.delete(int)
print(i[0:length] == parameters)
print(i[0:length])
print(int)
int += 1
def getStudentList(self):
global fileDirectory # gets the directory that all the files are in
fileList = listdir(fileDirectory) # makes a list of files from the directory
self.studentList = [] # makes a new list
for file in fileList: # for loop that adds each item from the file list to the student list
self.studentList.append(file[:-4])
When you delete an item, everything below it moves up causing the index of all following items to change. The simplest solution to this sort of a problem (it's also common when deleting words from a text widget) is to delete backwards, starting at the end.
I think you already know the problem. When you delete an item, the index for the rest of the items change. For example, if you delete the 4th item, then the 5th item becomes the "new" 4th item. So you don't want to increment int whenever you delete an item. You can implement that with continue:
for i in currentList:
if not i[0:length] == parameters:
self.searchResults.delete(int)
continue # <-- Use continue so `int` does not increment.
int += 1
PS. It's not good coding style to use int as a variable name -- in Python it masks the built-in function of the same name.