416 lines
14 KiB
Python
416 lines
14 KiB
Python
#
|
|
# 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()
|