'''
This module contains the GUI of BuckPy.
'''
import os
import tkinter as tk
from tkinter import ttk
from tkinter import font
from tkinter import filedialog
import pandas as pd
from importlib.resources import files, as_file
from pathlib import Path
[docs]
class GUI:
'''
BuckPy Graphical User Interface (GUI) class.
This class creates and manages the main window for user interaction,
allowing users to select input files, enter pipeline and scenario IDs,
choose options, and view scenario data in a table. It handles all
user input validation and provides a structured interface for running
BuckPy workflows.
'''
def __init__(self, root):
'''
Initialize the BuckPy GUI.
Sets up the main window, configures fonts and geometry, creates
top and bottom frames, and initializes all widgets for user input.
'''
# Store the root window
self.root = root
# Set window title
self.root.title('BuckPy')
# Set window icon
try:
logo_res = files("buckpy").joinpath("_static", "logo.png")
with as_file(logo_res) as logo_path:
self.root.iconphoto(False, tk.PhotoImage(file=str(logo_path)))
except Exception:
# Fallback to source tree when running from repo
try:
fallback = Path(__file__).with_name("_static") / "logo.png"
if fallback.exists():
self.root.iconphoto(False, tk.PhotoImage(file=str(fallback)))
except Exception:
pass
# Update geometry info
self.root.update_idletasks()
window_width = 1366
window_height = 768
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
self.root.geometry(f'{window_width}x{window_height}+{x}+{y}')
# Set modern default font
default_font = font.nametofont('TkDefaultFont')
default_font.configure(family='Segoe UI', size=11)
self.root.option_add('*Font', default_font)
# Create top and bottom frames for layout
self.top_frame = tk.Frame(self.root, padx=20, pady=20)
self.top_frame.grid(row=0, column=0, sticky='ew')
self.bottom_frame = tk.Frame(self.root, padx=20)
self.bottom_frame.grid(row=1, column=0, sticky='nsew')
# Add a bottom padding frame for spacing
self.bottom_padding = tk.Frame(self.root, height=20)
self.bottom_padding.grid(row=2, column=0, sticky='ew')
# Configure grid weights for resizing behavior
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_rowconfigure(1, weight=1)
self.root.grid_rowconfigure(2, weight=0)
# Set minimum sizes and weights for columns in the top frame
self.top_frame.grid_columnconfigure(1, minsize=200)
self.top_frame.grid_columnconfigure(3, minsize=100)
self.top_frame.grid_columnconfigure(5, minsize=200, weight=1)
# Make the Treeview expand in the bottom frame
self.bottom_frame.grid_rowconfigure(0, weight=1)
self.bottom_frame.grid_columnconfigure(0, weight=1)
# Initialize file path variables
self.work_dir = None
self.input_file_name = None
# Add all widgets to the GUI
self.create_widgets()
[docs]
def open_file(self):
'''
Open a file dialog for the user to select an Excel input file.
Updates the displayed file name and triggers loading of scenario
data into the table if a valid file is selected.
'''
# Open a file dialog for Excel files
file_path = filedialog.askopenfilename(
initialdir=os.getcwd(),
filetypes=[('Excel files', '*.xlsx *.xls *.xlsm')],
title='Select Excel input file'
)
# If the user cancels the dialog, exit the method
if not file_path:
return
# Store the directory of the selected file
self.work_dir = os.path.dirname(file_path)
# Store the file name of the selected file
self.input_file_name = os.path.basename(file_path)
# Update the label to display the selected file name
self.excel_label.config(text=self.input_file_name)
# Load the scenario data into the Treeview
self.setup_treeview()
[docs]
def setup_treeview(self):
'''
Set up and populate the Treeview widget with scenario data.
Reads the selected Excel file, configures the table columns and
headings, and inserts scenario data rows for user review.
'''
# Read the 'Scenario' sheet from the selected Excel file into a DataFrame
self.df_sens = pd.read_excel(rf'{self.work_dir}/{self.input_file_name}', sheet_name='Scenario')
columns = self.df_sens.columns.tolist()
# Create the Treeview widget with columns from the DataFrame
self.tree = ttk.Treeview(self.bottom_frame, columns=self.df_sens.columns.tolist(), show='headings')
self.tree.grid(row=0, column=0, sticky='nsew')
# Configure Treeview and heading styles
style = ttk.Style()
style.theme_use('default')
style.configure('Treeview.Heading', font=('Segoe UI', 10, 'bold'), padding=[0, 10], background='#f5f5f5')
style.configure('Treeview',font=('Segoe UI', 10), rowheight=30, borderwidth=1, relief='solid', background='#f5f5f5', fieldbackground='#f5f5f5')
# Set up column headings and widths
for i, col in enumerate(columns):
# Last column: left-aligned, stretchable
if i == len(columns) - 1:
self.tree.heading(col, text=col, anchor='w')
self.tree.column(col, stretch=True, anchor='w')
# Second and third last columns: center-aligned, fixed width
elif (i == len(columns) - 2) or (i == len(columns) - 3):
self.tree.heading(col, text=col, anchor='center')
self.tree.column(col, width=140, stretch=False, anchor='center')
# All other columns: center-aligned, smaller fixed width
else:
self.tree.heading(col, text=col, anchor='center')
self.tree.column(col, width=100, stretch=False, anchor='center')
# Insert each row of the DataFrame into the Treeview
for _, row in self.df_sens.iterrows():
self.tree.insert('', 'end', values=list(row))
# Reset other relevant fields
self.pipeline_id_entry.delete(0, tk.END)
self.scenario_id_entry.delete(0, tk.END)
self.pipeline_id_entry.insert(0, self.df_sens['Pipeline'].iloc[0])
self.scenario_id_entry.insert(0, self.df_sens['Scenario'].iloc[0])
[docs]
def close_app(self):
'''
Validate user input and close the GUI if all inputs are valid.
Checks that required fields are filled and formatted correctly.
If validation passes, stores user selections and closes the window.
Otherwise, displays appropriate warning or error messages.
'''
# Get the current values from the entry widgets
pipeline_id_value = self.pipeline_id_entry.get()
scenario_id_value = self.scenario_id_entry.get()
# Check if work_dir and input_file_name are set
if not self.work_dir or not self.input_file_name:
tk.messagebox.showwarning('Input Required', 'Please select a valid Excel input file.')
return
# Check for empty or placeholder values
if not pipeline_id_value or pipeline_id_value == 'Empty' or \
not scenario_id_value or scenario_id_value == 'e.g. 1,2,3':
tk.messagebox.showwarning('Input Required', 'Please enter valid Pipeline ID and Scenario IDs.')
return
# Optional: Check if Pipeline ID is a string
if pipeline_id_value.isdigit():
tk.messagebox.showwarning('Invalid Input', 'Pipeline ID should be a string.')
return
# Optional: Check if Scenario IDs are comma-separated numbers
scenario_ids = [s.strip() for s in scenario_id_value.split(',')]
if not all(s.isdigit() for s in scenario_ids):
tk.messagebox.showwarning('Invalid Input', 'Scenario IDs should be comma-separated numbers (e.g. 1,2,3).')
return
# Additional checks: Ensure pipeline_id_value and scenario_id_value(s) exist in the DataFrame
if hasattr(self, 'df_sens'):
# Check if the Pipeline ID exists in the DataFrame
if pipeline_id_value not in self.df_sens['Pipeline'].astype(str).unique():
tk.messagebox.showwarning('Invalid Input', f'Pipeline ID "{pipeline_id_value}" not found in the input file.')
return
# Check if each Scenario ID exists in the DataFrame
scenario_unique = set(self.df_sens['Scenario'].astype(str).unique())
invalid_scenarios = [sid for sid in scenario_ids if sid not in scenario_unique]
if invalid_scenarios:
tk.messagebox.showwarning(
'Invalid Input',
f'Scenario ID(s) {", ".join(invalid_scenarios)} not found in the input file.'
)
return
try:
# Store the validated values in instance variables
self.pipeline_id = pipeline_id_value
self.scenario_id = scenario_id_value
self.bl_verbose = self.combobox_bl_verbose.get()
self.output_combination = self.combobox_bl_output_combination.get()
# Close the GUI window
self.root.destroy()
except Exception as e:
# Show an error message if something unexpected happens
tk.messagebox.showerror('Error', f'An unexpected error occurred: {e}')