Home > database >  Efficient text-wrapping for variable window sizes in tkinter
Efficient text-wrapping for variable window sizes in tkinter

Time:03-03

I am making a program that outputs a large amount of scrollable text. I wanted the text to wrap automatically based on the window size, and was able to do so by configuring and event function that adjusted it automatically. Originally, the user needed to click a button to switch pages to display the results, something like this:

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

I have excluded the auto-wrapping and scrollbar here. The problem with this model, as quickly becomes apparent, is that putting in too much text will eventually cause the program to freeze up when trying to display the data. I tried to find a workaround using a ScrolledText box (Text field with scrollbar built in), storing it in a second tab in a ttk notebook, and writing to it in real time like so:

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

While this lacks the initial lag on open, the entire program begins to slow after a large amount of data is added, and the scrollbar lags terribly, even worse than with an equivalent amount of data that successfully loaded in the previous version.

What I am asking is effectively this: Is there any efficient way to create wrapped text of a large size, with a scrollbar? Trying to load an extremely large list all at once freezes the program, and loading it in real time as it is created causes the program as a whole to slow down the larger it gets. I considered loading it in real time with myriad labels created iteratively, but wasn't sure how to wrap them as a collective. Another question about scrollbar lag suggested inserting newline characters automatically after a certain distance, rather than using automatic wrapping. The problem is that if the window is ever resized, the wrapping will no longer be accurate, and there is no easy way to fix it after that point. The simple Notepad app on Windows makes child's play of this issue, so I was wondering if there was an obvious solution that was evading me.

CodePudding user response:

The text widget doesn't perform well when lines of text are very long. From a paper written very early in the life of the tk toolkit (written around 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.

CodePudding user response:

The workaround was successful (though it introduced new problems in my original program, but that's beyond the scope of this question). Scrolling with labels was much smoother than with the text field, so I went back to that. Loading an excessively large label all at once caused problems, so I avoided this by automatically breaking the input into separate labels of a certain size. While this does cause imperfect line breaks in those places, the labels sizes are large enough that this doesn't cause much of an issue overall. The code would be something like this:

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

After reaching a certain number of large labels, the text will get cut off. In the tests on my original program, it would cut off after about ten labels, regardless of size, but that doesn't seem to consistently happen here. I do not know why this happens, and will certainly be trying to find out soon, but that is beyond the scope of my original question, and I believe this proof-of-concept shows how large amounts of data can be displayed at least without massive program lag. You can change self.ending_thousand and the additions to starting and ending thousand under thousand_wrap() to alter the size of each label.

  • Related