Tkinter.text - 如何计算动态字符串的高度?

时间:2017-08-02 20:15:38

标签: python python-3.x tkinter

我有一个Text小部件,其中包含一个包含\n个字符(多行)的自定义字符串。

小部件位于垂直panedwindow内,我想调整panedwindow的窗框以在Text窗口小部件中显示整个字符串。

字符串本质上是动态的(这意味着,我的应用程序中的其他方法正在更新它)。

由于Text窗口小部件配置了wrap='word',如何计算字符串高度(以像素为单位)来相应调整窗框?

在将字符串加载到窗口小部件后,我尝试使用text.dlineInfo('end -1c')[1] + text.dlineinfo('end -1c')[3](对于行的y坐标+高度)。问题是如果最后一行不可见,则dlineinfo返回none

我也尝试使用Font.measure例程,但这不包括Text小部件的换行方面。

这是一个最小的,完整的,可验证的例子:

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

2 个答案:

答案 0 :(得分:1)

我不知道任何内置方法可以在tkinter Text中返回总行数( 包括换行 )窗口小部件。

但是,您可以通过将“文本”窗口小部件中未断开的字符串的长度与“文本”窗口小部件的精确宽度(减去填充)进行比较来手动计算此数字。这就是下面LineCounter类的作用:

Screenshot

# 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:])

使用示例

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

在您的具体情况中,ScrolledText中包装文字的高度可以在update_text()中确定,如下所示:

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 
        ...

答案 1 :(得分:0)

您知道文本中的行数。当 dlineinfo 返回 None 时,您可以判断一行何时离开滚动区域。因此,检查每一行并“查看”它,以确保在对其运行 dlineinfo() 调用之前它是可见的。然后将它们全部加起来,这就是所有线条以当前宽度显示所需的最小新高度。从一行的bbox的高度和行中最大字体的高度,您可以确定该行是否被换行,如果是,多少次,如果您关心的话。诀窍是然后使用 paneconfig() 修改窗格窗口的高度。即使子窗口会正常自动调整大小,窗格窗口也不会。必须通过 paneconfig() 调用告诉它调整大小。

如果您在测量前“看到”每一行,您将获得所有测量值。 “看到”每一行应该没什么大不了的,因为无论如何你都打算在最后展示它们。