Tkinter: multiple choice quiz app, can't change question (and other issues) - python

I'm trying to create a multiple choice question app with tkinter but I'm having three problems that I can't solve by myself:
The question don't change, it stuck on the first question.
I have a file with all my questions (more then 50) what I'd like to do is select randomly only 10 of them from the list (five from easy_question list and 5 from the hard_question list).
Is there a way to save in an exel file the ten code of the question that where selected and to know what that person answered (wrong or right doesn't metter)? something like this:
1 2 3 [...]
question 1e 3h 2e
answer 2 3 4
This is a simple version of my file with all my questions, options and correct answers:
easy_questions2=[
"1e. Name?",
"2e. Last name?",
"3e. Birthdate?",
"4e. Food?"
]
easy_options=[
['Marck', 'Mary','Joseph','John'],
['Smith', 'Hartnett','Pitt','Pacino'],
['June', 'October','November','April'],
['All', 'Fries','Pasta','Chicken']
]
easy_answers=[
1,
2,
3,
3
]
hard_questions2=[
"1h. Number?",
"2h. Word?",
"3h. Hour?",
"4h. Color?"
]
hard_options=[
['10', '11','21','55'],
['Book', 'Table','en','Pacino'],
['11', '21','24','18'],
['Yellow', 'Blue','Red','Green']
]
hard_answers=[
3,
4,
1,
2
]
This is my code:
import tkinter as tk
import tkinter.ttk as ttk
from questionslist import easy_questions2, easy_answers, easy_options
from openpyxl import load_workbook
from tkinter import messagebox as mb
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.q_no=0
self.display_title()
self.display_question()
self.opt_selected=tk.IntVar()
self.opts=self.radio_buttons()
self.display_options()
self.buttons()
self.data_size=len(question)
self.correct=0
def display_result(self):
wrong_count = self.data_size - self.correct
correct = f"Correct: {self.correct}"
wrong = f"Wrong: {wrong_count}"
score = int(self.correct / self.data_size * 100)
result = f"Score: {score}%"
mb.showinfo("Result", f"{result}\n{correct}\n{wrong}")
def check_ans(self, q_no):
if self.opt_selected.get() == answer[q_no]:
return True
def next_button(self):
if self.check_ans(self.q_no):
self.correct += 1
self.q_no += 1
if self.q_no==self.data_size:
self.display_result()
self.submit()
self.destroy()
else:
self.display_question()
self.display_options()
def buttons(self):
next_button = tk.Button(self, text="Next",command=self.next_button, width=10)
next_button.pack(pady=50, side="bottom")
def display_options(self):
val=0
self.opt_selected.set(0)
for option in options[self.q_no]:
self.opts[val]['text']=option
val+=1
def display_question(self):
q_no = tk.Label(self, text=question[self.q_no], width=60)
q_no.pack(padx=19, pady=31, anchor="w")
def radio_buttons(self):
q_list = []
while len(q_list) < 4:
radio_button = ttk.Radiobutton(self,text=" ",variable=self.opt_selected,
value = len(q_list)+1)
q_list.append(radio_button)
radio_button.pack(padx=19, anchor="w")
return q_list
def submit(self):
wb = load_workbook('D:\\Python\\quiz\\template.xlsx')
sheet = wb.active
sheet.cell(row=1, column=2).value = "first_name"
sheet.cell(row=1, column=3).value = "correct"
sheet.cell(row=1, column=4).value = "wrong"
sheet.cell(row=2, column=3).value = self.correct
sheet.cell(row=2, column=4).value = self.data_size - self.correct
excel_filename = "D:\\Python\\quiz\\" + "data" + ".xlsx"
wb.save(excel_filename)
question = easy_questions2
options = easy_options
answer = easy_answers
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root).pack(side="top", fill="both", expand=True)
root.title("Quiz")
root.geometry('800x450')
root.mainloop()

