1
\$\begingroup\$

Below is an example on how to scroll two canvasses in two separate frames simultaneously. The trick is on how to pass on *args from the bindings to function onscroll for scrolling and onresize for changing the dimensions of the rectangle using lambda in the commands: Scrollbar(frame3, orient='vertical', command=lambda *args: onscroll('axis', *args)) and Button(frame3, text='reduce ', command=lambda *args: onresize(False, *args)).

Is this the best way to handle this?

import tkinter as tk
from tkinter import Frame, Canvas, Scrollbar, Button

global size_x, size_y, canvas1, canvas2, block1, block2
size_x = size_y = 150
block1 = block2 = ''


def onscroll(axis, *args):
    global canvas1, canvas2
    print(f'axis: {axis} and args are {[*args]}                           ',
          end='\r')
    if axis == 'x-axis':
        canvas1.xview(*args)
        canvas2.xview(*args)
    elif axis == 'y-axis':
        canvas1.yview(*args)
        canvas2.yview(*args)

    else:
        assert False, f"axis {axis} is incorrect, use 'x-axis' or 'y-axis'"


def onresize(enlarge, *args):
    global size_x, size_y
    print(f'enlarge: {enlarge} and args are {[*args]}                    ',
          end='\r')
    if enlarge:
        size_x *= 1.1
        size_y *= 1.1
    else:
        size_x /= 1.1
        size_y /= 1.1


def main():
    global canvas1, canvas2, block1, block2
    root = tk.Tk()
    frame1 = Frame(root, bg='grey')
    frame1.grid(row=0, column=0)
    frame2 = Frame(root, bg='grey')
    frame2.grid(row=0, column=1)
    frame3 = Frame(root, bg='grey')
    frame3.grid(row=0, column=2)

    yscrollbar = Scrollbar(frame3, orient='vertical',
                           command=lambda *args: onscroll('y-axis', *args))
    yscrollbar.pack(side='right', fill='y', expand='yes')
    xscrollbar = Scrollbar(frame3, orient='horizontal',
                           command=lambda *args: onscroll('x-axis', *args))
    xscrollbar.pack(side='bottom', fill='x', expand='yes')

    reduce_button = Button(frame3, text='reduce ',
                           command=lambda *args: onresize(False, *args))
    reduce_button.pack(side='right', anchor='ne')
    enlarge_button = Button(frame3, text='enlarge',
                            command=lambda *args: onresize(True, *args))
    enlarge_button.pack(side='right', anchor='ne')

    canvas1 = Canvas(frame1, width=200, height=200, bg="blue",
                     scrollregion=(0, 0, 500, 500),
                     yscrollcommand=yscrollbar.set,
                     xscrollcommand=xscrollbar.set)
    canvas1.pack(side='right')
    canvas2 = Canvas(frame2, width=200, height=200, bg="yellow",
                     scrollregion=(0, 0, 500, 500),
                     yscrollcommand=yscrollbar.set,
                     xscrollcommand=xscrollbar.set)
    canvas2.pack()

    canvas3 = Canvas(frame3, width=200, height=200, bg='pink')
    canvas3.pack()

    while True:
        canvas1.delete(block1)
        canvas2.delete(block2)
        block1 = canvas1.create_rectangle(100, 100, size_x, size_y,
                                          fill='orange')
        block2 = canvas2.create_rectangle(100, 100, size_x, size_y,
                                          fill='blue')
        root.update()

    root.mainloop()


if __name__ == "__main__":
    main()
\$\endgroup\$
3
\$\begingroup\$

Remove unnecessary imports

You're importing parts of tkinter twice, Remove this line:

from tkinter import Frame, Canvas, Scrollbar, Button

Then, whenever you need to use Frame, Canvas, etc, use tk.Frame, tk.Canvas, etc.

Don't create your own event loop

You have code that looks like this:

while True:
    <some code>
    root.update()

Instead, call mainloop once, move <some code> into a function, and call it with after if you need it to run in a loop.

def redraw():
    <some code>
    root.after(50, redraw)
root.mainloop()

Don't continually delete and recreate items on a canvas

You're continually deleting and recreating identical items on a canvas. This is inefficient and will eventually call tkinter to crash. The canvas widget does not re-use canvas ids, so each time you create a new object you get a new id. There is a limited number of ids that tkinter can manage.

Instead, create the items once, and then if you need to change them, change the existing items

def draw():
    global block1, block2
    block1 = canvas1.create_rectangle(100, 100, size_x, size_y,
                                      fill='orange')
    block2 = canvas2.create_rectangle(100, 100, size_x, size_y,
                                      fill='blue')

def redraw():
    global block1, block2
    canvas1.itemconfigure(block1, ...)
    canvas2.itemconfigure(block2, ...)

Group your layout code together

Intermixing widget creation and widget layout makes the code hard to read.

Consider this:

frame1 = Frame(root, bg='grey')
frame1.grid(row=0, column=0)
frame2 = Frame(root, bg='grey')
frame2.grid(row=0, column=1)
frame3 = Frame(root, bg='grey')
frame3.grid(row=0, column=2)

I recommend splitting this into two blocks:

frame1 = Frame(root, bg='grey')
frame2 = Frame(root, bg='grey')
frame3 = Frame(root, bg='grey')

frame1.grid(row=0, column=0)
frame2.grid(row=0, column=1)
frame3.grid(row=0, column=2)

I think this makes the code much easier to read because I can see at a glance that you're creating three frames, and that each frame is going into a separate column. In the original code it's not nearly as easy to see this without closely reading the code.

Use separate functions for scrolling in x and y direction

Your use of an argument to specify x-axis or y-axis makes the code a little bit harder to understand. Instead, I recommend using two separate functions.

For example:

def scroll_x(*args):
    global canvas1, canvas2
    canvas1.xview(*args)
    canvas2.xview(*args)

def scroll_y(*args):
    global canvas1, canvas2
    canvas1.yview(*args)
    canvas2.yview(*args)
...
yscrollbar = Scrollbar(frame3, orient='vertical', command=scroll_y)
xscrollbar = Scrollbar(frame3, orient='horizontal', command=scroll_x)

Notice how it makes the code much easier to read, since we can replace the complicated lambda with a much simpler command.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the very useful tips. Three questions: 1) as I am changing the dimensions of the rectangle (100, 100, size_x, size_y) I am not sure how to implement this with itemconfigure as it seems only the options can be used as input and not the positional paramters? 2) am I correct that redraw is a recursive function, i.e. the function calls itself? 3) what is a graceful way to exit the redraw function, do I use root.destroy on some condition? \$\endgroup\$ – Bruno Vermeulen Aug 21 '18 at 3:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.