Automatically resize text widget's height to fit all text - python

How can one automatically resize a text widget to fit a text widget's height
There will not be any \n om the text widget, instead the text will wrap (whole word) around and continue down. wrap=WORD
How can this done?
My idea of approaching the problem statement: I was wondering if it was possible to count every time the text was wrapped around in the text widget, and by that given value somehow calculate the height of the text widget? - Just a thought .. I have no clue whether it's possible.
**WHY THIS IS NOT A DUPLICATE **
This is not a duplicate, link to the question you claim it is a duplicate of if you think so. They all have implemented a solution in which it check a '\n' in each keystroke in the text widget. My text widget won't have any '\n' in it at all. But instead wrap the words around !
This is NOT the solution I am looking for, since it is looking for '\n' and changes the height accordingly to how many of them it finds. Since I won't be using any '\n' but instead wrap the words around (Text(frame, wrap=WORDS)) no '\n' will not appeare making that solution USELESS!"
That is why this code, from the question people claim this is a duplicate of, WONT fix this question, this is NOT a duplicate.
wont fix my problem since it looks for '\n':
import Tkinter
class TkExample(Tkinter.Frame):
def __init__(self, parent):
Tkinter.Frame.__init__(self, parent)
self.init_ui()
def init_ui(self):
self.pack()
text_box = Tkinter.Text(self)
text_box.pack()
text_box.bind("<Key>", self.update_size)
def update_size(self, event):
widget_width = 0
widget_height = float(event.widget.index(Tkinter.END))
for line in event.widget.get("1.0", Tkinter.END).split("\n"):
if len(line) > widget_width:
widget_width = len(line)+1
event.widget.config(width=widget_width, height=widget_height)
if __name__ == '__main__':
root = Tkinter.Tk()
TkExample(root)
root.mainloop()
edit example
This is the reason why I am not using message widgets, they doesn't rearrange the text to fill out the text widget.

The tkinter text widget isn't designed to grow or shrink to fit its contents like a label widget. If all you need is to display plain text with no need to interactively edit more text, a Label is probably a better choice than Text.
That being said, it's possible to get the number of displayed lines in a text widget, and with that information you can resize the widget.
Here's an example that shows how to cause it to resize when you insert text programatically. It won't handle resizing as you type, though it can be made to do it.
The trick is to know that internally the text widget has a count method which you can call to get the number of displayed lines. Unfortunately, this method isn't exposed at the tkinter layer and thus requires a bit of knowledge of how tkinter works internally.
class ExpandoText(tk.Text):
def insert(self, *args, **kwargs):
result = tk.Text.insert(self, *args, **kwargs)
self.reset_height()
return result
def reset_height(self):
height = self.tk.call((self._w, "count", "-update", "-displaylines", "1.0", "end"))
self.configure(height=height)
Here is an example of how to use it:
root = tk.Tk()
text = ExpandoText(root, width=20, wrap="word")
text.pack(fill="both", expand=True)
root.update_idletasks()
text.insert("1.0", "This is a line of text that will initially be wrapped.")
root.after(5000, text.insert, "end", "This is more text")
root.mainloop()
When you run the code, you should see a window that looks like this:
If you don't manually resize the window, and wait 5 seconds, the window will grow to show the added text:
Unfortunately you must call update_idletasks before the insertion so that tkinter knows how wide the window will actually be. This might have visual side effects depending on how the rest of your code works (read: you might see a flash when the UI first starts up).

Related

How to make Text widgets adaptive in Tkinter?

Ok so let's say I've got a Text object. How can I make it so that it gets an extra line (or height + 1) whenever I fill in the current line? like when it starts hiding the left of the line to show you the end?
Edit: since the question wasn't clear, I'll describe it more carefully.
Take this code as reference:
from tkinter import *
root = Tk()
text = Text(root, width=50, height=1)
text.pack
What it does, is creating a new Text widget of 1 line, and packs it. You may ask: Why don't you use the Entry widget? Because I want it to add more lines, instead of hiding what you already wrote to make some room for what you're writing, as shown below:
from tkinter import *
from threading import Thread
def adjustheight():
while True:
#check if whatever it's written takes more than a line to show
if takesmorethan1line == True:
text.config(height=(text.cget("height") + 1)
root = Tk()
text = Text(root, width=50, height=1)
text.pack
Thread(target = adjustheight).start()
root.mainloop()
I couldn't understand your question very well, But I assume that you have problem with wrapping a text. For instance when you reduce the window size, the text should break into multi lines. Here is as example:
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.my_label = tk.Label(
self, text="Helllllooooooooooooooooooooooooo")
self.my_label.bind('<Configure>',
lambda e: self.my_label.config(
wraplength=self.my_label.winfo_width()
))
self.my_label.pack()
self.mainloop()
if __name__ == '__main__':
window = MainWindow()
Line breaking, also known as word wrapping, is breaking a section of text into lines so that it will fit into the available width of a page, window or other display area, Read more.
Solution
Tkinter's Text widget have this feature already built in. wrap is the option that will do it.
From the docs:
This option controls the display of lines that are too wide.
With the default behavior, wrap=tk.CHAR, any line that gets too long will be broken at any character.
Set wrap=tk.WORD and it will break the line after the last word that will fit.
Example
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, wrap=tk.WORD)
text.pack(fill=tk.BOTH, expand=1)
root.mainloop()

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

