I created an Quiz app in Kivy, when a question is presented there are 4 options (4 toggle buttons). Selecting a toggle button selects an answer and your answer is stored so to calculate the results.
At the end of the quiz, there is an option to review the quiz. The same screen is used as during the test but this time the 4 toggle buttons are disabled. Here is where I am having difficult trying to recreate a feature in KivyMD that I had in Kivy... I have included the original KIVY code at the end of this post and the KivyMD version below.
During the review, each toggle button is disabled but the text color indicates the right or wrong answer. The correct answer text color is "Green", if the user selected a wrong answer this text color is "Red" and all other buttons are just left as default. If the user selected the correct answer there is just one "Green" text and all others are default.
In Kivy I achevied this by creating four properties:
Button_A_Text_Color = StringProperty() Button_B_Text_Color = StringProperty()
then I created an attribute UpdateButtons() that when called would check if it was "Test Mode" or "Review Mode". If the test was in "Test Mode", these properties would be set to a default color but if it was in "Review Mode" the color would be set based on the correct answer and on the answer given by the user. The UpdateButtons() was called ever time the user went from one questions to another...
Now, I think my problem lies with KivyMD using themes and there is much more things happening in the background and so my UpdateButtons() is being over written but a kivy toggle button attribute!
I have spent all morning looking through the KivyMD modules and did find some attributes that I tried to modify when I created my MyToggleButton class but the best i could do was to get the text to "Green" or "Red" or whatever during the test mode, in the "Review Mode" the text color kept defaulting back. In default mode the buttons are all set to Disable so I am thinking that there is an attribute somewhere that sets the button text to a certain color when its disabled. But I couldn't find it!!
Any ideas how to do this????
Sorry about the long question but just wanted to give sufficient detail... Below is the code and KV file, I tried to peel out all the unnecessary code and just leave in enough to show what I am trying to do!!
Thanks
Eoin
PY code:
from kivymd.app import MDApp
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.screen import MDScreen
from kivy.properties import StringProperty, BooleanProperty,ObjectProperty, NumericProperty
from random import shuffle
from kivymd.uix.behaviors.toggle_behavior import MDToggleButton
from kivymd.uix.button import MDRoundFlatButton
source = [
{"question":"Press 1","A":"1","B":"2","C":"3","D":"4","answer":"A"},
{"question":"Press 2","A":"3","B":"2","C":"1","D":"4","answer":"B"},
{"question":"Press 3","A":"2","B":"2","C":"3","D":"4","answer":"C"},
{"question":"Press 4","A":"1","B":"4","C":"2","D":"3","answer":"D"},
]
TestQuestions = {}
for dict in source:
TestQuestions[dict["question"]] = ([dict["A"].title(),dict["B"].title(),dict["C"].title(),dict["D"].title() ],dict[dict["answer"]].title() )
def CreateTest(test = TestQuestions):
Test = {}
for Number, Question in enumerate([x for x in test.keys()]):
Test[Number+1] = {
"Current Question" : Question,
"Correct Answer" : TestQuestions[Question][1],
"Chosen Answer" : "Blank",
"Multiple Chocies" : TestQuestions[Question][0]}
return Test
class QuestionsScreen(MDScreen):
pass
class ResultScreen(MDScreen):
pass
class TestScreen(MDScreen):
pass
class MyToggleButton(MDRoundFlatButton, MDToggleButton):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background_down = MDApp.get_running_app().theme_cls.primary_dark
class QuizTestApp(MDApp):
Status = BooleanProperty(False)
TestComplete = BooleanProperty(False)
Test = ObjectProperty()
PassMark = 70
TestCurrentQuestion = StringProperty()
CurrentQuestionText = StringProperty("1")
TotalQuestionsText = StringProperty("1")
ChosenAnswerText = StringProperty()
CurrentQuestion = 1
TotalQuestions = NumericProperty(1)
AnswerButtonA = StringProperty()
AnswerButtonB = StringProperty()
AnswerButtonC = StringProperty()
AnswerButtonD = StringProperty()
AnswerButtonA_state = StringProperty("normal")
AnswerButtonB_state = StringProperty("normal")
AnswerButtonC_state = StringProperty("normal")
AnswerButtonD_state = StringProperty("normal")
Score = NumericProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.Status = False
self.CorrectAnswer = ""
self.TestComplete = False
self.CurrentQuestion = 1
self.Test = CreateTest()
self.TotalQuestions = 4
self.TotalQuestionsText = str(self.TotalQuestions)
self.UpdateQuestionAndAnswerButtons()
def UpdateChosenAnswer(self, button):
self.Test[self.CurrentQuestion]["Chosen Answer"] = button.text
self.ChosenAnswerText = self.Test[self.CurrentQuestion]["Chosen Answer"]
self.DisableSubmitButton()
def NextQuestion(self):
self.CurrentQuestion +=1
self.UpdateQuestionAndAnswerButtons()
def PreviousQuestion(self):
self.CurrentQuestion -=1
self.UpdateQuestionAndAnswerButtons()
def StartTest(self, number):
self.Status = False
self.CorrectAnswer = ""
self.TestComplete = False
self.CurrentQuestion = 1
self.Test = CreateTest(int(number), occuarnces = self.Stats["Questions Breakdown"])
self.TotalQuestions = int(number)
self.TotalQuestionsText = str(self.TotalQuestions)
self.UpdateQuestionAndAnswerButtons()
def UpdateQuestionAndAnswerButtons(self):
self.CurrentQuestionText = str(self.CurrentQuestion)
self.TestCurrentQuestion = self.Test[self.CurrentQuestion]["Current Question"]
self.AnswerButtonA, self.AnswerButtonB, self.AnswerButtonC, self.AnswerButtonD = self.Test[self.CurrentQuestion]["Multiple Chocies"]
self.ChosenAnswerText = self.Test[self.CurrentQuestion]["Chosen Answer"]
self.HighLightButtonIfAnswered()
def HighLightButtonIfAnswered(self):
self.AnswerButtonA_state = "normal"
self.AnswerButtonB_state = "normal"
self.AnswerButtonC_state = "normal"
self.AnswerButtonD_state = "normal"
def func(button_text):
if self.Test[self.CurrentQuestion]["Chosen Answer"] == button_text:
return "down"
elif self.Test[self.CurrentQuestion]["Chosen Answer"] == "Blank":
return "normal"
else:
return "normal"
self.AnswerButtonA_state = func(self.AnswerButtonA)
self.AnswerButtonB_state = func(self.AnswerButtonB)
self.AnswerButtonC_state = func(self.AnswerButtonC)
self.AnswerButtonD_state = func(self.AnswerButtonD)
def SubmitTest(self):
if self.Status == False:
self.Score = self.CalculateScoreAndUpdateTestQuestionsBreakdown()
def DisableSubmitButton(self):
for Question in self.Test.keys():
if self.Test[Question]["Chosen Answer"] == "Blank":
self.TestComplete = False
break
else:
self.TestComplete = True
def CalculateScoreAndUpdateTestQuestionsBreakdown(self):
score = 0
for Question in self.Test.keys():
if self.Test[Question]["Chosen Answer"]== self.Test[Question]["Correct Answer"]:
score+=1
return score
def StartReview(self):
self.TestComplete = True
self.Status = True
self.CurrentQuestion = 1
self.CurrentQuestionText = str(self.CurrentQuestion)
def build(self):
self.theme_cls.theme_style = "Light"
self.theme_cls.primary_palette = "BlueGray"
sm = MDScreenManager()
sm.add_widget(TestScreen(name='test'))
sm.add_widget(ResultScreen(name='result'))
return sm
QuizTestApp().run()
KV file...
<MDLabel>:
halign: "center"
theme_text_color: "Secondary"
font_style: "Body1"
<ResultScreen>
MDBoxLayout:
orientation: "vertical"
padding: "20px"
spacing: "20px"
MDBoxLayout:
orientation: "vertical"
MDLabel:
text: f"Your Score was {app.Score} out of {app.TotalQuestions}\n{round(app.Score/app.TotalQuestions*100)}%"
MDLabel:
text: "PASS" if app.Score/app.TotalQuestions*100 >= app.PassMark else "FAIL"
MDBoxLayout:
size_hint: (1,None)
min_height: dp(10)
orientation: "horizontal"
spacing: "20px"
MDRoundFlatButton:
size_hint: (1,1)
text: "Review Questions"
on_press: app.StartReview(); root.manager.current = "test"; app.UpdateQuestionAndAnswerButtons()
MDRoundFlatButton:
size_hint: (1,1)
text: "exit"
on_press: app.get_running_app().stop()
<TestScreen>
id: TestScreen
MDBoxLayout:
orientation: "vertical"
MDBoxLayout:
id: TopNumbers
size_hint: 1, None
height: dp(30)
orientation: "horizontal"
MDBoxLayout:
id: TopNumbers2
orientation: "horizontal"
size_hint: 1, None
height: dp(30)
MDBoxLayout:
id: TopNumbers3
orientation: "horizontal"
size_hint: 1, None
height: dp(30)
MDBoxLayout:
padding: "20px"
orientation: "vertical"
MDLabel:
text: app.TestCurrentQuestion
text_size: self.width, None
halign: "center"
MDBoxLayout:
orientation: "vertical"
padding: "20px"
spacing: "20px"
MDBoxLayout:
orientation: "horizontal"
MyToggleButton:
id: AnswerButtonA
group: 1
text_size: self.width, None
#text_color: app.AnswerButtonA_color
text: app.AnswerButtonA
on_press: app.UpdateChosenAnswer(AnswerButtonA)
state: app.AnswerButtonA_state
disabled: True if self.text == "" or app.Status else False
size_hint: (0.5,1)
MyToggleButton:
id: AnswerButtonB
group: 1
text_size: self.width, None
#text_color: app.AnswerButtonB_color
text: app.AnswerButtonB
on_press: app.UpdateChosenAnswer(AnswerButtonB)
state: app.AnswerButtonB_state
disabled: True if self.text == "" or app.Status else False
size_hint: (0.5,1)
MDBoxLayout:
orientation: "horizontal"
MyToggleButton:
id: AnswerButtonC
group: 1
text_size: self.width, None
#text_color: app.AnswerButtonC_color
text: app.AnswerButtonC
on_press: app.UpdateChosenAnswer(AnswerButtonC)
state: app.AnswerButtonC_state
disabled: True if self.text == "" or app.Status else False
size_hint: (0.5,1)
MyToggleButton:
id: AnswerButtonD
group: 1
text_size: self.width, None
#text_color: app.AnswerButtonD_color
text: app.AnswerButtonD
on_press: app.UpdateChosenAnswer(AnswerButtonD)
state: app.AnswerButtonD_state
disabled: True if self.text == "" or app.Status else False
size_hint: (0.5,1)
MDBoxLayout:
size_hint: (1,None)
min_height: dp(10)
orientation: "horizontal"
padding: "20px"
spacing: "20px"
MDRoundFlatButton:
text_size: self.width, None
text: "Previous"
size_hint: (0.3,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal"; app.PreviousQuestion()
disabled: True if app.CurrentQuestionText == "1" else False
MDRoundFlatButton:
text_size: self.width, None
text: " Close Review" if app.Status else "Answer All Questions" if app.TestComplete==False else "Submit"
#text_size: (50, None) if app.Status else (75, None) if app.TestComplete==False else (50, None)
size_hint: (0.4,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal";
on_release: app.SubmitTest(); root.manager.current = "result"
disabled: True if app.Status==False and app.TestComplete==False else False
MDRoundFlatButton:
text_size: self.width, None
text: "Next"
size_hint: (0.3,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal"; app.NextQuestion()
disabled: True if str(app.CurrentQuestionText) == str(app.TotalQuestions) else False
Here is the original KIVY code that acheived the result. In the review page the button text color changes as per the answers...
from kivy.app import App
from kivy.properties import StringProperty, BooleanProperty,ObjectProperty, NumericProperty
from kivy.uix.screenmanager import ScreenManager, Screen
source = [
{"question":"Press 1","A":"1","B":"2","C":"3","D":"4","answer":"A"},
{"question":"Press 2","A":"3","B":"2","C":"1","D":"4","answer":"B"},
{"question":"Press 3","A":"2","B":"1","C":"3","D":"4","answer":"C"},
{"question":"Press 4","A":"1","B":"4","C":"2","D":"3","answer":"B"},
]
TestQuestions = {}
for dict in source:
TestQuestions[dict["question"]] = ([dict["A"].title(),dict["B"].title(),dict["C"].title(),dict["D"].title() ],dict[dict["answer"]].title() )
def CreateTest(test = TestQuestions):
Test = {}
for Number, Question in enumerate([x for x in test.keys()]):
Test[Number+1] = {
"Current Question" : Question,
"Correct Answer" : TestQuestions[Question][1],
"Chosen Answer" : "Blank",
"Multiple Chocies" : TestQuestions[Question][0]}
return Test
class QuestionsScreen(Screen):
pass
class ResultScreen(Screen):
pass
class TestScreen(Screen):
pass
class QuizTestApp(App):
Status = BooleanProperty(False)
TestComplete = BooleanProperty(False)
Test = ObjectProperty()
PassMark = 70
TestCurrentQuestion = StringProperty()
CurrentQuestionText = StringProperty("1")
TotalQuestionsText = StringProperty("1")
ChosenAnswerText = StringProperty()
CurrentQuestion = 1
TotalQuestions = NumericProperty(1)
AnswerButtonA = StringProperty()
AnswerButtonB = StringProperty()
AnswerButtonC = StringProperty()
AnswerButtonD = StringProperty()
AnswerButtonA_state = StringProperty("normal")
AnswerButtonB_state = StringProperty("normal")
AnswerButtonC_state = StringProperty("normal")
AnswerButtonD_state = StringProperty("normal")
AnswerButtonA_color = ObjectProperty([1,1,1,1])
AnswerButtonB_color = ObjectProperty([1,1,1,1])
AnswerButtonC_color = ObjectProperty([1,1,1,1])
AnswerButtonD_color = ObjectProperty([1,1,1,1])
Score = NumericProperty()
CurrentQuestionWidgetList = []
StatsTestsCompleted = NumericProperty()
StatsTestsPassed = NumericProperty()
StatsQuestionsAnswered = NumericProperty()
StatsCorrectAnswers = NumericProperty()
StatsAverageResult = NumericProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.Status = False
self.CorrectAnswer = ""
self.TestComplete = False
self.CurrentQuestion = 1
self.Test = CreateTest()
self.TotalQuestions = 4
self.TotalQuestionsText = str(self.TotalQuestions)
self.UpdateQuestionAndAnswerButtons()
def UpdateChosenAnswer(self, button):
self.Test[self.CurrentQuestion]["Chosen Answer"] = button.text
self.ChosenAnswerText = self.Test[self.CurrentQuestion]["Chosen Answer"]
self.DisableSubmitButton()
def NextQuestion(self):
self.CurrentQuestion +=1
self.UpdateQuestionAndAnswerButtons()
def PreviousQuestion(self):
self.CurrentQuestion -=1
self.UpdateQuestionAndAnswerButtons()
def UpdateQuestionAndAnswerButtons(self):
self.CurrentQuestionText = str(self.CurrentQuestion)
self.TestCurrentQuestion = self.Test[self.CurrentQuestion]["Current Question"]
self.AnswerButtonA, self.AnswerButtonB, self.AnswerButtonC, self.AnswerButtonD = self.Test[self.CurrentQuestion]["Multiple Chocies"]
self.ChosenAnswerText = self.Test[self.CurrentQuestion]["Chosen Answer"]
self.HighLightButtonIfAnswered()
def HighLightButtonIfAnswered(self):
self.AnswerButtonA_state = "normal"
self.AnswerButtonB_state = "normal"
self.AnswerButtonC_state = "normal"
self.AnswerButtonD_state = "normal"
self.AnswerButtonA_color = [1,1,1,1]
self.AnswerButtonB_color = [1,1,1,1]
self.AnswerButtonC_color = [1,1,1,1]
self.AnswerButtonD_color = [1,1,1,1]
def func(button_text):
if self.Test[self.CurrentQuestion]["Chosen Answer"] == button_text:
return "down"
elif self.Test[self.CurrentQuestion]["Chosen Answer"] == "Blank":
return "normal"
else:
return "normal"
self.AnswerButtonA_state = func(self.AnswerButtonA)
self.AnswerButtonB_state = func(self.AnswerButtonB)
self.AnswerButtonC_state = func(self.AnswerButtonC)
self.AnswerButtonD_state = func(self.AnswerButtonD)
if self.Status == True: #Review mode
def color_func(button_text):
if self.Test[self.CurrentQuestion]["Chosen Answer"] == button_text:
if self.Test[self.CurrentQuestion]["Chosen Answer"] == self.Test[self.CurrentQuestion]["Correct Answer"]:
return [0,1,0,1] #Green
else:
return [1,0,0,1] #Red
elif self.Test[self.CurrentQuestion]["Correct Answer"] == button_text:
return [0,1,0,1] #Green
else:
return [1,1,1,0.5]
self.AnswerButtonA_color = color_func(self.AnswerButtonA)
self.AnswerButtonB_color = color_func(self.AnswerButtonB)
self.AnswerButtonC_color = color_func(self.AnswerButtonC)
self.AnswerButtonD_color = color_func(self.AnswerButtonD)
def SubmitTest(self):
if self.Status == False:
self.Score = self.CalculateScoreAndUpdateTestQuestionsBreakdown()
def DisableSubmitButton(self):
for Question in self.Test.keys():
if self.Test[Question]["Chosen Answer"] == "Blank":
self.TestComplete = False
break
else:
self.TestComplete = True
def CalculateScoreAndUpdateTestQuestionsBreakdown(self):
score = 0
for Question in self.Test.keys():
if self.Test[Question]["Chosen Answer"]== self.Test[Question]["Correct Answer"]:
score+=1
return score
def StartReview(self):
self.TestComplete = True
self.Status = True
self.CurrentQuestion = 1
self.CurrentQuestionText = str(self.CurrentQuestion)
def build(self):
sm = ScreenManager()
sm.add_widget(TestScreen(name='test'))
sm.add_widget(ResultScreen(name='result'))
return sm
QuizTestApp().run()
KV
<Label>:
text_size: self.width, None
halign: "center"
<ResultScreen>
BoxLayout:
orientation: "vertical"
padding: "20px"
spacing: "20px"
BoxLayout:
orientation: "vertical"
Label:
text: f"Your Score was {app.Score} out of {app.TotalQuestions}\n{round(app.Score/app.TotalQuestions*100)}%"
Label:
text: "PASS" if app.Score/app.TotalQuestions*100 >= app.PassMark else "FAIL"
BoxLayout:
size_hint: (1,None)
min_height: dp(10)
orientation: "horizontal"
spacing: "20px"
Button:
text: "Review Questions"
on_press: app.StartReview(); root.manager.current = "test"; app.UpdateQuestionAndAnswerButtons()
Button:
text: "exit"
on_press: app.get_running_app().stop()
<TestScreen>
id: TestScreen
BoxLayout:
orientation: "vertical"
BoxLayout:
id: TopNumbers
size_hint: 1, None
height: dp(30)
orientation: "horizontal"
BoxLayout:
id: TopNumbers2
orientation: "horizontal"
size_hint: 1, None
height: dp(30)
BoxLayout:
id: TopNumbers3
orientation: "horizontal"
size_hint: 1, None
height: dp(30)
BoxLayout:
padding: "20px"
orientation: "vertical"
Label:
text: app.TestCurrentQuestion
text_size: self.width, None
halign: "center"
GridLayout:
padding: "20px"
spacing: "20px"
cols: 2
rows: 2
ToggleButton:
id: AnswerButtonA
group: 1
color: app.AnswerButtonA_color
text: app.AnswerButtonA
on_press: app.UpdateChosenAnswer(AnswerButtonA)
state: app.AnswerButtonA_state
disabled: True if self.text == "" or app.Status else False
ToggleButton:
id: AnswerButtonB
group: 1
color: app.AnswerButtonB_color
text: app.AnswerButtonB
on_press: app.UpdateChosenAnswer(AnswerButtonB)
state: app.AnswerButtonB_state
disabled: True if self.text == "" or app.Status else False
ToggleButton:
id: AnswerButtonC
group: 1
color: app.AnswerButtonC_color
text: app.AnswerButtonC
on_press: app.UpdateChosenAnswer(AnswerButtonC)
state: app.AnswerButtonC_state
disabled: True if self.text == "" or app.Status else False
ToggleButton:
id: AnswerButtonD
group: 1
color: app.AnswerButtonD_color
text: app.AnswerButtonD
on_press: app.UpdateChosenAnswer(AnswerButtonD)
state: app.AnswerButtonD_state
disabled: True if self.text == "" or app.Status else False
BoxLayout:
size_hint: (1,None)
min_height: dp(10)
orientation: "horizontal"
padding: "20px"
spacing: "20px"
Button:
text: "Previous"
size_hint: (0.3,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal"; app.PreviousQuestion()
disabled: True if app.CurrentQuestionText == "1" else False
Button:
text: " Close Review" if app.Status else "Answer All Questions" if app.TestComplete==False else "Submit"
#text_size: (50, None) if app.Status else (75, None) if app.TestComplete==False else (50, None)
size_hint: (0.4,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal";
on_release: app.SubmitTest(); root.manager.current = "result"
disabled: True if app.Status==False and app.TestComplete==False else False
Button:
text: "Next"
size_hint: (0.3,1)
on_press: AnswerButtonA.state = "normal"; AnswerButtonB.state = "normal"; AnswerButtonC.state = "normal"; AnswerButtonD.state = "normal"; app.NextQuestion()
disabled: True if str(app.CurrentQuestionText) == str(app.TotalQuestions) else False
