[Python] Why is my Tkinter canvas not allowing me to scroll all the way down after generating...

Discussão em 'Python' iniciado por Stack, Outubro 2, 2024.

  1. Stack

    Stack Membro Participativo

    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 = {
    "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.


    1. 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.


    2. 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.


    3. 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

    Continue reading...

Compartilhe esta Página