Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import cv2
- import numpy as np
- import os
- import threading
- import time
- import tkinter as tk
- from tkinter import ttk, Frame, Label, Canvas, Spinbox, messagebox, filedialog, Scrollbar
- from PIL import Image, ImageTk
- import imghdr
- # =============================================================================
- # Global Settings and Variables
- # =============================================================================
- DEBUG_MODE = True # Toggle debugging messages
- def debug_print(message):
- """Print debugging messages if DEBUG_MODE is enabled."""
- if DEBUG_MODE:
- print(message)
- debug_print("π DEBUG ENABLED")
- # Global state variables and configuration
- image_display = None # Tkinter image for current preview display
- processed_image = None # Processed image used for display
- latest_image_path = None # Latest loaded image (currently unused)
- lock = threading.Lock() # For thread safety
- debounce_time = 0 # Timestamp for debouncing slider changes
- display_mode = None # Tkinter StringVar for display mode selection
- threshold_value = 205 # Default threshold for image processing
- frame_counter = 1 # Frame counter (used for unique filenames)
- processed_count = 0 # Total count of processed strips
- image_queue = [] # List of image file paths to process
- update_pending = False # Flag indicating if preview update is underway
- update_requested = False # Flag to queue a preview update if one is already running
- stop_requested = False # Flag to signal a processing stop
- # Adjustable cropping parameters
- frame_x_offset = 50 # Horizontal offset for cropped frames
- frame_y_offset = 250 # Vertical offset for cropped frames
- frame_width_max = 850 # Maximum width for cropped frames
- frame_height_max = 1700 # Maximum height for cropped frames
- # Folder paths (to be set in the GUI)
- input_folder = ""
- output_folder = ""
- # Global variables for dialogs and overlays
- loading_window = None # The modal processing dialog
- loading_label = None # The overlay label for preview updates
- # =============================================================================
- # Image Processing Functions
- # =============================================================================
- def load_image(image_path):
- """
- Loads and validates an image from the given path.
- Returns:
- numpy.ndarray: Loaded image, or None if loading fails.
- """
- try:
- debug_print(f"π Attempting to load image: {image_path}")
- formatted_path = os.path.normpath(image_path)
- debug_print(f"π Reformatted path: {formatted_path}")
- if not os.path.exists(formatted_path):
- debug_print(f"β File does not exist: {formatted_path}")
- return None
- time.sleep(1) # Pause to ensure file writing is complete
- file_type = imghdr.what(formatted_path)
- if file_type not in ['jpeg', 'png']:
- debug_print(f"β Unsupported file type: {file_type}")
- return None
- debug_print(f"β Validated file type: {file_type}")
- image = cv2.imread(formatted_path)
- if image is None:
- debug_print(f"β Failed to load image: {formatted_path}")
- else:
- debug_print(f"β Image loaded with shape: {image.shape}")
- return image
- except cv2.error as e:
- debug_print(f"β OpenCV error in load_image: {e}")
- return None
- except Exception as e:
- debug_print(f"β Unexpected error in load_image: {e}")
- return None
- def detect_and_crop_frames(image_path):
- """
- Detects sprocket holes and calculates cropping areas.
- Skips the first cropped frame (which is assumed to be unwanted).
- Returns:
- list: Cropped frames as numpy arrays.
- """
- try:
- sprocket_count = 0
- cropped_frames = []
- original_image = load_image(image_path)
- if original_image is None:
- debug_print(f"β Error loading image: {image_path}")
- return []
- rotated_image = cv2.rotate(original_image, cv2.ROTATE_180)
- gray = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
- _, thresh = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
- cnts, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- debug_print(f"π Found {len(cnts)} contours.")
- # Sort contours by x-coordinate (right to left), and for matching x-values, sort by y (top to bottom)
- cnts = sorted(cnts, key=lambda x: (cv2.boundingRect(x)[0], -cv2.boundingRect(x)[1]), reverse=True)
- # **Validation debug print** - Check the order of sorted contours
- for idx, c in enumerate(cnts):
- x, y, w, h = cv2.boundingRect(c)
- debug_print(f"π Contour {idx}: x={x}, y={y}, w={w}, h={h}")
- for c in cnts:
- x, y, w, h = cv2.boundingRect(c)
- aspect_ratio = w / float(h)
- if 0.2 < aspect_ratio < 1.0 and w < 300 and h > 50:
- sprocket_count += 1
- debug_print(f"π΄ Sprocket #{sprocket_count}: x={x}, y={y}, w={w}, h={h}")
- frame_x = max(x + frame_x_offset, 0)
- frame_y = max(y - frame_y_offset, 0)
- frame_w = min(frame_width_max, rotated_image.shape[1] - frame_x)
- frame_h = min(frame_height_max, rotated_image.shape[0] - frame_y)
- debug_print(f"π Cropping frame: x={frame_x}, y={frame_y}, w={frame_w}, h={frame_h}")
- frame = rotated_image[frame_y:frame_y + frame_h, frame_x:frame_x + frame_w]
- debug_print(f"Appending frame: x={frame_x}, y={frame_y}, w={frame_w}, h={frame_h}")
- if frame.size > 0:
- cropped_frames.append(frame)
- else:
- debug_print("β οΈ Skipped empty frame.")
- debug_print(f"β Sprocket count: {sprocket_count}, frames before skip: {len(cropped_frames)}")
- if cropped_frames:
- debug_print("π§ Skipping first cropped frame (likely unwanted).")
- cropped_frames = cropped_frames[1:]
- debug_print(f"β Final cropped frames count: {len(cropped_frames)}")
- return cropped_frames
- except cv2.error as e:
- debug_print(f"β OpenCV error in detect_and_crop_frames: {e}")
- return []
- except Exception as e:
- debug_print(f"β Unexpected error in detect_and_crop_frames: {e}")
- return []
- # =============================================================================
- # GUI Update Functions
- # =============================================================================
- def update_preview():
- """
- Updates the preview image on the canvas.
- Loads the first image from the input folder and applies rotation, thresholding,
- and optional overlay drawing.
- """
- global update_pending, update_requested, image_display, processed_image
- if update_pending:
- return
- update_pending = True
- # Show the preview loading overlay
- loading_label.grid(row=0, column=0, sticky="nsew")
- root.update_idletasks() # Ensure UI is updated immediately
- try:
- # Use the first file in the input folder for preview
- if input_folder and os.listdir(input_folder):
- sample_file = os.path.join(input_folder, sorted(os.listdir(input_folder))[0])
- else:
- debug_print("β οΈ No image available for preview.")
- return
- debug_print(f"π Loading image for preview: {sample_file}")
- original_image = load_image(sample_file)
- if original_image is None:
- debug_print(f"β Error loading preview image: {sample_file}")
- return
- rotated_image = cv2.rotate(original_image, cv2.ROTATE_180)
- debug_print(f"π’ Display mode: {display_mode.get()}")
- # Apply threshold for contour detection
- gray = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
- _, thresh = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
- cnts, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- debug_print(f"π Detected {len(cnts)} contours.")
- # Choose the base image for preview depending on display mode
- if display_mode.get() == "Threshold Image":
- base_image = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR)
- debug_print("π€ Using Threshold Image mode.")
- else:
- base_image = rotated_image
- debug_print("πΌ Using Original Image mode.")
- # If overlay is enabled, draw the detection overlay
- if overlay_enabled.get():
- debug_print("π’ Overlay enabled.")
- overlay = base_image.copy()
- sprocket_count = 0
- for c in cnts:
- x, y, w, h = cv2.boundingRect(c)
- aspect_ratio = w / float(h)
- if 0.2 < aspect_ratio < 1.0 and w < 300 and h > 50:
- sprocket_count += 1
- debug_print(f"π΄ Sprocket: x={x}, y={y}, w={w}, h={h} (Count: {sprocket_count})")
- frame_x = max(x + frame_x_offset, 0)
- frame_y = max(y - frame_y_offset, 0)
- frame_w = min(frame_width_max, rotated_image.shape[1] - frame_x)
- frame_h = min(frame_height_max, rotated_image.shape[0] - frame_y)
- box_color = (0, 255, 0) if sprocket_count % 2 == 0 else (255, 0, 0)
- box_overlay = overlay.copy()
- cv2.rectangle(box_overlay, (frame_x, frame_y),
- (frame_x + frame_w, frame_y + frame_h),
- box_color, -1)
- cv2.addWeighted(box_overlay, 0.3, overlay, 0.7, 0, overlay)
- valid_frames = sprocket_count - 1 if sprocket_count > 0 else 0
- frame_count_label.config(
- text=f"[ {valid_frames} (+1 rubbish) ] Frame(s) Detected, [ {sprocket_count} ] Sprocket(s) Detected"
- )
- base_image = overlay
- else:
- debug_print("π΄ Overlay disabled.")
- # Resize for preview (scale factor 0.2)
- scale_factor = 0.2
- preview_resized = cv2.resize(base_image,
- (int(base_image.shape[1] * scale_factor),
- int(base_image.shape[0] * scale_factor)))
- debug_print(f"πΌ Resized preview dimensions: {preview_resized.shape}")
- processed_image = cv2.cvtColor(preview_resized, cv2.COLOR_BGR2RGB)
- image_display = ImageTk.PhotoImage(image=Image.fromarray(processed_image))
- preview_canvas.create_image(0, 0, anchor='nw', image=image_display)
- preview_canvas.config(scrollregion=preview_canvas.bbox('all'))
- debug_print("π Preview updated.")
- except Exception as e:
- debug_print(f"β Error in update_preview: {e}")
- finally:
- update_pending = False
- loading_label.grid_forget() # Hide the loading overlay
- if update_requested:
- update_requested = False
- threading.Thread(target=update_preview, daemon=True).start()
- def update_threshold(value):
- """
- Updates the threshold value used for processing, using a 0.3-second debounce.
- """
- global threshold_value, debounce_time, threshold_label, update_pending, update_requested
- current_time = time.time()
- if current_time - debounce_time < 0.3:
- return
- debounce_time = current_time
- with lock:
- threshold_value = int(value)
- debug_print(f"π§ Threshold updated to: {threshold_value}")
- if threshold_label is not None:
- threshold_label.config(text=f"Threshold: {threshold_value}")
- if update_pending:
- update_requested = True
- else:
- threading.Thread(target=update_preview, daemon=True).start()
- def reset_gui():
- """
- Resets the GUI state for a new run.
- Clears the preview and resets frame and sprocket counts.
- """
- global processed_count, frame_counter
- processed_count = 0
- frame_counter = 1
- frame_count_label.config(text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected")
- preview_canvas.delete("all")
- debug_print("π GUI reset for a new run.")
- def select_input_folder():
- """
- Opens a dialog to select the input folder.
- Updates the input folder label and refreshes the preview.
- """
- global input_folder
- selected = filedialog.askdirectory(title="Select Input Folder")
- if selected:
- input_folder = selected
- input_folder_label.config(text="Input: " + os.path.basename(input_folder))
- else:
- input_folder_label.config(text="Input: Not Selected")
- threading.Thread(target=update_preview, daemon=True).start()
- def select_output_folder():
- """
- Opens a dialog to select the output folder and updates the folder label.
- """
- global output_folder
- selected = filedialog.askdirectory(title="Select Output Folder")
- if selected:
- output_folder = selected
- output_folder_label.config(text="Output: " + os.path.basename(output_folder))
- else:
- output_folder_label.config(text="Output: Not Selected")
- # =============================================================================
- # Loading Dialog / Progress Indicator Functions
- # =============================================================================
- def show_loading_dialog():
- """
- Displays a modal dialog with an indeterminate progress bar and a
- 'Stop Processing' button. Resets the stop flag.
- """
- global loading_window, stop_requested
- stop_requested = False # Reset for a new processing run
- loading_window = tk.Toplevel(root)
- loading_window.title("Processing...")
- tk.Label(loading_window, text="Processing, please wait...").grid(row=0, column=0, padx=10, pady=10)
- progress = ttk.Progressbar(loading_window, mode='indeterminate', length=200)
- progress.grid(row=1, column=0, padx=10, pady=10)
- progress.start()
- ttk.Button(loading_window, text="Stop Processing", command=stop_processing).grid(row=2, column=0, pady=10)
- loading_window.grab_set() # Make the dialog modal
- def stop_processing():
- """
- Signals the processing loop to stop, resets counters and clears the preview,
- then hides the loading dialog.
- """
- global stop_requested, processed_count, frame_counter
- stop_requested = True
- processed_count = 0
- hide_loading_dialog()
- preview_canvas.delete("all")
- frame_count_label.config(text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected")
- debug_print("βΉοΈ Processing stopped and state reset.")
- def hide_loading_dialog():
- """Closes the loading dialog if it exists."""
- global loading_window
- if loading_window is not None:
- loading_window.destroy()
- loading_window = None
- def processing_complete_dialog(total_strips, total_frames):
- """
- Hides the loading dialog and shows a message box indicating
- that processing is complete.
- """
- hide_loading_dialog()
- messagebox.showinfo("Processing Complete", f"Processed {total_strips} strips.\nTotal frames saved: {total_frames}")
- # =============================================================================
- # Image Saving Function
- # =============================================================================
- def save_detected_frames(detected_frames, strip_filename):
- """
- Saves each detected frame after rotating 90Β° counterclockwise and flipping vertically.
- **Now ensures frames are saved in the correct order by reversing the list before saving.**
- Returns:
- int: Number of frames saved.
- """
- global frame_counter
- frames_saved = 0
- # β Reverse the order of detected frames before saving
- detected_frames.reverse()
- try:
- for idx, frame in enumerate(detected_frames):
- if frame is None or not isinstance(frame, np.ndarray):
- debug_print(f"β Invalid frame at index {idx}; skipping.")
- continue
- rotated_frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
- flipped_frame = cv2.flip(rotated_frame, 0)
- filename = os.path.join(output_folder, f"frame_{frame_counter:06d}.jpg")
- cv2.imwrite(filename, flipped_frame, [cv2.IMWRITE_JPEG_QUALITY, 100])
- debug_print(f"β Frame saved: {filename}")
- frame_counter += 1
- frames_saved += 1
- print(f"Strip {strip_filename}: Frames saved: {frames_saved}")
- return frames_saved
- except cv2.error as e:
- debug_print(f"β OpenCV error when saving frames: {e}")
- return frames_saved
- except OSError as e:
- debug_print(f"β OS error when saving frames: {e}")
- return frames_saved
- except Exception as e:
- debug_print(f"β Unexpected error when saving frames: {e}")
- return frames_saved
- # =============================================================================
- # Process Image Queue Function
- # =============================================================================
- def process_image_queue():
- """
- Processes all images in the input folder by detecting and cropping frames.
- Introduces a waiting period before starting to allow cancellation.
- If uninterrupted, shows a processing complete dialog.
- """
- global image_queue, processed_count, frame_counter, stop_requested
- if not input_folder or not output_folder:
- messagebox.showerror("Error", "Please select both input and output folders before starting the process.")
- hide_loading_dialog()
- return
- image_queue = sorted(
- [os.path.join(input_folder, f)
- for f in os.listdir(input_folder)
- if f.lower().endswith(('.jpg', '.png', '.jpeg'))]
- )
- if not image_queue:
- messagebox.showerror("Error", "No valid images found in the input folder.")
- hide_loading_dialog()
- return
- if stop_requested:
- debug_print("π Processing stopped before starting the loop.")
- hide_loading_dialog()
- return
- debug_print("β³ Waiting for user input before starting...")
- for _ in range(20): # 20 iterations x 0.1 s delay = 2 seconds
- if stop_requested:
- debug_print("π Processing stopped before it began.")
- hide_loading_dialog()
- return
- time.sleep(0.1)
- total_strips = 0
- for image_path in image_queue:
- if stop_requested:
- debug_print("π Processing stopped by user.")
- break
- total_strips += 1
- debug_print(f"π Processing strip: {image_path}")
- try:
- cropped_frames = detect_and_crop_frames(image_path)
- if not cropped_frames:
- debug_print(f"β οΈ No frames detected in {image_path}. Skipping.")
- continue
- for idx, frame in enumerate(cropped_frames):
- debug_print(f"Frame order before saving: index={idx}, size={frame.shape}")
- frames_saved = save_detected_frames(cropped_frames, os.path.basename(image_path))
- processed_count += 1
- except Exception as e:
- debug_print(f"β Error processing {image_path}: {e}")
- debug_print("β Processing loop completed.")
- if not stop_requested:
- root.after(0, lambda: processing_complete_dialog(total_strips, frame_counter - 1))
- # =============================================================================
- # Tkinter GUI Setup
- # =============================================================================
- root = tk.Tk()
- root.title("CineFrame Cutter")
- root.grid_rowconfigure(0, weight=1)
- root.grid_columnconfigure(0, weight=1)
- main_frame = Frame(root)
- main_frame.grid(row=0, column=0, sticky='nsew')
- main_frame.grid_rowconfigure(0, weight=1)
- main_frame.grid_columnconfigure(0, weight=1)
- # Input/Output Folder Selection Panel
- io_frame = Frame(main_frame, relief=tk.RIDGE, borderwidth=2)
- io_frame.grid(row=0, column=0, sticky='ew', padx=5, pady=5)
- io_frame.columnconfigure(1, weight=1)
- input_folder_label = Label(io_frame, text="Input: Not Selected")
- input_folder_label.grid(row=0, column=0, padx=5, pady=5)
- ttk.Button(io_frame, text="Browse Input", command=select_input_folder).grid(row=0, column=1, padx=5, pady=5)
- output_folder_label = Label(io_frame, text="Output: Not Selected")
- output_folder_label.grid(row=1, column=0, padx=5, pady=5)
- ttk.Button(io_frame, text="Browse Output", command=select_output_folder).grid(row=1, column=1, padx=5, pady=5)
- # Image Preview Frame (600x450)
- preview_frame = Frame(main_frame, relief=tk.SUNKEN, borderwidth=2)
- preview_frame.grid(row=1, column=0, sticky='nsew', padx=5, pady=5)
- preview_frame.grid_rowconfigure(0, weight=1)
- preview_frame.grid_columnconfigure(0, weight=1)
- preview_canvas = Canvas(preview_frame, bg='white', width=600, height=450)
- preview_canvas.grid(row=0, column=0, sticky='nsew')
- h_scroll = Scrollbar(preview_frame, orient=tk.HORIZONTAL, command=preview_canvas.xview)
- h_scroll.grid(row=1, column=0, sticky='ew')
- v_scroll = Scrollbar(preview_frame, orient=tk.VERTICAL, command=preview_canvas.yview)
- v_scroll.grid(row=0, column=1, sticky='ns')
- preview_canvas.configure(xscrollcommand=h_scroll.set, yscrollcommand=v_scroll.set)
- loading_label = Label(preview_frame, text="Loading preview...", font=("Arial", 14), fg="red", bg="white")
- loading_label.grid(row=0, column=0, sticky="nsew")
- loading_label.grid_forget() # Hide the loading overlay by default
- # Frame and Sprocket Count Label
- frame_count_label = Label(main_frame, text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected", font=("Arial", 12))
- frame_count_label.grid(row=2, column=0, padx=5, pady=5)
- # Controls Panel (Threshold, Display Mode, Overlay, Frame Area Adjusters)
- controls_frame = Frame(main_frame, relief=tk.GROOVE, borderwidth=2)
- controls_frame.grid(row=3, column=0, sticky='ew', padx=5, pady=5)
- controls_frame.columnconfigure(0, weight=1)
- threshold_label = Label(controls_frame, text=f"Threshold: {threshold_value}", font=("Arial", 10))
- threshold_label.grid(row=0, column=0, padx=5, pady=5, sticky='w')
- threshold_slider = ttk.Scale(controls_frame, from_=0, to=255, orient='horizontal',
- command=lambda v: update_threshold(int(float(v))))
- threshold_slider.set(threshold_value)
- threshold_slider.grid(row=0, column=1, padx=5, pady=5, sticky='ew')
- controls_frame.columnconfigure(1, weight=1)
- Label(controls_frame, text="Display Mode:").grid(row=1, column=0, padx=5, pady=5, sticky='w')
- display_mode = tk.StringVar(value="Threshold Image")
- display_dropdown = ttk.OptionMenu(controls_frame, display_mode, "Threshold Image", "Threshold Image", "Original Image")
- display_dropdown.grid(row=1, column=1, padx=5, pady=5, sticky='ew')
- display_mode.trace_add("write", lambda *args: threading.Thread(target=update_preview, daemon=True).start())
- overlay_enabled = tk.BooleanVar(value=True)
- overlay_checkbox = ttk.Checkbutton(controls_frame, text="Enable Overlay", variable=overlay_enabled,
- command=lambda: threading.Thread(target=update_preview, daemon=True).start())
- overlay_checkbox.grid(row=2, column=0, padx=5, pady=5, sticky='w')
- # Frame Area Adjustment Controls
- area_frame = Frame(controls_frame, relief=tk.RIDGE, borderwidth=1)
- area_frame.grid(row=3, column=0, columnspan=2, sticky='ew', padx=5, pady=5)
- Label(area_frame, text="Frame X Offset:").grid(row=0, column=0, padx=3, pady=3, sticky='w')
- x_offset_spin = Spinbox(area_frame, from_=0, to=500, width=5)
- x_offset_spin.delete(0, tk.END)
- x_offset_spin.insert(0, frame_x_offset)
- x_offset_spin.grid(row=0, column=1, padx=3, pady=3)
- Label(area_frame, text="Frame Y Offset:").grid(row=0, column=2, padx=3, pady=3, sticky='w')
- y_offset_spin = Spinbox(area_frame, from_=0, to=500, width=5)
- y_offset_spin.delete(0, tk.END)
- y_offset_spin.insert(0, frame_y_offset)
- y_offset_spin.grid(row=0, column=3, padx=3, pady=3)
- Label(area_frame, text="Max Width:").grid(row=1, column=0, padx=3, pady=3, sticky='w')
- width_spin = Spinbox(area_frame, from_=100, to=2000, width=5)
- width_spin.delete(0, tk.END)
- width_spin.insert(0, frame_width_max)
- width_spin.grid(row=1, column=1, padx=3, pady=3)
- Label(area_frame, text="Max Height:").grid(row=1, column=2, padx=3, pady=3, sticky='w')
- height_spin = Spinbox(area_frame, from_=100, to=3000, width=5)
- height_spin.delete(0, tk.END)
- height_spin.insert(0, frame_height_max)
- height_spin.grid(row=1, column=3, padx=3, pady=3)
- def refresh_preview():
- """
- Updates global cropping parameters from the spinboxes,
- then refreshes the preview.
- """
- global frame_x_offset, frame_y_offset, frame_width_max, frame_height_max
- try:
- frame_x_offset = int(x_offset_spin.get())
- frame_y_offset = int(y_offset_spin.get())
- frame_width_max = int(width_spin.get())
- frame_height_max = int(height_spin.get())
- debug_print(f"Updated frame area: x_offset={frame_x_offset}, y_offset={frame_y_offset}, "
- f"width_max={frame_width_max}, height_max={frame_height_max}")
- threading.Thread(target=update_preview, daemon=True).start()
- except Exception as e:
- debug_print(f"Error refreshing preview: {e}")
- refresh_button = ttk.Button(area_frame, text="Apply Frame Size", command=refresh_preview)
- refresh_button.grid(row=2, column=0, columnspan=4, pady=5)
- # Bottom Control Panel (Start, Quit, Reset Buttons)
- buttons_frame = Frame(main_frame)
- buttons_frame.grid(row=4, column=0, sticky='ew', padx=5, pady=5)
- buttons_frame.grid_columnconfigure(3, weight=1) # Spacer column to right-align the Quit button
- start_button = ttk.Button(
- buttons_frame,
- text="Start",
- command=lambda: [show_loading_dialog(),
- threading.Thread(target=process_image_queue, daemon=True).start()]
- )
- start_button.grid(row=0, column=0, padx=5, pady=5)
- quit_button = ttk.Button(buttons_frame, text="Quit", command=root.destroy)
- quit_button.grid(row=0, column=3, padx=5, pady=5, sticky='e') # Placed in far-right column
- def confirm_reset():
- """Prompts the user to confirm before resetting the GUI."""
- if messagebox.askyesno("Confirm Reset", "Are you sure you want to reset the GUI for a new run?"):
- reset_gui()
- reset_button = ttk.Button(buttons_frame, text="Reset GUI", command=confirm_reset)
- reset_button.grid(row=0, column=2, padx=5, pady=5)
- # =============================================================================
- # Main Event Loop
- # =============================================================================
- try:
- root.mainloop()
- except KeyboardInterrupt:
- debug_print("π Keyboard Interrupt: Exiting.")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement