Click here to Skip to main content
15,883,705 members
Articles / Desktop Programming / MFC
Article

Non volatile variables and configuration settings for MFC applications

Rate me:
Please Sign up or sign in to vote.
4.67/5 (3 votes)
21 May 200212 min read 103K   1.1K   25   17
Two classes for saving and restoring string, BOOL and numeric variables in a textfile - much like INI files, using a set of preprocessor macros.

Sample Image - Cfg.jpg

Introduction

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 string list from file, write string list 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() have 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 string list 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 typedef'ed 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 index number for a variable in an Item. Index 0 is the first variable. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior) Svep DesignCenter
Sweden Sweden
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhy not use standard .ini files? Pin
Jessn27-Oct-09 23:18
Jessn27-Oct-09 23:18 
AnswerRe: Why not use standard .ini files? Pin
Ruben Jönsson27-Oct-09 23:54
Ruben Jönsson27-Oct-09 23:54 
GeneralHelp on make it work! Pin
cosh10-May-03 4:35
cosh10-May-03 4:35 
GeneralRe: Help on make it work! Pin
cosh11-May-03 10:06
cosh11-May-03 10:06 
GeneralRe: Help on make it work! Pin
Ruben Jönsson11-May-03 12:18
Ruben Jönsson11-May-03 12:18 
Well, at first I planned to be able to put in comments separated by a semi colon character ';', but it doesn't work that well - only the first word is kept and comments on their own line is likely to end up in the wrong line since the order of items within one section can change.

I never had any use of it so I didn't fix it. In my own programs the user never has a reason to read or change the settings in the cfg file since I provide an interface to change user selectable settings (preferences/options).

I also try to give the item a meaningful name. Just remember to not put any spaces in the name. For instance: instead of "Simulator file", use "Simulator_file". Of course, this doesn't apply to the variables themselfs, just the item name.

Regards

Ruben
GeneralRe: Help on make it work! Pin
cosh12-May-03 0:53
cosh12-May-03 0:53 
GeneralDemo (small function) on how to access data in your .cfg Pin
JoeSox31-Dec-02 22:19
JoeSox31-Dec-02 22:19 
GeneralRe: Demo (small function) on how to access data in your .cfg Pin
Ruben Jönsson1-Jan-03 2:53
Ruben Jönsson1-Jan-03 2:53 
GeneralRe: Demo (small function) on how to access data in your .cfg Pin
JoeSox1-Jan-03 7:26
JoeSox1-Jan-03 7:26 
GeneralAnother Option I use all the time Pin
Christopher Lord13-Jan-02 22:08
Christopher Lord13-Jan-02 22:08 
GeneralRe: Another Option I use all the time Pin
Uwe Keim14-Jan-02 0:47
sitebuilderUwe Keim14-Jan-02 0:47 
GeneralRe: Another Option I use all the time Pin
Christopher Lord14-Jan-02 12:21
Christopher Lord14-Jan-02 12:21 
GeneralRe: Another Option I use all the time Pin
Uwe Keim14-Jan-02 19:48
sitebuilderUwe Keim14-Jan-02 19:48 
GeneralRe: Another Option I use all the time Pin
Ruben Jönsson14-Jan-02 2:01
Ruben Jönsson14-Jan-02 2:01 
GeneralRe: Another Option I use all the time Pin
Felix Cho14-Jan-02 5:57
Felix Cho14-Jan-02 5:57 
GeneralRe: Some XML Info Pin
Christopher Lord14-Jan-02 12:26
Christopher Lord14-Jan-02 12:26 
GeneralRe: Another Option I use all the time Pin
25-Mar-02 14:59
suss25-Mar-02 14:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.