If you want to read the questions from a file (or more), you could use (suppose the file is a .txt and you've wrote a question per line):
question_list = []
with open('filename.txt', 'r+') as question_file:
lines = question_file.readlines()
# Let's move the pointer of the file back to the start to resave the questions
# as we don't want to lose them.
question_file.seek(0)
question_file.truncate()
for line in lines:
question_list.append(line) # Put each question at the end of the list.
question_file.write(line)
Use this method to retrieve all the questions and answers from all your files and to put them into lists.
Now you should have a list for the easy questions and one for the hard questions, so, to pick a random question from one list you can use a specific function from the random module, randint(a, b) which is a function that return a value in between 'a' and 'b' ('a' and 'b' included). Try it this way:
from random import randint
# You have a list of 25 hard question, let's call it hard_list
# and a list of 25 easy question, let's call it easy_list
# Now let's use our randint function to retrieve 10 random value between 0 and 24.
# As you haven't done any statement about how to choose the order of the questions,
# I'll take the first five from the hards one and the last 5 from the easy,
# but you can do it in any way you want.
# It may be a little tricky to manage the possibility of same random numbers to come out,
# surely there are better ways to do it, but this works fine.
random_questions = [] # Here I write the questions randomly chosen.
random_numbers = [] # I use this to take into account the random values.
i = 0 # Our counter.
while i < 10:
# If 5 questions have already been chosens, we change the list
# and clear the random values counter once.
if i >= 5:
if i == 5:
random_numbers.clear()
val = randint(0, 24)
if val not in random_numbers:
i += 1
random_numbers.append(val)
random_questions.append(easy_list[val])
else:
val = randint(0, 24)
if val not in random_numbers:
i += 1
random_numbers.append(val)
random_questions.append(hard_list[val])
Now, with your code, I see that the 'check_button' function doesn't check if the user have select an option or not, but maybe it's what you wanted, secondly, your button don't change the question because.. you're assigning a new Label to q_no (which, I suppose, is the number of questions answered so far)? You should assign the Label to another variable and update its text value each time the user click the button.
For example:
self.question_label = tk.Label(self, text=question[self.q_no], ....)
self.question_label.pack(...)
And when the user press the button to the next question, you check if the answer is correct, then:
self.q_no += 1
if self.q_no==(self.data_size + 1):
#
# Do what you want to do if that was the last question.
#
else:
self.question_label['text'] = question[self.q_no]
#
# Here change the value of the buttons with the answers.
#
For the third question, what format do you exactly want? There are differences between .csv, .xlsx, .xlsb, etc. But for the conversion you can use the pandas module.
These methods need a specific type of object for the conversion, so be shure to format your data in that specific way before using them:
to csv to xlsx. This is the class you can use as data structure.
From personal experience I know that you can format your datas as json to convert them to csv, but I'm not shure if it works for xlsx and other formats too.
Hope I've helped a little bit.

Related

Using guizero and a text datafile to create GUI

So I'm trying to help a student figure out how to create a simple quiz by reading from a text file with the questions, options, and answers. He wants to use guizero instead of the simple built-in input() and print() functions.
I would like him to avoid creating a separate check function for each question, but I don't have much experience with guizero. I've been reading the manual pages, and the below code approximates what we are trying to accomplish but doesn't work because selected_value is not defined until after the program runs. Am I approaching this the wrong way?
from guizero import App, Text, ButtonGroup
app = App(title="Quiz Test", height=800, width=600)
def check_answer(selected_value, answer):
if selected_value == answer:
result.value = "Correct"
else:
result.value = "Wrong"
question_data = []
data_file = open("quiz_ques.txt", 'r')
for line in data_file.read().splitlines():
question_data = line.split(", ")
question_data.reverse() ; question = question_data.pop()
question_data.reverse() ; answer = question_data.pop()
q_options = question_data
text = Text(app, text=question)
choice = ButtonGroup(app, options=q_options, selected=1, command=check_answer, args=[selected_value, answer])
result = Text(app)
data_file.close()
app.display()
try changing
command=check_answer()
to
command=check_answer
in the original version you were calling check_answer() as soon as you defined your program... in the second version it will not be called until the button is clicked
I figured this out using a couple of lists for anyone looking for a solution. Someone who is more of a python expert could probably simplify this, using some pythonic idiom, built-in functions, or standard modules, but this solution works even if it's a bit of a hack. Improvements on this are welcomed. :)
from guizero import App, Text, ButtonGroup
app = App(title="Quiz Test", height=800, width=600)
def check_answer(answer, result, cnt):
if choices[cnt].value == answer:
result.text_color = 'green'
result.value = "Correct"
update_score()
else:
result.text_color = 'red'
result.value = "Wrong"
update_score()
def update_score():
score = 0
for result in results:
if result.value == "Correct":
score += 1
score_txt.value = "Score: " + str(score)
question_data = []
data_file = open("quiz_ques.txt", 'r')
cnt = 0
results = []
choices = []
for line in data_file.read().splitlines():
question_data = line.split(", ")
question_data.reverse() ; question = question_data.pop()
question_data.reverse() ; answer = question_data.pop()
q_options = question_data
question = Text(app, text=question)
question.text_color = 'white'
results.append(Text(app))
choices.append(ButtonGroup(app, options=q_options, command=check_answer, args=[answer, results[cnt], cnt]))
cnt += 1
score_txt = Text(app, color='white', size=40)
data_file.close()
app.display()

Altering python variable identifiers in a loop which defines a tkinter button

I've got a function in a class in which I am creating several tkinter buttons within, where the number of buttons and the button properties depends on a file (so I can't create a specific number of variables to hold the buttons).
The code I have looks like this (sparing the complexities of the whole code):
import os
import tkinter as tk
Class(GUI):
def ButtonCreator(self):
self.HomeworkList = open("Files\HWNameList.txt", "r")
x = self.HomeworkList.readline()
while not x == "END":
x = x[0:-1]
HomeworkFileName = str("Files\HW-" + x + ".txt")
locals()["self.Button" + x] = tk.Button(master, text = x, command = lambda: self.DisplayHomeworkFile(FileName))
locals()["self.Button" + x].pack()
x = self.HomeworkList.readline()
self.HomeworkList.close()
def DisplayHomeworkFile(self, filename):
os.startfile(filename)
The file I am opening looks like this...
HomeworkName1
HomeworkName2
HomeworkName3
END
When the code runs, it displays the buttons with the correct text written on them but when i click on them they only ever display the file who's file name is written last in the HomeworkList file. Not sure what I've done wrong.
If there is another way to achieve what I'm trying I'm open to all suggestions.
Thanks.
This is a classic beginners problem that comes from misunderstanding how lambda works. For this case you need to use functools.partial.
You also need to forget about modifying locals(). Make a list or dictionary to hold the button instances.
from functools import partial
def ButtonCreator(self):
self.HomeworkList = open("Files\HWNameList.txt", "r")
x = self.HomeworkList.readline()
self.buttons = []
while not x == "END":
x = x[0:-1]
HomeworkFileName = str("Files\HW-" + x + ".txt")
btn = tk.Button(master, text = x, command = partial(self.DisplayHomeworkFile, HomeworkFileName))
btn.pack()
self.buttons.append(btn)
x = self.HomeworkList.readline()
self.HomeworkList.close()

