Reputation: 21
I am trying to write a multiple choice quiz using Python Tkinter. I have a 2 part question. I have radio buttons that display the choices and collect the selected option. I also have a created a button to navigate to the next question or back to the previous question as well as another button to view the score.
Part 1 - How do I keep the selected option of the radio button present for each question when navigation backwards/forwards through the quiz?
Part 2 - The way I have thought out how the view score button should work is:
list
?) to the correct answer
Points 2 and 3 are the easiest part for me. Can you indicate to me the right direction to go on Point number one?
from tkinter import messagebox
import tkinter as tk
from tkinter import *
# question list
q = [
"question 1", "question 2", "question 3", "question 4"
]
# options list
options = [
["a","b","c","d"],
["b","c","d","a"],
["c","d","a","b"],
["d","a","b","c"],
]
# correct answers list
a = [3,4,1,2]
class Quiz:
def __init__(self, master):
self.opt_selected = IntVar()
self.qn = 0
self.correct = 0
self.ques = self.create_q(master, self.qn)
self.opts = self.create_options(master, 4)
self.display_q(self.qn)
self.button = Button(master, text="Previous Question", command=self.back_btn,
width=16, borderwidth=3, relief=RAISED)
self.button.pack(side=LEFT)
self.button = Button(master, text="Next Question", command=self.next_btn,
width=16, borderwidth=3, relief=RAISED)
self.button.pack(side=LEFT)
self.button = Button(master, text="View Score", command=self.score_viewer,
width=16, borderwidth=3, relief=RAISED)
self.button.pack(side=LEFT)
# define questions
def create_q(self, master, qn):
w = Label(master, text=q[qn],
anchor='w',
wraplength=400, justify=LEFT)
w.pack(anchor='w')
return w
# define multiple options
def create_options(self, master, n):
b_val = 0
b = []
while b_val < n:
btn = Radiobutton(master, text="foo", variable=self.opt_selected, value=b_val+1)
b.append(btn)
btn.pack(side=TOP, anchor="w")
b_val = b_val + 1
return b
# define questions for display when clicking on the NEXT Question Button
def display_q(self, qn):
b_val = 0
self.opt_selected.set(0)
self.ques['text'] = q[qn]
for op in options[qn]:
self.opts[b_val]['text'] = op
b_val = b_val + 1
# define questions for display when clicking on the PREVIOUS Question Button
def display_prev_q(self, qn):
b_val = 0
self.opt_selected.set(0)
self.ques['text'] = q[qn]
for op in options[qn]:
self.opts[b_val]['text'] = op
b_val = b_val + 1
# check option selected against correct answer list
def check_q(self, qn):
if self.opt_selected.get() == a[qn]:
self.correct += 1
else:
self.correct += 0
# print results
def print_results(self):
print("Score: ", self.correct, "/", len(q))
# define PREVIOUS button
def back_btn(self):
self.qn = self.qn - 1
self.display_prev_q(self.qn)
# define NEXT button
def next_btn(self):
# if self.check_q(self.qn):
# print("Correct")
# self.correct += 1
self.qn = self.qn + 1
self.display_prev_q(self.qn)
# if self.qn >= len(q):
# self.print_results()
# else:
# self.display_q(self.qn)
# define SCORE view button and score results
def score_viewer(self):
score_viewer = messagebox.askquestion("Warning", 'Would you like to view your current score?', icon='warning')
if score_viewer == 'yes':
self.check_q(self.qn)
corr_ans = self.correct
total_quest = len(q)
output = '{:.1%}'.format(self.correct / len(q))
score_text = "\nScore: %s " % output
output_text = "Correctly answered %a out of %d questions. %s" % (corr_ans, total_quest, score_text)
messagebox.showinfo("Score", output_text)
else:
tk.messagebox.showinfo('Return', 'Returning to quiz')
Upvotes: 2
Views: 249
Reputation: 123501
Unfortunately, I think you need change the fundamental architecture of your program and make it much more object-oriented. Specifically, instead of having a bunch of separate list
s like you have:
# question list
q = [
"question 1", "question 2", "question 3", "question 4"
]
# options list
options = [
["a","b","c","d"],
["b","c","d","a"],
["c","d","a","b"],
["d","a","b","c"],
]
# correct answers list
a = [3,4,1,2]
I think you should define a custom class
to encapsulate questions and their current state, and then create a (single) list
of them during application initialization. This approach not only makes it relatively easily to switch from displaying one to another (not to mention keeping track of the current state of each), it also makes it fairly straight-forward to do all the related things you say you wish to do.
Here's a complete implementation illustrating what I mean. Note it uses @Bryan Oakley's frame-switching technique similar to what's in his answer to the question Switch between two frames in tkinter to display each question. The primary difference being that the "pages" (questions) are stored in a list
referenced via an index instead of in a dict
accessed by a class name.
Another nice aspect of this design is that the question data is completely separate from the Quiz
code, which mean it could be stored externally in a file or database if desired.
I also tried to make the code conform to PEP 8 - Style Guide for Python Code (which you should also do as much as possible).
import tkinter as tk
from tkinter.constants import *
from tkinter import messagebox
class Question(tk.Frame):
""" Frame subclass encapsulating a multiple-option question. """
def __init__(self, master, text, options, correct_ans):
super(Question, self).__init__(master)
self.text = text
self.options = options
self.correct_ans = correct_ans
self.opt_selected = tk.IntVar()
tk.Label(self, text=self.text, anchor=W, wraplength=400,
justify=LEFT).pack(anchor=W)
for b_val, option in enumerate(self.options, start=1):
tk.Radiobutton(self, text=option, variable=self.opt_selected,
value=b_val).pack(side=TOP, anchor=W)
def check_q(self):
""" Check if currently selected option is correct answer. """
return self.opt_selected.get() == self.correct_ans
class Quiz:
def __init__(self, master, quiz_questions):
self.master = master
# The container is a stack of question Frames on top of one another.
# The one we want visible will be raised above the others.
container = tk.Frame(master)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
# Create internal list of question Frames.
self.questions = []
for args in quiz_questions:
q_frame = Question(container, *args)
q_frame.grid(row=0, column=0, sticky=NSEW)
self.questions.append(q_frame)
self.qn = 0 # Current question number.
self.display_q() # Show it.
# Create naviagtion Buttons.
btn = tk.Button(master, width=16, borderwidth=3, relief=RAISED,
text="Previous Question", command=self.display_prev_q)
btn.pack(side=LEFT)
btn = tk.Button(master, width=16, borderwidth=3, relief=RAISED,
text="Next Question", command=self.display_next_q)
btn.pack(side=LEFT)
btn = tk.Button(master, width=16, borderwidth=3, relief=RAISED,
text="View Score", command=self.score_viewer)
btn.pack(side=LEFT)
def display_q(self):
""" Show the current question by lifting it to top. """
frame = self.questions[self.qn]
frame.tkraise()
def display_next_q(self):
""" Increment question number, wrapping to first one at end,
and display it.
"""
self.qn = (self.qn+1) % len(self.questions)
self.display_q()
def display_prev_q(self):
""" Decrement question number, wrapping to last one at beginning,
and display it.
"""
self.qn = (self.qn-1) % len(self.questions)
self.display_q()
def score_viewer(self):
""" Score results with user consent. """
view_score = messagebox.askquestion(
"Warning", 'Would you like to view your current score?',
icon='warning')
if view_score != 'yes':
tk.messagebox.showinfo('Return', 'Returning to quiz')
else:
# Calculate number of correct answers and percentage correct.
correct = sum(question.check_q() for question in self.questions)
accuracy = correct / len(self.questions) * 100
messagebox.showinfo("Score",
"You have correctly answered %d out of %d questions.\n"
"Score: %.1f%%" % (correct, len(self.questions), accuracy))
if __name__ == '__main__':
# Note this data could also be stored separately, such as in a file.
question_data = [('Question 1', ("a1", "b1", "c1", "d1"), 3),
('Question 2', ("b2", "c2", "d2", "a2"), 4),
('Question 3', ("c3", "d3", "a3"), 1),
('Question 4', ("d4", "a4", "b4", "c4"), 2)]
root = tk.Tk()
root.title('Quiz')
quiz = Quiz(root, question_data)
root.mainloop()
Upvotes: 1