import tkinter as tk
from tkinter import *
from tkinter import ttk
from datetime import datetime
import ttkbootstrap as ttk
from ttkbootstrap import window
import pandas as pd
import calendar
import random
from tkinter import font
import tkinter.messagebox as tkmb
root = ttk.Window(themename="darkly")
root.title("shifts_scheduler")
w_int = root.winfo_screenwidth()
h_int = root.winfo_screenheight()
root.geometry(f"{w_int}x{h_int}")
root.update_idletasks()
root.resizable(True, True)
# Create a canvas and a vertical scrollbar
canvas = ttk.Canvas(root, width=root.winfo_screenwidth(), height=root.winfo_screenheight())
# Create a frame inside the canvas
master_window = ttk.Frame(master=canvas, )
master_window.bind("<Configure>", lambda e: on_frame_configure(canvas))
# Pack the frame into the canvas and center it horizontally
canvas.create_window((root.winfo_screenwidth() // 2, 0), window=master_window, anchor="n")
canvas.grid(row=1, column=1, rowspan=6, padx=0, pady=0)
scrollbar = ttk.Scrollbar(master=root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.grid(column=1, row=0, sticky="ne")
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
canvas.bind("<Configure>", lambda e: on_frame_configure(canvas))
# Track the entries in a dictionary keyed by the row number
max_entries = {}
worker_entries = []
workers_data = []
workers_list = []
def on_frame_configure(canvas):
# Reset the scroll region to encompass the inner frame
canvas.configure(scrollregion=canvas.bbox("all"))
def max_box(mvar, row):
if mvar.get() == 1:
# Create a new Entry widget for the given row
max_entry = ttk.Entry(master=generated_frame, font="Calibri 14")
max_entry.grid(row=row, column=7, padx=10, pady=10)
max_entries[row] = max_entry # Store it in the dictionary
else:
# Destroy the entry if it exists and remove it from the dictionary
if row in max_entries:
max_entries[row].destroy()
del max_entries[row]
def is_weekend(date_str):
date_obj = datetime.strptime(date_str, '%d %m %Y')
day_num = date_obj.weekday() # 0 for Monday, 1 for Tuesday, ..., 6 for Sunday
return day_num >= 5 # Returns True if it's Saturday or Sunday, indicating a weekend
def day_of_week(date_str):
date_obj = datetime.strptime(date_str, '%d %m %Y')
day_num = date_obj.weekday() # 0 for Monday, 1 for Tuesday, ..., 6 for Sunday
return day_num
def get_days_in_month(year_int, month):
return calendar.monthrange(year_int, month)[1]
def generate_workers():
global worker_entries # Declare global at the start of the function
number_workers = workers_int.get()
# Destroy all previous widgets in the generated frame
for widget in generated_frame.winfo_children():
widget.destroy()
# Reset the max_entries dictionary and worker names list
max_entries.clear()
worker_entries.clear() # Clear the list to avoid old entries
workers_data.clear()
workers_list.clear() # Clear the worker data list
# Clear the worker data list
# Generate labels and entries for each worker
tested_label = ttk.Label(master=generated_frame, text="✔", font="Calibri 21")
untested_label = ttk.Label(master=generated_frame, text="❌", font="Calibri 15")
name_label = ttk.Label(master=generated_frame, text="Name", font="Calibri 24")
unavilable_label = ttk.Label(master=generated_frame, text="Unavailable", font="Calibri 24")
avilable_label = ttk.Label(master=generated_frame, text="Available", font="Calibri 24")
maxs_label = ttk.Label(master=generated_frame, text="Max", font="Calibri 24")
tested_label.grid(row=0, padx=10, pady=10, column=1)
untested_label.grid(row=0, padx=10, pady=10, column=2)
name_label.grid(row=0, padx=10, pady=10, column=3)
unavilable_label.grid(row=0, padx=10, pady=10, column=4)
avilable_label.grid(row=0, padx=10, pady=10, column=5)
maxs_label.grid(row=0, padx=10, pady=10, column=6)
for i in range(number_workers):
y_var = tk.IntVar()
n_var = tk.IntVar()
m_var = tk.IntVar()
tested_box = tk.Checkbutton(master=generated_frame, variable=y_var, onvalue=1, offvalue=0)
untested_box = tk.Checkbutton(master=generated_frame, variable=n_var,onvalue=1, offvalue=0)
workers_name = ttk.Entry(master=generated_frame, font="Calibri 14")
unavailable_input = ttk.Entry(master=generated_frame, font="Calibri 14")
available_input = ttk.Entry(master=generated_frame, font="Calibri 14")
maxs_box = tk.Checkbutton(master=generated_frame, variable=m_var, command=lambda mvar=m_var, row=i+1: max_box(mvar, row), onvalue=1, offvalue=0)
tested_box.grid(row=i+1, pady=10, padx=10, column=1)
untested_box.grid(row=i+1, pady=10, padx=10, column=2)
workers_name.grid(row=i+1, pady=10, padx=10, column=3)
unavailable_input.grid(row=i+1, pady=10, padx=10, column=4)
available_input.grid(row=i+1, pady=10, padx=10, column=5)
maxs_box.grid(row=i+1, pady=10, padx=10, column=6)
# Add the worker details to the appropriate lists
workers_list.append({
"name": workers_name,
"available": available_input,
"unavailable": unavailable_input,
"max": max_entries,
"tested": y_var,
"untested": n_var
})
canvas.update_idletasks()
def generate_schedule():
output_window.delete("1.0", "end")
global workers_list
month = month_int.get()
if month > 12 or month < 1:
tkmb.showerror("Error:", f"Month must be beetwen 1 - 12.")
return
days_in_month = get_days_in_month(year_int, month)
worker_info = {}
full_day = {i: None for i in range(1, days_in_month + 1)} # Initialize the schedule for each day
stand_day = {i: None for i in range(1, days_in_month + 1)} # Standby worker assignments
# Gather worker data and store it in worker_info
for i, worker in enumerate(workers_list):
name = worker["name"].get()
available = worker["available"].get()
unavailable = worker["unavailable"].get()
# Get the status from the checkboxes (assuming both checkboxes exist and are mutually exclusive)
tested = worker["tested"].get() # Checkbox for 'tested'
untested = worker["untested"].get() # Checkbox for 'untested'
# Determine if the worker is tested or untested
if tested:
is_tested = True
elif untested:
is_tested = False
else:
tkmb.showerror("Error:", f"Error: Worker '{name}' must be marked as either tested or untested.\n", )
return
# Skip this worker if no valid status is selected
# Convert strings to lists of integers
available_days = [int(d.strip()) for d in available.split(",") if d.strip().isdigit()] if available else []
unavailable_days = [int(d.strip()) for d in unavailable.split(",") if d.strip().isdigit()] if unavailable else []
# Check for conflicting entries between available and unavailable days
conflicting_days = set(available_days).intersection(unavailable_days)
if conflicting_days:
tkmb.showerror("Error", f"Error: Worker '{name}' has conflicting days in available and unavailable inputs: {conflicting_days}\n")
return # Skip this worker in case of an error
# Get the maximum shifts if provided
max_shifts = max_entries.get(i + 1)
max_value = int(max_shifts.get()) if max_shifts and max_shifts.get().isdigit() else None
worker_info[i] = {
"name": name,
"available_days": available_days,
"unavailable_days": unavailable_days,
"tested": is_tested, # Store whether the worker is tested or not
"max": max_value,
"count": 0,
"shifts": 0,
"standby": 0 # Initial shift count
}
# Step 1: Assign workers to available days first (shift assignments)
for day in range(1, days_in_month + 1):
available_workers = [
worker for worker in worker_info.values()
if worker["available_days"] and day in worker["available_days"] and worker["name"] != stand_day[day] and (worker["max"] is None or worker["count"] < worker["max"])
]
if available_workers:
# Sort available workers by shifts and standbys (lowest first)
available_workers.sort(key=lambda w: (w["shifts"]))
selected_worker = available_workers[0] # Choose the worker with the fewest shifts/standbys
full_day[day] = selected_worker["name"]
selected_worker["shifts"] += 1
selected_worker["count"] += 1
for day in range(1, days_in_month + 1):
if full_day[day] is None: # No worker assigned
available_workers = [
worker for worker in worker_info.values()
if not worker["available_days"] and day not in worker["unavailable_days"] and worker["name"] != stand_day[day] and (worker["max"] is None or worker["count"] < worker["max"])
]
if available_workers:
available_workers.sort(key=lambda w: (w["shifts"]))
selected_worker = available_workers[0] # Choose the worker with the fewest shifts
full_day[day] = selected_worker["name"]
selected_worker["shifts"] += 1
selected_worker["count"] += 1
# Step 2: Assign workers to standby roles on available days
for day in range(1, days_in_month + 1):
if not stand_day[day]: # If no standby assigned
shift_worker = full_day[day]
shift_worker_info = next((worker for worker in worker_info.values() if worker["name"] == shift_worker), None)
available_workers = [
worker for worker in worker_info.values()
if worker["available_days"] and day in worker["available_days"] and worker["name"] != full_day[day]
]
# Check if the shift worker is untested, and filter available workers accordingly
if shift_worker_info and not shift_worker_info["tested"]:
available_workers = [worker for worker in available_workers if worker["tested"]]
if available_workers:
# Sort available workers by shifts and standbys (lowest first)
available_workers.sort(key=lambda w: (w["standby"], w["shifts"]))
selected_worker = available_workers[0] # Choose the worker with the fewest standbys
stand_day[day] = selected_worker["name"]
selected_worker["standby"] += 1
selected_worker["count"] += 1
# Step 3: Assign standby workers for unassigned days
for day in range(1, days_in_month + 1):
if stand_day[day] is None:
shift_worker = full_day[day]
shift_worker_info = next((worker for worker in worker_info.values() if worker["name"] == shift_worker), None)
available_workers = [
worker for worker in worker_info.values()
if not worker["available_days"] and day not in worker["unavailable_days"] and worker["name"] != full_day[day]
]
# Check if the shift worker is untested, and filter available workers accordingly
if shift_worker_info and not shift_worker_info["tested"]:
available_workers = [worker for worker in available_workers if worker["tested"]]
if available_workers:
available_workers.sort(key=lambda w: (w["standby"], w["shifts"]))
selected_worker = available_workers[0]
stand_day[day] = selected_worker["name"]
selected_worker["standby"] += 1
selected_worker["count"] += 1
for day in range(1, days_in_month + 1):
fday = format(day, '02d')
date_str = f'{fday} {month} {year_int}'
weekday_num = day_of_week(date_str)
if is_weekend(date_str):
output_window.insert("end", f"{fday}-{month}-{year_int} - Shift: {full_day[day]} - Standby: {stand_day[day]}\n")
output_window.tag_add(f"day{day}", f"{day}.0", f"{day}.end")
output_window.tag_configure(f"day{day}", font=("Calibri", 18, "bold"))
else:
output_window.insert("end", f"{fday}-{month}-{year_int} - Shift: {full_day[day]} - Standby: {stand_day[day]}\n")
output_window.tag_add(f"day{day}", f"{day}.0", f"{day}.end")
output_window.tag_configure(f"day{day}", font=("Calibri", 14))
for worker in worker_info.values():
output_window.insert("end",f"Worker: {worker['name']} - Shifts: {worker['shifts']} - Standby: {worker['standby']}\n")
canvas.update_idletasks()
def load():
pass
def save():
pass
def export():
pass
def copy():
master_window.clipboard_clear() # Optional.
master_window.clipboard_append(output_window.get('1.0', tk.END).rstrip())
# Input fields
input_frame = ttk.Frame(master=master_window)
month_int = tk.IntVar()
month_label = ttk.Label(master=input_frame, text="Month:", font="Calibri 24")
month_entry = ttk.Entry(master=input_frame, font="Calibri 14", textvariable=month_int)
year_int = datetime.now().year
year_label = ttk.Label(master=input_frame, text="Year:", font="Calibri 24")
year_entry = ttk.Entry(master=input_frame, font="Calibri 14")
year_entry.insert(0, str(year_int))
save_buton = ttk.Button(master=input_frame, text="Save", command=save)
load_buton = ttk.Button(master=input_frame, text="Load", command=load)
input_frame.pack(pady=10)
load_buton.pack(side="left", padx=10)
save_buton.pack(side="left", padx=10)
year_label.pack(side="left", padx=10)
year_entry.pack(side="left")
month_label.pack(side="left", padx=10)
month_entry.pack(side="left")
# Generate worker fields
workers_frame = ttk.Frame(master=master_window)
workers_int = ttk.IntVar()
workers_label = ttk.Label(master=workers_frame, text="Workers:", font="Calibri 14")
workers_entry = ttk.Entry(master=workers_frame, font="Calibri 14", textvariable=workers_int)
workers_button = ttk.Button(master=workers_frame, text="Generate", command=generate_workers)
workers_frame.pack(pady=10)
workers_label.pack(side="left", padx=10)
workers_entry.pack(side="left")
workers_button.pack(side="left", padx=10)
# Frame to hold generated fields
generated_frame = ttk.Frame(master=master_window)
generated_frame.pack(pady=10)
# Output frame with centered elements
def on_output_window_mousewheel(event):
output_window.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break" # Prevents the event from propagating to other widgets
def on_canvas_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
# Create output_frame and related widgets
output_frame = ttk.Frame(master=master_window)
output_button = ttk.Button(master=output_frame, text="Generate Schedule", command=generate_schedule)
output_window = ttk.Text(master=output_frame, width=int(w_int / 16))
exporttoexcel_button = ttk.Button(master=output_frame, text="Export to excel", command=export)
copy_button = ttk.Button(master=output_frame, text="Copy to clipboard", command=copy)
# Pack the widgets
output_frame.pack(pady=10)
output_button.pack(pady=10)
output_window.pack()
exporttoexcel_button.pack(side="left", pady=10)
copy_button.pack(side="left", pady=10, padx=10)
# Bind mouse wheel event to the output_window for internal scrolling
output_window.bind("<MouseWheel>", on_output_window_mousewheel)
# Bind mouse wheel event to the canvas for overall scrolling
canvas.bind_all("<MouseWheel>", on_canvas_mousewheel)
root.mainloop()
When I run my Tkinter code, the initial window loads as expected, but I encounter several issues related to scrolling once I generate new widgets dynamically.
Scrolling Issue: After generating some widgets, I am unable to scroll all the way down to see the newly added content. The scrolling seems to be restricted to a certain height, preventing me from reaching the bottom of the window. This behavior persists even though I expect the scroll region to expand as new widgets are created.
Immediate Upward Scrolling: Right after starting the program, I'm able to scroll up even though there's no content above the visible window. Ideally, I shouldn't be able to scroll up until new content is generated that exceeds the window height.
Scrollbar Positioning: The scrollbar is not appearing on the right-hand side of the window where it should be. Instead, the scrollbar seems misaligned, making the UI less intuitive to use.
I’ve already attempted solutions like adjusting the scroll region dynamically, but these issues persist. Any suggestions for ensuring proper scrolling behavior, both upward and downward, and fixing the scrollbar positioning
The problem isn't with the scrolling per se, the problem is that the scrollbar is tiny, stucky in the upper-right corner of the UI.
My recommendation is to use pack
for the scrollbar and the canvas. Using grid
is possible, but requires more likes of code and is more difficult to visualize in the code.
If you remove the calls to scrollbar.grid
and canvas.grid
, and then create the scrollbar and canvas like the following, it should work as you expect with less code:
# Create a canvas and a vertical scrollbar as the
# only two widgets in the root, so we can scroll the
# entire UI
canvas = ttk.Canvas(root)
scrollbar = ttk.Scrollbar(master=root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)