from tkinter import * from functools import partial # to prevent unwanted windows import all_constant as c import conversion_rounding as cr from datetime import date class Converter: def __init__(self): """ Multi-currency converter GUI with live exchange rates. Fetches live rates from Frankfurter API on startup. Falls back to hardcoded constants if internet is unavailable. Supports NZD, JPY, USD and AUD. """ self.all_calculations_list = [] # Tracks which currency is selected as From and To self.from_currency = None self.to_currency = None # Fetch live rates on startup — fallback to constants if unavailable self.rates = cr.get_live_rates() self.main_frame = Frame(padx=10, pady=10) self.main_frame.grid() self.heading = Label(self.main_frame, text="Currency Converter", font=("Arial", "16", "bold")) self.heading.grid(row=0) instructions = ("Select a FROM currency, then a TO currency, " "enter an amount and press Convert.") self.instructions = Label(self.main_frame, text=instructions, wraplength=300, width=45, justify="left") self.instructions.grid(row=1) # Show whether live or fallback rates are being used if self.rates == c.RATES_FROM_NZD: rate_status = "Using fallback rates (could not connect)" rate_colour = "#9C0000" else: rate_status = "Live rates loaded" rate_colour = "#2D6A4F" self.rate_status_label = Label(self.main_frame, text=rate_status, font=("Arial", "9", "italic"), fg=rate_colour) self.rate_status_label.grid(row=2) # Currency selection buttons frame self.currency_frame = Frame(self.main_frame) self.currency_frame.grid(row=3, pady=5) # Build one button per currency — driven by CURRENCIES list in constants self.currency_buttons = {} for i, currency in enumerate(c.CURRENCIES): btn = Button(self.currency_frame, text=currency, bg="#AAAAAA", fg="#FFFFFF", font=("Arial", "12", "bold"), width=6, command=lambda cur=currency: self.select_currency(cur)) btn.grid(row=0, column=i, padx=4, pady=4) self.currency_buttons[currency] = btn # Label showing current From / To selection self.selection_label = Label(self.main_frame, text="From: — To: —", font=("Arial", "11"), fg="#004C99") self.selection_label.grid(row=4) # Amount entry self.amount_entry = Entry(self.main_frame, font=("Arial", "14")) self.amount_entry.grid(row=5, padx=10, pady=10) # Answer / error label self.answer_error = Label(self.main_frame, text="Select currencies above to begin", fg="#004C99", font=("Arial", "13", "bold")) self.answer_error.grid(row=6) # Bottom buttons frame self.button_frame = Frame(self.main_frame) self.button_frame.grid(row=7) # button list (button text | bg colour | command | row | column) button_details_list = [ ["Convert", "#009900", self.check_amount, 0, 0], ["Help / Info", "#CC6600", self.to_help, 0, 1], ["History / Export", "#004C99", self.to_history, 0, 2] ] self.button_ref_list = [] for item in button_details_list: btn = Button(self.button_frame, text=item[0], bg=item[1], fg="#FFFFFF", font=("Arial", "12", "bold"), width=14, command=item[2]) btn.grid(row=item[3], column=item[4], padx=5, pady=5) self.button_ref_list.append(btn) # Retrieve buttons for state management self.convert_button = self.button_ref_list[0] self.convert_button.config(state=DISABLED) self.to_help_button = self.button_ref_list[1] self.to_history_button = self.button_ref_list[2] self.to_history_button.config(state=DISABLED) def select_currency(self, currency): """ Handles currency button selection logic. First click sets From currency (highlights green). Second click sets To currency (highlights blue). If both already selected, resets and treats new click as From. Prevents selecting same currency for both From and To. """ # If both already selected, reset and start again with new selection if self.from_currency and self.to_currency: self.reset_selection() # If nothing selected yet, set as From currency if self.from_currency is None: self.from_currency = currency self.currency_buttons[currency].config(bg="#2D6A4F") # green = From self.update_selection_label() # If From is set and user picks a different currency, set as To elif self.to_currency is None and currency != self.from_currency: self.to_currency = currency self.currency_buttons[currency].config(bg="#004C99") # blue = To self.update_selection_label() self.convert_button.config(state=NORMAL) def reset_selection(self): """ Clears From / To selections and resets all currency button colours. """ self.from_currency = None self.to_currency = None self.convert_button.config(state=DISABLED) # Reset all currency buttons back to grey for btn in self.currency_buttons.values(): btn.config(bg="#AAAAAA") self.update_selection_label() def update_selection_label(self): """ Updates the From / To display label based on current selection. """ from_text = self.from_currency if self.from_currency else "—" to_text = self.to_currency if self.to_currency else "—" self.selection_label.config(text=f"From: {from_text} To: {to_text}") def check_amount(self): """ Checks that the entered amount is a valid number above the minimum. Invokes convert() if valid, otherwise shows a custom error message. """ # Retrieve amount to be converted to_convert = self.amount_entry.get() # Reset label and entry box (if we had an error from before) self.answer_error.config(fg="#004C99", font=("Arial", "13", "bold")) self.amount_entry.config(bg="#FFFFFF") error = f"Enter a number more than / equal to {c.MIN_AMOUNT}" has_errors = "no" # Check that the amount is a valid number above the minimum try: to_convert = float(to_convert) if to_convert >= c.MIN_AMOUNT: self.convert(to_convert) else: has_errors = "yes" except ValueError: has_errors = "yes" if has_errors == "yes": self.answer_error.config(text=error, fg="#9C0000", font=("Arial", "10", "bold")) self.amount_entry.config(bg="#F4CCCC") self.amount_entry.delete(0, END) def convert(self, to_convert): """ Converts the entered amount between the selected currencies. Uses the live rates fetched on startup (or fallback constants). Updates the answer label and stores the calculation for history/export. """ # Pass self.rates so conversion uses live rates throughout the session answer = cr.convert_currency(to_convert, self.from_currency, self.to_currency, self.rates) sym_from = cr.get_currency_symbol(self.from_currency) sym_to = cr.get_currency_symbol(self.to_currency) answer_statement = (f"{sym_from}{to_convert} {self.from_currency} " f"is {sym_to}{answer} {self.to_currency}") # Enable history / export button as soon as we have a valid calculation self.to_history_button.config(state=NORMAL) self.answer_error.config(text=answer_statement) self.all_calculations_list.append(answer_statement) print(self.all_calculations_list) def to_help(self): """ Opens help dialogue box and disables help button (so that users can't create multiple help boxes). """ DisplayHelp(self) def to_history(self): """ Opens history dialogue box and disables history button (so that users can't create multiple history boxes). """ HistoryExport(self, self.all_calculations_list) class DisplayHelp: """ Displays help dialogue box """ def __init__(self, partner): # Setup dialogue box and background colour background = "#ffe6cc" self.help_box = Toplevel() # Disable help button partner.to_help_button.config(state=DISABLED) # If users press the cross at the top, close help and # 'release' the help button self.help_box.protocol("WM_DELETE_WINDOW", partial(self.close_help, partner)) self.help_frame = Frame(self.help_box, width=300, height=200) self.help_frame.grid() self.help_heading_label = Label(self.help_frame, text="Help / Info", font=("Arial", "14", "bold")) self.help_heading_label.grid(row=0) help_text = ("To use the program, first click a currency button " "to select your FROM currency (it will turn green). " "Then click a different currency button to select " "your TO currency (it will turn blue).\n\n" "Supported currencies: NZD, JPY, USD, AUD.\n\n" "Enter the amount you wish to convert and press " "Convert. You cannot enter negative amounts.\n\n" "To change your currency selection, simply click " "any currency button to start again.\n\n" "Exchange rates are fetched live on startup. If no " "internet connection is available, the program will " "use built-in fallback rates automatically.\n\n" "To see your calculation history and export it to " "a text file, please click the 'History / Export' button.") self.help_text_label = Label(self.help_frame, text=help_text, wraplength=350, justify="left") self.help_text_label.grid(row=1, padx=10) self.dismiss_button = Button(self.help_frame, font=("Arial", "12", "bold"), text="Dismiss", bg="#CC6600", fg="#FFFFFF", command=partial(self.close_help, partner)) self.dismiss_button.grid(row=2, padx=10, pady=10) # List and loop to set background colour on everything except buttons recolour_list = [self.help_frame, self.help_heading_label, self.help_text_label] for item in recolour_list: item.config(bg=background) def close_help(self, partner): """ Closes help dialogue box (and enables help button). """ partner.to_help_button.config(state=NORMAL) self.help_box.destroy() class HistoryExport: """ Displays history dialogue box """ def __init__(self, partner, calculations): self.history_box = Toplevel() # Disable history button partner.to_history_button.config(state=DISABLED) # If users press cross at top, closes history and # 'releases' history button self.history_box.protocol("WM_DELETE_WINDOW", partial(self.close_history, partner)) self.history_frame = Frame(self.history_box) self.history_frame.grid() # Background colour and text for calculation area if len(calculations) <= c.MAX_CALCS: calc_back = "#D5E804" calc_amount = "all your" else: calc_back = "#ffe6cc" calc_amount = (f"your recent calculations - " f"showing {c.MAX_CALCS} / {len(calculations)}") recent_intro_txt = (f"Below are your {calc_amount} calculations.") # Create string from calculations list (newest first) newest_first_string = "" newest_first_list = list(reversed(calculations)) if len(newest_first_list) <= c.MAX_CALCS: for item in newest_first_list[:-1]: newest_first_string += item + "\n" newest_first_string += newest_first_list[-1] else: for item in newest_first_list[:c.MAX_CALCS - 1]: newest_first_string += item + "\n" newest_first_string += newest_first_list[c.MAX_CALCS - 1] export_instruction_txt = ("Please push to save your calculations in " "a file. If the filename already exists, it will be " "overwritten.") # Store full calculations list before resetting local variable for display self.calculations = calculations calculations = "" # Label list (label text | format | bg) history_labels_list = [ ["History / Export", ("Arial", "16", "bold"), None], [recent_intro_txt, ("Arial", "11"), None], [newest_first_string, ("Arial", "14"), calc_back], [export_instruction_txt, ("Arial", "11"), None] ] history_label_ref = [] for count, item in enumerate(history_labels_list): make_label = Label(self.history_box, text=item[0], font=item[1], bg=item[2], wraplength=300, justify="left", pady=10, padx=20) make_label.grid(row=count) history_label_ref.append(make_label) # Retrieve export instructions label to update after export self.export_filename_label = history_label_ref[3] # Make frame to hold buttons (two columns) self.hist_button_frame = Frame(self.history_box) self.hist_button_frame.grid(row=4) # button list (button text | bg colour | command | row | column) button_details_list = [ ["Export", "#004C99", lambda: self.export_data(self.calculations), 0, 0], ["Close", "#666666", partial(self.close_history, partner), 0, 1], ] for btn in button_details_list: self.make_button = Button(self.hist_button_frame, font=("Arial", "12", "bold"), text=btn[0], bg=btn[1], fg="#FFFFFF", width=12, command=btn[2]) self.make_button.grid(row=btn[3], column=btn[4], padx=10, pady=10) def export_data(self, calculations): """ Exports the calculation history to a dated text file. """ # Get current date for heading and filename today = date.today() day = today.strftime("%d") month = today.strftime("%m") year = today.strftime("%Y") file_name = f"currency_{year}_{month}_{day}" # Edit label so users know that their export has been done success_string = ("Export Successful! The file is called " f"{file_name}.txt") self.export_filename_label.config(fg="#009900", text=success_string, font=("Arial", "12", "bold")) write_to = f"{file_name}.txt" with open(write_to, "w") as text_file: text_file.write("***** Currency Conversion Calculations ******\n") text_file.write(f"Generated: {day}/{month}/{year}\n\n") text_file.write("Here is your calculation history (oldest to newest)... \n") # Write each calculation to file for item in calculations: text_file.write(item) text_file.write("\n") def close_history(self, partner): """ Closes history dialogue box (and enables history button). """ partner.to_history_button.config(state=NORMAL) self.history_box.destroy() # main routine if __name__ == "__main__": root = Tk() root.title("Currency Converter") Converter() root.mainloop()