python 如何计算动态字符串的高度?

dhxwm5r4  于 6个月前  发布在  Python
关注(0)|答案(2)|浏览(57)

我有一个Text小部件,它保存一个包含\n字符(多行)的自定义字符串。
小部件放置在一个垂直的panedwindow中,我想调整panedwindow的窗扇以显示Text小部件中的整个字符串。
字符串本质上是动态的(这意味着,它正在由应用程序中的其他方法更新)。
由于Text小部件是用wrap='word'配置的,我如何计算字符串的像素高度来相应地调整窗扇?
在字符串加载到小部件后,我尝试使用text.dlineInfo('end -1c')[1] + text.dlineinfo('end -1c')[3](表示行的y坐标+高度)。问题是,如果最后一行不可见,那么dlineinfo将返回none
我还尝试使用Font.measure例程,但这不包括Text小部件的 Package 方面。
下面是一个最小的,完整的,可验证的例子:

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

字符串

jfgube3f

jfgube3f1#

我不知道有任何内置方法可以返回tkinter Text小部件中的总行数(* 包括换行 *)。
但是,您可以手动计算这个数字,方法是将文本小部件中未断开字符串的长度与文本小部件的精确宽度(减去填充)进行比较。下面的LineCounter类就是这样做的:


的数据

# 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 = textWidget.cget("wrap")
        if wrap == "none":
            return len(textLines)
        else:
            # Get Text widget font
            font = Font(textWidget, font=textWidget.cget("font"))
            totalLines_count = 0
            maxLineWidth_px = textWidget.winfo_width() - 2*textWidget.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 
        ...

nx7onnlm

nx7onnlm2#

你知道文本中的行数。当dlineinfo返回None时,你可以判断出一行何时离开了滚动区域。所以在运行dlineinfo()调用之前,遍历每一行并“看到”它,以确保它是可见的。然后将它们相加,这是所有行都以当前宽度显示所需的最小新高度。根据行的bbox高度和行中最大字体的高度,如果你关心的话,你可以确定行是否被换行,如果是的话,多少次。诀窍是然后使用paneconfig()来修改窗格窗口的 height。即使子窗口会自动调整大小,窗格窗口也不会。它必须通过paneconfig()调用来告诉它调整大小。
如果你在测量之前“看到”每一行,你就会得到所有的测量结果。“看到”每一行不应该是一个大问题,因为你打算在最后显示它们。

相关问题