How to wrap the text in a tkinter label dynamically?

The text in a label in tkinter can be wrapped into multiple lines when exceeding the limit given in the parameter wraplength.
However, this is a number of pixels, but instead, I'd like to use the full window width for this, and the wrap lenght should change whenever the user is changing the window size.
One approach could be to update the parameter manually with something like this:
def update_wraplength(id, root):
id.configure(wraplength=root.winfo_width())
root.after(10, lambda: update_wraplength(id,root))
Is there another way of doing this, maybe a parameter I do not know about?
You would have to update the wraplength every time the window size changes. You can detect when the window size changes with the "<Configure>" event.
my_label.bind('<Configure>', update_wraplength)
Remember it only works if you have the Label set up to expand to all available space.
Lets see if you can make sense of this code:
import Tkinter as tk
class WrappingLabel(tk.Label):
'''a type of Label that automatically adjusts the wrap to the size'''
def __init__(self, master=None, **kwargs):
tk.Label.__init__(self, master, **kwargs)
self.bind('<Configure>', lambda e: self.config(wraplength=self.winfo_width()))
def main():
root = tk.Tk()
root.geometry('200x200')
win = WrappingLabel(root, text="As in, you have a line of text in a Tkinter window, a Label. As the user drags the window narrower, the text remains unchanged until the window width means that the text gets cut off, at which point the text should wrap.")
win.pack(expand=True, fill=tk.X)
root.mainloop()
if __name__ == '__main__':
main()

change size of tkinter messagebox

In python, I am attempting the change the width of the tkinter messagebox window so that text can fit on one line.
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
messagebox.showinfo("info","this information goes beyond the width of the messagebox")
root.mainloop()
It's not possible to adjust the size of messagebox.
When to use the Message Widget
The widget can be used to display short text messages, using a single font. You can often use a plain Label instead. If you need to display text in multiple fonts, use a Text widget. -effbot
Also see:
Can I adjust the size of message box created by tkMessagebox?
#CharleyPathak is correct. You either need to put a newline in the middle of the text, because message boxes can display multiple lines, or create a custom dialog box.
Heres another method that gets the effect youre looking for but doesnt use messagebox. it looks a lot longer but it just offers much more in terms of customization.
def popupmsg():
popup = tk.Tk()
def leavemini():
popup.destroy()
popup.wm_title("Coming Soon")
popup.wm_attributes('-topmost', True) # keeps popup above everything until closed.
popup.wm_attributes("-fullscreen", True) # I chose to make mine fullscreen with transparent effects.
popup.configure(background='#4a4a4a') # this is outter background colour
popup.wm_attributes("-alpha", 0.95) # level of transparency
popup.config(bd=2, relief=FLAT) # tk style
# this next label (tk.button) is the text field holding your message. i put it in a tk.button so the sizing matched the "close" button
# also want to note that my button is very big due to it being used on a touch screen application.
label = tk.Button(popup, text="""PUT MESSAGE HERE""", background="#3e3e3e", font=headerfont,
width=30, height=11, relief=FLAT, state=DISABLED, disabledforeground="#3dcc8e")
label.pack(pady=18)
close_button = tk.Button(popup, text="Close", font=headerfont, command=leavemini, width=30, height=6,
background="#4a4a4a", relief=GROOVE, activebackground="#323232", foreground="#3dcc8e",
activeforeground="#0f8954")
close_button.pack()
I managed to have a proper size for my
"tkMessageBox.showinfo(title="Help", message = str(readme))" this way:
I wanted to show a help file (readme.txt).
def helpfile(filetype):
if filetype==1:
with open("readme.txt") as f:
readme = f.read()
tkMessageBox.showinfo(title="Help", message = str(readme))
I opened the file readme.txt and EDITED IT so that the length of all lines did not exeed about 65 chars. That worked well for me. I think it is important NOT TO HAVE LONG LINES which include CR/LF in between. So format the txt file properly.

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)

Categories