Title: Make a flash card app in Python
Finally, here's the flash card app! These earlier posts lead up to this one:
The Python code used by this example is relatively straightforward, it's keeping track of how the program moves from one state to another that's the real trick.
Using the Program
Before we get to the program's states and code, it will help if you know how to use the program.
When it starts, the program searches the FlashCards subdirectory to find the flash card images. It pairs them up so, for example, it matches the file ki_a.png with the file ki_a.png. The code randomizes the list of file pairs and displays the first _a file to show you a challenge image.
When you see a challenge image, you should mentally decide what it means and then click the blank canvas on the right to see the mnemonic in the _b file.
If you were correct, click the happy button. If you were wrong, click the sad button. In either case, the button increments the appropriate count, displays the next challenge image, and clears the result image.
When you have finished looking at all of the images, the program blanks both the challenge and result images and won't display any more pictures. You can see your score below the happy and sad buttons.
Moving Between States
The state transition diagram on the right shows how the program moves through its states as you work with the flash cards. It basically does what I said in the preceding section. It loads and randomizes the file pairs, displays a challenge image, waits until you click the result canvas, displays the result, etc.
You might want to run the program and give it a try now while you follow the program's states in the diagram. When you're ready, move on to the next section to see the Python code that controls the program's flow.
Loading Files
Then the program starts, the app's constructor uses the following code to get started.
def __init__(self):
# Make the main interface.
self.window = tk.Tk()
self.window.title('flash_cards')
self.window.protocol('WM_DELETE_WINDOW', self.kill_callback)
# Build the rest of the UI.
self.build_ui()
# Load the image file names.
self.get_file_list()
# Initially there is no current image.
self.current_pair = None
# Display the first image.
self.show_a_image(None)
# Display the window.
self.window.focus_force()
self.window.mainloop()
This code performs the usual tkinter startup chores and then calls build_ui to build the user interface. That code isn't spectacularly innovative, so I won't describe it here. Download the example to see it in all its glory.
Next, the code calls the following get_file_list method to load the names of the flash card files.
def get_file_list(self):
'''Get the list of file names.'''
# Make dictionaries to hold the _a and _b files.
a_files = {}
b_files = {}
path = Path('./FlashCards')
for item in path.iterdir():
if item.is_file(): # Only process files.
# Get the hiragana character's name.
char = item.name.split('_')[0]
if '_a.' in item.name:
a_files[char] = item
else:
b_files[char] = item
# Make a list holding tuples of character names and files.
self.files = []
for char in a_files:
if char in b_files:
self.files.append((a_files[char], b_files[char]))
# Randomize the list.
random.shuffle(self.files)
The files come in pairs, for example, ne_a.png and ne_b.png. This method first loads those file names into the dictionaries a_files and b_files.
To load the file names, the code uses path.iterfiles to list the objects in the FlashCards subdirectory. If an object is a file (not a directory, link, or something else exotic), the code gets the file's name minus the _a or _b and its extension. For example, if the file's name is ne_a.png, the code sets char to ne.
If the file's name contains _a, the program adds the file item to the a_files dictionary using the reduced name (ne) as its key. If the name doesn't include a_, the code adds it to the b_files dictionary.
After it has loaded all of the flash card file names, it loops through the names in a_files. If the file's key (e.g. ne) is also in the b_files dictionary, the program adds the file items in both dictionaries as a pair in the new self.files list. (Checking that the hiragana character has a file in both dictionaries protects the program in case one of the files is missing, for example, if ne_a.png exists but ne_b.png does not.)
Once it has built the list of file item pairs, the method calls random.shuffle to randomize the list. Later, the program can just remove the last item from the list to get the files in random order. Note that picking a random element from the list or shuffling it multiple times does not make it "more random." Shuffling the list once at the beginning is enough and it a common technique when a program needs to generate items in a random order.
Skip back up a bit to the FlashCardsApp constructor. After that code calls get_file_list, it sets self.current_pair = None because the we are not currently working with a flash card pair. The program then calls show_a_image described in the next section. It also forces focus to the window and enters the tkinter main loop.
Displaying a Challenge Image
The following code shows how the show_a_image method displays a challenge image.
def show_a_image(self, event):
'''Show the next a image.'''
# Make sure we have more images to display.
if len(self.files) == 0:
self.a_canvas.delete(tk.ALL)
self.b_canvas.delete(tk.ALL)
messagebox.showinfo('Out Of Images',
'Sorry, all out of images')
self.current_pair = None
return
# Get the next image pair.
self.current_pair = self.files.pop()
# Display the a image.
filename = self.current_pair[0].resolve()
self.show_image(self.a_canvas, filename)
# Clear the b image.
self.b_canvas.delete(tk.ALL)
This code first checks whether self.files is empty. If the list is empty, it displays a message saying there are no more images.
If the list is not empty, the code uses pop to remove the last pair from the list and saves if in self.current_pair.
Next, the code resolves the first image's file item to get a complete path to the challenge image file and calls show_image (described shortly) to display that image in the window's left canvas.
The method finishes by clearing the right canvas to remove any previous mnemonic image displayed for the last flash card.
Displaying Images
The following show_image method displays a file's image on a canvas widget.
def show_image(self, canvas, filename):
'''Display the image file in the canvas.'''
# Load the image.
image = Image.open(filename)
# Scale to fit.
canvas.update()
wid = canvas.winfo_width()
hgt = canvas.winfo_height()
margin = 5
image, x, y = fit_image(image, margin, margin, wid - margin, hgt - margin)
# Display.
canvas.photo_image = ImageTk.PhotoImage(image)
canvas.create_image(x, y, image=canvas.photo_image, anchor=tk.NW)
This method loads the file into a PIL image. It gets the size of the canvas that should display the image and uses the fit_image to resize the image to fit. (See my earlier post Fit an image to a target rectangle and center it in Python for information on fit_image.) The code adds a margin around the image so it isn't clipped at the canvas's edges. (That happens because this program makes the canvas display a sunken border.)
Next, the program converts the PIL image into a PhotoImage that tkinter can understand, saves it in the canvas's photo_image property so the garbage collector can't throw it away, and draws the image on the canvas.
Displaying the Mnemonic Image
At this point, the program has displayed the challenge image in the left canvas. You should think of what that image means and then click the canvas on the right to make the following method execute.
def show_b_image(self, event):
'''Show the next b image.'''
# Make sure we have a current pair of images.
if self.current_pair is None:
messagebox.showinfo('Out Of Images',
'Sorry, all out of images')
return
# Display the b image.
self.show_image(self.b_canvas, self.current_pair[1])
This method first checks that there is a current file pair. If self.current_pair is None, that means we have displayed all of the flash cards and are done.
If there is a current image pair, the code calls show_image to display the second image of the pair in the right canvas.
Scoring
After you see the result image, you should click either the happy button or the sad button to indicate whether you were correct. (We're on the honor system, here. No cheating!) The following code executes when you click the happy button.
def correct_click(self):
'''The user clicked the right button.'''
# Make sure the b image is visible.
if len(self.b_canvas.find_all()) == 0:
messagebox.showinfo(
'See Mnemonic First',
'Click on the right canvas to see the mnemonic first')
return
# Increment the right count.
self.correct_count += 1
self.correct_button['text'] = f'{self.correct_count}'
# Display the next image.
self.show_a_image(None)
The code first checks to see if the canvas on the right contains anything, in particular, the mnemonic image. If no mnemonic image is visible, the user needs to think of the flash card's meaning and then click the canvas on the right to see the mnemonic image, so the code says so.
If the mnemonic image is visible, the code increments the correct_count, updates the happy button's text to show the new count, and calls show_a_image to display the next flash card.
If you didn't guess the flash card's meaning so you clicked the sad button, the following code executes.
def wrong_click(self):
'''The user clicked the wrong button.'''
# Make sure the b image is visible.
if len(self.b_canvas.find_all()) == 0:
messagebox.showinfo(
'See Mnemonic First',
'Click on the right canvas to see the mnemonic first')
return
# Increment the wrong count.
self.wrong_count += 1
self.wrong_button['text'] = f'{self.wrong_count}'
# Display the next image.
self.show_a_image(None)
This code is almost identical to the code for the happy button except is increments the wrong_count and updates the sad button's text.
Conclusion
It may seem like there should be more code, but that's it! Clicking the various parts of the program move it from state to state until it has finished displaying all of the flash cards.
You could make a lot of changes to the program. One simple change would be to have the program display a final score when you finish with the last flash card.
A slightly more involved modification would make the program save the final score into a file.
An even more ambitious project might repeat the flash cards several times, repeating those that you got wrong more than the others to help you master all of the cards.
Download the example and experiment with it. Then create new flash cards so you can train on other subjects. You should be able to make the program work with any topic where you can present challenge images and then results.
|