Fixed a bug where window was not big enough to view text/labels on Windows OS.
This commit is contained in:
+75
-82
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# Description: A Python-GUI interface for the HAM Operator Test.
|
# Description: A Python GUI interface for the HAM Operator Test.
|
||||||
# Usage: python3 ham-test-gui.py
|
# Usage: python3 ham-test-gui.py
|
||||||
# Author: Justin Oros
|
# Author: Justin Oros
|
||||||
# Source: https://github.com/JustinOros
|
# Source: https://github.com/JustinOros
|
||||||
@@ -13,10 +13,10 @@ from datetime import datetime
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
# --- Setup logging ---
|
# Setup logging
|
||||||
logging.basicConfig(level=logging.WARNING, format='[%(levelname)s] %(message)s')
|
logging.basicConfig(level=logging.WARNING, format='[%(levelname)s] %(message)s')
|
||||||
|
|
||||||
# --- URLs and Local file paths ---
|
# URLs and Local file paths
|
||||||
URLS = {
|
URLS = {
|
||||||
"Technician": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/technician-2022-2026/technician.json",
|
"Technician": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/technician-2022-2026/technician.json",
|
||||||
"General": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/general-2023-2027/general.json",
|
"General": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/general-2023-2027/general.json",
|
||||||
@@ -29,7 +29,7 @@ LOCAL_FILES = {
|
|||||||
"Extra": "extra.json"
|
"Extra": "extra.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Helper functions to sync question files ---
|
# Helper functions to sync question files
|
||||||
|
|
||||||
def get_remote_last_modified(url):
|
def get_remote_last_modified(url):
|
||||||
try:
|
try:
|
||||||
@@ -81,15 +81,16 @@ def load_questions(test_name):
|
|||||||
with open(local_path, "r", encoding='utf-8') as f:
|
with open(local_path, "r", encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
# --- GUI Classes ---
|
# Main GUI Class
|
||||||
|
|
||||||
class QuizGUI:
|
class HamTestGUI:
|
||||||
def __init__(self, master, questions):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.master.title("HAM Radio Quiz")
|
self.master.title("HAM Radio Test")
|
||||||
self.questions = questions
|
self.center_window(800, 300)
|
||||||
random.shuffle(self.questions)
|
self.master.resizable(False, False)
|
||||||
|
|
||||||
|
self.questions = []
|
||||||
self.score = 0
|
self.score = 0
|
||||||
self.total = 0
|
self.total = 0
|
||||||
self.current_index = -1
|
self.current_index = -1
|
||||||
@@ -97,36 +98,57 @@ class QuizGUI:
|
|||||||
self.next_pending = False
|
self.next_pending = False
|
||||||
self.correct_answer_text = ""
|
self.correct_answer_text = ""
|
||||||
|
|
||||||
self.feedback_label = tk.Label(master, text="", font=("Arial", 14), wraplength=800, justify="left")
|
# Container frame for dynamic content
|
||||||
self.feedback_label.pack(pady=5)
|
self.content_frame = tk.Frame(self.master)
|
||||||
|
self.content_frame.pack(fill="both", expand=True)
|
||||||
|
|
||||||
self.question_label = tk.Label(master, text="", wraplength=600, justify="left", font=("Arial", 12))
|
self.show_test_selection()
|
||||||
self.question_label.pack(padx=10, pady=10)
|
|
||||||
|
|
||||||
self.selected_answer = tk.StringVar()
|
def center_window(self, width=800, height=600):
|
||||||
self.answers_frame = tk.Frame(master)
|
self.master.update_idletasks()
|
||||||
self.answers_frame.pack(pady=5)
|
screen_width = self.master.winfo_screenwidth()
|
||||||
|
screen_height = self.master.winfo_screenheight()
|
||||||
|
x = (screen_width // 2) - (width // 2)
|
||||||
|
y = (screen_height // 2) - (height // 2)
|
||||||
|
self.master.geometry(f"{width}x{height}+{x}+{y}")
|
||||||
|
|
||||||
nav_frame = tk.Frame(master)
|
def clear_content(self):
|
||||||
nav_frame.pack(pady=10)
|
for widget in self.content_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
self.quit_button = tk.Button(nav_frame, text="Quit", command=self.quit_quiz)
|
# Test selection UI
|
||||||
self.quit_button.pack(side=tk.LEFT, padx=5)
|
def show_test_selection(self):
|
||||||
|
self.clear_content()
|
||||||
|
|
||||||
self.next_button = tk.Button(nav_frame, text="Next", command=self.check_answer)
|
label = tk.Label(self.content_frame, text="Select the test you want to take:", font=("Arial", 14))
|
||||||
self.next_button.pack(side=tk.LEFT, padx=5)
|
label.pack(padx=10, pady=10)
|
||||||
|
|
||||||
self.load_next_question()
|
for test_name in URLS.keys():
|
||||||
|
btn = tk.Button(self.content_frame, text=test_name, width=20, font=("Arial", 12),
|
||||||
|
command=lambda tn=test_name: self.start_test(tn))
|
||||||
|
btn.pack(pady=5)
|
||||||
|
|
||||||
def resize_to_fit_text(self, text):
|
def start_test(self, test_name):
|
||||||
avg_char_width = 7
|
print(f"Selected test: {test_name}")
|
||||||
padding = 60
|
print("Syncing question files...")
|
||||||
max_width = 1000
|
sync_files()
|
||||||
min_width = 400
|
|
||||||
estimated_width = min(max(len(text) * avg_char_width + padding, min_width), max_width)
|
|
||||||
self.master.geometry(f"{estimated_width}x400")
|
|
||||||
|
|
||||||
def load_next_question(self):
|
questions = load_questions(test_name)
|
||||||
|
if not questions:
|
||||||
|
messagebox.showerror("Error", f"No questions found for {test_name}.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.questions = questions
|
||||||
|
random.shuffle(self.questions)
|
||||||
|
self.score = 0
|
||||||
|
self.total = 0
|
||||||
|
self.current_index = -1
|
||||||
|
|
||||||
|
self.show_question()
|
||||||
|
|
||||||
|
# Quiz UI
|
||||||
|
def show_question(self):
|
||||||
|
self.clear_content()
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
|
|
||||||
if self.current_index >= len(self.questions):
|
if self.current_index >= len(self.questions):
|
||||||
@@ -135,20 +157,20 @@ class QuizGUI:
|
|||||||
self.master.destroy()
|
self.master.destroy()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.feedback_label.config(text="")
|
self.feedback_label = tk.Label(self.content_frame, text="", font=("Arial", 14), wraplength=780, justify="left")
|
||||||
self.selected_answer.set(None)
|
self.feedback_label.pack(pady=5)
|
||||||
|
|
||||||
for widget in self.answers_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
q = self.questions[self.current_index]
|
q = self.questions[self.current_index]
|
||||||
question_text = q.get("question")
|
question_text = q.get("question")
|
||||||
answers = q.get("answers", [])
|
answers = q.get("answers", [])
|
||||||
correct_index = q.get("correct")
|
correct_index = q.get("correct")
|
||||||
|
|
||||||
width = min(max(400, len(question_text) * 7), 800)
|
self.question_label = tk.Label(self.content_frame, text=question_text, wraplength=780, justify="left", font=("Arial", 12))
|
||||||
self.master.geometry(f"{width}x400")
|
self.question_label.pack(padx=10, pady=10)
|
||||||
self.question_label.config(text=question_text)
|
|
||||||
|
self.selected_answer = tk.StringVar()
|
||||||
|
self.answers_frame = tk.Frame(self.content_frame)
|
||||||
|
self.answers_frame.pack(pady=5)
|
||||||
|
|
||||||
options = list(enumerate(answers))
|
options = list(enumerate(answers))
|
||||||
random.shuffle(options)
|
random.shuffle(options)
|
||||||
@@ -161,7 +183,7 @@ class QuizGUI:
|
|||||||
text=ans_text,
|
text=ans_text,
|
||||||
variable=self.selected_answer,
|
variable=self.selected_answer,
|
||||||
value=ans_text,
|
value=ans_text,
|
||||||
wraplength=width - 50,
|
wraplength=760,
|
||||||
justify="left",
|
justify="left",
|
||||||
anchor="w",
|
anchor="w",
|
||||||
padx=10,
|
padx=10,
|
||||||
@@ -172,6 +194,15 @@ class QuizGUI:
|
|||||||
self.correct_answer = ans_text
|
self.correct_answer = ans_text
|
||||||
self.correct_answer_text = ans_text
|
self.correct_answer_text = ans_text
|
||||||
|
|
||||||
|
nav_frame = tk.Frame(self.content_frame)
|
||||||
|
nav_frame.pack(pady=10)
|
||||||
|
|
||||||
|
self.quit_button = tk.Button(nav_frame, text="Quit", command=self.quit_quiz)
|
||||||
|
self.quit_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.next_button = tk.Button(nav_frame, text="Next", command=self.check_answer)
|
||||||
|
self.next_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
self.next_pending = False
|
self.next_pending = False
|
||||||
|
|
||||||
def check_answer(self):
|
def check_answer(self):
|
||||||
@@ -191,57 +222,19 @@ class QuizGUI:
|
|||||||
else:
|
else:
|
||||||
feedback_text = f"Incorrect! The correct answer was: {self.correct_answer_text}"
|
feedback_text = f"Incorrect! The correct answer was: {self.correct_answer_text}"
|
||||||
self.feedback_label.config(text=feedback_text, fg="red")
|
self.feedback_label.config(text=feedback_text, fg="red")
|
||||||
self.resize_to_fit_text(feedback_text)
|
|
||||||
delay = 3500 # 3.5 sec
|
delay = 3500 # 3.5 sec
|
||||||
|
|
||||||
self.next_pending = True
|
self.next_pending = True
|
||||||
self.master.after(delay, self.load_next_question)
|
self.master.after(delay, self.show_question)
|
||||||
|
|
||||||
def quit_quiz(self):
|
def quit_quiz(self):
|
||||||
pct = (self.score / self.total) * 100 if self.total > 0 else 0
|
pct = (self.score / self.total) * 100 if self.total > 0 else 0
|
||||||
messagebox.showinfo("Quit Quiz", f"Questions answered: {self.total}\nCorrect: {self.score}\nPercentage: {pct:.2f}%")
|
messagebox.showinfo("Quit Quiz", f"Questions answered: {self.total}\nCorrect: {self.score}\nPercentage: {int(pct)}%")
|
||||||
self.master.destroy()
|
self.master.destroy()
|
||||||
|
|
||||||
|
|
||||||
class TestSelectGUI:
|
|
||||||
def __init__(self, master):
|
|
||||||
self.master = master
|
|
||||||
self.master.title("Select Test")
|
|
||||||
self.selected_test = None
|
|
||||||
|
|
||||||
label = tk.Label(master, text="Select the test you want to take:", font=("Arial", 14))
|
|
||||||
label.pack(padx=10, pady=10)
|
|
||||||
|
|
||||||
for test_name in URLS.keys():
|
|
||||||
btn = tk.Button(master, text=test_name, width=20, command=lambda tn=test_name: self.select_test(tn))
|
|
||||||
btn.pack(pady=5)
|
|
||||||
|
|
||||||
def select_test(self, test_name):
|
|
||||||
self.selected_test = test_name
|
|
||||||
self.master.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
select_gui = TestSelectGUI(root)
|
app = HamTestGUI(root)
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
test_name = select_gui.selected_test
|
|
||||||
if test_name is None:
|
|
||||||
print("No test selected. Exiting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Selected test: {test_name}")
|
|
||||||
print("Syncing question files...")
|
|
||||||
sync_files()
|
|
||||||
|
|
||||||
questions = load_questions(test_name)
|
|
||||||
if not questions:
|
|
||||||
print("No questions found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
root = tk.Tk()
|
|
||||||
app = QuizGUI(root, questions)
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user