rbrt
rbrt

Reputation: 21

Radiobutton navigation and value storing

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.

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

Answers (1)

martineau
martineau

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 lists 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

Related Questions