Multiple ComboBoxes in Tkinter Python

I'm trying to generate multiple ComboBoxes with values from a "config.ini" file, the config.ini file data is:
priority1 = Normal:farty-blobble-fx.wav:2
priority8 = Reclamacao:buzzy-blop.wav:3
priority3 = Critico:farty-blobble-fx.wav:5
priority2 = Urgente:echo-blip-thing.wav:4
and the goal is turning the sound files names to the select values in the comboboxes.
My code to generate the comboboxes is:
content_data = []
for name, value in parser.items(section_name):
if name=="name":
self.note.add(self.tab2, text = value)
else:
data_prior = value.split(":")
self.PRIOR_LABEL = Label(self.tab2, text=data_prior[0])
self.PRIOR_LABEL.grid(row=data_prior[2],column=0,pady=(10, 2),padx=(40,0))
self.PRIOR_SOUNDS = None
self.PRIOR_SOUNDS = None
self.box_value = StringVar()
self.PRIOR_SOUNDS = Combobox(self.tab2, textvariable=self.box_value,state='readonly',width=35)
self.PRIOR_SOUNDS['values'] = getSoundsName()
self.PRIOR_SOUNDS.current(int(getSoundsName().index(data_prior[1])))
self.PRIOR_SOUNDS.grid(row=data_prior[2],column=1,pady=(10, 2),padx=(30,0))
self.PLAY = Button(self.tab2)
self.PLAY["width"] = 5
self.PLAY["text"] = "Play"
self.PLAY["command"] = lambda:playSound(self.PRIOR_SOUNDS.get())
self.PLAY.grid(row=data_prior[2], column=3,pady=(10,2),padx=(5,0))
And i was unable to show the current values of the "config.ini" file in the comboboxes.
Thank you in advance.
The problem is that you're creating more than one combobox, yet you keep overwriting the variables in each iteration of the loop. At the end of the loop, self.PRIOR_SOUNDS will always point to the last combobox that you created. The same is true for self.box_value, self.PLAY, etc.
The simplest solution is to use an array or dictionary to store all of your variables. A dictionary lets you reference each widget or variable by name; using a list lets you reference them by their ordinal position.
A solution using a dictionary would look something like this:
self.combo_var = {}
self.combo = {}
for name, value in parser.items(section_name):
...
self.combo_var[name] = StringVar()
self.combo[name] = Combobox(..., textvariable = self.combo_var[name])
...

raw_input stops GUI from appearing

