Display flash cards in C#

[flash cards]

This example displays flash cards created by two pervious examples. The post Split images into halves in C# shows how to separate the prompt and result flash card images. The post Label images at the bottom in C# shows how to add a label to the bottom of the result images to make them easier to understand.

This example displays the flash cards created by the two previous posts.

A and B Files

Physical flash cards typically have a challenge on one side and a solution on the other. The user would look at the challenge picture, mentally decide what it means, and then flip the card over to see if the guess is correct.

This program uses pairs of image files to represent the sides of the flash cards. The two files should have the same names but the challenge file’s name should end in _a and the solution file’s name should end in _b, both before the file extension.

For example, the following pictures show the files a_a.png and a_b.png. (Actually the file names on the web site are slightly different.)

[flash cards] [flash cards]

Using the Program

To use the program, invoke the File menu’s Open command to select a file in the directory that contains the flash cards. (I would use the folder selection dialog to let you select the folder instead of a file, but that dialog is terrible.)

When you select the file, the program reads the images in that file’s directory and saves the pairs. (If a file is missing its pair, for example an _a file without a corresponding _b file, then the file is ignored.)

The program randomizes the pairs and displays one of the challenge images in an _a file.

At this point, you should mentally decide what the image means and then click the image to reveal the solution image. The program then enables the happy and sad face buttons. Click one of them to record whether you were correct. The program updates the counts of correct and incorrect answers and then displays the next challenge image.

The program continues displaying images until you have seen all of the images in the directory. It then displays a message indicating your overall score.

If you like, you can use the File menu’s Open command to open a file, possibly in a new directory, and start over again.

Program States

The example’s code isn’t terribly complicated. The main challenge is keeping track of the program’s state because it has several and you move among them in different ways. The following flow chart shows how the program moves through its states. The boxes represent states. Text between the boxes represents user actions.


[flash cards]

Code

The following sections describe the program’s code.

The Card Class

The program stores information about the flash cards in the following Card class.

public class Card
{
    public Bitmap ASide, BSide;

    public Card(FileInfo a_info, FileInfo b_info)
    {
        ASide = LoadBitmapUnlocked(a_info.FullName);
        BSide = LoadBitmapUnlocked(b_info.FullName);
    }

    // Load a bitmap without locking it.
    private Bitmap LoadBitmapUnlocked(string file_name)
    {
        using (Bitmap bm = new Bitmap(file_name))
        {
            return new Bitmap(bm);
        }
    }
}

This class’s job is to hold the pairs of images that define the flash cards. It stores the images in its ASide and BSide properties.

The class’s only constructor takes as parameters two FileInfo objects that represent the flash card’s A and B sides. The constructor calls the LoadBitmapUnlocked method to load them into bitmaps and saves the bitmaps in the ASide and BSide properties. For more information on the LoadBitmapUnlocked method, see the post Load images without locking their files in C#.

Loading Files

When you select the File menu’s Open command, the program uses the following code to load flash cards.

private void mnuFileOpen_Click(object sender, EventArgs e)
{
    if (ofdDirectory.ShowDialog() == DialogResult.OK)
    {
        // Load the flash cards.
        LoadFiles(ofdDirectory.FileName);

        // Randomize the cards and start a session.
        Cards.Randomize();
        NumCorrect = 0;
        NumWrong = 0;
        RoundNumber = -1;

        ShowNextRound();
    }
}

This code displays an OpenFileDialog to let you select a file in the flash cards’ directory. If you select a file and click Open, the program calls the LoadFiles method described shortly to load the flash cards into the Cards list.

It then calls the list’s Randomize extension method to randomize the cards. For information on that extension method, see the post Make extension methods that randomize arrays and lists in C#.

The code resets the NumCorrect, NumWrong, and RoundNumber values, and calls the ShowNextRound method to start the new round of testing.

The following code shows the LoadFiles method, which performs some interesting LINQ queries.

private List<Card> Cards = null;

