Non volatile variables and configuration settings for MFC applications

Environment: VC6 sp5 tested on W95 and W98

This article describes two classes for storing and retrieving string, numeric and BOOL variables to/from files in ASCII text format - much like INI files. The first class - CTxtVar, is used to dynamically add and remove variables in the text file. The second class - CCfg, which is derived from CTxtVar, is used as a base class to map a fixed set of variables or configuration settings in the text file to constant ID's which are used anywhere in the application to access (read and write) the actual variables/settings in the text file.

The CTxtVar class

I have chosen to put the variables in text files in human readable format instead of a binary format in order to make it easy to examine and modify the variables from any text editor. The format of the text file is divided into sections, items and variables:
[SECTION_HEADER]
ITEM VAR,VAR,VAR
ITEM VAR
ITEM VAR,VAR,VAR,VAR,VAR

[SECTION_HEADER]
ITEM VAR
ITEM VAR,VAR,VAR
Every item can have up to 20 variables attached to it (this is #defined to 20 in the CTxtVar header file for now - in the future this may be changed to a dynamic CStringList instead of a fixed array as now). For a real example, see the edit box on the right in the picture of the demo app above.

All variables are handled as strings in the CTxtVar class. Each line in the text file (item and section headers) are stored in a CStringList in the CTxtVar class. Items are accessed with a section header name and an item name. There are also functions to split the item and it's variables from a single string to an array of strings, one for each variable, which makes it possible to extract a single variable from an item. String variables and item names that contains white spaces or commas must be embedded within double quotes to be treated as one single variable. The items and it's variables are written back to the string list as one complete string. It is up to the calling function to build this string before it is written back.

Other functions in the CTxtVar class is: Read stringlist from file, write stringlist to file, find section, find item in section, add item, remove item, remove section and iterate through a section getting the items one by one.

The CTxtVar class is used when the number of items in a section is dynamic - items are added and removed on the fly, or when the item name and number of variables for an item is unknown at design time. Even the section name could be unknown. Example of this is list or combo boxes which could have it's contents manipulated by the user. The demo app demonstrates this example.

The functions SetItem() and SetSectionItem() has a BOOL parameter called bAdd which, if set to TRUE, allows items with identical names to be added to one section as separate items. An example where this could be useful is where the item represents a graphical object of some sort in a view. The name of the item is given by the user - not known at design time. The graphical object could be displayed on several places, perhaps in different modes on the same view. The item is the name supplied by the user, variable 1 could be the drawing mode, variable 2 the x position, variable 3 the y position and so on. This would mean that the item name would show up for every object of this type that is placed in the view. To retrieve the variables for these items FindItem() and FindSectionItem() would only find the first item with this name after the section header. In this case it is more useful to use the function FindSectionHeader() for the section with the items of interest and then iterate through every item of this section with GetNextItem(), perhaps building a linked list or an array of objects for these items, which is then used to access the objects within the application. One way to write back changed settings for the objects (added/removed objects, changed position) is to simply remove all items from the section in the string list with the function EmptySection() and then iterate through the list or array of objects in the app and for each one build the item string and write it back with SetSection() or SetSectionItem().

There is a BOOL member variable in the CTxtVar, m_bChanged, that is set to TRUE whenever a string is added, removed or changed in the string list. This value is checked in the destructor of the class which, if TRUE, automatically updates the text file. This means that there is generally no need to write the stringlist back to the text file manually (with Flush()) unless another class needs to read the text file during the lifetime of the CTxtVar class. This is the case in the demo app since the text file itself is displayed in a rich edit control and updated whenever there has been a change in the string list of the CTxtVar class. Of course, if the app exits unnaturally the destructor Flush() may not work so it will be a good idea Flush() critical changes back to the text file right after they are changed.

See the CTxtVar.cpp file for more information about member functions and variables.

The CCfg class

Whenever I do an MFC app I have a lot of variables and settings that should be saved between sessions. Call me old fashioned but I rather save these variables in a separate file in my app's directory than in the registry for several reasons: It is easy to read and modify (if it is in text format), it is easy to clean up, the same app can have several settings by simply starting it in different directories, just to mention some. The variables used for configuration settings are all known at design time. The CCfg class associates every setting/variable (and section and item) with an ID which is used instead of the section name and item name to access it's value. Furthermore, the variables can be of type string, numeric, limited numeric (limited by a max and min value) and BOOL. Every variable also has a default value which is used if the setting isn't found in the text file (which it isn't the first time the app starts or if the text file is deleted). If a value for a setting isn't found in the text file the default value will be written to the text file.

