I want to create an object akin to Label, however I want it to also be selectable for copy-pasting. I have done so using the Text widget:
class CopyLabel (Text):
def __init__ (self, master, text = '', font = None):
if font is None:
super().__init__(master = master, height = 1,
borderwidth = 0, width = len(text),
bg = master['background'])
else:
super().__init__(master = master, height = 1,
borderwidth = 0, font = font,
width = len(text), bg = master['background'])
self.insert(1.0, text)
self.configure(state = 'disabled')
I end up displaying this widget with a grid. However, I randomly find the last 1 or 2 characters not showing. When investigating this, it seems the Text widget is splitting those characters off to a new line (when selecting the text, it is possible to drag down to see this second line). The biggest problem is the unpredictable nature of this splitting (I tried doing width = len(text) + 2, but I still occasionally get this line splitting behaviour). Is there any way to remedy this behaviour?
EDIT: setting wrap = 'none' fixed the line splitting behaviour, but the text is still getting cutoff. Now I have to scroll horizontally instead of vertically to see the text, but I guess that is a different question from what I posed.
What you're experiencing is called wrapping and can be disabled modifying Text widget's wrap option, as in :
self['wrap'] = 'none'
or
self.config(wrap='none')
Related
I am attempting to add a small automatic margin inside my text widget however I am having a hard time with writing the tags.
I have a text box and I am trying to insert text into that box while keeping a margin.
I can get the text that is inserted to have a margin but when I type past the last line the margin is gone. So far all I can dig up is how to write the tag and use it with insert() but I want to keep the margin always.
Question: Is there a way to keep the margin on all lines and not just the ones that were inserted from a file or string?
Note the same question extends to Offset tag because I experience the same problem with typing after the inserted text.
Here is what I have tried in a Minimal, Complete, and Verifiable example example.
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, width = 10, height = 10)
text.pack()
text.tag_configure("marg", lmargin1 = 10, lmargin2 = 10)
text.insert("end", "Some random text!", ("marg"))
root.mainloop()
Unfortunately, the edge cases of adding and deleting text at the very start and end of the widget makes working with tags difficult.
If your goal is to maintain a margin, one solution is to create a proxy for the text widget so that you can intercept all inserts and deletes, and always add the margin each time the contents of the widget changes.
For example, start with a custom widget that generates a <<TextModified>> event whenever the widget is modified:
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, command, *args):
cmd = (self._orig, command) + args
result = self.tk.call(cmd)
if command in ("insert", "delete", "replace"):
self.event_generate("<<TextModified>>")
return result
(see https://stackoverflow.com/a/40618152/7432)
Next, modify your program to use this proxy to force the margin tag to always apply to the entire contents:
def add_margin(event):
event.widget.tag_add("marg", "1.0", "end")
text = CustomText(root, width = 10, height = 6)
text.bind("<<TextModified>>", add_margin)
If you add the tag to the entire range of text (including the final trailing newline), then new characters you type will inherit that tag.
Add the following, and perhaps it will work like you expect:
text.tag_add("marg", "1.0", "end")
Unfortunately, you'll lose this if you delete all of the text in the widget, but that can be worked around.
I have a Text widget that holds a custom string that contains \n chars (multiple lines).
The widget is placed within a vertical panedwindow which I want to adjust the panedwindow's sash to display the whole string in the Text widget.
The string is dynamic by nature (which means, it is being updated by other methods in my application).
As the Text widget is configured with wrap='word', how can I calculate the string height in pixels to adjust the sash accordingly?
I tried to use text.dlineInfo('end -1c')[1] + text.dlineinfo('end -1c')[3] (for line's y coordinate + height) after the string was loaded to the widget. The problem is that if the last line is not visible, then dlineinfo returns none.
I also tried to use Font.measure routine, but this doesn't include wrap aspects of the Text widget.
Here is a Minimal, Complete, and Verifiable example:
import tkinter
from tkinter import scrolledtext
class GUI():
def __init__(self, master):
self.master = master
self.body_frame = tkinter.PanedWindow(self.master, orient='vertical', sashwidth=4)
self.body_frame.pack(expand=1, fill='both')
self.canvas_frame = tkinter.Frame(self.body_frame)
self.description_frame = tkinter.Frame(self.body_frame)
self.body_frame.add(self.canvas_frame, sticky='nsew')
self.body_frame.add(self.description_frame, sticky='nsew')
tkinter.Button(self.canvas_frame, text='Update Text', command = lambda : self.update_text("""
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
""")).pack(fill='x')
self.field_description = scrolledtext.ScrolledText(self.description_frame, width=20, wrap='word')
self.field_description.pack(expand=1, fill='both')
self.master.update()
self.body_frame.sash_place(0,0,self.body_frame.winfo_height() - 50) # force sash to be lower
def update_text(self, description):
self.field_description.delete('1.0', 'end')
self.field_description.insert('1.0', description)
height = self.body_frame.winfo_height()
lastline_index = self.field_description.index('end - 1c')
text_height = self.field_description.dlineinfo(lastline_index)[1] + \
self.field_description.dlineinfo(lastline_index)[3]
self.body_frame.sash_place(0, 0, height - text_height)
root = tkinter.Tk()
my_gui = GUI(root)
root.mainloop()
I don't know of any built-in method that returns the total number of lines (including wrapped lines) in a tkinter Text widget.
However, you can manually calculate this number by comparing the lengths of the unbroken strings in the Text widget to the Text widget's exact width (minus padding). This is what the LineCounter class below does:
# python 2.x
# from tkFont import Font
# python 3.x
from tkinter.font import Font
class LineCounter():
def __init__(self):
"""" This class can count the total number of lines (including wrapped
lines) in a tkinter Text() widget """
def count_total_nb_lines(self, textWidget):
# Get Text widget content and split it by unbroken lines
textLines = textWidget.get("1.0", "end-1c").split("\n")
# Get Text widget wrapping style
wrap = text.cget("wrap")
if wrap == "none":
return len(textLines)
else:
# Get Text widget font
font = Font(root, font=textWidget.cget("font"))
totalLines_count = 0
maxLineWidth_px = textWidget.winfo_width() - 2*text.cget("padx") - 1
for line in textLines:
totalLines_count += self.count_nb_wrapped_lines_in_string(line,
maxLineWidth_px, font, wrap)
return totalLines_count
def count_nb_wrapped_lines_in_string(self, string, maxLineWidth_px, font, wrap):
wrappedLines_count = 1
thereAreCharsLeftForWrapping = font.measure(string) >= maxLineWidth_px
while thereAreCharsLeftForWrapping:
wrappedLines_count += 1
if wrap == "char":
string = self.remove_wrapped_chars_from_string(string,
maxLineWidth_px, font)
else:
string = self.remove_wrapped_words_from_string(string,
maxLineWidth_px, font)
thereAreCharsLeftForWrapping = font.measure(string) >= maxLineWidth_px
return wrappedLines_count
def remove_wrapped_chars_from_string(self, string, maxLineWidth_px, font):
avgCharWidth_px = font.measure(string)/float(len(string))
nCharsToWrap = int(0.9*maxLineWidth_px/float(avgCharWidth_px))
wrapLine_isFull = font.measure(string[:nCharsToWrap]) >= maxLineWidth_px
while not wrapLine_isFull:
nCharsToWrap += 1
wrapLine_isFull = font.measure(string[:nCharsToWrap]) >= maxLineWidth_px
return string[nCharsToWrap-1:]
def remove_wrapped_words_from_string(self, string, maxLineWidth_px, font):
words = string.split(" ")
nWordsToWrap = 0
wrapLine_isFull = font.measure(" ".join(words[:nWordsToWrap])) >= maxLineWidth_px
while not wrapLine_isFull:
nWordsToWrap += 1
wrapLine_isFull = font.measure(" ".join(words[:nWordsToWrap])) >= maxLineWidth_px
if nWordsToWrap == 1:
# If there is only 1 word to wrap, this word is longer than the Text
# widget width. Therefore, wrapping switches to character mode
return self.remove_wrapped_chars_from_string(string, maxLineWidth_px, font)
else:
return " ".join(words[nWordsToWrap-1:])
Example of use:
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, wrap='word')
text.insert("1.0", "The total number of lines in this Text widget is " +
"determined accurately, even when the text is wrapped...")
lineCounter = LineCounter()
label = tk.Label(root, text="0 lines", foreground="red")
def show_nb_of_lines(evt):
nbLines = lineCounter.count_total_nb_lines(text)
if nbLines < 2:
label.config(text="{} line".format(nbLines))
else:
label.config(text="{} lines".format(nbLines))
label.pack(side="bottom")
text.pack(side="bottom", fill="both", expand=True)
text.bind("<Configure>", show_nb_of_lines)
text.bind("<KeyRelease>", show_nb_of_lines)
root.mainloop()
In your specific case, the height of the wrapped text in your ScrolledText can be determined in update_text() as follows:
from tkinter.font import Font
lineCounter = LineCounter()
...
class GUI():
...
def update_text(self, description):
...
nbLines = lineCounter.count_total_nb_lines(self.field_description)
font = Font(font=self.field_description.cget("font"))
lineHeight = font.metrics("linespace")
text_height = nbLines * lineHeight
...
You know the number of lines in your Text. And you can tell when a line is off the scrolled region when dlineinfo returns None. So go through each line and "see" it, to make sure it's visible before you run the dlineinfo() call on it. Then sum them all up, and that's the minimum new height you need for the lines to all appear at the current width. From the height of a line's bbox and the height of the biggest font in the line, you can determine if the line is wrapped, and if so, how many times, if you care about that. The trick is to then use paneconfig() to modify the height of the paned window. Even if the child window would resize automatically normally, the paned window will not. It must be told to resize through the paneconfig() call.
If you "see" each line before measuring, you'll get all the measurements. And "seeing" each line shouldn't be a big deal since you intend to show them all at the end anyway.
I'm trying to make a program which will fit text into a rectangle (x by y) depending on the text, the font and the font size
Here is the code
def fit_text(screen, width, height, text, font):
measure_frame = Frame(screen) # frame
measure_frame.pack()
measure_frame.pack_forget()
measure = Label(measure_frame, font = font) # make a blank label
measure.grid(row = 0, column = 0) # put it in the frame
##########################################################
# make a certain number of lines
##########################################################
words = text.split(" ")
lines = []
num = 0
previous = 0
while num <= len(words):
measure.config(text = " ".join(words[previous:num])) # change text
line_width = measure.winfo_width() # get the width
print(line_width)
if line_width >= width: # if the line is now too long
lines.append(" ".join(words[previous:num - 1])) # add the last vsion which wasn't too long
previous = num - 1 # previous is now different
num = num + 1 # next word
lines.append(" ".join(words[previous:])) # add the rest of it
return "\n".join(lines)
from tkinter import *
window = Tk()
screen = Canvas(window)
screen.pack()
text = fit_text(screen, 200, 80, "i want to fit this text into a rectangle which is 200 pixels by 80 pixels", ("Purisa", 12))
screen.create_rectangle(100, 100, 300, 180)
screen.create_text(105, 105, text = text, font = ("Purisa", 12), anchor = "nw")
The problem with this is no matter what text is in the label the result from measure.winfo_width() is always 1. Here is where I found this from but it doesn't seem to work for me
The problem with your code is that you're using the width of a widget, but the width will be 1 until the widget is actually laid out on the screen and made visible, since the actual width depends on a number of factors that aren't present until that happens.
You don't need to put the text in a widget in order to measure it. You can pass a string to font.measure() and it will return the amount of space required to render that string in the given font.
For python 3.x you can import the Font class like this:
from tkinter.font import Font
For python 2.x you import it from the tkFont module:
from tkFont import Font
You can then create an instance of Font so that you can get information about that font:
font = Font(family="Purisa", size=18)
length = font.measure("Hello, world")
print "result:", length
You can also get the height of a line in a given font with the font.metrics() method, giving it the argument "linespace":
height = font.metrics("linespace")
The widget will not have a width until it is packed. You need to put the label into the frame, then pack it, then forget it.
I've actually stumbled across a way of doing this through trial and error
By using measure.update_idletasks() it calculates the width properly and it works! Bryan Oakley definitely has a more efficient way of doing it though but I think this method will be useful in other situations
P.S. I wouldn't mind some votes to get a nice, shiny, bronze, self-learner badge ;)
I am having this issue with Python Tkinter. I am trying to make a user interface form screen which requires the user to enter values into entry box's displayed on screen. I have set it so the two Entry Box's are in the same class (that class being the interface screen). The problem is that while I type into one of the box's, the text which I type not only displays in the box in which I am typing into, but also in the other box.
Below is the code in question.
class GenericSkeleton: # The template for all the screens in the program
def __init__(self):
self.GenericGui = Tk()
self.GenericGui.title('Radial Arc Calculator')
self.GenericGui.geometry('360x540')
self.GenericGui.resizable(width = FALSE, height = FALSE)
Label(self.GenericGui,text = 'Radial Arc Calculator',font = ('Ariel',18)).place(x=65,y=35)
def destroy(self):
self.GenericGui.destroy()
class InputScreen(GenericSkeleton):
def __init__(self):
GenericSkeleton.__init__(self)
Button(self.GenericGui,text = 'CALCULATE',height = 1, width = 25, command = calculate, font = ('TkDefaultFont',14)).place(x=37,y=400)
Button(self.GenericGui,text = 'CLOSE',height = 1, width = 11, command = close, font = ('TkDefaultFont',14)).place(x=37, y=450)
Button(self.GenericGui,text = 'HELP', height = 1, width = 11, command = DisplayHelp, font = ('TkDefaultFont',14)).place(x=190, y=450)
Label(self.GenericGui,text = 'Enter Radius (mm):', font = ('TkDefaultFont',14)).place(x=37, y=180)
Label(self.GenericGui,text = 'Enter point distance (mm):', font = ('TkDefaultFont',14)).place(x=37, y=250)
Entry(self.GenericGui,textvariable = Radius, width = 10, font = ('TkDefaultFont',14)).place(x=210, y=180)
Entry(self.GenericGui,textvariable = Distance, width = 5, font = ('TkDefaultFont',14)).place(x=265, y=250)
run = InputScreen()
The entry box's are at the bottom of the code, I hope its enough/not too much to solve the problem.
The problem is that they both share the same textvariable (you use different variable names, but they have the same value which makes them the same in the eyes of tkinter). My advice is to not use the textvariable attribute. You don't need it.
However, if you remove the use of textvariable then you need to separate your widget creation from widget layout so that you can keep a reference to the widget. Then you can use the get method on the widget (rather than on the variable) to get the value:
self.entry1 = Entry(...)
self.entry2 = Entry(...)
self.entry1.place(...)
self.entry2.place(...)
Later, you can get the values like this:
radius = int(self.entry1.get())
distance = int(self.entry2.get())
If you do need the textvariable (usually only if you're using the trace feature of a tkinter variable), you must use a tkinter variable (StringVar, IntVar, etc) rather than a regular variable.
I'm building a program that needs to display a large amount of text, and I need to have a scrollbar attached to a Text widget.
I'm using Windows 7 , python 3.3...
Here's a small(est possible) example of what I'm working with. I feel like I'm missing something really obvious here and this is driving me **ing bonkers.
import datetime
import tkinter as tk
import tkinter.messagebox as tkm
import sqlite3 as lite
class EntriesDisplayArea(tk.Text):
"""
Display area for the ViewAllEntriesInDatabaseWindow
"""
def __init__(self,parent):
tk.Text.__init__(self, parent,
borderwidth = 3,
height = 500,
width = 85,
wrap = tk.WORD)
self.parent = parent
class EntriesDisplayFrame(tk.Frame):
"""
Containing frame for the text DisplayArea
"""
def __init__(self, parent):
tk.Frame.__init__(self, parent, relief = tk.SUNKEN,
width = 200,
borderwidth = 2)
self.parent = parent
self.grid(row = 0, column = 0)
self.entriesDisplayArea = EntriesDisplayArea(self)
self.entriesDisplayArea.grid(row = 1, column = 0, sticky = 'ns')
self.scrollVertical = tk.Scrollbar(self, orient = tk.VERTICAL,
command = self.entriesDisplayArea.yview)
self.entriesDisplayArea.config(yscrollcommand = self.scrollVertical.set)
for i in range(1000):
self.entriesDisplayArea.insert(tk.END,'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf')
self.scrollVertical.grid(row=1,column=1,sticky = 'ns')
class ViewAllEntriesInDatabaseWindow(tk.Toplevel):
"""
Window in which the user can view all of the entries entered ever
entered into the database.
"""
def __init__(self, parent = None):
tk.Toplevel.__init__(self,parent,
height = 400,
width = 400)
self.grid()
self.entriesDisplayFrame = EntriesDisplayFrame(self)
if __name__ == '__main__':
t0 = ViewAllEntriesInDatabaseWindow(None)
I think your problem exists because of two issues with your code. One, you're setting the height of the text widget to 500. That value represents characters rather than pixels, so you're setting it to a few thousand pixels tall. Second, you are only ever inserting a single line of text, albeit one that is 40,000 characters long. If you set the height to something more sane, such as 50 rather than 500, and insert line breaks in the data you're inserting, you'll see your scrollbar start to behave properly.
On an unrelated note, the call to self.grid() in the __init__ method of ViewAllEntriesInDatabaseWindow is completely useless. You can't pack, place or grid toplevel widgets into other widgets.
Finally, I recommend you do not have any class constructor call grid (or pack, or place) on itself -- this will make your code hard to maintain over time. When a widget is created, the parent widget should be responsible for calling grid, pack or place. Otherwise, if you decide to reorganize a window, you'll have to edit every child widget.