feat: Add Quiz Application
This commit is contained in:
parent
0bb9a452a7
commit
63fc0244d7
7
maths/.gitignore
vendored
Normal file
7
maths/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Local development files (uv)
|
||||
quiz_questions.json
|
||||
pyproject.toml
|
||||
.venv
|
||||
.python-version
|
||||
|
||||
# As this code is for internal review only, dependencies will not be included.
|
||||
3
maths/README.md
Normal file
3
maths/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# All parts of this folder are subject to the Will of Steel Proprietary License Agreement. For more info visit https://willofsteel.me/license
|
||||
|
||||
Last updated: 28-06-2025
|
||||
371
maths/db.py
Normal file
371
maths/db.py
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
#
|
||||
# Will of Steel Proprietary License (WOSPL)
|
||||
#
|
||||
# Copyright (c) 2025-present Will of Steel
|
||||
#
|
||||
# This software and its accompanying source code ("the Software") are proprietary and confidential. Unauthorized actions are strictly prohibited. Specifically, you are not permitted to:
|
||||
#
|
||||
# - Use the Software for any purpose other than internal review, unless explicitly authorized in writing by the Licensor.
|
||||
# - Modify, adapt, translate, or create derivative works based on the Software.
|
||||
# - Distribute, disclose, sublicense, lease, rent, or otherwise make available the Software to any third party.
|
||||
# - Reverse engineer, decompile, disassemble, or attempt to derive the source code, object code, or underlying structure, ideas, or algorithms of the Software.
|
||||
# - Remove, alter, or obscure any proprietary notices, labels, or marks on the Software.
|
||||
# - Use the Software for any commercial purposes or in any commercial environment.
|
||||
# - Claim ownership or authorship of the Software or any part thereof.
|
||||
# - Use the Software in any manner that violates applicable laws or regulations.
|
||||
#
|
||||
# The Software is provided "as is" without warranty of any kind, express or implied. The Licensor shall not be liable for any damages arising out of or in connection with the use or performance of the Software.
|
||||
#
|
||||
# Any violation of these terms will result in immediate termination of your rights to use the Software and may subject you to legal action.
|
||||
#
|
||||
# For permissions beyond the scope of this license, please contact neil@willofsteel.me
|
||||
# For more information, please visit https://willofsteel.me/license
|
||||
|
||||
# This file is in compliance with Pep 8 and PEP 257 standards. -- Neil 28-06-2025
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class QuestionManager:
|
||||
def __init__(self, filename: str = "quiz_questions.json"):
|
||||
self.filename = filename
|
||||
self.questions = []
|
||||
self.load_questions()
|
||||
|
||||
def load_questions(self):
|
||||
"""Load questions from JSON file"""
|
||||
if os.path.exists(self.filename):
|
||||
try:
|
||||
with open(self.filename, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
self.questions = data.get("questions", [])
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: Invalid JSON format in {self.filename}")
|
||||
self.questions = []
|
||||
else:
|
||||
print(f"File {self.filename} not found. Starting with empty question list.")
|
||||
self.questions = []
|
||||
|
||||
def save_questions(self):
|
||||
"""Save questions to JSON file"""
|
||||
data = {"questions": self.questions}
|
||||
try:
|
||||
with open(self.filename, "w", encoding="utf-8") as file:
|
||||
json.dump(data, file, indent=2, ensure_ascii=False)
|
||||
print(f"Questions saved to {self.filename}")
|
||||
except Exception as e:
|
||||
print(f"Error saving questions: {e}")
|
||||
|
||||
def add_question(self):
|
||||
"""Add a new question interactively"""
|
||||
print("\n" + "=" * 50)
|
||||
print("ADD NEW QUESTION")
|
||||
print("=" * 50)
|
||||
|
||||
question_text = input("Enter the question: ").strip()
|
||||
if not question_text:
|
||||
print("Question cannot be empty!")
|
||||
return
|
||||
|
||||
# Ask for question image
|
||||
question_image = input(
|
||||
"Enter question image path (optional, press Enter to skip): "
|
||||
).strip()
|
||||
if question_image and not os.path.exists(question_image):
|
||||
print(
|
||||
f"Warning: Image file '{question_image}' not found. Continuing without image."
|
||||
)
|
||||
question_image = ""
|
||||
|
||||
print("\nEnter 4 answer options:")
|
||||
options = []
|
||||
for i in range(4):
|
||||
option = input(f"Option {i+1}: ").strip()
|
||||
if not option:
|
||||
print("Option cannot be empty!")
|
||||
return
|
||||
options.append(option)
|
||||
|
||||
print("\nOptions:")
|
||||
for i, option in enumerate(options, 1):
|
||||
print(f"{i}. {option}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
correct_num = int(input("\nWhich option is correct? (1-4): "))
|
||||
if 1 <= correct_num <= 4:
|
||||
correct_answer = options[correct_num - 1]
|
||||
break
|
||||
else:
|
||||
print("Please enter a number between 1 and 4.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
explanation = input("Enter explanation (optional): ").strip()
|
||||
|
||||
# Ask for explanation image
|
||||
explanation_image = input(
|
||||
"Enter explanation image path (optional, press Enter to skip): "
|
||||
).strip()
|
||||
if explanation_image and not os.path.exists(explanation_image):
|
||||
print(
|
||||
f"Warning: Image file '{explanation_image}' not found. Continuing without image."
|
||||
)
|
||||
explanation_image = ""
|
||||
|
||||
# Create question object
|
||||
new_question = {
|
||||
"question": question_text,
|
||||
"options": options,
|
||||
"correct_answer": correct_answer,
|
||||
}
|
||||
|
||||
if question_image:
|
||||
new_question["question_image"] = question_image
|
||||
|
||||
if explanation:
|
||||
new_question["explanation"] = explanation
|
||||
|
||||
if explanation_image:
|
||||
new_question["explanation_image"] = explanation_image
|
||||
|
||||
self.questions.append(new_question)
|
||||
print(
|
||||
f"\n✅ Question added successfully! Total questions: {len(self.questions)}"
|
||||
)
|
||||
|
||||
print("\nQuestion Summary:")
|
||||
print(f"Question: {question_text}")
|
||||
if question_image:
|
||||
print(f"Question Image: {question_image}")
|
||||
print(f"Correct Answer: {correct_answer}")
|
||||
if explanation:
|
||||
print(f"Explanation: {explanation}")
|
||||
if explanation_image:
|
||||
print(f"Explanation Image: {explanation_image}")
|
||||
|
||||
def view_questions(self):
|
||||
"""Display all questions"""
|
||||
if not self.questions:
|
||||
print("\nNo questions available.")
|
||||
return
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"ALL QUESTIONS ({len(self.questions)} total)")
|
||||
print("=" * 50)
|
||||
|
||||
for i, q in enumerate(self.questions, 1):
|
||||
print(f"\n{i}. {q['question']}")
|
||||
if q.get("question_image"):
|
||||
print(f" 📷 Question Image: {q['question_image']}")
|
||||
for j, option in enumerate(q["options"], 1):
|
||||
marker = "✅" if option == q["correct_answer"] else " "
|
||||
print(f" {marker} {j}. {option}")
|
||||
if "explanation" in q:
|
||||
print(f" 💡 Explanation: {q['explanation']}")
|
||||
if q.get("explanation_image"):
|
||||
print(f" 📷 Explanation Image: {q['explanation_image']}")
|
||||
print("-" * 40)
|
||||
|
||||
def edit_question(self):
|
||||
"""Edit an existing question"""
|
||||
if not self.questions:
|
||||
print("\nNo questions available to edit.")
|
||||
return
|
||||
|
||||
self.view_questions()
|
||||
|
||||
while True:
|
||||
try:
|
||||
question_num = int(
|
||||
input(
|
||||
f"\nEnter question number to edit (1-{len(self.questions)}): "
|
||||
)
|
||||
)
|
||||
if 1 <= question_num <= len(self.questions):
|
||||
break
|
||||
else:
|
||||
print(f"Please enter a number between 1 and {len(self.questions)}.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
question_index = question_num - 1
|
||||
old_question = self.questions[question_index]
|
||||
|
||||
print(f"\nEditing question {question_num}:")
|
||||
print(f"Current: {old_question['question']}")
|
||||
|
||||
new_question_text = input(
|
||||
"Enter new question (press Enter to keep current): "
|
||||
).strip()
|
||||
if new_question_text:
|
||||
old_question["question"] = new_question_text
|
||||
|
||||
# Ask about question image
|
||||
current_q_image = old_question.get("question_image", "")
|
||||
print(
|
||||
f"\nCurrent question image: {current_q_image if current_q_image else 'None'}"
|
||||
)
|
||||
new_q_image = input(
|
||||
"Enter new question image path (press Enter to keep current, 'none' to remove): "
|
||||
).strip()
|
||||
if new_q_image.lower() == "none":
|
||||
old_question.pop("question_image", None)
|
||||
elif new_q_image:
|
||||
if os.path.exists(new_q_image):
|
||||
old_question["question_image"] = new_q_image
|
||||
else:
|
||||
print(f"Warning: Image file '{new_q_image}' not found.")
|
||||
|
||||
print("\nCurrent options:")
|
||||
for i, option in enumerate(old_question["options"], 1):
|
||||
marker = "✅" if option == old_question["correct_answer"] else " "
|
||||
print(f"{marker} {i}. {option}")
|
||||
|
||||
edit_options = input("\nEdit options? (y/n): ").lower().strip()
|
||||
if edit_options == "y":
|
||||
new_options = []
|
||||
for i in range(4):
|
||||
current = (
|
||||
old_question["options"][i]
|
||||
if i < len(old_question["options"])
|
||||
else ""
|
||||
)
|
||||
new_option = input(f"Option {i+1} (current: '{current}'): ").strip()
|
||||
if new_option:
|
||||
new_options.append(new_option)
|
||||
else:
|
||||
new_options.append(current)
|
||||
|
||||
old_question["options"] = new_options
|
||||
|
||||
print("\nNew options:")
|
||||
for i, option in enumerate(new_options, 1):
|
||||
print(f"{i}. {option}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
correct_num = int(input("\nWhich option is correct? (1-4): "))
|
||||
if 1 <= correct_num <= 4:
|
||||
old_question["correct_answer"] = new_options[correct_num - 1]
|
||||
break
|
||||
else:
|
||||
print("Please enter a number between 1 and 4.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
current_explanation = old_question.get("explanation", "")
|
||||
print(f"\nCurrent explanation: {current_explanation}")
|
||||
new_explanation = input(
|
||||
"Enter new explanation (press Enter to keep current): "
|
||||
).strip()
|
||||
if new_explanation:
|
||||
old_question["explanation"] = new_explanation
|
||||
|
||||
# Ask about explanation image
|
||||
current_exp_image = old_question.get("explanation_image", "")
|
||||
print(
|
||||
f"\nCurrent explanation image: {current_exp_image if current_exp_image else 'None'}"
|
||||
)
|
||||
new_exp_image = input(
|
||||
"Enter new explanation image path (press Enter to keep current, 'none' to remove): "
|
||||
).strip()
|
||||
if new_exp_image.lower() == "none":
|
||||
old_question.pop("explanation_image", None)
|
||||
elif new_exp_image:
|
||||
if os.path.exists(new_exp_image):
|
||||
old_question["explanation_image"] = new_exp_image
|
||||
else:
|
||||
print(f"Warning: Image file '{new_exp_image}' not found.")
|
||||
|
||||
print("✅ Question updated successfully!")
|
||||
|
||||
def delete_question(self):
|
||||
"""Delete a question"""
|
||||
if not self.questions:
|
||||
print("\nNo questions available to delete.")
|
||||
return
|
||||
|
||||
self.view_questions()
|
||||
|
||||
while True:
|
||||
try:
|
||||
question_num = int(
|
||||
input(
|
||||
f"\nEnter question number to delete (1-{len(self.questions)}): "
|
||||
)
|
||||
)
|
||||
if 1 <= question_num <= len(self.questions):
|
||||
break
|
||||
else:
|
||||
print(f"Please enter a number between 1 and {len(self.questions)}.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
question_index = question_num - 1
|
||||
question_to_delete = self.questions[question_index]
|
||||
|
||||
print(f"\nQuestion to delete:")
|
||||
print(f"{question_to_delete['question']}")
|
||||
if question_to_delete.get("question_image"):
|
||||
print(f"Question Image: {question_to_delete['question_image']}")
|
||||
if question_to_delete.get("explanation"):
|
||||
print(f"Explanation: {question_to_delete['explanation']}")
|
||||
if question_to_delete.get("explanation_image"):
|
||||
print(f"Explanation Image: {question_to_delete['explanation_image']}")
|
||||
|
||||
confirm = (
|
||||
input("\nAre you sure you want to delete this question? (y/n): ")
|
||||
.lower()
|
||||
.strip()
|
||||
)
|
||||
if confirm == "y":
|
||||
self.questions.pop(question_index)
|
||||
print("✅ Question deleted successfully!")
|
||||
else:
|
||||
print("Delete cancelled.")
|
||||
|
||||
def run(self):
|
||||
"""Main menu loop"""
|
||||
while True:
|
||||
print(f"\n{'='*50}")
|
||||
print("QUIZ QUESTION MANAGER")
|
||||
print("=" * 50)
|
||||
print(f"Current file: {self.filename}")
|
||||
print(f"Total questions: {len(self.questions)}")
|
||||
print("\nOptions:")
|
||||
print("1. Add new question")
|
||||
print("2. View all questions")
|
||||
print("3. Edit question")
|
||||
print("4. Delete question")
|
||||
print("5. Save questions")
|
||||
print("6. Exit")
|
||||
|
||||
choice = input("\nEnter your choice (1-6): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
self.add_question()
|
||||
elif choice == "2":
|
||||
self.view_questions()
|
||||
elif choice == "3":
|
||||
self.edit_question()
|
||||
elif choice == "4":
|
||||
self.delete_question()
|
||||
elif choice == "5":
|
||||
self.save_questions()
|
||||
elif choice == "6":
|
||||
save_choice = (
|
||||
input("Save changes before exiting? (y/n): ").lower().strip()
|
||||
)
|
||||
if save_choice == "y":
|
||||
self.save_questions()
|
||||
print("Goodbye!")
|
||||
break
|
||||
else:
|
||||
print("Invalid choice. Please enter a number between 1 and 6.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = QuestionManager()
|
||||
manager.run()
|
||||
415
maths/main.py
Normal file
415
maths/main.py
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
#
|
||||
# Will of Steel Proprietary License (WOSPL)
|
||||
#
|
||||
# Copyright (c) 2025-present Will of Steel
|
||||
#
|
||||
# This software and its accompanying source code ("the Software") are proprietary and confidential. Unauthorized actions are strictly prohibited. Specifically, you are not permitted to:
|
||||
#
|
||||
# - Use the Software for any purpose other than internal review, unless explicitly authorized in writing by the Licensor.
|
||||
# - Modify, adapt, translate, or create derivative works based on the Software.
|
||||
# - Distribute, disclose, sublicense, lease, rent, or otherwise make available the Software to any third party.
|
||||
# - Reverse engineer, decompile, disassemble, or attempt to derive the source code, object code, or underlying structure, ideas, or algorithms of the Software.
|
||||
# - Remove, alter, or obscure any proprietary notices, labels, or marks on the Software.
|
||||
# - Use the Software for any commercial purposes or in any commercial environment.
|
||||
# - Claim ownership or authorship of the Software or any part thereof.
|
||||
# - Use the Software in any manner that violates applicable laws or regulations.
|
||||
#
|
||||
# The Software is provided "as is" without warranty of any kind, express or implied. The Licensor shall not be liable for any damages arising out of or in connection with the use or performance of the Software.
|
||||
#
|
||||
# Any violation of these terms will result in immediate termination of your rights to use the Software and may subject you to legal action.
|
||||
#
|
||||
# For permissions beyond the scope of this license, please contact neil@willofsteel.me
|
||||
# For more information, please visit https://willofsteel.me/license
|
||||
|
||||
# This file is in compliance with Pep 8 and PEP 257 standards. -- Neil 28-06-2025
|
||||
|
||||
import customtkinter as ctk
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from tkinter import messagebox
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class QuizApp:
|
||||
def __init__(self):
|
||||
ctk.set_appearance_mode("dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
self.root = ctk.CTk()
|
||||
self.root.title("Python Quiz Application")
|
||||
self.root.geometry("1000x800")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
self.questions = []
|
||||
self.current_question = 0
|
||||
self.score = 0
|
||||
self.selected_answer = ctk.StringVar()
|
||||
|
||||
self.load_questions()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
if self.questions:
|
||||
self.display_question()
|
||||
|
||||
def load_questions(self):
|
||||
"""Load questions from JSON file"""
|
||||
try:
|
||||
with open("quiz_questions.json", "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
self.questions = data.get("questions", [])
|
||||
random.shuffle(self.questions)
|
||||
except FileNotFoundError:
|
||||
self.load_questions()
|
||||
except json.JSONDecodeError:
|
||||
messagebox.showerror("Error", "Invalid JSON format in quiz_questions.json")
|
||||
self.questions = []
|
||||
|
||||
def load_image(self, image_path, max_width=500, max_height=400):
|
||||
"""Load and resize an image"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
print(f"Image not found: {image_path}")
|
||||
return None
|
||||
|
||||
pil_image = Image.open(image_path)
|
||||
|
||||
width_ratio = max_width / pil_image.width
|
||||
height_ratio = max_height / pil_image.height
|
||||
ratio = min(width_ratio, height_ratio)
|
||||
|
||||
new_width = int(pil_image.width * ratio)
|
||||
new_height = int(pil_image.height * ratio)
|
||||
|
||||
pil_image = pil_image.resize(
|
||||
(new_width, new_height), Image.Resampling.LANCZOS
|
||||
)
|
||||
|
||||
ctk_image = ctk.CTkImage(
|
||||
light_image=pil_image,
|
||||
dark_image=pil_image,
|
||||
size=(new_width, new_height),
|
||||
)
|
||||
return ctk_image
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading image {image_path}: {e}")
|
||||
return None
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create and arrange GUI widgets with scrollable content"""
|
||||
self.scrollable_frame = ctk.CTkScrollableFrame(self.root, width=950, height=750)
|
||||
self.scrollable_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
self.title_label = ctk.CTkLabel(
|
||||
self.scrollable_frame,
|
||||
text="Python Quiz",
|
||||
font=ctk.CTkFont(size=28, weight="bold"),
|
||||
)
|
||||
self.title_label.pack(pady=(20, 10))
|
||||
|
||||
self.progress_label = ctk.CTkLabel(
|
||||
self.scrollable_frame, text="", font=ctk.CTkFont(size=14)
|
||||
)
|
||||
self.progress_label.pack(pady=(0, 20))
|
||||
|
||||
self.question_frame = ctk.CTkFrame(self.scrollable_frame)
|
||||
self.question_frame.pack(fill="x", padx=20, pady=(0, 20))
|
||||
|
||||
self.question_label = ctk.CTkLabel(
|
||||
self.question_frame,
|
||||
text="",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
wraplength=800,
|
||||
justify="left",
|
||||
)
|
||||
self.question_label.pack(pady=(20, 10), padx=20)
|
||||
|
||||
self.question_image_label = ctk.CTkLabel(
|
||||
self.question_frame, text="", image=None
|
||||
)
|
||||
|
||||
self.options_frame = ctk.CTkFrame(self.scrollable_frame)
|
||||
self.options_frame.pack(fill="x", padx=20, pady=(0, 20))
|
||||
|
||||
self.option_buttons = []
|
||||
|
||||
self.feedback_frame = ctk.CTkFrame(self.scrollable_frame)
|
||||
|
||||
self.feedback_label = ctk.CTkLabel(
|
||||
self.feedback_frame,
|
||||
text="",
|
||||
font=ctk.CTkFont(size=14),
|
||||
wraplength=800,
|
||||
justify="left",
|
||||
anchor="w",
|
||||
)
|
||||
self.feedback_label.pack(pady=(15, 5), padx=20, fill="x")
|
||||
|
||||
self.feedback_image_label = ctk.CTkLabel(
|
||||
self.feedback_frame, text="", image=None
|
||||
)
|
||||
|
||||
self.buttons_frame = ctk.CTkFrame(self.scrollable_frame)
|
||||
self.buttons_frame.pack(fill="x", padx=20, pady=(0, 20))
|
||||
|
||||
self.submit_button = ctk.CTkButton(
|
||||
self.buttons_frame,
|
||||
text="Submit Answer",
|
||||
command=self.submit_answer,
|
||||
font=ctk.CTkFont(size=14, weight="bold"),
|
||||
height=40,
|
||||
)
|
||||
self.submit_button.pack(side="left", padx=(20, 10), pady=20)
|
||||
|
||||
self.next_button = ctk.CTkButton(
|
||||
self.buttons_frame,
|
||||
text="Next Question",
|
||||
command=self.next_question,
|
||||
font=ctk.CTkFont(size=14, weight="bold"),
|
||||
height=40,
|
||||
state="disabled",
|
||||
)
|
||||
self.next_button.pack(side="left", padx=10, pady=20)
|
||||
|
||||
self.restart_button = ctk.CTkButton(
|
||||
self.buttons_frame,
|
||||
text="Restart Quiz",
|
||||
command=self.restart_quiz,
|
||||
font=ctk.CTkFont(size=14, weight="bold"),
|
||||
height=40,
|
||||
)
|
||||
self.restart_button.pack(side="right", padx=(10, 20), pady=20)
|
||||
|
||||
self.score_label = ctk.CTkLabel(
|
||||
self.scrollable_frame,
|
||||
text="Score: 0/0",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
)
|
||||
self.score_label.pack(pady=(0, 20))
|
||||
|
||||
def display_question(self):
|
||||
"""Display the current question and options"""
|
||||
if self.current_question < len(self.questions):
|
||||
question_data = self.questions[self.current_question]
|
||||
|
||||
self.progress_label.configure(
|
||||
text=f"Question {self.current_question + 1} of {len(self.questions)}"
|
||||
)
|
||||
|
||||
self.question_label.configure(text=question_data["question"])
|
||||
|
||||
self.question_image_label.pack_forget()
|
||||
if "question_image" in question_data:
|
||||
image = self.load_image(question_data["question_image"])
|
||||
if image:
|
||||
self.question_image_label.configure(image=image)
|
||||
self.question_image_label.pack(pady=(0, 20), padx=20)
|
||||
|
||||
for button in self.option_buttons:
|
||||
button.destroy()
|
||||
self.option_buttons.clear()
|
||||
|
||||
self.selected_answer.set("")
|
||||
|
||||
for i, option in enumerate(question_data["options"]):
|
||||
radio_btn = ctk.CTkRadioButton(
|
||||
self.options_frame,
|
||||
text=option,
|
||||
variable=self.selected_answer,
|
||||
value=option,
|
||||
font=ctk.CTkFont(size=14),
|
||||
)
|
||||
radio_btn.pack(anchor="w", padx=20, pady=5)
|
||||
self.option_buttons.append(radio_btn)
|
||||
|
||||
self.score_label.configure(
|
||||
text=f"Score: {self.score}/{len(self.questions)}"
|
||||
)
|
||||
|
||||
self.submit_button.configure(state="normal")
|
||||
self.next_button.configure(state="disabled")
|
||||
self.next_button.configure(text="Next Question")
|
||||
|
||||
self.feedback_frame.pack_forget()
|
||||
self.feedback_image_label.pack_forget()
|
||||
|
||||
self.feedback_label.configure(text="")
|
||||
|
||||
self.scrollable_frame._parent_canvas.yview_moveto(0)
|
||||
|
||||
self.root.update_idletasks()
|
||||
else:
|
||||
self.show_final_results()
|
||||
|
||||
def submit_answer(self):
|
||||
"""Check the selected answer and provide feedback"""
|
||||
if not self.selected_answer.get():
|
||||
messagebox.showwarning("Warning", "Please select an answer!")
|
||||
return
|
||||
|
||||
question_data = self.questions[self.current_question]
|
||||
correct_answer = question_data["correct_answer"]
|
||||
selected = self.selected_answer.get()
|
||||
|
||||
if selected == correct_answer:
|
||||
self.score += 1
|
||||
feedback_text = "✅ Correct!"
|
||||
else:
|
||||
feedback_text = f"❌ Incorrect! The correct answer is: {correct_answer}"
|
||||
|
||||
if "explanation" in question_data:
|
||||
feedback_text += f"\n\nExplanation: {question_data['explanation']}"
|
||||
|
||||
self.feedback_label.configure(text=feedback_text)
|
||||
if selected == correct_answer:
|
||||
self.feedback_label.configure(text_color=("green", "lightgreen"))
|
||||
else:
|
||||
self.feedback_label.configure(text_color=("red", "lightcoral"))
|
||||
|
||||
self.feedback_image_label.pack_forget()
|
||||
if "explanation_image" in question_data:
|
||||
image = self.load_image(question_data["explanation_image"])
|
||||
if image:
|
||||
self.feedback_image_label.configure(image=image)
|
||||
self.feedback_image_label.pack(pady=(5, 15), padx=20)
|
||||
|
||||
self.feedback_frame.pack(
|
||||
fill="x", padx=20, pady=(0, 20), before=self.buttons_frame
|
||||
)
|
||||
|
||||
self.root.update_idletasks()
|
||||
self.feedback_frame.update()
|
||||
|
||||
self.submit_button.configure(state="disabled")
|
||||
if self.current_question < len(self.questions) - 1:
|
||||
self.next_button.configure(state="normal")
|
||||
else:
|
||||
self.next_button.configure(text="Show Results", state="normal")
|
||||
|
||||
self.score_label.configure(text=f"Score: {self.score}/{len(self.questions)}")
|
||||
|
||||
self.root.after(100, self.scroll_to_feedback)
|
||||
|
||||
def scroll_to_feedback(self):
|
||||
"""Scroll to show the feedback section"""
|
||||
try:
|
||||
feedback_y = self.feedback_frame.winfo_y()
|
||||
scrollable_height = self.scrollable_frame.winfo_height()
|
||||
|
||||
if feedback_y > 0:
|
||||
scroll_position = min(1.0, feedback_y / scrollable_height)
|
||||
self.scrollable_frame._parent_canvas.yview_moveto(scroll_position)
|
||||
except:
|
||||
pass
|
||||
|
||||
def next_question(self):
|
||||
"""Move to the next question"""
|
||||
self.current_question += 1
|
||||
if self.current_question < len(self.questions):
|
||||
self.display_question()
|
||||
else:
|
||||
self.show_final_results()
|
||||
|
||||
def show_final_results(self):
|
||||
"""Display final quiz results"""
|
||||
percentage = (self.score / len(self.questions)) * 100 if self.questions else 0
|
||||
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
results_frame = ctk.CTkFrame(self.scrollable_frame)
|
||||
results_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
||||
|
||||
results_title = ctk.CTkLabel(
|
||||
results_frame,
|
||||
text="Quiz Complete!",
|
||||
font=ctk.CTkFont(size=32, weight="bold"),
|
||||
)
|
||||
results_title.pack(pady=(40, 20))
|
||||
|
||||
score_text = (
|
||||
f"Your Score: {self.score}/{len(self.questions)} ({percentage:.1f}%)"
|
||||
)
|
||||
final_score = ctk.CTkLabel(
|
||||
results_frame, text=score_text, font=ctk.CTkFont(size=24, weight="bold")
|
||||
)
|
||||
final_score.pack(pady=20)
|
||||
|
||||
if percentage >= 80:
|
||||
message = "Excellent! 🎉"
|
||||
color = "green"
|
||||
elif percentage >= 60:
|
||||
message = "Good job! 👍"
|
||||
color = "blue"
|
||||
else:
|
||||
message = "Keep practicing! 📚"
|
||||
color = "orange"
|
||||
|
||||
performance_label = ctk.CTkLabel(
|
||||
results_frame, text=message, font=ctk.CTkFont(size=20), text_color=color
|
||||
)
|
||||
performance_label.pack(pady=20)
|
||||
|
||||
restart_final = ctk.CTkButton(
|
||||
results_frame,
|
||||
text="Take Quiz Again",
|
||||
command=self.restart_quiz,
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
height=50,
|
||||
width=200,
|
||||
)
|
||||
restart_final.pack(pady=30)
|
||||
|
||||
exit_button = ctk.CTkButton(
|
||||
results_frame,
|
||||
text="Exit",
|
||||
command=self.root.quit,
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
height=50,
|
||||
width=200,
|
||||
)
|
||||
exit_button.pack(pady=10)
|
||||
|
||||
# Scroll to top
|
||||
self.scrollable_frame._parent_canvas.yview_moveto(0)
|
||||
|
||||
def restart_quiz(self):
|
||||
"""Restart the quiz"""
|
||||
self.current_question = 0
|
||||
self.score = 0
|
||||
self.selected_answer.set("")
|
||||
|
||||
self.load_questions()
|
||||
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
if self.questions:
|
||||
self.display_question()
|
||||
else:
|
||||
messagebox.showerror("Error", "No questions available!")
|
||||
error_label = ctk.CTkLabel(
|
||||
self.scrollable_frame,
|
||||
text="No questions found!\nPlease check quiz_questions.json",
|
||||
font=ctk.CTkFont(size=16),
|
||||
)
|
||||
error_label.pack(expand=True)
|
||||
|
||||
def run(self):
|
||||
"""Start the application"""
|
||||
if not self.questions:
|
||||
messagebox.showerror(
|
||||
"Error", "No questions loaded! Check quiz_questions.json"
|
||||
)
|
||||
return
|
||||
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QuizApp()
|
||||
app.run()
|
||||
Loading…
Reference in a new issue