I have written a program in Python that allow me to change the names of many files all at once. I have one issue that is quite odd.
When I use raw_input to get my desired extension, the GUI will not launch. I don't get any errors, but the window will never appear.
I tried using raw_input as a way of getting a file extension from the user to build the file list. This program will works correctly when raw_input is not used.The section of code that I am referring to is in my globList function. For some reason when raw_imput is used the window will not launch.
import os
import Tkinter
import glob
from Tkinter import *
def changeNames(dynamic_entry_list, filelist):
for index in range(len(dynamic_entry_list)):
if(dynamic_entry_list[index].get() != filelist[index]):
os.rename(filelist[index], dynamic_entry_list[index].get())
print "The files have been updated!"
def drawWindow(filelist):
dynamic_entry_list = []
my_row = 0
my_column = 0
for name in filelist:
my_column = 0
label = Tkinter.Label(window, text = name, justify = RIGHT)
label.grid(row = my_row, column = my_column)
my_column = 1
entry = Entry(window, width = 50)
dynamic_entry_list.append(entry)
entry.insert(0, name)
entry.grid(row = my_row, column = my_column)
my_row += 1
return dynamic_entry_list
def globList(filelist):
#ext = raw_input("Enter the file extension:")
ext = ""
desired = '*' + ext
for name in glob.glob(desired):
filelist.append(name)
filelist = []
globList(filelist)
window = Tkinter.Tk()
user_input = drawWindow(filelist)
button = Button(window, text = "Change File Names", command = (lambda e=user_input: changeNames(e, filelist)))
button.grid(row = len(filelist) + 1 , column = 1)
window.mainloop()
Is this a problem with raw_input?
What would be a good solution to the problem?
This is how tkinter is defined to work. It is single threaded, so while it's waiting for user input it's truly waiting. mainloop must be running so that the GUI can respond to events, including internal events such as requests to draw the window on the screen.
Generally speaking, you shouldn't be mixing a GUI with reading input from stdin. If you're creating a GUI, get the input from the user via an entry widget. Or, get the user input before creating the GUI.
A decent tutorial on popup dialogs can be found on the effbot site: http://effbot.org/tkinterbook/tkinter-dialog-windows.htm

Passing a variable between two functions

The following is a piece of code that I have been working on for awhile. I've been able to compile and run the code without error. However, I am having a difficult time with passing a variable from one function to another in my code.
The problem seems to occur after I run choose() and create self.newLists based on the desired indices. You'll notice that I added print(self.newLists) at the end of this function so that I can check to see if it is producing what I want.
The next function, simplify(), is where my issue arises. When I try to pass self.newLists from the previous function it doesn't seem to produce anything. I also tried printing and/or returning the variable named answer but it returns "none". I've been stumbling over this obstacle for awhile without any progress. Below is the code I am working on along with an example of what I want simplify() to produce.
from tkinter import *
from tkinter.filedialog import askopenfilename
class myFileOpener:
def __init__(self, master):
frame = Frame(master)
frame.pack()
print()
self.newLists = ()
self.printButton = Button(frame, text="Select File", command=self.openfile)
self.printButton.pack(side=LEFT)
self.runButton = Button(frame, text="Run", command=self.combine)
self.runButton.pack(side=LEFT)
self.quitButton = Button(frame, text="Quit", command=frame.quit)
self.quitButton.pack(side=LEFT)
def openfile(self):
filename = askopenfilename(parent=root)
self.lines = open(filename)
# print(self.lines.read())
def choose(self):
g = self.lines.readlines()
for line in g:
matrix = line.split()
JD = matrix[2]
mintime = matrix[5]
maxtime = matrix[7]
self.newLists = [JD, mintime, maxtime]
print(self.newLists)
def simplify(self):
dates = {}
for sub in self.newLists:
date = sub[0]
if date not in dates:
dates[date] = []
dates[date].extend(sub[1])
answer = []
for date in sorted(dates):
answer.append([date] + dates[date])
return answer
def combine(self):
self.choose()
self.simplify()
root = Tk()
b = myFileOpener(root)
root.mainloop()
Example of desired output from simplify():
[['2014-158', '20:07:11.881', '20:43:04.546', '20:43:47.447', '21:11:08.997', '21:11:16.697', '21:22:07.717'],
['2014-163', '17:12:09.071', '17:38:08.219', '17:38:28.310', '17:59:25.649', '18:05:59.536', '18:09:53.243', '18:13:47.671', '18:16:53.976', '18:20:31.538', '18:23:02.243']]
It essentially groups times by certain dates.
You are not producing a list of lists. You are resetting self.newLists each loop iteration, to a single list with 3 elements:
for line in g:
matrix = line.split()
JD = matrix[2]
mintime = matrix[5]
maxtime = matrix[7]
self.newLists = [JD, mintime, maxtime]
You need to instead use list.append() to add those 3 elements to a list you set once, outside of the loop:
self.newLists = []
for line in g:
matrix = line.split()
JD = matrix[2]
mintime = matrix[5]
maxtime = matrix[7]
self.newLists.append([JD, mintime, maxtime])
Your simplify method is adding the individual characters of mintime to your output lists:
for sub in self.newLists:
date = sub[0]
if date not in dates:
dates[date] = []
dates[date].extend(sub[1])
You want to use list.append() there, not list.extend(). That loop can be simplified using dict.setdefault() rather than test for the key manually:
for date, mintime, maxtime in self.newLists:
dates.setdefault(date, []).append(mintime)

Categories

Resources