A Tk-based program that can help you prepare for your music final with randomly-generated listening quizzes
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

411 lignes
15 KiB

  1. #! /usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. MusicQuizzer is a Python program that can help you prepare for any test that
  5. involves listening to excerpts of music pieces and answering multiple choice
  6. questions about them. For more information, see the included README.md.
  7. """
  8. from __future__ import division
  9. import ConfigParser as configparser
  10. import os
  11. from pygame import mixer, error
  12. import random
  13. import thread
  14. import time
  15. from Tkinter import *
  16. from tkFont import Font
  17. from urllib import urlretrieve
  18. __author__ = "Ben Kurtovic"
  19. __copyright__ = "Copyright (c) 2011-2012 Ben Kurtovic"
  20. __license__ = "MIT License"
  21. __version__ = "0.1.2"
  22. __email__ = "ben.kurtovic@verizon.net"
  23. config_filename = "config.cfg"
  24. config = None
  25. piece_dir = None
  26. download_complete = False
  27. master_width = 500
  28. master_height = 500
  29. question_height = 100
  30. class AnswerSheet(object):
  31. def __init__(self, master):
  32. self.master = master
  33. self.order = generate_piece_order()
  34. self.init_widgets()
  35. self.generate_questions()
  36. self.grid_questions()
  37. def init_widgets(self):
  38. self.scroll = Scrollbar(self.master)
  39. self.scroll.grid(row=1, column=1, sticky=N+S)
  40. self.canvas = Canvas(self.master, yscrollcommand=self.scroll.set,
  41. width=master_width, height=master_height-30)
  42. self.canvas.grid(row=1, column=0, sticky=N+S+E+W)
  43. self.canvas.grid_propagate(0)
  44. self.scroll.config(command=self.canvas.yview)
  45. self.master.grid_rowconfigure(0, weight=1)
  46. self.master.grid_columnconfigure(0, weight=1)
  47. self.frame = Frame(self.canvas)
  48. self.frame.rowconfigure(1, weight=1)
  49. self.frame.columnconfigure(1, weight=1)
  50. self.header = Frame(self.master, bd=2, relief="groove")
  51. self.header.grid(row=0, column=0, columnspan=2)
  52. self.header_buttons = Frame(self.header, width=250, height=30)
  53. self.header_buttons.grid_propagate(0)
  54. self.header_buttons.grid(row=0, column=0)
  55. self.playing_container = Frame(self.header, width=master_width-240, height=30)
  56. self.playing_container.grid_propagate(0)
  57. self.playing_container.grid(row=0, column=1)
  58. self.play_button = Button(self.header_buttons, text="Start Quiz",
  59. command=self.play)
  60. self.play_button.grid(row=0, column=0)
  61. self.submit_button = Button(self.header_buttons, text="Submit Answers",
  62. command=self.submit)
  63. self.submit_button.grid(row=0, column=1)
  64. self.playing = StringVar()
  65. self.playing.set("Now Playing: None")
  66. self.now_playing = Label(self.playing_container, textvar=self.playing)
  67. self.now_playing.grid(row=0, column=0)
  68. def generate_questions(self):
  69. num_answers = config.getint("general", "answers")
  70. answer_choices = {} # dict of {category1: {choice1, choice2...}, ...}
  71. questions = {} # dict of {piece1: [question1, question2...], ...}
  72. self.number_of_questions = 0
  73. for piece in self.order:
  74. for category, answer_choice in config.items(piece):
  75. if category == "url":
  76. continue
  77. try:
  78. answer_choices[category].add(answer_choice)
  79. except KeyError:
  80. answer_choices[category] = set([answer_choice])
  81. for piece in self.order:
  82. questions[piece] = dict()
  83. for category in config.options(piece):
  84. if category == "url":
  85. continue
  86. correct_choice = config.get(piece, category)
  87. questions[piece][category] = [correct_choice]
  88. all_choices = list(answer_choices[category])
  89. all_choices.remove(correct_choice)
  90. for x in range(num_answers - 1):
  91. try:
  92. choice = random.choice(all_choices)
  93. except IndexError:
  94. break # if there aren't enough choices in the choice
  95. # bank, there will be fewer answer choices than
  96. # we want, but what else can we do?
  97. all_choices.remove(choice)
  98. questions[piece][category].append(choice)
  99. question_choices = questions[piece][category]
  100. questions[piece][category] = randomize_list(question_choices)
  101. self.number_of_questions += 1
  102. self.questions = questions
  103. def grid_questions(self):
  104. self.answers = {}
  105. self.stuff_to_disable = [] # what gets turned off when we press submit?
  106. question_grid = Frame(self.frame)
  107. this_row_number = 0
  108. excerpt = "A"
  109. for piece in self.order:
  110. question_row_number = 1
  111. piece_questions = self.questions[piece].keys()
  112. piece_questions.reverse() # correct ordering issues
  113. self.answers[piece] = {}
  114. height = question_height * len(piece_questions) + 20
  115. piece_grid = Frame(question_grid, width=master_width,
  116. height=height)
  117. title = Label(piece_grid, text="Excerpt {0}".format(excerpt),
  118. font=Font(family="Verdana", size=10, weight="bold"))
  119. excerpt = chr(ord(excerpt) + 1) # increment excerpt by 1 letter
  120. title.grid(row=0, column=0, columnspan=3)
  121. for question in piece_questions:
  122. agrid = LabelFrame(piece_grid, text=question.capitalize(),
  123. width=master_width, height=question_height)
  124. a = StringVar()
  125. self.answers[piece][question] = a
  126. w = (master_width / 2) - 4
  127. lhgrid = Frame(agrid, width=w, height=question_height)
  128. rhgrid = Frame(agrid, width=w, height=question_height)
  129. this_row = 0
  130. left_side = True
  131. for choice in self.questions[piece][question]:
  132. if left_side:
  133. r = Radiobutton(lhgrid, text=choice,
  134. value=choice, variable=a)
  135. left_side = False
  136. else:
  137. r = Radiobutton(rhgrid, text=choice,
  138. value=choice, variable=a)
  139. left_side = True
  140. this_row += 1
  141. r.grid(row=this_row, column=0, sticky=W)
  142. self.stuff_to_disable.append(r)
  143. lhgrid.grid_propagate(0)
  144. lhgrid.grid(row=0, column=1)
  145. rhgrid.grid_propagate(0)
  146. rhgrid.grid(row=0, column=2)
  147. agrid.grid_propagate(0)
  148. agrid.grid(row=question_row_number, column=0)
  149. question_row_number += 1
  150. piece_grid.grid_propagate(0)
  151. piece_grid.grid(row=this_row_number, column=0)
  152. this_row_number += 1
  153. question_grid.grid(row=0, column=0)
  154. def play(self):
  155. self.play_button.configure(state=DISABLED)
  156. thread.start_new_thread(self.play_pieces, ())
  157. def play_pieces(self):
  158. self.excerpt_length = (config.getfloat("general", "excerpt_length") - 5) * 1000
  159. break_length = config.getfloat("general", "break_length")
  160. cur_excerpt = "A"
  161. mixer.init()
  162. for piece in self.order:
  163. try:
  164. self.playing.set("Now Playing: Excerpt {0}".format(cur_excerpt))
  165. before = time.time()
  166. self.play_piece(piece)
  167. after = time.time()
  168. while after - before < 3: # if the piece played for less than
  169. before = time.time() # 3 seconds, assume something went
  170. self.play_piece(piece) # wrong loading and try to replay it
  171. after = time.time()
  172. self.playing.set("That was Excerpt {0}...".format(cur_excerpt))
  173. cur_excerpt = chr(ord(cur_excerpt) + 1)
  174. time.sleep(break_length)
  175. except error: # Someone quit our mixer? STOP EVERYTHING.
  176. break
  177. self.playing.set("Finished playing.")
  178. def play_piece(self, piece):
  179. mixer.music.load(os.path.join(piece_dir, piece))
  180. mixer.music.play()
  181. fadeout_enabled = False
  182. while mixer.music.get_busy():
  183. if mixer.music.get_pos() >= self.excerpt_length:
  184. if not fadeout_enabled:
  185. mixer.music.fadeout(5000)
  186. fadeout_enabled = True
  187. time.sleep(1)
  188. def submit(self):
  189. self.submit_button.configure(state=DISABLED)
  190. self.play_button.configure(state=DISABLED)
  191. for item in self.stuff_to_disable:
  192. item.configure(state=DISABLED)
  193. try:
  194. mixer.quit()
  195. except error: # pygame.error
  196. pass # music was never played, so we can't stop it
  197. right = 0
  198. wrong = []
  199. excerpt = "A"
  200. for piece in self.order:
  201. questions = self.questions[piece].keys()
  202. questions.reverse() # correct question ordering
  203. for question in questions:
  204. correct_answer = config.get(piece, question)
  205. given_answer = self.answers[piece][question].get()
  206. if given_answer == u"Der Erlk\xf6nig": # unicode bugfix
  207. given_answer = "Der Erlk\xc3\xb6nig"
  208. if correct_answer == given_answer:
  209. right += 1
  210. else:
  211. wrong.append((excerpt, config.get(piece, "title"),
  212. question, given_answer, correct_answer))
  213. excerpt = chr(ord(excerpt) + 1)
  214. results = Toplevel() # make a new window to display results
  215. results.title("Results")
  216. noq = self.number_of_questions
  217. text = "{0} of {1} answered correctly ({2}%):".format(right, noq,
  218. round((right / noq) * 100, 2))
  219. if right == noq:
  220. text += "\n\nCongratulations, you got everything right!"
  221. else:
  222. text += "\n"
  223. for excerpt, title, question, given_answer, correct_answer in wrong:
  224. if not given_answer:
  225. if question == "title":
  226. text += "\nYou left the title of Excerpt {0} blank; it's \"{1}\".".format(
  227. excerpt, correct_answer)
  228. else:
  229. text += "\nYou left the {0} of \"{1}\" blank; it's {2}.".format(
  230. question, title, correct_answer)
  231. elif question == "title":
  232. text += "\nExcerpt {0} was {1}, not {2}.".format(
  233. excerpt, correct_answer, given_answer)
  234. else:
  235. text += "\nThe {0} of \"{1}\" is {2}, not {3}.".format(
  236. question, title, correct_answer, given_answer)
  237. label = Label(results, text=text, justify=LEFT, padx=15, pady=10,
  238. font=Font(family="Verdana", size=8))
  239. label.pack()
  240. def randomize_list(old):
  241. new = []
  242. while old:
  243. obj = random.choice(old)
  244. new.append(obj)
  245. old.remove(obj)
  246. return new
  247. def generate_piece_order():
  248. pieces = config.sections()
  249. pieces.remove("general") # remove config section that is not a piece
  250. return randomize_list(pieces)
  251. def load_config():
  252. global config, piece_dir
  253. config = configparser.SafeConfigParser()
  254. config.read(config_filename)
  255. if not config.has_section("general"):
  256. exit("Your config file is missing or malformed.")
  257. piece_dir = os.path.abspath(config.get("general", "piece_dir"))
  258. def get_missing_pieces(root):
  259. pieces = config.sections()
  260. pieces.remove("general")
  261. missing_pieces = []
  262. for piece in pieces:
  263. if not os.path.exists(os.path.join(piece_dir, piece)):
  264. missing_pieces.append(piece)
  265. if missing_pieces:
  266. window = Toplevel()
  267. window.title("PyQuizzer")
  268. window.protocol("WM_DELETE_WINDOW", root.destroy)
  269. status = StringVar()
  270. status.set("I'm missing {0} music ".format(len(missing_pieces)) +
  271. "pieces;\nwould you like me to download them for you now?")
  272. head_label = Label(window, text="Download Music Pieces", font=Font(
  273. family="Verdana", size=10, weight="bold"))
  274. head_label.grid(row=0, column=0, columnspan=2)
  275. status_label = Label(window, textvar=status, justify=LEFT, padx=15,
  276. pady=10)
  277. status_label.grid(row=1, column=0, columnspan=2)
  278. quit_button = Button(window, text="Quit", command=lambda: exit())
  279. quit_button.grid(row=2, column=0)
  280. dl_button = Button(window, text="Download",
  281. command=lambda: do_pieces_download(missing_pieces, status,
  282. dl_button, status_label, window))
  283. dl_button.grid(row=2, column=1)
  284. window.mainloop()
  285. else:
  286. global download_complete
  287. download_complete = True
  288. def do_pieces_download(pieces, status, dl_button, status_label, window):
  289. global download_complete
  290. dl_button.configure(state=DISABLED)
  291. if not os.path.exists(piece_dir):
  292. os.mkdir(piece_dir)
  293. counter = 1
  294. for piece in pieces:
  295. url = config.get("general", "base_url") + config.get(piece, "url")
  296. name = "{0} of {1}: {2}".format(counter, len(pieces),
  297. config.get(piece, "title"))
  298. urlretrieve(url, os.path.join(piece_dir, piece),
  299. lambda x, y, z: progress(x, y, z, name, status, status_label))
  300. counter += 1
  301. window.quit()
  302. window.withdraw()
  303. download_complete = True
  304. def progress(count, block_size, total_size, name, status, label):
  305. percent = int(count * block_size * 100 / total_size)
  306. status.set("Downloading pieces...\n" + name + ": %2d%%" % percent)
  307. label.update_idletasks()
  308. def run():
  309. root = Tk()
  310. root.withdraw()
  311. load_config()
  312. get_missing_pieces(root)
  313. while not download_complete:
  314. time.sleep(0.5)
  315. window = Toplevel()
  316. window.title("MusicQuizzer")
  317. answer_sheet = AnswerSheet(window)
  318. answer_sheet.canvas.create_window(0, 0, anchor=NW,
  319. window=answer_sheet.frame)
  320. answer_sheet.frame.update_idletasks()
  321. answer_sheet.canvas.config(scrollregion=answer_sheet.canvas.bbox("all"))
  322. window.protocol("WM_DELETE_WINDOW", root.destroy) # make the 'x' in the
  323. # corner quit the entire program, not just this window
  324. window.mainloop()
  325. if __name__ == "__main__":
  326. run()