0

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.

Programer Beginner
  • 1,377
  • 6
  • 21
  • 47
  • 1
    What's the "middle" of infinity? – martineau Dec 21 '18 at 23:55
  • oh I meant that when initializing the widget, the scrollbar would be in the middle, so the user can scroll to the left and right (infinitely). If the scrollbar started normally on the left, the user would not be able to scroll to the past (left). – Programer Beginner Dec 21 '18 at 23:57
  • @martineau if we take `inf/2` we get a value of `inf`. This also works for `inf/8754339` as well. It seems that infinity is kind of big. I want to know how big, so I am counting to infinity. I will comment when I finish and tell you how big it is. – Eb946207 Dec 21 '18 at 23:57
  • @EthanK: OK—but I won't be holding my breath. `;¬)` – martineau Dec 22 '18 at 00:04
  • @martineau one quadrillion and forty-seven, one quadrillion and forty-eight, one quadrillion and forty-nine.... *(I must be close)* – Eb946207 Dec 22 '18 at 00:07
  • @martineau I am sorry, I did not understand what do you mean by _converting scroll indicator_. I don't see how can **infinite scrolling** not be able to work on Tkinter. – Programer Beginner Dec 22 '18 at 00:12
  • Beginner: Sorry my previous comment got cut off before I was finished. I don't think this can be done because to scroll something proportionally you have to be able to scale the scrollbar control's movements over the size of the whole item, and in this case there's no such thing. You could do something by constraining it to +/- `X` days, months, years, or whatever from some given point-in-time (like the present). – martineau Dec 22 '18 at 00:14
  • @martineau well, wouldn't scaling the scrollbar as the user go be a solution to that issue? I think apps with infinite-scrolls are doing that: as the user scroll closer to the edge, quickly add more content and re-scale the scrollbar right? – Programer Beginner Dec 22 '18 at 00:17
  • Beginner: Scale it to what? That's the problem. Maybe you could use a `tkinter.Spinbox` instead. Here's a [question](https://stackoverflow.com/questions/30447170/tkinter-column-getting-stretched-when-using-grid-geometry) I found that contains a graphic showing what they look like—there's a group of six of them over on the upper right side of the first illustration. You're going to have to "think outside the scrollbox" to solve this GUI design issue. – martineau Dec 22 '18 at 00:27
  • 1
    Please limit your question to a single issue. – Bryan Oakley Dec 22 '18 at 00:27
  • @martineau Maybe scale to the current number of _loaded_ days (aka columns). Initially, it would let's say load 15 days and the 8th day is the date today. The scrollbar is scaled for 15 days. The user will scroll to the left (past), the app would load let's say 5 more days in the past and the scrollbar will scale for 20 days. Wouldn't this work? – Programer Beginner Dec 22 '18 at 00:33
  • @BryanOakley I have limited this post to a single issue: _implementing the **infinite scrolling** effect in tkinter_ . (see edit). Is that ok? – Programer Beginner Dec 22 '18 at 00:37
  • Beginner: I agree there's only one primary issue here. I suppose you could do something like what you said (and keep extending the range in the direction they're scrolling or last scrolled). What might difficult/awkward is determining where the control's indicator should be after a scroll operation completes—having it reset itself back to the "middle" each time seems like it would be little weird (but might be OK regardless). – martineau Dec 22 '18 at 00:39
  • I think I never saw a calendar with this. Usually the GUI has next and previous buttons that act on day, week, month, year. A goto date field is also useful, and so on. What I mean is if you should be doing this in the first place (from a UI perspective). As an aside I don't like infinite scroll in web pages also, but that may be seen as my personal preference only. – progmatico Dec 22 '18 at 16:15
  • @progmatico Well perhaps a number of people (and maybe the majority) does not like infinite scroll but as you said, its depends on the user's preference. I, for example, like infinite scroll from the design POV (even though I know its kinda less efficient). – Programer Beginner Dec 22 '18 at 18:15
  • I found interesting reasoning about infinite scroll or pagination [here](https://ux.stackexchange.com/questions/112957/why-does-google-still-use-paging-instead-of-progressive-loading) and an example using labels to give a location clue [here](https://ux.stackexchange.com/questions/121351/design-patterns-to-indicate-status-on-infinite-scroll-pages). This is more for web UI of course but UI issues don't necessarily depend on the technology. – progmatico Dec 23 '18 at 14:42

1 Answers1

1

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.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Please check my _Edit #2*_. I _somehow_ manage to make it work without recomputing the `scrollregion` (I think it somehow recalculate the `scrollregion` but I don't know even where or how). As for the _proxy for the scrollbar_, I already conveniently had it in my code under `onFrameConfigure(self, event)`. Now I have the issue to add **columns to the left** – Programer Beginner Dec 22 '18 at 16:04
  • @ProgramerBeginner: you say you don't know how the scrollbar reconfigures itself, but you're clearly resetting the `scrollregion`. – Bryan Oakley Dec 22 '18 at 16:49
  • Well, some part of the code was copy-pasted from other posts and I did few _random_ stuff to make it work so yes, the code is probably resetting the `scrollregion`, but no, I don't know how or where in the code. – Programer Beginner Dec 22 '18 at 18:18