The CCfg class holds the information for every variable, item and section in a list of CCfgNode derived classes. These classes have information about the ID, the current value, the default value and a maximum and minimum value for the limited numeric type. The CCfg class uses the string list of the CTxtVar to load the CCfgNodes with current values for the settings. The CCfgNode list is written back to the text file in two steps, first the CCfgList is converted back to the string list in CTxtVar and then the base class CTxtVar writes back the string list to the text file. This isn't done until the Flush() function is called which could be as late as in the destructor of the CCfg and CTxtVar classes. The CCfg class dynamically builds the CCfgNode list upon initialization from information in an array of structures typedefed as CFGDEF.

The CFGDEF array is where the programmer specifies what section names, item names types of variables, default values and ID's should be used for the settings. In order to keep things simple and not having to remember too much about how this CFGDEF structure works I have created a set of macros that does most of the job during compile time. Also, these macros automatically assigns the ID a unique number which makes it impossible to have two ID's of the same number by mistake. How can this be done, you may ask, how can a macro both define a structure array and declare a constant with a different value at the same time? Well it can't, but by putting the macros for the definition of the CFGDEF structure in the header file of the CCfg derived class (the user class) and redefining the macros and including this header file twice from the implementation file (.cpp file) of the CCfg derived class it can. The macros that generate the unique ID's and builds the CFGDEF array can look something like this:

BEGIN_CFGDEF(cfgdef)

   CFG_SECTION(CFGID_SETTINGS,"Settings")
      CFG_WINDOWPOS(CFGID_INITWINDOWPOS,"MainWindowPos",
                      100,100,620,510,0)
      CFG_FONT(CFGID_FONT,"Font","System",12,FW_NORMAL,FALSE)

      CFG_ITEM(CFGID_CURRENTCOMBOLIST,1,"Currentcombolist")
      CFG_STRING(CFGID_CURRENTCOMBOLIST_NAME,"")

      CFG_ITEM(CFGID_BOOL,1,"Bool")
      CFG_BOOL(CFGID_BOOLVALUE,0)

      CFG_ITEM(CFGID_NUM,1,"Numeric")
      CFG_NUM(CFGID_NUMVALUE,0)

      CFG_ITEM(CFGID_DIR,1,"Directory")
      CFG_STRING(CFGID_DIRSTRING,"")

END_CFGDEF
The resulting text file with default values for this CFGDEF looks like this:
[Settings]
MainWindowPos 100,100,620,510,0
Font "System",12,400,0
Currentcombolist ""
Bool 0
Numeric 0
Directory ""
The order of the items in the section may vary though.

I have chosen to make the CCfg derived class global in order to make it accessible from all classes that has included the header file of the CCfg derived class. I think this is one case where a global variable is a good thing in a C++ application. This way it is easy for the objects that needs to access non volatile variables or configuration settings themselves without obtaining a pointer from another (global) class.

The variables/settings can be read by the following functions: GetBool(int VariableId), GetString(int VariableId) and GetNum(int VariableId) or GetBool(int ItemId, int VariableIndex), GetString(int ItemId, int VariableIndex) and GetNum(int ItemId, int VariableIndex) where VariableIndex is the indexnumber for a variable in an Item. Index 0 is the first varaible. Use the following functions to write variables/settings: SetBool(int VariableId,BOOL Value), SetString(int VariableId,CString Value) and SetNum(int VariableId,long Value) or SetBool(int ItemId, int VariableIndex, BOOL Value), SetString(int ItemId, int VariableIndex, CString Value) and SetNum(int ItemId, int VariableIndex, CString Value).

