ragardner
ragardner

Reputation: 1975

Python 3 tkinter x scroll using arrow very slow

In Python 3.x using a horizontal scrollbar and ttk treeview the x scroll by clicking on the arrows is very very slow, several pixels per second, I don't know how to fix this, here is a minimal example I made:

import tkinter as tk
from tkinter import ttk

app = tk.Tk()
t = ttk.Treeview(app)
t.pack(side="top",fill="both",expand=1)
xscroll = tk.Scrollbar(app,command=t.xview,orient="horizontal")
t.configure(xscrollcommand=xscroll.set)
xscroll.pack(side="top",fill="x")

tcols = ["header " + str(i)
         for i in range(50)]
t.config(columns=tcols)
for h in tcols:
    t.heading(h,text=h)

for i in range(5):
    t.insert("","end",
             text = "item" + str(i),
             values = ["value" + str(x) for x in range(49)])
app.geometry("{}x{}".format(800, 600))

it's the arrow clicking scrolling that is slow, the dragging using the bar is fine

Upvotes: 2

Views: 1578

Answers (2)

CommonSense
CommonSense

Reputation: 4482

Trivia:

All you need to know that xview method of XView "mix-in"-class is just another one callback, that acts differently depending on a user interactions:

  • If the user drags the scrollbar slider, xview is called as xview(self, *('moveto', some_offset))

  • If the user clicks in the trough, xview is called as xview(self, *('scroll', some_step, 'pages')).

  • If the user clicks the arrows, xview is called as xview(self, *('scroll', some_step, 'units')).

As you can see - not so complicated! Your problem lies in fact, that some_step is always either 1 or -1 string value in the last case, so all you need just multiply that value by some multiplier. Easy as pie!

Solution:

As a solution, the most lightweight approach that I see here creates a derived class, that inherits from ttk.Treeview with overrided xview method.

Try this Treeview in your programm:

class CustomTreeview(ttk.Treeview):
    def __init__(self, parent, *args, **kwargs):
        ttk.Treeview.__init__(self, parent, *args, **kwargs)
        self.vanilla_xview = tk.XView.xview

    def xview(self, *args):
        #   here's our multiplier
        multiplier = 100

        if 'units' in args:
            #   units in args - user clicked the arrows
            #   time to build a new args with desired increment
            mock_args = args[:1] + (str(multiplier * int(args[1])),) + args[2:]
            return self.vanilla_xview(self, *mock_args)
        else:
            #   just do default things
            return self.vanilla_xview(self, *args)

Upvotes: 5

Mike - SMT
Mike - SMT

Reputation: 15226

One way we can speed up the scrolling is to create a few functions to help us along and use some bindings.

first we need a variable we can update to monitor if the mouse button is clicked down or released.

scrolling = False

next we need to create a few functions that we can use to start and stop the scrolling as well as creating a looping function that increases the speed of scrolling.

# uses that status of `scrolling` to keep scrolling or to stop for either arrow button
def scrolling_active(arrow, *args):
    global scrolling
    if scrolling == True:
        if arrow == "arrow1":
            t.tk.call(t._w,'xview', 'scroll', -10, 'units')
        if arrow == "arrow2":
            t.tk.call(t._w,'xview', 'scroll', 10, 'units')
        app.after(10, lambda a = arrow: scrolling_active(a))

# sets scrolling to True and activates the looping function to start scrolling
def start_scrolling(event):
    global scrolling
    scrolling = True
    scrolling_active(xscroll.identify(event.x, event.y))

# on mouse release this sets scrolling to false. This stops the loop above
def stop_scrolling(event): 
    global scrolling
    scrolling = False

Now we need to bind the start and stop functions to our scroll widget for mouse click and release.

xscroll.bind("<Button-1>", start_scrolling)
xscroll.bind('<ButtonRelease-1>', stop_scrolling)

With those changes we get arrows that scroll faster when clicked!

You can change the speed up to your liking by editing the int value in:

t.tk.call(t._w,'xview', 'scroll', -10, 'units')
t.tk.call(t._w,'xview', 'scroll', 10, 'units')

Take a look at the below code:

import Tkinter as tk
import ttk

app = tk.Tk()
t = ttk.Treeview(app)
t.pack(side="top",fill="both",expand=1)

scrolling = False
xscroll = tk.Scrollbar(app,command=t.xview,orient="horizontal")
t.configure(xscrollcommand=xscroll.set)
xscroll.pack(side="top",fill="x")

def scrolling_active(arrow, *args):
    global scrolling
    if scrolling == True:
        if arrow == "arrow1":
            t.tk.call(t._w,'xview', 'scroll', -10, 'units')
        if arrow == "arrow2":
            t.tk.call(t._w,'xview', 'scroll', 10, 'units')
        app.after(10, lambda a = arrow: scrolling_active(a))

def start_scrolling(event):
    global scrolling
    scrolling = True
    scrolling_active(xscroll.identify(event.x, event.y))

def stop_scrolling(event):
    global scrolling
    scrolling = False

xscroll.bind("<Button-1>", start_scrolling)
xscroll.bind('<ButtonRelease-1>', stop_scrolling)

tcols = ["header " + str(i)
         for i in range(50)]
t.config(columns=tcols)
for h in tcols:
    t.heading(h,text=h)

for i in range(5):
    t.insert("","end",
             text = "item" + str(i),
             values = ["value" + str(x) for x in range(49)])
app.geometry("{}x{}".format(800, 600))

app.mainloop()

Upvotes: 1

Related Questions