How to get selected text from Python Tkinter Text widget - python

I am developing a Text based application using Python Tkinter, In my Text widget created some words are tag_configured, on double clicking mouse on that tagged words selection appears with blue color, how can I get this selected text for further processing, Code as follows.........
self.area.tag_configure('errorword',font=('MLU-Panini', 15,foreground="black",underline=True)
self.area.tag_bind("errorword","<Double-Button-1>",self.mouse_click,add=None)
def mouse_click(self,event):
errorstr=self.area.get(tk.SEL_FIRST,tk.SEL_LAST)
print("mmmmmm",errorstr)
Shows error
File "C:\Python34\lib\tkinter\__init__.py", line 3082, in get
return self.tk.call(self._w, 'get', index1, index2)
_tkinter.TclError: text doesn't contain any characters tagged with "sel"
.......................................................................
Can someone guide me on how to solve this error.

Exactly like tobias_k mentions in his comment, the order in which the event bindings are executed is key here, because you are trying to get the selected text before the text is actually selected. You can see the order of binding execution using the bindtags() widget method. When you do this for a Text widget you will see something like
('.38559496', 'Text', '.', 'all')
Which means that the order, from left to right, of binding event execution is so that first events that are unique to this specific widget are evaluated, then those specific to the widget class, then those to your root window and finally everything else on application level (source).
Your double-click event is on widget level, since it is applied only to that specific widget, but the actual selection of the text is an event on the Text class level. Therefore, you will have to rearrange the order so that the class events come before the widget events. You can get the order by calling bindtags without arguments and then define a new order by calling it again with a tuple containing the order:
order = self.area.bindtags()
self.area.bindtags((order[1], order[0], order[2], order[3]))
This makes sure that the selection of the text is performed before you try to read the selection.

Related

python 3 tkinter continue text widget tag with natural input after tag range terminator

Background:
I'm trying to get an MS Word like behavior. I want text inserted immediately adjacent to the end of a tag range to be automatically included in that tag's range.
Text that is inserted within a tag range acts as I would expect within MS word. Colored text added is displays as the correct color, font, styles immediately.
The same is not true when updating added text with tag_add(). You can see the update as a visual 'hiccup'. Text starts as the default text and pops into the tag styling. Additionally, when typing quickly the tag_add() 'loses' the tag range. That is not acceptable behavior for my application.
I have been looking for a few days and can't find anything even related to how tkinter decided how to deal with growing tags naturally i.e. without Text.tag_add() or Text.insert().
Question:
How do I set tkinter to prefer the tag in the left adjacent index when deciding what range to add the new keyboard input text.
Demo:
I have a demo video on my blog along with the coed of my current solution. I do not consider the current state usable/functional. So I need something that updates the styling seamlessly.
Tri it! Blog Post
Required Technology:
python 3+ and tkinter on windows
Desired Results:
The base range, insert is just after the 'l' in 'terminal'
The next character insert should continue the left styling as if it were within those tag ranges. Without any visual pop from default text to the new styling. And should be unconcerned with the speed of user typing.
Thank you!
The simplest way to avoid the artifacting I show in the demo. Is to have the tag_add() in a method bound to the root windows "key" event. I am honestly not sure why this is. But the application behaves as expected when bound to the root instead of the Text widget.
For context here is a trimmed down sample of the code from my blog post. I smashed all the check functions down to a few lines. But the important part is the "__ init __" when I bind self.master.bind('', foobar)
Check out video demo on blog for results.
https://valtyrtriit.blogspot.com/2022/02/python-3-win10-tkinter-word-processor_22.html
Do note that "Tab" will not show up on events now because it is a reserved event to navigate window elements. You can break the default binding and rebind it to fix this.
import tkinter as tk
import re
class Document(tk.Text):
def __init__(self, master, *args, **kewargs):
super().__init__(*args, **kewargs)
self.master = master
self.master.bind("<KeyPress>", self.any_key_down)
self.tag_add('<b>', "1.0", "1.2")
self.tag_config('<b>', foreground='red')
def any_key_down(self, event=None):
print(event)
char = re.findall(r"[a-zA-Z0-9\S ]", event.char)
if 0 < len(char) and event.keysym not in ["BackSpace", "Escape"] or '\t' == event.char:
insert = event.widget.index('insert-2c')
tags = event.widget.tag_names(insert)
for tag in tags:
event.widget.tag_add(tag, 'insert-1c', 'insert')
root = tk.Tk()
text = Document(root, height=4)
text.pack()
text.insert("1.0", "This is helvetica font", "<b>")
text.insert("1.0", "This is terminal font", "font_term")
text.tag_config('<b>', font='Helvetica 12')
text.tag_config('font_term', font='Terminal 12')
text.insert("3.0", "This is terminal font\n", "font_term")
root.mainloop()

Text box entry in a GUI

For a class I am coding a small Chess simulator with a GUI using python. To enter a move I would like to have a text box that you can type in, and once you press the btn move, it moves that piece.
Currently I have the GUI with radio buttons for each piece. Under the function for the btn to move them I have:
if btn == 4:
cmds.select('A4')
cmds.move()
I however do not know how to write a code for a simple text box in a gui or what the propper code to reference the text box is.
Q: How to code a simple working text box, and how to write the code so the functions can reference the gui.
We can create the text box using the textField command. To create a typical textField command:
my_textfield = cmds.textField()
In Maya, every UI element has a unique identifier name string. When we call the textField command in create mode, it will return the name of the textField it created. Here, my_textfield would be a Python variable that contains the name of the created textField that we can refer to later. To access the text value of this textField we would do something like:
text_entered = cmds.textField(my_textfield, query=True, text=True)
Here, to access the text entered in the textField, we are calling the textField command in query mode, by setting the query flag as True and setting the text flag as True. Setting text=True in query mode, i.e. query=True tells the command to return the current text value of the textField. Now text_entered would be the Python variable that will contain the text entered.
Text fields can take any sort of textual inputs that might need validation. To avoid validation troubles you could use the intField if you know that the inputs need to be only integers. (There is also a floatField.) Using an intField is also very similar to textField. To create one:
my_intfield = cmds.intField(minValue=1, maxValue=8)
minValue and maxValue are optional parameters that let you set the minimum and maximum values that this field can accept. To access the value entered:
val_entered = cmds.intField(my_intfield, query=True, value=True)
Have a look at the documentation for these two fields for more information on what else they offer:
textField
intField

Readonly tkinter text widget

I want to use tkinter text widget as a readonly widget. It should act as a transcript area. My idea is to keep this transcript in a file and whenever the user writes anything, just remove all the contents of the widget, and rewrite it again.
The code will look like:
transcript_entry = SimpleEditor() # SimpleEditor is inherited from ScrolledText
transcript_entry.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
transcript_entry.text.insert("1.0", "This is test transcript")
transcript_entry.text.bind("<KeyPress>", transcript_entry.readonly)
And readonly function will look like:
def readonly(self, event):
self.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
self.text.insert("1.0", "This is test transcript")
The bug here is that the last character entered by the user is added to the transcript. I suspect the reason is that the readonly function is called, then the user input is wrote to the widget. How to reverse this order & let the readonly function be called after the user input is wrote to the widget?
Any hints?
The reason that the last character is inserted is because the default bindings (which causes the insert) happens after custom bindings you put on the widget. So your bindings fire first and then the default binding inserts the characters. There are other questions and answers here that discuss this in more depth. For example, see https://stackoverflow.com/a/11542200/
However, there is a better way to accomplish what you are trying to do. If you want to create a readonly text widget, you can set the state attribute to "disabled". This will prevent all inserts and deletes (and means you need to revert the state whenever you want to programmatically enter data).
On some platforms it will seem like you can't highlight and copy text, but that is only because the widget won't by default get focus on a mouse click. By adding a binding to set the focus, the user can highlight and copy text but they won't be able to cut or insert.
Here's an example using python 2.x; for 3.x you just have to change the imports:
import Tkinter as tk
from ScrolledText import ScrolledText
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
t = ScrolledText(self, wrap="word")
t.insert("end", "Hello\nworld")
t.configure(state="disabled")
t.pack(side="top", fill="both", expand=True)
# make sure the widget gets focus when clicked
# on, to enable highlighting and copying to the
# clipboard.
t.bind("<1>", lambda event: t.focus_set())
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Please do not delete and reinsert your text :
It is huge performance issue.
It will remove any tags and marks set on the text
This will be visible to the user, and users don't like flickering interfaces
This is not necessary, Tkinter is customizable enough to just not allow the user change the content.
The best way I found to create a read only Text is to disable all the bindings leading to a text change.
My solution is to create a new Widget binding map containing only "read only commands". Then, just reconfigure your widget to use the new RO binding map instead of the default one :
from Tkinter import *
# This is the list of all default command in the "Text" tag that modify the text
commandsToRemove = (
"<Control-Key-h>",
"<Meta-Key-Delete>",
"<Meta-Key-BackSpace>",
"<Meta-Key-d>",
"<Meta-Key-b>",
"<<Redo>>",
"<<Undo>>",
"<Control-Key-t>",
"<Control-Key-o>",
"<Control-Key-k>",
"<Control-Key-d>",
"<Key>",
"<Key-Insert>",
"<<PasteSelection>>",
"<<Clear>>",
"<<Paste>>",
"<<Cut>>",
"<Key-BackSpace>",
"<Key-Delete>",
"<Key-Return>",
"<Control-Key-i>",
"<Key-Tab>",
"<Shift-Key-Tab>"
)
class ROText(Text):
tagInit = False
def init_tag(self):
"""
Just go through all binding for the Text widget.
If the command is allowed, recopy it in the ROText binding table.
"""
for key in self.bind_class("Text"):
if key not in commandsToRemove:
command = self.bind_class("Text", key)
self.bind_class("ROText", key, command)
ROText.tagInit = True
def __init__(self, *args, **kwords):
Text.__init__(self, *args, **kwords)
if not ROText.tagInit:
self.init_tag()
# Create a new binding table list, replace the default Text binding table by the ROText one
bindTags = tuple(tag if tag!="Text" else "ROText" for tag in self.bindtags())
self.bindtags(bindTags)
text = ROText()
text.insert("1.0", """A long text with several
lines
in it""")
text.pack()
text.mainloop()
Note that just the bindings are changed. All the Text command (as insert, delete, ...) are still usable.
I recently worked a different, slightly simpler solution. Rather than changing all the bindings, one can add a function to delete all input characters as soon as they are written:
def read_only(self, event):
if event.char is not '': # delete only if the key pressed
# corresponds to an actual character
self.text.delete('insert-1c')
and just bind it to any event:
root.bind('<Key>', self.read_only)

Tkinter, Entry widget, is detecting input text possible?

I have an Entry widget on a simple calculator. The user can choose to enter an equation via the keypad. I was wondering if there was a way to detect a character(from the keypad in my case) being typed into the Entry widget. So, focus is on the widget, user presses '4', it comes up on the widget... can I detect this act, for basic purposes of logging the input?
Every time you press a key inside a Tkinter window, a Tkinter.Event instance is created. All you need to do is access that instance. Here is a simple script that demonstrates just how:
from Tkinter import Tk, Entry
root = Tk()
def click(key):
# print the key that was pressed
print key.char
entry = Entry()
entry.grid()
# Bind entry to any keypress
entry.bind("<Key>", click)
root.mainloop()
key (being a Tkinter.Event instance) contains many different attributes that can be used to get almost any type of data you want on the key that was pressed. I chose to use the .char attribute here, which will have the script print what each keypress is.
Yes. There are a few different ways to do this, in fact.
You can create a StringVar, attach it to the Entry, and trace it for changes; you can bind all of the relevant events; or you can add a validation command that fires at any of several different points in the sequence. They all do slightly different things.
When a user types 4, there's a key event with just the 4 in it (which doesn't let you distinguish whether the user was adding 4 to the end, or in the middle, or replacing a whole selected word, or…), and then a modification event is fired with the old text,* and then the "key" or "all" validation function is called with the (proposed) new text, and the variable is updated with the (accepted) new text (unless the validation function returned false, in which case the invalidcommand is called instead).
I don't know which one of those you want, so let's show all of them, and you can play around with them and pick the one you want.
import Tkinter as tk
root = tk.Tk()
def validate(newtext):
print('validate: {}'.format(newtext))
return True
vcmd = root.register(validate)
def key(event):
print('key: {}'.format(event.char))
def var(*args):
print('var: {} (args {})'.format(svar.get(), args))
svar = tk.StringVar()
svar.trace('w', var)
entry = tk.Entry(root,
textvariable=svar,
validate="key", validatecommand=(vcmd, '%P'))
entry.bind('<Key>', key)
entry.pack()
root.mainloop()
The syntax for variable trace callbacks is a bit complicated, and not that well documented in Tkinter; if you want to know what the first two arguments mean, you need to read the Tcl/Tk docs, and understand how Tkinter maps your particular StringVar to the Tcl name 'PY_VAR0'… Really, it's a lot easier to just build a separate function for each variable and mode you want to trace, and ignore the args.
The syntax for validation functions is even more complicated, and a lot more flexible than I've shown. For example, you can get the inserted text (which can be more than one character, in case of a paste operation), its position, and all kinds of other things… but none of this is described anywhere in the Tkinter docs, so you will need to go the Tcl/Tk docs. The most common thing you want is the proposed new text as the argument, and for that, use (vcmd, '%P').
Anyway, you should definitely play with doing a variety of different things and see what each mechanism gives you. Move the cursor around or select part of the string before typing, paste with the keyboard and with the mouse, drag and drop the selection, hit a variety of special keys, etc.
* I'm going to ignore this step, because it's different in different versions of Tk, and not very useful anyway. In cases where you really need a modified event, it's probably better to use a Text widget and bind <<Modified>>.
If you just need to do simple things without using trace module you can try
def objchangetext(self, textwidget):
print(textwidget.get()) #print text out to terminal
text1 = tk.Entry(tk.Tk())
text1.bind("<KeyRelease>", lambda event, arg=(0): objchangetext(text1))

Function when entered in text box?

I'm using Tkinter for a small Python application. It has a set of ratio buttons, a text box, and a button. Is there a way to make it so the user can simply press Enter/Return on a keyboard and run the same function the button runs? The text box stays selected even when a radio is changed, so that won't cause any problems.
You should be able to bind an event handler to either the text box widget or the whole application that will be called when the event happens. Assuming you have a function to handle the event, something along the lines of:
widget.bind('<Return>', event_handler)
You can also bind a handler function at the application level by calling the bind_all() method of any widget, e.g.:
self.bind_all('<Return>', self.event_handler)
Note the key name is Return not Enter. See Key Names for a list of them all. You can also prefix the key name with a modifier like Shift- and Control- if desired.
There's a decent online reference for tkinter 8.4 here.

Categories