Tkinter | Custom widget: Infinite (horizontal) scr

2019-03-06 09:17发布

CONTEXT

I am trying to create a custom calendar (Tkinter widget) that has these following attributes/features:

  • Each column represents 1 day
  • Each row will represent a person
  • It has infinite horizontal scrolling -> so it can go infinitely to the past and present
  • the cells (boxes) in the calendar are going to be interactive in a way that if I hold down Left-Mouse-Button, I can select specific cells for further operation/function

It would look something like this:

sketch of how the widget should look like

(Note to the image: the RED column on the left is will be a separate frame-widget. I just included in the sketch to show the purpose of the rows in the calendar)


MY PLAN

To be able to interact with the cells after building the calendar, I am planning to have each widget object stored in an array called self.cells.

Also since I want it to go infinitely to the past and future, at initialization, the calendar scrollbar will be in the middle.


MY PROGRESS (CODE)

import tkinter as tk
import datetime

CELL_SIZE = (100, 50)
FIRST_ROW_HEIGHT = 20

class Cell(tk.Canvas):
    def __init__(self, root,
        width = CELL_SIZE[0],
        height = CELL_SIZE[1],
        highlightthickness = 1,
        background = 'white',
        highlightbackground = 'black',
        highlightcolor = 'red',
        *args, **kwargs):

        tk.Canvas.__init__(self, root,
            width = width,
            height = height,
            background = background,
            highlightthickness = highlightthickness,
            highlightbackground = highlightbackground,
            highlightcolor = highlightcolor,
            *args, **kwargs)

class Calendar(tk.Frame):
    def __init__(self, root, rows=0, *args, **kwargs):
        tk.Frame.__init__(self, root, *args, **kwargs)

        # create the canvas and frame
        self.calendar_canvas = tk.Canvas(self)
        self.calendar_frame = tk.Frame(self.calendar_canvas)
        self.calendar_canvas.create_window((4,4), window=self.calendar_frame, anchor="nw", tags="self.calendar_frame")
        self.calendar_canvas.pack(side="top", fill="both", expand=True)

        # building scrollbar
        self.scrollbar = tk.Scrollbar(self, orient='horizontal', command=self.calendar_canvas.xview)
        self.scrollbar.pack(side="bottom", fill="x")

        # hooking up scrollbar
        self.calendar_canvas.configure(xscrollcommand=self.scrollbar.set)
        self.calendar_frame.bind("<Configure>", self.onFrameConfigure)

        # variables
        self.rows = rows
        self.cells = []


    def onFrameConfigure(self, event):
        self.calendar_canvas.configure(scrollregion=self.calendar_canvas.bbox("all"))

    def set(self, day=0):
        today = datetime.date.today()

        for i in range(day):
            self.cells.append([])

            # create first row (indicating the date)
            cell = Cell(self.calendar_frame, height=FIRST_ROW_HEIGHT)
            cell.grid(row=0, column=i)

            # add the date label into the first row
            cell.create_text(
                CELL_SIZE[0]/2,
                FIRST_ROW_HEIGHT/2,
                text = (today + datetime.timedelta(days=i)).strftime('%d/%m/%y'))

            for c in range(self.rows):
                cell = Cell(self.calendar_frame)
                cell.grid(row=c+1, column=i)

                self.cells[i].append(cell)

The Calendar is my under-development custom calendar widget. I manage to hook up this spreadsheet-like structure with scrollbar thanks to this answer (from @Ethan Field) which also served as my starting point (base code).

Currently, the Calendar widget is capable of creating n number of days (starting today's day) by Calendar.set() function.

You can try the widget using this code:

root = tk.Tk()
calendar = Calendar(root, rows=3)
calendar.set(day=10)
calendar.pack(fill='both', expand=True)
root.mainloop()

THE ISSUE/QUESTION

How can I implement the infinite scrolling effect? I have no clue how to make it work.


Miscellaneous Notes

  • Python 3, 64-bit
  • Windows 7, 64-bit


EDIT #1 - created addFuture() function

# under class Calendar:
def addFuture(self, day=0):
    today = datetime.date.today()

    for i in range(day):
        index = i + self.lastColumn
        self.cells.append([])

        # create first row (indicating the date)
        cell = Cell(self.calendar_frame, height=FIRST_ROW_HEIGHT)
        cell.grid(row=0, column=index)

        # add the date label into the first row
        cell.create_text(
            CELL_SIZE[0]/2,
            FIRST_ROW_HEIGHT/2,
            text = (today + datetime.timedelta(days=index)).strftime('[%a] %d/%m/%y'))

        for c in range(self.rows):
            cell = Cell(self.calendar_frame)
            cell.grid(row=c+1, column=index)

            self.cells[i].append(cell)

    self.lastColumn = self.lastColumn + day

This addFuture() function is just slightly modified set() function. The addFuture() can be called multiple time and each time it will add day amount of days to the calendar. Just need to hook scrollbar up. However, how should I addPast()?


EDIT #2 - infinite scrolling to the future works!

The onFrameConfigure commands is called whenever the user drags the scrollbar, thus I added if self.scrollbar.get()[1] > 0.9: statement, to check if the x-axis of the scrollbar is getting closer to the rightmost end. If it is, it execute a function to add more days and the scrollbar somehow automatically readjust the scale (I have no clue why but it works).

def onFrameConfigure(self, event):
    self.calendar_canvas.configure(scrollregion=self.calendar_canvas.bbox("all"))
    if self.scrollbar.get()[1] > 0.9:
        self.addFuture(day=10)

Thus, my window has infinite scroll to the future. My question is now how to make it infinitely scroll to the past (aka to the left)?.

I can detect the scrollbar when it nears the left-side by using this statement: if self.scrollbar.get()[1] < 0.1:. However, what I need is some kind of self.addPast() command which serve the same purpose as self.addFuture() command but (as the name imply) add days to the left.

1条回答
The star\"
2楼-- · 2019-03-06 09:53

What you want to do is to make sure that the canvas always has enough columns to scroll one whole screen right or left. The first thing to do is start with that many columns. If you want each column to be 100 pixels, and you want the canvas itself to be 400 pixels, then you need a total of 1200 pixels worth of columns: 4 columns to the left, 4 visible columns, and 4 columns to the right.

Next, create a proxy for the scrollbar -- a custom command that gets called whenever the scrollbar is dragged. The first thing it should do is call the xview method of the canvas to do the actual scrolling. Then, once the canvas has been scrolled you need to calculate if you need to add any more columns to the right or the left to always maintain the buffer.

Once you've added any new columns to the right or left, you need to recompute the scrollregion of the canvas. When you do that, tkinter will automatically adjust the position and size of the scrollbar thumb.

查看更多
登录 后发表回答