Felipe Martins
Felipe Martins

Reputation: 220

Trying to change a scrolled canvas width with mouse wheel

I'm trying to control multiple canvases widths with the mouse wheel. What I have so far is this:

import tkinter as tk

class App(tk.Frame):
    row_amount = 3

    def __init__(self, root):
        super(App, self).__init__(root)
        self.root = root

        self.main_frame = tk.Frame(root)
        self.main_frame.pack(expand=True, fill=tk.BOTH)

        self.row_collection = RowCollection(root, self.main_frame)
        for i in range(App.row_amount): self.row_collection.row()

        window_height = App.row_amount * 100
        window_width = root.winfo_screenwidth() - 30
        root.geometry(f'{window_width}x{window_height}+0+0')

        self.row_collection.right_frame.grid_columnconfigure(0, weight=1)
        self.row_collection.left_frame.grid_columnconfigure(0, weight=1)
        self.pack()


class RowCollection:
    """Collection of rows"""

    def __init__(self, root, frame):
        self.row_list = []
        self.root = root
        self.frame = frame
        self.right_frame = tk.Frame(self.frame, bg='red')
        self.right_frame.pack(side=tk.RIGHT, expand=tk.YES, fill=tk.BOTH)
        self.left_frame = tk.Frame(self.frame)
        self.left_frame.pack(side=tk.LEFT, fill=tk.Y)

        self.scrollbar = tk.Scrollbar(self.right_frame, orient=tk.HORIZONTAL)
        self.scrollbar.config(command=self.scroll_x)

    def row(self):
        row = Row(self)
        self.row_list.append(row)

        return row

    def scroll_x(self, *args):
        for row in self.row_list:
            row.canvas.xview(*args)

    def zoomer(self, event=None):
        print('zooming')
        for row in self.row_list:
            scale_factor = 0.1
            curr_width = row.canvas.winfo_reqwidth()
            print(f'event delta={event.delta}')
            if event.delta > 0:
                row.canvas.config(width=curr_width * (1 + scale_factor))
            elif event.delta < 0:
                row.canvas.config(width=curr_width * (1 - scale_factor))
            row.canvas.configure(scrollregion=row.canvas.bbox('all'))



class Row:
    """Every row consists of a label on the left side and a canvas with a line on the right side"""

    row_count = 0
    label_width = 15
    line_weight = 3
    line_yoffset = 3
    padx = 20

    def __init__(self, collection):
        self.frame = collection.frame
        self.root = collection.root
        self.collection = collection
        self.canvas = None
        self.label = None
        self.text = f'Canvas {Row.row_count}'
        self.height = 100

        self.root.update()

        self.label = tk.Label(self.collection.left_frame,
                              text=self.text,
                              height=1,
                              width=Row.label_width,
                              relief='raised')
        self.label.grid(row=Row.row_count, column=0, sticky='ns')

        # configure row size to match future canvas height
        self.collection.left_frame.grid_rowconfigure(Row.row_count, minsize=self.height)

        self.root.update()

        self.canvas = tk.Canvas(self.collection.right_frame,
                             width=10000,
                             height=self.height,
                             bg='white',
                             highlightthickness=0)

        self.canvas.grid(row=Row.row_count, column=0, sticky=tk.W)

        self.root.update()

        # draw line
        self.line = self.canvas.create_rectangle(self.padx,
                                                 self.canvas.winfo_height() - Row.line_yoffset,
                                                 self.canvas.winfo_width() - self.padx,
                                                 self.canvas.winfo_height() - Row.line_yoffset + Row.line_weight,
                                                 fill='#000000', width=0, tags='line')

        # config canvas
        self.canvas.config(scrollregion=self.canvas.bbox('all'))
        self.canvas.config(xscrollcommand=self.collection.scrollbar.set)
        self.canvas.bind('<Configure>', lambda event: self.canvas.configure(scrollregion=self.canvas.bbox('all')))
        self.canvas.bind('<MouseWheel>', self.collection.zoomer)

        # Create point at canvas edge to prevent scrolling from removing padding
        self.bounding_point = self.canvas.create_rectangle(0, 0, 0, 0, width=0)
        self.bounding_point = self.canvas.create_rectangle(self.canvas.winfo_width(), self.canvas.winfo_width(),
                                                           self.canvas.winfo_width(), self.canvas.winfo_width(),
                                                           width=0)

        Row.row_count += 1

        self.collection.scrollbar.grid(row=Row.row_count, column=0, sticky='ew')


if __name__ == '__main__':

    root = tk.Tk()
    app = App(root)
    root.mainloop()

The canvases themselves are inside right_frame, and the number of canvases is given by row_amount. The left_frame contains labels for each of the canvases. The canvases should be allowed to be pretty wide, so I initially set a width value of 10000. Because of that, they start partially visible, with the rest being accessible via a scrollbar.

What I would like is for the mouse wheel to control the size of the canvas as a whole (that is, both what is currently visible and what could be viewed using the scrollbar), similar to what would happen in an audio or video editing software timeline.

Right now, when I use the mouse wheel, what seems to get resized is not the whole canvas, but only the 'visible' portion. Resize it to be small enough and you can start to see it's frame background on the right portion of the window.

What am I missing here?

Upvotes: 0

Views: 200

Answers (1)

Bryan Oakley
Bryan Oakley

Reputation: 386240

What am I missing here?

I think what you're missing is that the drawable area of the canvas is not at all related to the physical size of the canvas widget. You do not need to resize the canvas once it has been created. You can draw well past the borders of the widget.

If you want to be able to scroll elements into view that are not part of the visible canvas, you must configure the scrollregion to define the area of the virtual canvas that should be visible.

You said in a comment you're trying to create a timeline. Here's an example of a canvas widget that "grows" by adding a tickmark every second. Notice that the canvas is only 500,100, but the drawable area gets extended every second.

import tkinter as tk

root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=100, bg="black")

vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
hsb = tk.Scrollbar(root, orient="horizontal", command=canvas.xview)
canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
canvas.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")

root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)

counter = 0
def add_tick():
    global counter

    # get the current state of the scrollbar. We'll use this later
    # to determine if we should auto-scroll
    xview = canvas.xview()

    # draw a new tickmark
    counter += 1
    x = counter * 50
    canvas.create_text(x, 52, anchor="n", text=counter, fill="white")
    canvas.create_line(x, 40, x, 50, width=3, fill="red")

    # update the scrollable region to include the new tickmark
    canvas.configure(scrollregion=canvas.bbox("all"))

    # autoscroll, only if the widget was already scrolled
    # as far to the right as possible
    if int(xview[1]) == 1:
        canvas.xview_moveto(1.0)

    canvas.after(1000, add_tick)


add_tick()
root.mainloop()

screenshot

Upvotes: 1

Related Questions