private void LoadFiles(string filename)
{
    DirectoryInfo dir_info = (new FileInfo(filename)).Directory;
    var a_query =
        from FileInfo file_info in dir_info.GetFiles("*_a.*")
        orderby file_info.Name
        select file_info;
    List<FileInfo> a_s = a_query.ToList();
    var b_query =
        from FileInfo file_info in dir_info.GetFiles("*_b.*")
        orderby file_info.Name
        select file_info;
    List<FileInfo> b_s = b_query.ToList();

    var card_query =
        from FileInfo a_info in a_s
        join FileInfo b_info in b_s
          on a_info.Name.Replace("_a.", ".") equals
             b_info.Name.Replace("_b.", ".")
        select new Card(a_info, b_info);
    Cards = card_query.ToList();

    btnCorrect.Text = "";
    btnWrong.Text = "";
    btnCorrect.Enabled = true;
    btnWrong.Enabled = false;
}

The program defines the Cards list at the form level. This list holds the list of Card objects that represent the loaded flash cards.

The LoadFiles method takes as a parameter the name of the file that you selected to define the flash card directory. It creates a FileInfo object for that file, and then gets the DirectoryInfo object that represents its containing directory.

The code then defines two LINQ queries. The first selects FileInfo objects for the files in the directory that have names matching the pattern *_a.*. The second query is similar except it selects files with names matching *_b.*. The code executes both queries into lists.

Next the code creates a third LINQ query to select pairs of items from the two lists where replacing _a. in the first list with . gives the same name as replacing _b. in the second list with .. For example, the new query would pair files with names ee_a.png and ee_b.png. (I think you could fool this with some very strange file names like A_a._b.png and _b._a.png, but why would you want to do that? Seriously. Go watch a movie or something instead.)

This is an “inner join” so if a name in the _a list is not matched with a name in the _b list, or vice versa, then no pair is created. That allows you to store non-flash card files in the same directory with the flash cards and the program will ignore those files as long as they do not come in matched pairs of the form *_a.* and *_b.*.

The program executes the query and saves the resulting list of FileInfo objects in the Cards list.

Clicking the Challenge Picture

After the program displays a challenge image, you mentally decide what it means and click the image. When you do, the following code executes.

private void picASide_Click(object sender, EventArgs e)
{
    btnCorrect.Enabled = true;
    btnWrong.Enabled = true;
    picBSide.Image = Cards[RoundNumber].BSide;
}

This code simply enables the happy and sad buttons, and displays the result picture.

Clicking Happy or Sad

After the program displays a result picture, you click the happy or sad buttons to indicate whether you were correct. That executes one of the following event handlers.

private int NumCorrect, NumWrong, RoundNumber;

// Record a correct answer.
private void btnCorrect_Click(object sender, EventArgs e)
{
    NumCorrect++;
    ShowNextRound();
}

// Record an incorrect answer.
private void btnWrong_Click(object sender, EventArgs e)
{
    NumWrong++;
    ShowNextRound();
}

When you click the happy button, the btnCorrect_Click event handler executes. It increments the NumCorrect variable and calls ShowNextRound to display the next challenge picture.

The sad button works similarly except it increments the NumWrong variable.

Showing the Next Round

The ShowNextRound method shown in the following code displays the next round.

private void ShowNextRound()
{
    picBSide.Image = null;
    btnCorrect.Text = NumCorrect.ToString();
    btnWrong.Text = NumWrong.ToString();
    btnCorrect.Enabled = false;
    btnWrong.Enabled = false;

    RoundNumber++;
    if (RoundNumber < Cards.Count)
    {
        picASide.Image = Cards[RoundNumber].ASide;
        picASide.Enabled = true;
    }
    else
    {
        picASide.Image = null;
        picASide.Enabled = false;
        int total = NumCorrect + NumWrong;
        double percent = NumCorrect / (double)total;
        MessageBox.Show("You answered " +
            NumCorrect.ToString() + " out of " +
            total.ToString() +
            " correctly for a score of " +
            percent.ToString("P0"));
    }
}

This method displays the next challenge image. To get the program ready to handle your next selection, it clears the solution image, displays the current number of correct and incorrect answers in the happy and sad buttons, and disables those buttons.

The code then increments the round number. If RoundNumber is now less than the number of objects in the Cards list, then the program has not displayed all of the flash cards. In that case, the code displays the next challenge image in the picASide control and enables that control so you can click it when you are ready.

If the program has displayed all of the flash cards, the code clears the challenge image and disables its control. The code calculates the percentage of correct selections and displays the score. At this point you must select a new file in the flash cards directory to start over.

Conclusion

This program isn’t terribly complicated, it just seems that way because the user changes the program’s state in several different ways. If you keep track of each transition separately, the code isn’t too bad.

As always, download the example to experiment with it and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in image processing and tagged , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.