I'm aware of the fact that text widgets are disabled using widget.config(state = tk.DISABLED), making the user unable to change the content of the widget. Content can be added before this statement is run using widget.insert(tk.END, text-to-add), but after that you'd have to alternate between the NORMAL and DISABLED states to add text content, using a command like this:
def add_text(widget, text):
widget.config (state = tk.NORMAL)
widget.insert (tk.END, text)
widget.config (state = tk.DISABLED)
Is there any other, more efficient way to do that?
Is there any other, more efficient way to do that?
No, there is not. That is the most efficient way to do what you want.
Related
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()
I've recently made a text editor with tkinter for python.
I need a way to disable tab from being able to be used normally, so it doesn't indent.
Does anyone have any idea as to how I would achieve this?
Thank you for your time.
It really depends on exactly what you did. Without more information I'm going to assume that you have a text widget somewhere and that you want to disable tab from indenting there.
Example:
from tkinter import Tk, Text
def no_tab(event):
return 'break'
root = Tk()
text_widget = Text()
text_widget.pack()
text_widget.bind('<Tab>', no_tab)
root.mainloop()
In this example we bind the <Tab> key to the function no_tab. So everytime tab is pressed within the text widget the no_tab function is called. The no_tab function returns the magic string 'break' which means that the action of the key won't be preformed and thus disabling the indentation that the tab key would have created otherwise.
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)
I am facing the problem to need tabs in a pygtk app. Pretty much just like gedit has, but without any per-child widget content.
I’ve come across gtk.Notebook, but that requires me to put a widget for each tab, which I don't want.
The reason is, that I have one widget, but would only like to updates its content based on which tab is selected.
Any hints on how to do that?
My idea so far would be to just add some invisible widget for each tab and then connect to the select-page signal. Which widget could I use as invisible widget, or is there a better/alternative way of achieving my goal?
The invisble widget idea works. But not with gtk.Invisible (this just crashes), but with gtk.HBox() or any other thing that seems empty.
self.notebook.append_page(gtk.HBox(), gtk.Label("title"))
Now if I want to display stuff inside the tab actually, I can use reparent to move the widget to the current tab like this.
class Tab(gtk.HBox):
def __init__(self, child):
self.child = child
self.notebook.append_page(Tab(myWidget), gtk.Label("title"))
def pageSelected(self, notebook, page, pagenum):
box = notebook.get_nth_page(pagenum)
box.child.reparent(box)
You can have global widgets, one per tab as you want, in order to access them easily when the tab is selected.
self.notebook.append_page(self.rightBox, gtk.Label("Orders"))
Then connect to the "switch page" signal
self.notebook.connect("switch-page", self.pageSelected)
and :
def pageSelected(self, notebook, page, pagenum):
name = notebook.get_tab_label(notebook.get_nth_page(pagenum))
Now you have "name" with the label of the currently selected page. Just test it (if name == "Orders" ...) to interact.
Hope this was of some help !
Does anybody know how to create a text field using PyGTK that only accepts number. I am using Glade to build my UI.
Cheers,
I wouldn't know about a way to do something like this by simple switching a settings, I guess you will need to handle this via signals, one way would be to connect to the changed signal and then filter out anything that's not a number.
Simple approach(untested but should work):
class NumberEntry(gtk.Entry):
def __init__(self):
gtk.Entry.__init__(self)
self.connect('changed', self.on_changed)
def on_changed(self, *args):
text = self.get_text().strip()
self.set_text(''.join([i for i in text if i in '0123456789']))
If you want formatted Numbers you could of course go more fancy with a regex or something else, to determine which characters should stay inside the entry.
EDIT
Since you may not want to create your Entry in Python I'm going to show you a simple way to "numbify" an existing one.
def numbify(widget):
def filter_numbers(entry, *args):
text = entry.get_text().strip()
entry.set_text(''.join([i for i in text if i in '0123456789']))
widget.connect('changed', filter_numbers)
# Use gtk.Builder rather than glade, you'll need to change the format of your .glade file in Glade accordingly
builder = gtk.Builder()
builder.add_from_file('yourprogram.glade')
entry = builder.get_object('yourentry')
numbify(entry)
If you don't want to sanitize user input, avoid allowing text input entirely. If you're trying to collect hours and minutes, how about spin buttons or other widgets where you can limit the user's choice.
Check out the spinbutton example:
http://www.pygtk.org/pygtk2tutorial/examples/spinbutton.py
Convert your text to a number and in case it doesn't handle the error and set the text to an empty string.
You can generalize this to match a regular expression the way you want
try:
val = float(entry.get_text())
entry.set_text(str(val))
except ValueError:
entry.set_text('')