Tkinter Canvas scroll slow rendering

Viewed 297

The canvas Widget in Tkinter is very slow at drawing, causing a lot of distortion to the applications visuals when scrolling even when using limited widgets.

I have had a search around but only seem to have answers from people drawing multiple things to a canvas rather than the scrollbar effects.

Is there any issues with my code that would cause this issue or are there any methods to fix the draw times to be more visually smooth. In the application this is meant for each row is a different colour which can make it extremely ugly to look at and hard to find the data the user is looking for.

MVCE:

#python 3.8.6
from tkinter import *
import random
class test:
    def __init__(self):
        self.words = ["troop","relieve","exact","appeal","shortage","familiar","comfortable","sniff","mold","clay","rack","square","color","book","velvet","address","elaborate","grip","neutral","pupil"]
    def scrollable_area2(self, holder):
        base_frame = Frame(holder, padx=5, pady=5)
        base_frame.pack(fill=BOTH, expand=1)
        base_frame.rowconfigure(0, weight=0) 
        base_frame.columnconfigure(0, weight=1)
        can = Canvas(base_frame, bg="white")
        can.pack(side=LEFT, expand=1, fill=BOTH)
        scrollArea = Frame(base_frame, bg="white", )
        scrollArea.pack(side=LEFT, expand=1, fill=BOTH)
        can.create_window(0, 0, window=scrollArea, anchor='nw')
        Scroll = Scrollbar(base_frame, orient=VERTICAL)
        Scroll.config(command=can.yview)
        Scroll.pack(side=RIGHT, fill=Y)
        can.config(yscrollcommand=Scroll.set)
        scrollArea.bind("<Configure>", lambda e=Event(), c=can: self.update_scrollregion(e, c))
        return scrollArea, can
    def update_scrollregion(self, event, can):
        if can.winfo_exists():
            can.configure(scrollregion=can.bbox("all"))
    def generate(self, count): #generates the rows
        for i in range(int(count.get())):
            row = Frame(self.holder)
            row.pack(side=TOP)
            for i in range(9):
                a = Label(row, text=self.words[random.randint(0, len(self.words)-1)])
                a.pack(side=LEFT)
            b = Button(row, text=self.words[random.randint(0, len(self.words)-1)])
            b.pack(side=LEFT)
    def main(self):
        opts = Frame(self.root)
        opts.pack(side=TOP)
        v= StringVar()
        e = Entry(opts, textvariable=v)
        e.pack(side=LEFT)
        b=Button(opts, text="Run", command=lambda e=Event(), v=v:self.generate(v))
        b.pack(side=LEFT)
        main_frame=Frame(self.root)
        main_frame.pack(side=TOP, fill=BOTH, expand=1)
        self.holder, can = self.scrollable_area2(main_frame)
    def run(self):
        self.root = Tk()
        self.main()
        self.root.mainloop()
if __name__ == "__main__":
    app = test()
    app.run()

I have left a box where you can type the number of rows. I have tried from 30 rows to over 300 rows and although the initial render time changes the scroll issue is always the same.

NOTE: sorry about the weird way I am creating a scroll region, its from a more complex piece of code which I have modified to fit here if that ends up being a factor.

1 Answers

Since you are just creating a vertical stack of frames, it will likely be more efficient to use a text widget as the container rather than a canvas and embedded frame.

Here's a simple example that creates 1000 rows similar to how you're doing it with the canvas. On my OSX machine it performs much better than the canvas.

def scrollable_area2(self, parent):
    base_frame = Frame(parent, padx=5, pady=5)
    base_frame.pack(fill="both", expand=True)

    holder = Text(base_frame)
    vsb = Scrollbar(base_frame, orient="vertical", command=holder.yview)
    holder.configure(yscrollcommand=vsb.set)
    holder.pack(side="left", fill="both", expand=True)
    vsb.pack(side="right", fill="y")
    return holder
...
def generate(self, count): #generates the rows
    for i in range(int(count.get())):
        row = Frame(self.holder)
        self.holder.window_create("end", window=row)
        self.holder.insert("end", "\n")
        ...
def main(self):
    ...
    self.holder = self.scrollable_area2(main_frame)

The above example keeps the inner frames, but you don't really need it. You can insert the text directly in the text widget, making the code even more efficient.

In a comment you said you aren't actually creating a stack of frames but rather a table of values. You can create a table in the text widget by using tabstops to create columns. By inserting text directly in the widget you're creating far fewer widgets which will definitely improve performance.

Here's an example using hard-coded the tabstops, but you could easily compute them based on the longest word in the list.

def scrollable_area2(self, parent):
    base_frame = Frame(parent, padx=5, pady=5)
    base_frame.pack(fill="both", expand=True)

    self.holder = Text(base_frame, wrap="none", tabs=100)
    vsb = Scrollbar(base_frame, orient="vertical", command=self.holder.yview)
    self.holder.configure(yscrollcommand=vsb.set)
    self.holder.pack(side="left", fill="both", expand=True)
    vsb.pack(side="right", fill="y")

Your generate function then might look something like this:

def generate(self, count): #generates the rows
    for i in range(int(count.get())):
        for i in range(9):
            text = "\t".join([random.choice(self.words) for x in range(9)])
            self.holder.insert("end", text + "\t")
            button = Button(self.holder, text=random.choice(self.words))
            self.holder.window_create("end", window=button)
            self.holder.insert("end", "\n")

screenshot

Related