To check the integrity of the CFGDEF array the debug version fails ASSERT macros if something is wrong. This could be: A CFG_SECTION isn't the first macro after BEGIN_CFGDEF or an item has more or less variables than specified. ASSERT also fails if the application is trying to use the wrong access function for a variable ID, for example trying to read a string variable with the GetNum() function.

How the CFGDEF macros work

When the header file (the declaration file) for the CCfg derived class is included from the cpp file (the implementation file) for the CCfg derived class the first time or whenever it is included from another class the macros generate a typedef of an enumerated variable that include all ID's from the CFGDEF macros. This way the ID's have been assigned unique consecutive numbers which is known by all classes that includes the declaration file. Before the header file is included a second time from the cpp file of the CCfg derived class it has defined an identifier to tell the header file that this time the macros should generate the implementation of the CFGDEF structure array. To accomplish this the macros are redefined. The definition and redefinition of the macros takes place in the header file for the CCfg class.

How to use it

In order to use the CCfg class you have to derive a class from it and put the macros for the CFGDEF structure array in the derived class' header file. The only function needed is the constructor which has to generate the CfgNode list with a call to MakeCfgList(const CFGDEF *cfg_def). It may be easiest to use the following implementation and declaration files as a template and change the names as you want.

The declaration file (header file)

// CfgDem.h: interface for the CCfgDem class.
//
///////////////////////////////////////////////////////

#if !defined( \
AFX_CFGDEM_H__20008C44_F97E_11D5_8C08_B343B9E2DD77__INCLUDED_)
#define \
AFX_CFGDEM_H__20008C44_F97E_11D5_8C08_B343B9E2DD77__INCLUDED_
#define __CFG_FIRST_RUN__   
#endif

#if defined(__CFG_FIRST_RUN__)
#define   __INCLUDE_CFGDEF__
#elif defined(__CFG_IMPLEMENTATION__)
#define   __INCLUDE_CFGDEF__
#endif

#if defined(__INCLUDE_CFGDEF__)
#include "Cfg.h"

/*
   This file must be #included twice from the CCfg 
   derived class, once to enumerate the ID's 
   (definition) and once to implement the array of 
   CFGDEF structures (declaration).
   The CFG_... macros are redefined when 
   __CFG_IMPLEMENTATION__ is defined.

   When this file is included from the CCfg derived 
   class and __CFG_IMPLEMENTATOIN__ is defined the 
   following macros generate the array of CFGDEF 
   structures used to access the cfg file.

   When this file is included without 
   __CFG_IMPLEMENTATION__ defined (the first time
   it is included in the CCfg derived class and 
   whenever it is included in another cpp file) all 
   ID's are enumerated to generate unique numbers 
   for all cfg ID's.
*/

   BEGIN_CFGDEF(cfgdef)  // cfgdef is the name 
                         //for the CFGDEF structure 
                    // array - CFGDEF cfgdef[]={

      // Put the rest of the CFGDEF macros here

   END_CFGDEF

#endif

#if defined(__CFG_FIRST_RUN__)

class CCfgDem : public CCfg  
{
public:
   CCfgDem();
   virtual ~CCfgDem();

};

extern CCfgDem cfg;    // Make the global cfg 
                     // variable visible to 
                // other classes that 
                     // includes this .h file.

#endif

#undef __INCLUDE_CFGDEF__
#undef __CFG_FIRST_RUN__
#undef __CFG_IMPLEMENTATION__
Note that the #pragma once can not be used in this header file. Rename all instances of CCfgDem to whatever name you want the derived class to have.

The implementation file (cpp file)

// CfgDem.cpp: implementation of the CCfgDem class.
//
//////////////////////////////////////////////////

