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. User selects From and To currencies using toggle buttons, enters an amount and converts between 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 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) # Currency selection buttons frame self.currency_frame = Frame(self.main_frame) self.currency_frame.grid(row=2, pady=5) # Build one button per currency - store refs in dict for easy access 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=3) # Amount entry self.amount_entry = Entry(self.main_frame, font=("Arial", "14")) self.amount_entry.grid(row=4, 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=5) # Bottom buttons frame self.button_frame = Frame(self.main_frame) self.button_frame.grid(row=6) # 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 (green). Second click sets To currency (blue). If both already selected, resets and sets new 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 selected 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. Updates the answer label and stores the calculation for history/export. """ answer = cr.convert_currency(to_convert, self.from_currency, self.to_currency) 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" "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()