Session 6

Persisting values between application runs.
(.ini files, TMemIniFile, properties, MessageDlg, Undo)

Session #6

Continuing from the last session, I've added a very useful feature that most applications support: The ability to remember settings between invocations. It was actually there last time, but I extended it and refactored it a bit. Plainly put, this means that, if you make changes to some of the settings in the program and then exit, the next time you run the program, those settings are the same. You won't "lose" the settings when you quit the program. This is done by saving the current settings to a file.

This mechanism has been around since the beginning of Windows and the files are called INI files (pronounced: eye-en-eye or innee). These files have a simple and well-known format and should be used as an alternative to the failed-experiment known as the "Windows registry". (Contrary to what the article says.) Follow the previous link for a refresher on the structure of these files.

Topics explored this session:

  1. Using properties to access private data Properties have been around in RAD Studio-related products since the early 1990's. They have recently gained wide popularity with C#. (Incidentally, the person that architected C# is the same person that architected Delphi. Delphi is the tool after which both C# and Turbo C++ are designed. The person's name is Anders Hejlsberg.) Properties give the user a cleaner syntax for reading/writing data of an object. Here's a sample:

    In the header file:

    private:
      bool FGridEnabled;      
      short FDefaultRowCount; 
      void SetDefaultRowCount(short RowCount); // To set FDefaultRowCount
    
    public:
      __property bool GridEnabled = {read=FGridEnabled, write=FGridEnabled};
      __property short DefaultRowCount = {read=FDefaultRowCount, write=SetDefaultRowCount};
    
    In the implementation file:
    void TfrmConfig::SetDefaultRowCount(short RowCount)
    {
        // Constrain the new value
      short minrows = std::min(RowCount, MAX_ROW_COUNT);
      FDefaultRowCount = std::max(minrows, MIN_ROW_COUNT);
    }
    
  2. Creating a class that holds the application's configuration. We created a class based on TForm called TfrmConfig that contains all of the user's settings. (These are also called "Preferences" in other applications.) This class also has a UI so that the user can modify these settings. This class is responsible for displaying a window for the user to change the settings. It will also persist these settings to a file before the program ends. The class will read the saved settings from disk when the application starts. See the source code for the details. We will add more to the configuration dialog as the application progresses.

    The "General" pageThe "Programs" page

    The bulk of the work is done in these two methods:
      // Copy the values from the UI components to the private member fields.	
      // This is called after the user makes changes and clicks the "OK" button.	
    void TfrmConfig::DialogToFields(void)
    {
      GameExecutable = edtGameExecutable->Text;
      GridEnabled = chkGridEnabled->Checked;
      DefaultRowCount = (short) StrToInt(edtRows->Text);
      DefaultColCount = (short) StrToInt(edtColumns->Text);
    }
    
      // Copy the values of the private member fields to the UI components. 
      // This is done when the UI is shown to the user. It can also be called 
      // after the user has made changes, but then clicks the "Cancel" button. 
    void TfrmConfig::FieldsToDialog(void)
    {
      edtGameExecutable->Text = GameExecutable;
      chkGridEnabled->Checked = GridEnabled;
      spnRows->Position = DefaultRowCount;
      spnColumns->Position = DefaultColCount;
    
    }
    
  3. Persisting the settings to a file This will be done using the TMemIniFile component. This works like TIniFile, except that it is buffered in memory, which may give it performance boost. These components are non-visual components. See the documentation for TIniFile for details.

    The bulk of the work is done in these methods in the configuration class:

    // Read the saved settings from the .ini file.
    void TfrmConfig::LoadSettings(void)
    {
        // TMemIniFile is a buffered ini file
      AnsiString filename = ExtractFilePath(ParamStr(0)) + FConfigFilename;
      TMemIniFile *inifile = new TMemIniFile(filename);
      
      GameExecutable = inifile->ReadString("Programs", "GameExecutable", "");
      GridEnabled = inifile->ReadBool("General", "GridEnabled", 0);
      DefaultRowCount = (short)inifile->ReadInteger("General", "DefaultRowCount", DEFAULT_ROW_COUNT);
      
      FieldsToDialog();
      delete inifile;
    }
    
    // Write the current settings to the .ini file.
    void TfrmConfig::SaveSettings(void)
    {
        // TMemIniFile is a buffered ini file
      AnsiString filename = ExtractFilePath(ParamStr(0)) + FConfigFilename;
      TMemIniFile *inifile = new TMemIniFile(filename);
      
      inifile->WriteString("Programs", "ExecutableFile", GameExecutable);
      inifile->WriteBool("General", "GridEnabled", GridEnabled);
      inifile->WriteInteger("General", "DefaultRowCount", DefaultRowCount);
      inifile->UpdateFile(); // Flush the buffer to disk before deleting!!!
      
      delete inifile;
    }
    
  4. Miscellaneous additions to the application.

    1. Undo - The user can undo changes made to the map. This can be done via the menu "Edit | Undo", a toolbar button, and a hotkey, Ctrl-Z. The relevant code can be found in the methods PushUndo and PopUndo.
    2. Creating a message dialog by calling the function MessageDlg. This gives you more control over the layout of the error messages. You can see an example in SetGameExecutable:
      void TfrmConfig::SetGameExecutable(const AnsiString &exe)
      {
          // Make sure that the file is valid
        if (FileExists(exe))
          FGameExecutable = exe;
        else // otherwise, display an error message
        {
          TMsgDlgButtons btns;
          btns << mbOK << mbCancel << mbHelp;
          MessageDlg(exe + " is not a valid filename.", mtError, btns, 0);
        }
      }
      
      Trying to set the name of the executable to E:\foobar\nonexistentfile.foobar:
      
      
      See the online help for MessageDlg and how to use it:
      
      
      const char *m = "You can put\n\nnewlines in\nthe "
                      "message\n\n\nas well as\ttabs\ttabs\ttabs\n"
                      "\n\nBut I wouldn't recommend a lot of buttons.";
      
      TMsgDlgButtons btns;
      btns << mbOK << mbYes << mbCancel
           << mbAbort << mbNo << mbRetry
           << mbIgnore << mbYesToAll << mbNoToAll;
      
      MessageDlg(m, mtWarning, btns, 0);
      
    Here's the current UI:
    
    


  5. Stand-alone TIniFile demo Here's another project that shows the use of .ini files to load and save application configuration settings. It's simpler than the one above in that it only deals with saving/loading the values. It uses TIniFile instead of TMemIniFile and it doesn't use properties or worry about the user "canceling" the dialog box after making changes. It also doesn't use exception handling when reading/writing the .ini files. See the source code for details.

    The applicationThe .ini file contents
    [General]
    Width=365
    Height=285
    Top=173
    Left=787
    Title=This is the title
    Description=This is the description
    [Level]
    Map=4
    Spectre=1
    Arachnotron=1
    Mancubus=1
    SpectreCount=10
    ArachnotronCount=15
    MancubusCount=7
    


  6. The "Completed" PlatForm Editor The final addition to the editor also required me to modify the CS230 Platform Game. The changes allow the game to be conifigured via a configuration file, rather than hard-coded into the source code. When the game starts up, it reads its configuration file (CS230.cfg), which is an .INI-style file like the one used in the platform editor. There really wasn't anything new to learn, as everything has already been presented. This is what things look like:
    UI for configuring the gameAn example config file
    [Platform]
    Gravity=-40
    Jump_Velocity=15
    Move_Velocity_Hero=10
    Move_Velocity_Enemy=2
    Enemy_Idle_Time=0
    Multi_Jump=1
    Hero_Lives=5
    

    Note about XE 3:
    For reasons I can't explain yet, when building version 3 or 4 (PEditor3/PEditor4) of the Platform Editor, the build fails with linker errors. The errors are related to using std::stack in the code. There is a "work-around" that will get it to link.

    The work-around is to build with the RTL (Run Time Library). You enable this in the C++ Linker options: Ctrl+Shift+F11, choose C++ Linker, and set Link with Dynamic RTL to true.

    It's possible that no one else will have this problem. It may be limited to my computer, maybe because of a configuration problem when it was installed or something.

Exercises

  1. Implement a Redo capability (it's already in the latest version). Hint: Use two stacks and move the data between them. Make sure you have a tool bar button, menu item, and hotkey (Ctrl-Y).
  2. Fix the bug. Bug? Well, not a bug, per se, but incorrect behavior. Here's the problem:
    1. Start the application
    2. Maximize the window
    3. Quit the application
    4. Start the application again
    When the app starts again, it has been sized to the maximum, yet it isn't maximized. You can see the that the icon in the upper right shows that it is not. (Hint: To solve this, if the window size is the size of the entire screen, maximize the window.)
  3. Add some polish to the UI. Some menu items still need icons and the icon for Undo and Redo are the same. Maybe an arrow that points in the opposite direction?
  4. Maybe take your own CS 230 platform game and see if you can integrate it with the editor?