#include "stdafx.h"
#include "CfgDemo.h"
#include "CfgDem.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

#define __CFG_IMPLEMENTATION__
#include "CfgDem.h"  // When __CFG_IMPLEMENTATION__ is 
                     // defined 
                     // CfgDem.h actually defines the CFGDEF
                     // structure array

CCfgDem     cfg;     // Global CCfgDem class.

/////////////////////////////////////////////////////////
// Construction/Destruction
/////////////////////////////////////////////////////////

CCfgDem::CCfgDem()
{
  MakeCfgList(cfgdef);  // Make the cfg node list from 
                        // the cfgdef struct array.
}

CCfgDem::~CCfgDem()
{

}
Rename all instances of CCfgDem to whatever name you want the derived class to have.

See the source files for more information about the CCfg class.

About the demo app

The view class together with some custom controls handles all the functionality of the CfgDemo app. The document class is not used at all.

All controls on the left side is used to show and manipulate settings in the text file. The CRichEditCtrl on the right side is used to show the actual contents of the text file itself and is updated whenever the text file is changed.

There are two combo boxes which are used to enter and display data of three lists. The first combo box is of type drop list which selects one of three lists. The other combo box is a drop down type used to select and enter data into the list selected by the first combo box. The selection of an item in the second combo doesn't do anything in this demo, though. There is also a button to delete the currently selected list. The lists are updated and accessed in the text file by CTxtVar (base class of the CCfg class) functions that directly reads and writes to and from the CStringList representing the text file in the CTxtVar class. This is done to demonstrate that dynamic sections and items can be read and written to the same text file as CFGID linked sections and items.

There is a read only edit box which displays a directory selection done with a SHBrowseForFolder dialog through a button next to the edit box.

There is an edit box with a spin control used for entering a numeric value.

There is a check box used for a BOOL value.

There is a button which opens up a modal font selection dialog box to set the font for the CRichEditCtrl. The font setting is also stored in the text file.

The app also uses a custom class derived from CFormView called CMyFormView as a base class for the view. This class automatically handles resizing and repositioning of controls on the form when the main frame is resized. The size, position and maximized/normal state is saved in the text file when the app closes and is restored when the app is loaded the next time.

Use the source as you want. If you do - please send me a mail to tell me.

Downloads

Download demo project - 21 Kb
Download source - 47 Kb


Comments

  • Thanks, works great

    Posted by Legacy on 02/05/2002 12:00am

    Originally posted by: gatman

    i've been trying to write code to do exactly what these functions do(without much success), plugged into my app and works great, i have several dialogs that need default values on init, this code worksb good, 1 question though, i have some item variables set to a empty string(""),in the macros,after a dialog runs once it saves the string to the file, but when i start application again, the macro values(""),overwrite the last saved values, is there any way to prevent this ??
    CFG_STRING(CFGID_TM_URLSTRINGLAST,"")

    and should i be able to add more var to a item at run time then the number that was set in the macro,
    CFG_ITEM(CFGID_DY_CB1,8,"DY_CB1")


    thanks alot for any help

    Reply
  • interest

    Posted by Legacy on 01/27/2002 12:00am

    Originally posted by: Moth

    Something between VB&VC

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The explosion in mobile devices and applications has generated a great deal of interest in APIs. Today's businesses are under increased pressure to make it easy to build apps, supply tools to help developers work more quickly, and deploy operational analytics so they can track users, developers, application performance, and more. Apigee Edge provides comprehensive API delivery tools and both operational and business-level analytics in an integrated platform. It is available as on-premise software or through …

  • The first phase of API management was about realizing the business value of APIs. This next wave of API management enables the hyper-connected enterprise to drive and scale their businesses as API models become more complex and sophisticated. Today, real world product launches begin with an API program and strategy in mind. This API-first approach to development will only continue to increase, driven by an increasingly interconnected web of devices, organizations, and people. To support this rapid growth, …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds