Skip to content Skip to sidebar Skip to footer

Tkinter.text - How To Calculate The Height Of A Dynamic String?

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

Solution 1:

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:

Screenshot

# python 2.x# from tkFont import Font# python 3.xfrom tkinter.font import Font

classLineCounter():
    def__init__(self):
        """" This class can count the total number of lines (including wrapped
        lines) in a tkinter Text() widget """defcount_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":
            returnlen(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") - 1for line in textLines:
                totalLines_count += self.count_nb_wrapped_lines_in_string(line,
                                                    maxLineWidth_px, font, wrap)
            return totalLines_count

    defcount_nb_wrapped_lines_in_string(self, string, maxLineWidth_px, font, wrap):
        wrappedLines_count = 1
        thereAreCharsLeftForWrapping = font.measure(string) >= maxLineWidth_px
        while thereAreCharsLeftForWrapping:
            wrappedLines_count += 1if 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

    defremove_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
        whilenot wrapLine_isFull:
            nCharsToWrap += 1
            wrapLine_isFull = font.measure(string[:nCharsToWrap]) >= maxLineWidth_px
        return string[nCharsToWrap-1:]

    defremove_wrapped_words_from_string(self, string, maxLineWidth_px, font):
        words = string.split(" ")
        nWordsToWrap = 0
        wrapLine_isFull = font.measure(" ".join(words[:nWordsToWrap])) >= maxLineWidth_px
        whilenot 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 modereturn 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()
...
classGUI():
    ...
    defupdate_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 
        ...

Solution 2:

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.

Post a Comment for "Tkinter.text - How To Calculate The Height Of A Dynamic String?"