tkinter 中变量 window 大小的高效 text-wrapping

Efficient text-wrapping for variable window sizes in tkinter

我正在制作一个输出大量可滚动文本的程序。我希望文本根据 window 大小自动换行,并且能够通过配置和自动调整它的事件函数来实现。本来,用户需要点击一个按钮来切换页面来显示结果,是这样的:

import tkinter as tk
class textwrap(tk.Frame):
    def __init__(self):
        tk.Frame.__init__(self)
        self.grid()
        self.page = 0
        self.results = []
        self.go = tk.Button(self, text = "Go", command = self.run)
        self.go.grid()
        self.swap_button = tk.Button(self, text = "Change page", command = self.swap)
        self.swap_button.grid()

    def run(self):
        for x in range(1000):
            self.results.append('abcdefg abcdefg')
    def swap(self):
        if self.page == 0:
            self.go.destroy()
            self.page = 1
            self.results_label = tk.Label(self, text = self.results, wraplength = self.master.winfo_width())
            self.results_label.grid()
        else:
            self.results_label.destroy()
            self.page = 0
            self.go = tk.Button(self, text = "Go", command = self.run)
            self.go.grid(row = 0)
    
frame01 = textwrap()
frame01.mainloop()

我在这里排除了 auto-wrapping 和滚动条。这个模型的问题很快就会变得明显,就是输入过多的文本最终会导致程序在尝试显示数据时死机。我尝试使用 ScrolledText 框(内置滚动条的文本字段)找到解决方法,将其存储在 ttk 笔记本的第二个选项卡中,并像这样实时写入它:

import tkinter as tk
from tkinter import scrolledtext
class textwrap2(tk.Frame):
    def __init__(self):
        tk.Frame.__init__(self)
        self.grid()
        self.text_field = scrolledtext.ScrolledText(state = 'disabled')
        self.text_field.grid()
        self.go = tk.Button(self, text = "Go", command = self.run)
        self.go.grid()

    def run(self):
        self.text_field.config(state='normal')
        for x in range(1000):
            self.text_field.insert(tk.END, x)
        self.text_field.config(state='disabled')
    
frame01 = textwrap2()
frame01.mainloop()

虽然这没有打开时的初始延迟,但在添加大量数据后整个程序开始变慢,并且滚动条非常延迟,甚至比之前成功加载等量数据时还要糟糕版本。

我要问的实际上是:是否有任何有效的方法来创建带有滚动条的大尺寸包装文本? 试图加载一个非常大的列表一旦冻结程序,并在创建时实时加载它会导致程序作为一个整体变慢,因为它变得更大。我考虑过用迭代创建的无数标签实时加载它,但不确定如何将它们包装为一个集体。另一个关于滚动条滞后的问题建议在一定距离后自动插入换行符,而不是使用自动换行。问题是,如果 window 被调整过大小,换行将不再准确,并且在那之后没有简单的方法来修复它。 Windows 上的简单记事本应用程序让 child 解决了这个问题,所以我想知道是否有一个明显的解决方案在逃避我。

当文本行很长时,文本小部件的性能不佳。来自 tk 工具包生命早期写的一篇论文(写于 1996 年左右):

Alas, very long lines are also slow in Tk text widgets, (...) . Tk uses a b-tree to represent the contents of the widget, but the leaves of the b-tree are lines (everything between two newline characters), so Tk has to do linear searches through lines to find anything.

解决方法成功(虽然它在我原来的程序中引入了新问题,但这超出了这个问题的范围)。使用标签滚动比使用文本字段滚动要流畅得多,所以我又回到了那个。一次加载过大的标签会导致问题,所以我通过自动将输入分成一定大小的单独标签来避免这种情况。虽然这确实会在这些地方造成不完美的换行,但标签尺寸足够大,总体上不会造成太大问题。代码将是这样的:

import tkinter as tk
class textwrap3(tk.Frame):
    def __init__(self):
        tk.Frame.__init__(self)
        self.pack()
        self.master.geometry("1280x720")
        self.master.update_idletasks() 
        self.inputs_list = []
        self.starting_thousand = 0
        self.ending_thousand = 7000
        
#Creates the canvas with frame to hold data, and vertical scrollbar.
#This part isn't the solution, but is required to visualize how the solution works.
        self.results_canvas = tk.Canvas(self, width = self.master.winfo_width()-20, height = self.master.winfo_height()-30)
        self.results_canvas.grid(sticky='new')
        self.scroll = tk.Scrollbar(self, orient = 'vertical', command = self.results_canvas.yview)
        self.results_canvas.configure(yscrollcommand=self.scroll.set)
        self.results_frame = tk.Frame(self.results_canvas)
        self.results_canvas.create_window((0,0), window = self.results_frame, anchor = 'n')
        self.results_frame.bind("<Configure>", lambda e: self.results_canvas.configure
                               (scrollregion = self.results_canvas.bbox("all")))
        self.scroll.grid(row = 0, column = 1, sticky = 'nse')
        self.current_thousand_label = tk.Label(self.results_frame, wraplength = self.master.winfo_width()-20)
        self.current_thousand_label.grid()

        self.go = tk.Button(self, text = "Go", command = self.run)
        self.go.grid()
        self.bind('<Configure>', self.wrap)
#Automaticlly wraps the text
    def wrap(self, event):
        self.results_canvas.config(width = self.master.winfo_width()-20, height = self.master.winfo_height() - 30)
        self.results_frame.config(width = self.master.winfo_width()-20)
        for widget in self.results_frame.winfo_children():
                widget.config(wraplength=self.master.winfo_width()-20)

    def run(self):
        for x in range(99700,100300):
            self.inputs_list.append(x)
        self.thousand_wrap()
        self.current_thousand_label.config(text = self.inputs_list[self.starting_thousand:])
#The workaround itself
    def thousand_wrap(self):
        if len(self.inputs_list) >= self.ending_thousand:
            self.current_thousand_label.destroy()
            self.previous_thousand_labels = tk.Label(self.results_frame,
                            text = self.inputs_list[self.starting_thousand:self.ending_thousand],
                            wraplength = self.master.winfo_width()-20)
            self.previous_thousand_labels.grid()
            self.starting_thousand += 7000
            self.ending_thousand += 7000
            self.current_thousand_label = tk.Label(self.results_frame, wraplength = self.master.winfo_width()-20)
            self.current_thousand_label.grid()
         
frame01 = textwrap3()
frame01.mainloop()

达到一定数量的大标签后,文本将被截断。我认为这与 tkinter 画布的固有大小限制有关,但这超出了我最初问题的范围,我相信这 proof-of-concept 显示了至少可以显示多少数据而不会出现大量程序延迟。您可以更改 self.ending_thousand 以及 thousand_wrap() 下开始和结束千位的添加,以更改每个标签的大小。