from tkinter import * from functools import partial import math import all_constants as c import conversion_rounding as cr from datetime import date class DialWidget: """ Custom rotary dial widget using Canvas. Drag up/down or left/right to change value. Scroll wheel also supported. """ def __init__(self, parent, min_val=0, max_val=500, initial=70, step=0.5, size=160, command=None): self.min_val = min_val self.max_val = max_val self.value = initial self.step = step self.size = size self.command = command # Drag tracking self._drag_start_y = None self._drag_start_val = None self.canvas = Canvas(parent, width=size, height=size, bg="#2b2b2b", highlightthickness=0) self.canvas.pack() self.canvas.bind("", self._on_press) self.canvas.bind("", self._on_drag) self.canvas.bind("", self._on_release) self.canvas.bind("", self._on_scroll) # Windows/macOS self.canvas.bind("", self._on_scroll) # Linux scroll up self.canvas.bind("", self._on_scroll) # Linux scroll down self._draw() def _draw(self): self.canvas.delete("all") cx = cy = self.size / 2 r = self.size / 2 - 10 # Outer ring self.canvas.create_oval(cx - r, cy - r, cx + r, cy + r, fill="#3a3a3a", outline="#555555", width=2) # Tick marks around the dial for i in range(0, 360, 30): angle = math.radians(i) inner = r - 8 outer = r - 2 x1 = cx + inner * math.sin(angle) y1 = cy - inner * math.cos(angle) x2 = cx + outer * math.sin(angle) y2 = cy - outer * math.cos(angle) self.canvas.create_line(x1, y1, x2, y2, fill="#666666", width=1) # Needle angle: map value across 270 degree arc (-135 to +135) fraction = (self.value - self.min_val) / (self.max_val - self.min_val) angle_deg = -135 + fraction * 270 angle_rad = math.radians(angle_deg) needle_len = r - 18 nx = cx + needle_len * math.sin(angle_rad) ny = cy - needle_len * math.cos(angle_rad) # Needle glow (thick, dark) self.canvas.create_line(cx, cy, nx, ny, fill="#004080", width=6, capstyle=ROUND) # Needle (bright) self.canvas.create_line(cx, cy, nx, ny, fill="#00aaff", width=2, capstyle=ROUND) # Centre hub hub_r = 8 self.canvas.create_oval(cx - hub_r, cy - hub_r, cx + hub_r, cy + hub_r, fill="#555555", outline="#888888", width=1) # Value text below centre self.canvas.create_text(cx, cy + r * 0.55, text=f"{self.value:.1f}", fill="#00aaff", font=("Courier", "14", "bold")) # Min / max labels self.canvas.create_text(10, self.size - 12, text=str(int(self.min_val)), fill="#888888", font=("Courier", "8")) self.canvas.create_text(self.size - 10, self.size - 12, text=str(int(self.max_val)), fill="#888888", font=("Courier", "8")) def _clamp(self, val): return max(self.min_val, min(self.max_val, val)) def _on_press(self, event): self._drag_start_y = event.y self._drag_start_val = self.value def _on_drag(self, event): if self._drag_start_y is None: return # Dragging UP increases value; pixels_per_unit controls sensitivity pixels_per_unit = 1.0 delta = (self._drag_start_y - event.y) * self.step / pixels_per_unit self.value = self._clamp(round(self._drag_start_val + delta, 1)) self._draw() if self.command: self.command(self.value) def _on_release(self, event): self._drag_start_y = None self._drag_start_val = None def _on_scroll(self, event): if event.num == 4 or event.delta > 0: self.value = self._clamp(round(self.value + self.step, 1)) else: self.value = self._clamp(round(self.value - self.step, 1)) self._draw() if self.command: self.command(self.value) def get(self): return self.value def set(self, val): self.value = self._clamp(round(val, 1)) self._draw() class Converter: """ Weight conversion tool (KG to LBS or LBS to KG) """ def __init__(self): self.all_calculations_list = [] self.weight_frame = Frame(padx=10, pady=10, bg="#2b2b2b") self.weight_frame.grid() self.weight_heading = Label(self.weight_frame, text="Weight Converter", font=("Courier", "16", "bold"), bg="#2b2b2b", fg="#00aaff") self.weight_heading.grid(row=0, pady=(10, 0)) instructions = ("Enter a weight using the dial below, then press " "a button to convert between Kilograms and Pounds.\n" "Drag the dial up/down or use your scroll wheel.") self.weight_instructions = Label(self.weight_frame, text=instructions, wraplength=280, width=40, justify="center", bg="#2b2b2b", fg="#aaaaaa", font=("Courier", "9")) self.weight_instructions.grid(row=1, pady=5) # --- Dial --- dial_frame = Frame(self.weight_frame, bg="#2b2b2b") dial_frame.grid(row=2, pady=10) self.dial = DialWidget(dial_frame, min_val=0, max_val=500, initial=70, step=0.5, size=180, command=self._on_dial_change) # Unit toggle label self.unit_label = Label(self.weight_frame, text="Unit: KG", font=("Courier", "11", "bold"), bg="#2b2b2b", fg="#ffaa00") self.unit_label.grid(row=3) # Answer / error label self.answer_error = Label(self.weight_frame, text="Adjust dial then convert", fg="#00aaff", font=("Courier", "12", "bold"), bg="#2b2b2b", wraplength=300, justify="center") self.answer_error.grid(row=4, pady=5) # Buttons self.button_frame = Frame(self.weight_frame, bg="#2b2b2b") self.button_frame.grid(row=5) button_details_list = [ ["To Kilograms", "#660099", lambda: self.check_weight("to_kg"), 0, 0], ["To Pounds", "#006600", lambda: self.check_weight("to_lbs"), 0, 1], ["Help / Info", "#CC6600", self.to_help, 1, 0], ["History / Export", "#004C99", self.to_history, 1, 1] ] self.button_ref_list = [] for item in button_details_list: btn = Button(self.button_frame, text=item[0], bg=item[1], fg="#FFFFFF", font=("Courier", "10", "bold"), width=14, command=item[2], relief=FLAT, activebackground="#333333", activeforeground="#ffffff") btn.grid(row=item[3], column=item[4], padx=5, pady=5) self.button_ref_list.append(btn) self.to_help_button = self.button_ref_list[2] self.to_history_button = self.button_ref_list[3] self.to_history_button.config(state=DISABLED) def _on_dial_change(self, value): """Called whenever dial moves — just resets any error styling.""" self.answer_error.config(fg="#00aaff") def check_weight(self, unit): to_convert = self.dial.get() max_weight = c.MAX_WEIGHT_KG if unit == "to_lbs" else c.MAX_WEIGHT_LBS to_lbs_error = (f"Value must be between {c.MIN_WEIGHT} and " f"{c.MAX_WEIGHT_KG} KG to convert to Pounds") to_kg_error = (f"Value must be between {c.MIN_WEIGHT} and " f"{c.MAX_WEIGHT_LBS} LBS to convert to Kilograms") if c.MIN_WEIGHT <= to_convert <= max_weight: # Update unit label unit_text = "KG" if unit == "to_lbs" else "LBS" self.unit_label.config(text=f"Unit: {unit_text}") self.convert(unit, to_convert) else: error = to_lbs_error if unit == "to_lbs" else to_kg_error self.answer_error.config(text=error, fg="#9C0000") def convert(self, unit, to_convert): if unit == "to_lbs": answer = cr.to_pounds(to_convert) answer_statement = f"{to_convert} KG → {answer} LBS" else: answer = cr.to_kilograms(to_convert) answer_statement = f"{to_convert} LBS → {answer} KG" self.to_history_button.config(state=NORMAL) self.answer_error.config(text=answer_statement, fg="#00ff99") self.all_calculations_list.append(answer_statement) print(self.all_calculations_list) def to_help(self): DisplayHelp(self) def to_history(self): HistoryExport(self, self.all_calculations_list) class DisplayHelp: def __init__(self, partner): background = "#ffe6cc" self.help_box = Toplevel() partner.to_help_button.config(state=DISABLED) 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, drag the dial up or down " "(or use your scroll wheel) to set the weight. " "Then press To Kilograms or To Pounds to convert.\n\n" "The minimum weight is 0 and the maximum is 500 " "KG / LBS. Weights outside this range will show an error.\n\n" "To see your calculation history and export it to a " "text file, press 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) for item in [self.help_frame, self.help_heading_label, self.help_text_label]: item.config(bg=background) def close_help(self, partner): 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() partner.to_history_button.config(state=DISABLED) self.history_box.protocol('WM_DELETE_WINDOW', partial(self.close_history, partner)) self.history_frame = Frame(self.history_box) self.history_frame.grid() if len(calculations) <= c.MAX_CALCS: calc_back = "#D5E804" intro_txt = ("Below are all your calculations. " "All calculations are shown to two decimal places.") else: calc_back = "#ffe6cc" intro_txt = (f"Below are your recent calculations - showing last " f"{c.MAX_CALCS} / {len(calculations)} calculations. " f"All calculations are shown to two decimal places.") # Build display string (newest first, max MAX_CALCS) newest_first_list = list(reversed(calculations)) display_list = newest_first_list[:c.MAX_CALCS] newest_first_string = "\n".join(display_list) export_instruction_txt = ("Press to save your calculations " "to a text file. If the filename already " "exists, it will be overwritten.") history_labels_list = [ ["History / Export", ("Arial", "16", "bold"), None], [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) self.export_filename_label = history_label_ref[3] self.hist_button_frame = Frame(self.history_box) self.hist_button_frame.grid(row=4) button_details_list = [ ["Export", "#004C99", lambda: self.export_history(calculations), 0, 0], ["Close", "#666666", partial(self.close_history, partner), 0, 1], ] for btn in button_details_list: Button(self.hist_button_frame, font=("Arial", "12", "bold"), text=btn[0], bg=btn[1], fg="#FFFFFF", width=12, command=btn[2]).grid(row=btn[3], column=btn[4], padx=10, pady=10) def export_history(self, calculations): today = date.today() day = today.strftime("%d") month = today.strftime("%m") year = today.strftime("%Y") file_name = f"weight_calculations_{day}_{month}_{year}" write_to = f"{file_name}.txt" success_string = (f"Export successful! File saved as {file_name}.txt") self.export_filename_label.config(bg="#009900", text=success_string, font=("Arial", "12", "bold")) with open(write_to, 'w') as text_file: text_file.write("***** Weight Calculations *****\n") text_file.write(f"Generated: {day}/{month}/{year}\n\n") text_file.write("Calculation history (oldest to newest):\n\n") for item in calculations: text_file.write(item + "\n") def close_history(self, partner): partner.to_history_button.config(state=NORMAL) self.history_box.destroy() # main routine if __name__ == "__main__": root = Tk() root.title("Weight Converter") root.configure(bg="#2b2b2b") Converter() root.mainloop()