Manage Your Configuration Files and Data: Difference between revisions
m removed superfluous tags |
mNo edit summary |
||
(9 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
Written by [[Stefan Ruck]] | ''Written by [[Stefan Ruck]]'' | ||
[NOTE: Here is a link to a zip of the | [NOTE: Here is a link to a zip of the http://www.edm2.com/0509/confsrc.zip source code for this article. Ed.] | ||
code | |||
== Introduction == | |||
Almost every program needs some configuration data. But where to store it? How to save and retrieve the data? | |||
In this article I will show you two different ways to manage your configuration data. One describes a template class which gives you the ability to store your data in a self-defined structure. The other gives you a class which is derived from IProfile. This class conforms to IBM's programming guide. | |||
When you look at the description of configuration files in IBM's programming guide vol. II [1], the guide is always talking about initialization files. I will make a distinction between configuration and initialization files. When I use 'configuration files' I mean those files which use a self-defined structure to store the data. 'Initialization files' are the files which store the data conforming to the programming guide using the IProfile class or my derived AIniFile class. I will use 'ini file' for the global meaning. | |||
configuration | |||
== Where To Place The Data? == | |||
programming | What I hadn't found in the programming guide is a specification about the name of the file and its location. Also IProfile accepts any filename passed to its constructor. | ||
[[Harald Wilhelm]] gave the base definition in his article 'Register me!' [2], which I expanded a little bit. He said that the filename should be the same as the program name except the filename suffix, which should almost always be 'ini'. The sequence for where to place the file was defined as follows: | |||
* The directory of the main program. If this is write protected, | |||
* The current directory. If this is write protected too, | |||
* The system directory (\os2). | |||
I think the second place to look for is the directory where the user ini file (os2.ini) is located. This is set in the config.sys by the environment variable USER_INI. Then follows the current and at last the system directory. | |||
Setting the filename suffix I distinguish between the configuration file (suffix 'cfg') and the initialization file (suffix 'ini'). | |||
I think you should NEVER use the os2.ini file to store your initialization data. There are several reasons why not. The first is a simple question: Have you ever corrupted the registry of your Win95 or NT system? Another point is when you save the configuration in a separate file, it's easy for the user to reconfigure his system without losing his application configurations. And thinking about it a little bit I guess you'll find a few more good reasons. | |||
the | |||
== Some Restrictions == | |||
As defined above, each application should have its own ini file. This concept is strictly realized in the classes I've written. It is a reduction against the IProfile class and the OS/2 API which gives you the ability to store data for different applications in the same initialization file. The unique data key in these files is always the combination application / user key. You can't pass a filename to the classes I've written. The filename is set by the object itself at runtime. | |||
As defined above, each application should have its own ini file. This | |||
concept is strictly realized in the classes I've written. It is a | |||
reduction against the IProfile class and the OS/2 API which gives you the | |||
ability to store data for different applications in the same | |||
initialization file. The unique data key in these files is always the | |||
combination application / user key. You can't pass a filename to the | |||
classes I've written. The filename is set by the object itself at runtime. | |||
So you can never read the ini file of another application or a global one. | So you can never read the ini file of another application or a global one. | ||
The Class Hierarchy: | The Class Hierarchy: | ||
<pre> | <pre> | ||
AConfigBase | |||
(abstract class) | |||
| | |||
----------------------------- | |||
| | | |||
AConfigFile AIniFile | |||
(abstract class) (abstract class) | |||
| | | |||
ACfgConfig AIniConfig | |||
(application specific class) (application specific class) | |||
</pre> | </pre> | ||
''Figure 1: The ini file class hierarchy.'' | |||
Figure 1: The ini file class hierarchy. | |||
AConfigBase is the base class of AConfigFile, which handles | AConfigBase is the base class of AConfigFile, which handles configuration files, and AIniFile, which covers initialization files. | ||
configuration files, and AIniFile, which covers initialization files. | AConfigBase handles building the filename, which is similar for both AConfigFile and AIniFile. | ||
AConfigBase handles building the filename, which is similar for both | |||
AConfigFile and AIniFile. | |||
AConfigFile and AIniFile are abstract classes as AConfigBase. This | AConfigFile and AIniFile are abstract classes as AConfigBase. This means you cannot create an object of these classes. You have to derive an application specific class from AConfigFile or AIniFile for use. | ||
means you cannot create an object of these classes. You have to derive an | |||
application specific class from AConfigFile or AIniFile for use. | |||
Why is that? In my opinion AConfigFile and AIniFile are still | Why is that? In my opinion AConfigFile and AIniFile are still incomplete. A complete ini class needs an interface to access the different configuration parameters. An example for complete ini classes are ACfgConfig and AIniConfig (see below). | ||
incomplete. A complete ini class needs an interface to access the | |||
different configuration parameters. An example for complete ini classes | |||
are ACfgConfig and AIniConfig (see below). | |||
== The Base For All Ini Files: AConfigBase == | |||
The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp. | The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp. | ||
<code> | |||
< | class AConfigBase:public IBase | ||
class AConfigBase : public IBase | |||
{ | { | ||
public: | public: | ||
AConfigBase (const char * pszSuffix); | AConfigBase (const char* pszSuffix); | ||
virtual ~AConfigBase () = 0; | virtual ~AConfigBase () = 0; | ||
const IString& getFullFilename () | const IString& getFullFilename () | ||
{ return (m_strFullFilename); } | { return (m_strFullFilename); } | ||
Line 119: | Line 65: | ||
const IString& getPathname () | const IString& getPathname () | ||
{ return (m_strPathname); } | { return (m_strPathname); } | ||
private: | private: | ||
AConfigBase& filenameFromAppDir (const char * pszSuffix); | AConfigBase& filenameFromAppDir (const char * pszSuffix); | ||
// build filename from app directory | // build filename from app directory | ||
// throws IAccessError | // throws IAccessError | ||
APIRET filenameFromIniDir (); | APIRET filenameFromIniDir (); | ||
// build filename from ini directory | // build filename from ini directory | ||
APIRET filenameFromCurrDir (); | APIRET filenameFromCurrDir (); | ||
// build filename from current directory | // build filename from current directory | ||
AConfigBase& filenameFromSystemDir (); | AConfigBase& filenameFromSystemDir (); | ||
// build filename from system | // build filename from system | ||
// directory | // directory | ||
// throws IAccessError | // throws IAccessError | ||
AConfigBase& buildPathname (); | AConfigBase& buildPathname (); | ||
// build the pathname from m_strFullFilename | // build the pathname from m_strFullFilename | ||
Boolean tryOpenFile (AFile& rFile, IString& rstrFile); | Boolean tryOpenFile (AFile& rFile, IString& rstrFile); | ||
// throws IAccessError | // throws IAccessError | ||
IString m_strFullFilename, // filename including path | IString m_strFullFilename, // filename including path | ||
m_strPureFilename, | m_strPureFilename, | ||
// filename without path, but with leading '\' | // filename without path, but with leading '\' | ||
m_strPathname; // only the path | m_strPathname; // only the path | ||
APIRET m_ret; | APIRET m_ret; | ||
// keeps the return value of the API calls | // keeps the return value of the API calls | ||
}; | }; | ||
</code> | |||
''Figure 2: Declaration of AConfigBase.'' | |||
The constructor of AConfigBase needs the suffix of the ini file as a parameter to be able to build the complete filename. It is passed by AConfigFile and AIniFile during the initialization of these classes. | |||
The | The destructor is declared as pure virtual to make AConfigBase an abstract class. | ||
The | The three public methods are to retrieve the filename including drive and path (AConfigBase::getFullFilename), the pure filename with a leading '\' (AConfigBase::getPureFilename) and only the path including the drive of the ini file (AConfigBase::getPathname). | ||
All other methods are declared as private. They are called by the constructor to build the name of the file. They can not be used from outside because the filename of an ini file object must not change during its lifetime. | |||
<code> | |||
All other methods are declared as private. They are called by the | |||
constructor to build the name of the file. They can not be used from | |||
outside because the filename of an ini file object must not change during | |||
its lifetime. | |||
< | |||
//********************************************************** | //********************************************************** | ||
// AConfigBase :: AConfigBase - Constructor of AConfigBase * | // AConfigBase :: AConfigBase - Constructor of AConfigBase * | ||
//********************************************************** | //********************************************************** | ||
AConfigBase :: AConfigBase (const char * pszSuffix) | AConfigBase :: AConfigBase (const char * pszSuffix) | ||
: m_strFullFilename (), | : m_strFullFilename (), | ||
Line 186: | Line 119: | ||
AFile file; | AFile file; | ||
IString strFile; | IString strFile; | ||
filenameFromAppDir (pszSuffix); | filenameFromAppDir (pszSuffix); | ||
// build the filename from the app directory | // build the filename from the app directory | ||
if(tryOpenFile (file, strFile)) | if(tryOpenFile (file, strFile)) | ||
// the ini data can be saved in the app | // the ini data can be saved in the app | ||
// directory | // directory | ||
return; | return; | ||
if(!filenameFromIniDir ()) // == NO_ERROR | if(!filenameFromIniDir ()) // == NO_ERROR | ||
{ | { | ||
Line 202: | Line 135: | ||
return; | return; | ||
} | } | ||
switch((m_ret = filenameFromCurrDir ())) | switch((m_ret = filenameFromCurrDir ())) | ||
// build the filename from the | // build the filename from the | ||
Line 222: | Line 155: | ||
// something unexpected has gone wrong | // something unexpected has gone wrong | ||
} | } | ||
filenameFromSystemDir (); | filenameFromSystemDir (); | ||
// build the filename from the system directory | // build the filename from the system directory | ||
if(!tryOpenFile (file, strFile)) | if(!tryOpenFile (file, strFile)) | ||
// the ini data can't be saved in the | // the ini data can't be saved in the | ||
Line 233: | Line 166: | ||
// something unexpected has gone wrong | // something unexpected has gone wrong | ||
} | } | ||
</ | </code> | ||
''Figure 3: Constructor of AConfigBase.'' | |||
Figure 3: Constructor of AConfigBase. | |||
The constructor tries to build the filename of the ini file using the sequence described above. | |||
The AConfigBase::filenameFrom... methods use the different API calls to determinate the values they need. AConfigBase::filenameFromAppDir has also the task of setting the pure filename. That's because DosQueryModuleName is the way to get the filename's prefix. Since the app directory is the first place where to place the ini file, there's no extra method needed to build the pure filename. | |||
These methods return different values. AConfigBase::filenameFromAppDir and AConfigBase::filenameFromSystemDir return a reference to the AConfigBase object. If a problem occurs in one of these methods, this will be a problem that cannot be handled correctly by the ini object, so an IAccessError is thrown. | |||
The constructor | The error that may be detected by AConfigBase::filenameFromAppDir is that the module name cannot be queried by the API function DosQueryModuleName. The error that may arise in AConfigBase::filenameFromSystemDir is that the system path can't be queried by DosQuerySysInfo. The exceptions are not caught by the constructor. You have catch them yourself. The source of the sample application shows one way how to do this. | ||
AConfigBase::filenameFromIniDir and AConfigBase::filenameFromCurrDir return the value received by the API call to query the environment variable USER_INI (DosScanEnv) and to query the current directory (DosQueryCurrentDir) respectively. | |||
the | |||
If AConfigBase::filenameFromIniDir does not return NO_ERROR, the value of USER_INI cannot be determined. I can't tell if the operating system misses this entry, but here that is not too bad. So we can continue trying to build the filename from the current directory. | |||
The constructor can handle three return values from AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was used to build the filename and we can try to put the ini file there. | |||
AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was | |||
used to build the filename and we can try to put the ini file there | |||
ERROR_NOT_DOS_DISK and ERROR_DRIVE_LOCKED mean we have to try to build the filename from the system directory. Any other return value means that something has gone wrong. In this case an IAccessError is thrown. For a complete list of return values please refer to the OS/2 API manual. | |||
IAccessError is thrown. | |||
< | If none of the four directories can be used to store the ini file (also ACongfigBase::tryOpenFile using the system directory returns false), an IAccessError is thrown. | ||
<code> | |||
//********************************************************** | //********************************************************** | ||
// AConfigBase :: tryOpenFile - tries to open the ini file * | // AConfigBase :: tryOpenFile - tries to open the ini file * | ||
Line 287: | Line 192: | ||
// throws IAccessError * | // throws IAccessError * | ||
//********************************************************** | //********************************************************** | ||
Boolean AConfigBase :: tryOpenFile (AFile& rFile, | Boolean AConfigBase :: tryOpenFile (AFile& rFile, | ||
IString& rstrFile) | IString& rstrFile) | ||
{ | { | ||
rstrFile = m_strPathname + "\\~tryopen.tmp"; | rstrFile = m_strPathname + "\\~tryopen.tmp"; | ||
switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL, | switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL, | ||
OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_CREATE_IF_NEW | ||
Line 322: | Line 227: | ||
} | } | ||
} | } | ||
return (false); | return (false); | ||
} | } | ||
</ | </code> | ||
''Figure 4: AConfigBase::tryOpenFile.'' | |||
Figure 4: AConfigBase::tryOpenFile. | |||
Figure 4 shows the key method to check out if the ini file can be | Figure 4 shows the key method to check out if the ini file can be placed inside of the desired directory. I'm using my class AFile (please refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose. | ||
placed inside of the desired directory. I'm using my class AFile (please | |||
refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose | |||
Of course you can use DosOpen instead. We need here OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_REPLACE_IF_EXISTS as open flags. Normally, the testfile ~tryopen.tmp should not exist. So we have to use OPEN_ACTION_CREATE_IF_NEW. In the really improbable case that this file exists, we have to try to recreate it to see if we have write permissions on the drive. Therefore we also need OPEN_ACTION_REPLACE_IF_EXISTS. | |||
we | |||
The default open modes of AFile::open are OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE. These must be expanded by OPEN_FLAGS_FAIL_ON_ERROR because the media I/O errors should not be handled by the system critical-error handler (the user gets a system message on the screen) but be reported as a return value. Now the parameters are what we need to see if the ini file can be used in the way we want it. Because we use a temporary file for testing it is guaranteed that we do not destroy any previously saved data. If AFile::open returns NO_ERROR, we can use the directory and the method returns true. If AFile::open returns ERROR_WRITE_PROTECT or ERROR_OPEN_FAILED, then we should try the next directory and AConfigBase::tryOpenFile returns false. | |||
Any other return values causes an exception of type IAccessError. | |||
== AConfigFile - Do It Non-Conform == | |||
The class AConfigFile is defined in cfgfile.h and cfgfile.c. | The class AConfigFile is defined in cfgfile.h and cfgfile.c. | ||
<code> | |||
< | |||
template <class T> class AConfigFile : public AConfigBase, | template <class T> class AConfigFile : public AConfigBase, | ||
public AFile | public AFile | ||
{ | { | ||
public: | public: | ||
AConfigFile (); | AConfigFile (); | ||
virtual ~AConfigFile () = 0; | virtual ~AConfigFile () = 0; | ||
T * getConfigData () { return (&m_ConfigData); } | T * getConfigData () { return (&m_ConfigData); } | ||
APIRET open (const ULONG ulFileAttribute = FILE_NORMAL, | APIRET open (const ULONG ulFileAttribute = FILE_NORMAL, | ||
const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE, | const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE, | ||
Line 379: | Line 264: | ||
// open the ini file | // open the ini file | ||
} | } | ||
APIRET read (const ULONG ulFileAttribute = FILE_NORMAL, | APIRET read (const ULONG ulFileAttribute = FILE_NORMAL, | ||
const ULONG ulOpenFlag = FILE_OPEN, | const ULONG ulOpenFlag = FILE_OPEN, | ||
Line 385: | Line 270: | ||
| OPEN_SHARE_DENYWRITE, | | OPEN_SHARE_DENYWRITE, | ||
const PEAOP2 pEABuf = 0L); | const PEAOP2 pEABuf = 0L); | ||
APIRET write (const ULONG ulFileAttribute = FILE_NORMAL, | APIRET write (const ULONG ulFileAttribute = FILE_NORMAL, | ||
const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE, | const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE, | ||
Line 391: | Line 276: | ||
| OPEN_SHARE_DENYREADWRITE, | | OPEN_SHARE_DENYREADWRITE, | ||
const PEAOP2 pEABuf = 0L); | const PEAOP2 pEABuf = 0L); | ||
private: | private: | ||
T m_ConfigData; | T m_ConfigData; | ||
}; | }; | ||
</ | </code> | ||
''Figure 5: Declaration of AConfigFile.'' | |||
Figure 5: Declaration of AConfigFile. | |||
As you can see in figure 5, AConfigFile is a template class derived | As you can see in figure 5, AConfigFile is a template class derived from AConfigBase. This gives you the ability to use it for any configuration structure needed. It is also derived from AFile, since the configuration data is stored in a simple file. | ||
from AConfigBase. This gives you the ability to use it for any | |||
configuration structure needed. It is also derived from AFile, since the | |||
configuration data is stored in a simple file. | |||
There is nothing mysterious about this class. AFile's read and write | There is nothing mysterious about this class. AFile's read and write methods are overridden because AConfigFile always keeps the file closed. | ||
methods are overridden because AConfigFile always keeps the file closed. | AConfigFile::read and AConfigFile::write checks if the file is open, open it if not, and close it again after reading/writing. When you use the makefiles included with the sample code, you will retrieve a warning that AConfigFile::open hides both AFile::open methods, AFile::read and AFile::write. That's because I want to avoid the use of a filename different from the one built by AConfigBase during initialization and the use of the AFile::methods for reading and writing. | ||
AConfigFile::read and AConfigFile::write checks if the file is open, open | |||
it if not, and close it again after reading/writing. When you use the | |||
makefiles included with the sample code, you will retrieve a warning that | |||
AConfigFile::open hides both AFile::open methods, AFile::read and | |||
AFile::write. That's because I want to avoid the use of a filename | |||
different from the one built by AConfigBase during initialization and the | |||
use of the AFile::methods for reading and writing. | |||
Since the configuration data is a private member, you have to access it | Since the configuration data is a private member, you have to access it by AConfigFile::getConfigData, which returns a pointer to the structure. | ||
by AConfigFile::getConfigData, which returns a pointer to the structure. | |||
== IBM Programming Guide Conform - AIniFile == | |||
The class AIniFile is defined in inifile.hpp and inifile.cpp. | The class AIniFile is defined in inifile.hpp and inifile.cpp. | ||
<code> | |||
< | |||
class AIniFile : public AConfigBase, | class AIniFile : public AConfigBase, | ||
public IProfile | public IProfile | ||
{ | { | ||
public: | public: | ||
Line 431: | Line 301: | ||
virtual ~AIniFile () = 0; | virtual ~AIniFile () = 0; | ||
}; | }; | ||
</ | </code> | ||
''Figure 6: Declaration of AIniFile.'' | |||
Figure 6: Declaration of AIniFile. | |||
As you can see in figure 6, AIniFile only contains a constructor and a | As you can see in figure 6, AIniFile only contains a constructor and a (empty) destructor. All functionality needed here is inherited from the base classes AConfigBase and IProfile. | ||
(empty) destructor. All functionality needed here is inherited from the | |||
base classes AConfigBase and IProfile. | |||
One really important thing is the sequence of the declaration of the | One really important thing is the sequence of the declaration of the base classes. AConfigBase must be declared before IProfile. Have a look at the constructor code in figure 7: | ||
base classes. AConfigBase must be declared before IProfile. Have a look at | <code> | ||
the constructor code in figure 7: | |||
< | |||
//********************************************************** | //********************************************************** | ||
// AIniFile :: AIniFile - Constructor of AIniFile * | // AIniFile :: AIniFile - Constructor of AIniFile * | ||
//********************************************************** | //********************************************************** | ||
AIniFile :: AIniFile () | AIniFile :: AIniFile () | ||
: AConfigBase ("INI"), | : AConfigBase ("INI"), | ||
IProfile (getFullFilename ()) | IProfile (getFullFilename ()) | ||
{ | { | ||
IString strApplicationName (getPureFilename ()); | IString strApplicationName (getPureFilename ()); | ||
strApplicationName. remove (0, 1); | strApplicationName. remove (0, 1); | ||
// remove the leading '\' | // remove the leading '\' | ||
strApplicationName. remove (strApplicationName. lastIndexOf ('.')); | strApplicationName. remove (strApplicationName. lastIndexOf ('.')); | ||
// remove the suffix '.ini' | // remove the suffix '.ini' | ||
setDefaultApplicationName (strApplicationName); | setDefaultApplicationName (strApplicationName); | ||
// set the default | // set the default | ||
// application name | // application name | ||
} | } | ||
</ | </code> | ||
''Figure 7: Constructor of AIniFile.'' | |||
Figure 7: Constructor of AIniFile. | |||
The constructor of IProfile needs the name of the initialization file as an argument. This filename is part of the base class AConfigBase and is built during the initialization of AConfigBase. So AConfigBase must be initialized before IProfile. The sequence, in which the base classes are initialized, is defined by the declaration order, not by the order in the initialization list of the constructor of the derived class, here AIniFile (see [4]). | |||
And because there is always just one application per initialization file by definition, we can set the default application name of IProfile during the initialization of AIniFile. It can be taken from the pure filename of AConfigBase. Only the leading backslash and the suffix have to be removed. | |||
== Handle The Configuration Data - ACfgConfig == | |||
The class ACfgConfig is defined in cfgcfg.hpp. | The class ACfgConfig is defined in cfgcfg.hpp. | ||
<code> | |||
< | |||
typedef struct _CFG_CONFIGURATION { | typedef struct _CFG_CONFIGURATION { | ||
SIZEL sizeWindow; | SIZEL sizeWindow; | ||
BOOL bSizeWindowSaved; | BOOL bSizeWindowSaved; | ||
} CFG_CONFIGURATION; | } CFG_CONFIGURATION; | ||
class ACfgConfig : public AConfigFile <CFG_CONFIGURATION> | class ACfgConfig : public AConfigFile <CFG_CONFIGURATION> | ||
{ | { | ||
public: | public: | ||
Boolean isSizeSaved () // was a size saved? | Boolean isSizeSaved () // was a size saved? | ||
{ return (getConfigData () -> bSizeWindowSaved); } | { return (getConfigData () -> bSizeWindowSaved); } | ||
SIZEL getSize () // retrieve the size | SIZEL getSize () // retrieve the size | ||
{ return (getConfigData () -> sizeWindow); } | { return (getConfigData () -> sizeWindow); } | ||
ACfgConfig& setSize (const SIZEL sSize) | ACfgConfig& setSize (const SIZEL sSize) | ||
// set the size | // set the size | ||
Line 514: | Line 362: | ||
} | } | ||
}; | }; | ||
</ | </code> | ||
''Figure 8: The configuration structure and the declaration of ACfgConfig.'' | |||
Figure 8: The configuration structure and the declaration of ACfgConfig. | |||
Figure 8 shows a very simple configuration file class. It is used by | Figure 8 shows a very simple configuration file class. It is used by the sample program to save the window's size when the program gets closed. The position is saved by an initialization file to show you the use of both ini file classes. | ||
the sample program to save the window's size when the program gets closed. | |||
The position is saved by an initialization file to show you the use of | |||
both ini file classes. | |||
As you can see, there are three methods on each (here the one and only) | As you can see, there are three methods on each (here the one and only) configuration value. One to check if a value was saved at least, one to retrieve and one to set the value. The method to set the configuration parameter also has to care about the is-saved flag. Setting such a flag is the easiest way to determinate whether a configuration file contains a value or not. Of course you can also check if, in this sample, the size has a height and a width. But what if a width and height of 0 is permitted? How do you detect whether the value is newly initialized, or is saved there? Using the flag protects you from such pain. | ||
configuration value. One to check if a value was saved at least, one to | |||
retrieve and one to set the value. The method to set the configuration | |||
parameter also has to care about the is-saved flag. Setting such a flag is | |||
the easiest way to determinate whether a configuration file contains a | |||
value or not. Of course you can also check if, in this sample, the size | |||
has a height and a width. But what if a width and height of 0 is | |||
permitted? How do you detect whether the value is newly initialized, or is | |||
saved there? Using the flag protects you from such pain. | |||
== Manage The Initialization Data - AIniConfig == | |||
The class AIniConfig is defined in inicfg.hpp and inicfg.cpp. | The class AIniConfig is defined in inicfg.hpp and inicfg.cpp. | ||
<code> | |||
< | |||
class AIniConfig : public AIniFile | class AIniConfig : public AIniFile | ||
{ | { | ||
public: | public: | ||
Line 547: | Line 380: | ||
// profile? | // profile? | ||
{ return (containsKeyName (m_pszPosition)); } | { return (containsKeyName (m_pszPosition)); } | ||
POINTL getPosition () const; // retrieve the position | POINTL getPosition () const; // retrieve the position | ||
AIniConfig& setPosition (const POINTL pPosition); | AIniConfig& setPosition (const POINTL pPosition); | ||
// save the position | // save the position | ||
private: | private: | ||
static const char * const m_pszPosition; | static const char * const m_pszPosition; | ||
// position key name | // position key name | ||
}; | }; | ||
</ | </code> | ||
''Figure 9: Declaration of AIniConfig.'' | |||
Figure 9: Declaration of AIniConfig. | |||
Looking at figure 9 you can see that using AIniFile makes it quite | Looking at figure 9 you can see that using AIniFile makes it quite easier to determine whether an initialization value exists or not. You just have to call IProfile::containsKeyName with the desired key name as parameter. If the initialization file contains the key, true is returned, otherwise false. In contrast to ACfgConfig you do not need an additional flag in the ini file. | ||
easier to determine whether an initialization value exists or not. You | <code> | ||
just have to call IProfile::containsKeyName with the desired key name as | |||
parameter. If the initialization file contains the key, true is returned, | |||
otherwise false. In contrast to ACfgConfig you do not need an additional | |||
flag in the ini file. | |||
< | |||
// define the string for the position key | // define the string for the position key | ||
const char * const AIniConfig :: m_pszPosition = "Position"; | const char * const AIniConfig :: m_pszPosition = "Position"; | ||
</ | </code> | ||
''Figure 10: Definition of AIniConfig::m_pszPosition.'' | |||
Figure 10: Definition of AIniConfig::m_pszPosition. | |||
As shown in Figure 9, this class has a private static const member | As shown in Figure 9, this class has a private static const member variable called m_pszPosition. It is a pointer to the key name used to access the position element of the initialization file. Because the key name is the same for every instance of AIniConfig, the pointer to it can be shared by all AIniConfig objects. So it is declared as static. And to | ||
variable called m_pszPosition. It is a pointer to the key name used to | |||
access the position element of the initialization file. Because the key | |||
name is the same for every instance of AIniConfig, the pointer to it can | |||
be shared by all AIniConfig objects. So it is declared as static. And to | |||
be sure it always points to the same string, it is declared as const too. | be sure it always points to the same string, it is declared as const too. | ||
Maybe you will ask why you should declare a pointer to each key name. I | Maybe you will ask why you should declare a pointer to each key name. I think using a variable instead of typing the key name itself makes it easier to maintain the program. When you decide to rename a key during the development process, you just have to change the string one time: at the declaration of the pointer. And the compiler will check if you typed the variable name the right way. When you mistype the key name, you have to find it yourself. | ||
think using a variable instead of typing the key name itself makes it | <code> | ||
easier to maintain the program. When you decide to rename a key during the | |||
development process, you just have to change the string one time: at the | |||
declaration of the pointer. And the compiler will check if you typed the | |||
variable name the right way. When you mistype the key name, you have to | |||
find it yourself. | |||
< | |||
//********************************************************** | //********************************************************** | ||
// AIniConfig :: getPosition - return the position * | // AIniConfig :: getPosition - return the position * | ||
//********************************************************** | //********************************************************** | ||
POINTL AIniConfig :: getPosition () const | POINTL AIniConfig :: getPosition () const | ||
{ | { | ||
POINTL pointRet = { 0, 0 };// initialize the return value; | POINTL pointRet = { 0, 0 };// initialize the return value; | ||
if(!isPositionSaved ()) | if(!isPositionSaved ()) | ||
// was an element saved for the position? | // was an element saved for the position? | ||
return (pointRet); // no, return | return (pointRet); // no, return | ||
// an element for the position exists, so retrieve it | // an element for the position exists, so retrieve it | ||
IString strPosition (elementWithKey (m_pszPosition)); | IString strPosition (elementWithKey (m_pszPosition)); | ||
memcpy (&pointRet, (char *) strPosition, sizeof (POINTL)); | memcpy (&pointRet, (char *) strPosition, sizeof (POINTL)); | ||
// convert IString to POINTL | // convert IString to POINTL | ||
return (pointRet); | return (pointRet); | ||
} | } | ||
</ | </code> | ||
''Figure 11: Retrieve a binary element of an initialization file.'' | |||
Figure 11: Retrieve a binary element of an initialization file. | |||
< | IProfile::elementWithKey always returns an IString. If the value itself isn't a string, you have to convert it to the desired data type. Figure 11 shows you how the saved window position of the sample application (binary data) is retrieved and converted. If the saved data is numeric, you can use one of IString's type conversation methods to get the desired type. | ||
<code> | |||
//********************************************************** | //********************************************************** | ||
// AIniConfig :: setPosition - set the position * | // AIniConfig :: setPosition - set the position * | ||
//********************************************************** | //********************************************************** | ||
AIniConfig& AIniConfig::setPosition (const POINTL pPosition) | AIniConfig& AIniConfig::setPosition (const POINTL pPosition) | ||
{ | { | ||
IString strPosition (&pPosition, sizeof (POINTL), 0x0); | IString strPosition (&pPosition, sizeof (POINTL), 0x0); | ||
addOrReplaceElementWithKey (m_pszPosition, strPosition); | addOrReplaceElementWithKey (m_pszPosition, strPosition); | ||
return (*this); | return (*this); | ||
} | } | ||
</ | </code> | ||
''Figure 12: Save a binary element of an initialization file.'' | |||
Figure 12: Save a binary element of an initialization file. | |||
Because IProfile::addOrReplaceElementWithKey only accepts key values of type long or IString, you have to convert binary data to IString before saving. This can be done by one of IString's constructors, which accepts a void pointer to a buffer, the buffer's size and a pad character as parameters. AIniConfig::setPosition uses this way to save the window's | |||
position of the sample application. After converting the POINTL structure to an IString, IProfile::addOrReplaceElementWithKey can be used to save the data. | |||
As you can see when you're looking at the sample code, using the | == Which File Shall I Use? == | ||
initialization file ends up in more coding and marginal slower performance | As you can see when you're looking at the sample code, using the initialization file ends up in more coding and marginal slower performance because of the many type conversations of binary data. But having a good concept of how to organize the keys and writing an easy to use | ||
because of the many type conversations of binary data. But having a good | initialization class can make it really easy for others to use the data and easy for you to maintain it. So play around with the sample to find out which you like most. | ||
concept of how to organize the keys and writing an easy to use | |||
initialization class can make it really easy for others to use the data | |||
and easy for you to maintain it. So play around with the sample to find | |||
out which you like most. | |||
== The Sample Application == | |||
The sample application initest shows you the use of both ways to handle your ini data. Of course this is not a typical sample, since you will almost never use both ini file classes in one application. But here it is done for demonstration purpose. | |||
When you run the sample, be sure to close it using the F3 key or the menu 'File' 'Exit'. Any other way to end it doesn't saves the current position and size of the window. The position is saved in initest.ini, the size in initest.cfg. | |||
When you | When you build the sample application, you can use the makefiles included in the source zip. There is one makefile for IBM's C Set++ (initest.mac) and one for IBM's VisualAge C++ (initest.mav). If you decide to use your own makefile, make sure all switches especially for the use of templates are set correctly. | ||
# IBM, Programming Guide Volume II, e.g. Developer Connection Disk 2, Directory Docs, pmv2base.inf, 1996 | |||
# Harald Wilhelm, Register me!, OS/2 Inside, 5/97, 1997, p. 65-69 | |||
( | # Stefan Ruck, [[A Progress-indicating Status Line in C++ (Part 2)]], EDM/2 Volume 4 Issue 6, 1996 | ||
# Bjarne Stroustrup, The C++ Programming Language, Second Edition, 1995, p. 580 | |||
[[Category:C++ Articles]] | |||
[ | |||
[ | |||
Latest revision as of 21:59, 10 October 2022
Written by Stefan Ruck
[NOTE: Here is a link to a zip of the http://www.edm2.com/0509/confsrc.zip source code for this article. Ed.]
Introduction
Almost every program needs some configuration data. But where to store it? How to save and retrieve the data?
In this article I will show you two different ways to manage your configuration data. One describes a template class which gives you the ability to store your data in a self-defined structure. The other gives you a class which is derived from IProfile. This class conforms to IBM's programming guide.
When you look at the description of configuration files in IBM's programming guide vol. II [1], the guide is always talking about initialization files. I will make a distinction between configuration and initialization files. When I use 'configuration files' I mean those files which use a self-defined structure to store the data. 'Initialization files' are the files which store the data conforming to the programming guide using the IProfile class or my derived AIniFile class. I will use 'ini file' for the global meaning.
Where To Place The Data?
What I hadn't found in the programming guide is a specification about the name of the file and its location. Also IProfile accepts any filename passed to its constructor.
Harald Wilhelm gave the base definition in his article 'Register me!' [2], which I expanded a little bit. He said that the filename should be the same as the program name except the filename suffix, which should almost always be 'ini'. The sequence for where to place the file was defined as follows:
- The directory of the main program. If this is write protected,
- The current directory. If this is write protected too,
- The system directory (\os2).
I think the second place to look for is the directory where the user ini file (os2.ini) is located. This is set in the config.sys by the environment variable USER_INI. Then follows the current and at last the system directory.
Setting the filename suffix I distinguish between the configuration file (suffix 'cfg') and the initialization file (suffix 'ini').
I think you should NEVER use the os2.ini file to store your initialization data. There are several reasons why not. The first is a simple question: Have you ever corrupted the registry of your Win95 or NT system? Another point is when you save the configuration in a separate file, it's easy for the user to reconfigure his system without losing his application configurations. And thinking about it a little bit I guess you'll find a few more good reasons.
Some Restrictions
As defined above, each application should have its own ini file. This concept is strictly realized in the classes I've written. It is a reduction against the IProfile class and the OS/2 API which gives you the ability to store data for different applications in the same initialization file. The unique data key in these files is always the combination application / user key. You can't pass a filename to the classes I've written. The filename is set by the object itself at runtime. So you can never read the ini file of another application or a global one.
The Class Hierarchy:
AConfigBase (abstract class) | ----------------------------- | | AConfigFile AIniFile (abstract class) (abstract class) | | ACfgConfig AIniConfig (application specific class) (application specific class)
Figure 1: The ini file class hierarchy.
AConfigBase is the base class of AConfigFile, which handles configuration files, and AIniFile, which covers initialization files. AConfigBase handles building the filename, which is similar for both AConfigFile and AIniFile.
AConfigFile and AIniFile are abstract classes as AConfigBase. This means you cannot create an object of these classes. You have to derive an application specific class from AConfigFile or AIniFile for use.
Why is that? In my opinion AConfigFile and AIniFile are still incomplete. A complete ini class needs an interface to access the different configuration parameters. An example for complete ini classes are ACfgConfig and AIniConfig (see below).
The Base For All Ini Files: AConfigBase
The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp.
class AConfigBase:public IBase
{
public:
AConfigBase (const char* pszSuffix);
virtual ~AConfigBase () = 0;
const IString& getFullFilename ()
{ return (m_strFullFilename); }
const IString& getPureFilename ()
{ return (m_strPureFilename); }
const IString& getPathname ()
{ return (m_strPathname); }
private:
AConfigBase& filenameFromAppDir (const char * pszSuffix);
// build filename from app directory
// throws IAccessError
APIRET filenameFromIniDir ();
// build filename from ini directory
APIRET filenameFromCurrDir ();
// build filename from current directory
AConfigBase& filenameFromSystemDir ();
// build filename from system
// directory
// throws IAccessError
AConfigBase& buildPathname ();
// build the pathname from m_strFullFilename
Boolean tryOpenFile (AFile& rFile, IString& rstrFile);
// throws IAccessError
IString m_strFullFilename, // filename including path
m_strPureFilename,
// filename without path, but with leading '\'
m_strPathname; // only the path
APIRET m_ret;
// keeps the return value of the API calls
};
Figure 2: Declaration of AConfigBase.
The constructor of AConfigBase needs the suffix of the ini file as a parameter to be able to build the complete filename. It is passed by AConfigFile and AIniFile during the initialization of these classes.
The destructor is declared as pure virtual to make AConfigBase an abstract class.
The three public methods are to retrieve the filename including drive and path (AConfigBase::getFullFilename), the pure filename with a leading '\' (AConfigBase::getPureFilename) and only the path including the drive of the ini file (AConfigBase::getPathname).
All other methods are declared as private. They are called by the constructor to build the name of the file. They can not be used from outside because the filename of an ini file object must not change during its lifetime.
//**********************************************************
// AConfigBase :: AConfigBase - Constructor of AConfigBase *
//**********************************************************
AConfigBase :: AConfigBase (const char * pszSuffix)
: m_strFullFilename (),
m_strPureFilename (),
m_strPathname (),
m_ret (NO_ERROR)
{
AFile file;
IString strFile;
filenameFromAppDir (pszSuffix);
// build the filename from the app directory
if(tryOpenFile (file, strFile))
// the ini data can be saved in the app
// directory
return;
if(!filenameFromIniDir ()) // == NO_ERROR
{
if(tryOpenFile (file, strFile))
// the ini data can be saved in the ini
// directory
return;
}
switch((m_ret = filenameFromCurrDir ()))
// build the filename from the
// current directory
{
case NO_ERROR:
if(tryOpenFile (file, strFile))
// the ini data can be saved in
// the current directory
return;
break;
case ERROR_NOT_DOS_DISK:
case ERROR_DRIVE_LOCKED:
break;
default:
ITHROW (IAccessError ("DosQueryModuleName",
m_ret,
IException :: unrecoverable));
// something unexpected has gone wrong
}
filenameFromSystemDir ();
// build the filename from the system directory
if(!tryOpenFile (file, strFile))
// the ini data can't be saved in the
// system directory
ITHROW (IAccessError("DosOpen", m_ret,
IException :: unrecoverable));
// something unexpected has gone wrong
}
Figure 3: Constructor of AConfigBase.
The constructor tries to build the filename of the ini file using the sequence described above.
The AConfigBase::filenameFrom... methods use the different API calls to determinate the values they need. AConfigBase::filenameFromAppDir has also the task of setting the pure filename. That's because DosQueryModuleName is the way to get the filename's prefix. Since the app directory is the first place where to place the ini file, there's no extra method needed to build the pure filename.
These methods return different values. AConfigBase::filenameFromAppDir and AConfigBase::filenameFromSystemDir return a reference to the AConfigBase object. If a problem occurs in one of these methods, this will be a problem that cannot be handled correctly by the ini object, so an IAccessError is thrown.
The error that may be detected by AConfigBase::filenameFromAppDir is that the module name cannot be queried by the API function DosQueryModuleName. The error that may arise in AConfigBase::filenameFromSystemDir is that the system path can't be queried by DosQuerySysInfo. The exceptions are not caught by the constructor. You have catch them yourself. The source of the sample application shows one way how to do this.
AConfigBase::filenameFromIniDir and AConfigBase::filenameFromCurrDir return the value received by the API call to query the environment variable USER_INI (DosScanEnv) and to query the current directory (DosQueryCurrentDir) respectively.
If AConfigBase::filenameFromIniDir does not return NO_ERROR, the value of USER_INI cannot be determined. I can't tell if the operating system misses this entry, but here that is not too bad. So we can continue trying to build the filename from the current directory.
The constructor can handle three return values from AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was used to build the filename and we can try to put the ini file there.
ERROR_NOT_DOS_DISK and ERROR_DRIVE_LOCKED mean we have to try to build the filename from the system directory. Any other return value means that something has gone wrong. In this case an IAccessError is thrown. For a complete list of return values please refer to the OS/2 API manual.
If none of the four directories can be used to store the ini file (also ACongfigBase::tryOpenFile using the system directory returns false), an IAccessError is thrown.
//**********************************************************
// AConfigBase :: tryOpenFile - tries to open the ini file *
// *
// throws IAccessError *
//**********************************************************
Boolean AConfigBase :: tryOpenFile (AFile& rFile,
IString& rstrFile)
{
rstrFile = m_strPathname + "\\~tryopen.tmp";
switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL,
OPEN_ACTION_CREATE_IF_NEW
| OPEN_ACTION_REPLACE_IF_EXISTS,
OPEN_FLAGS_FAIL_ON_ERROR
| OPEN_ACCESS_READWRITE
| OPEN_SHARE_DENYREADWRITE)))
{
case NO_ERROR: // the filename is ok
rFile. close ();
if (DosDelete (rstrFile))
// can't delete the testfile
ITHROW (IAccessError("DosDelete", m_ret,
IException :: recoverable));
return (true);
case ERROR_WRITE_PROTECT:
case ERROR_OPEN_FAILED:
// the ini data can't be saved using the
// filename
return (false);
default:
{
IString error(rstrFile);
error += " DosOpen";
ITHROW (IAccessError(error, m_ret,
IException :: unrecoverable));
// something unexpected has gone wrong
}
}
return (false);
}
Figure 4: AConfigBase::tryOpenFile.
Figure 4 shows the key method to check out if the ini file can be placed inside of the desired directory. I'm using my class AFile (please refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose.
Of course you can use DosOpen instead. We need here OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_REPLACE_IF_EXISTS as open flags. Normally, the testfile ~tryopen.tmp should not exist. So we have to use OPEN_ACTION_CREATE_IF_NEW. In the really improbable case that this file exists, we have to try to recreate it to see if we have write permissions on the drive. Therefore we also need OPEN_ACTION_REPLACE_IF_EXISTS.
The default open modes of AFile::open are OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE. These must be expanded by OPEN_FLAGS_FAIL_ON_ERROR because the media I/O errors should not be handled by the system critical-error handler (the user gets a system message on the screen) but be reported as a return value. Now the parameters are what we need to see if the ini file can be used in the way we want it. Because we use a temporary file for testing it is guaranteed that we do not destroy any previously saved data. If AFile::open returns NO_ERROR, we can use the directory and the method returns true. If AFile::open returns ERROR_WRITE_PROTECT or ERROR_OPEN_FAILED, then we should try the next directory and AConfigBase::tryOpenFile returns false.
Any other return values causes an exception of type IAccessError.
AConfigFile - Do It Non-Conform
The class AConfigFile is defined in cfgfile.h and cfgfile.c.
template <class T> class AConfigFile : public AConfigBase,
public AFile
{
public:
AConfigFile ();
virtual ~AConfigFile () = 0;
T * getConfigData () { return (&m_ConfigData); }
APIRET open (const ULONG ulFileAttribute = FILE_NORMAL,
const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
const ULONG ulOpenMode = OPEN_ACCESS_READWRITE
| OPEN_SHARE_DENYREADWRITE,
const PEAOP2 pEABuf = 0L)
{
return (AFile :: open (getFullFilename (), sizeof (T),
ulFileAttribute,
ulOpenFlag, ulOpenMode, pEABuf);
// open the ini file
}
APIRET read (const ULONG ulFileAttribute = FILE_NORMAL,
const ULONG ulOpenFlag = FILE_OPEN,
const ULONG ulOpenMode = OPEN_ACCESS_READONLY
| OPEN_SHARE_DENYWRITE,
const PEAOP2 pEABuf = 0L);
APIRET write (const ULONG ulFileAttribute = FILE_NORMAL,
const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
const ULONG ulOpenMode = OPEN_ACCESS_WRITEONLY
| OPEN_SHARE_DENYREADWRITE,
const PEAOP2 pEABuf = 0L);
private:
T m_ConfigData;
};
Figure 5: Declaration of AConfigFile.
As you can see in figure 5, AConfigFile is a template class derived from AConfigBase. This gives you the ability to use it for any configuration structure needed. It is also derived from AFile, since the configuration data is stored in a simple file.
There is nothing mysterious about this class. AFile's read and write methods are overridden because AConfigFile always keeps the file closed. AConfigFile::read and AConfigFile::write checks if the file is open, open it if not, and close it again after reading/writing. When you use the makefiles included with the sample code, you will retrieve a warning that AConfigFile::open hides both AFile::open methods, AFile::read and AFile::write. That's because I want to avoid the use of a filename different from the one built by AConfigBase during initialization and the use of the AFile::methods for reading and writing.
Since the configuration data is a private member, you have to access it by AConfigFile::getConfigData, which returns a pointer to the structure.
IBM Programming Guide Conform - AIniFile
The class AIniFile is defined in inifile.hpp and inifile.cpp.
class AIniFile : public AConfigBase,
public IProfile
{
public:
AIniFile ();
virtual ~AIniFile () = 0;
};
Figure 6: Declaration of AIniFile.
As you can see in figure 6, AIniFile only contains a constructor and a (empty) destructor. All functionality needed here is inherited from the base classes AConfigBase and IProfile.
One really important thing is the sequence of the declaration of the base classes. AConfigBase must be declared before IProfile. Have a look at the constructor code in figure 7:
//**********************************************************
// AIniFile :: AIniFile - Constructor of AIniFile *
//**********************************************************
AIniFile :: AIniFile ()
: AConfigBase ("INI"),
IProfile (getFullFilename ())
{
IString strApplicationName (getPureFilename ());
strApplicationName. remove (0, 1);
// remove the leading '\'
strApplicationName. remove (strApplicationName. lastIndexOf ('.'));
// remove the suffix '.ini'
setDefaultApplicationName (strApplicationName);
// set the default
// application name
}
Figure 7: Constructor of AIniFile.
The constructor of IProfile needs the name of the initialization file as an argument. This filename is part of the base class AConfigBase and is built during the initialization of AConfigBase. So AConfigBase must be initialized before IProfile. The sequence, in which the base classes are initialized, is defined by the declaration order, not by the order in the initialization list of the constructor of the derived class, here AIniFile (see [4]).
And because there is always just one application per initialization file by definition, we can set the default application name of IProfile during the initialization of AIniFile. It can be taken from the pure filename of AConfigBase. Only the leading backslash and the suffix have to be removed.
Handle The Configuration Data - ACfgConfig
The class ACfgConfig is defined in cfgcfg.hpp.
typedef struct _CFG_CONFIGURATION {
SIZEL sizeWindow;
BOOL bSizeWindowSaved;
} CFG_CONFIGURATION;
class ACfgConfig : public AConfigFile <CFG_CONFIGURATION>
{
public:
Boolean isSizeSaved () // was a size saved?
{ return (getConfigData () -> bSizeWindowSaved); }
SIZEL getSize () // retrieve the size
{ return (getConfigData () -> sizeWindow); }
ACfgConfig& setSize (const SIZEL sSize)
// set the size
{
getConfigData () -> sizeWindow = sSize;
getConfigData () -> bSizeWindowSaved = TRUE;
return (*this);
}
};
Figure 8: The configuration structure and the declaration of ACfgConfig.
Figure 8 shows a very simple configuration file class. It is used by the sample program to save the window's size when the program gets closed. The position is saved by an initialization file to show you the use of both ini file classes.
As you can see, there are three methods on each (here the one and only) configuration value. One to check if a value was saved at least, one to retrieve and one to set the value. The method to set the configuration parameter also has to care about the is-saved flag. Setting such a flag is the easiest way to determinate whether a configuration file contains a value or not. Of course you can also check if, in this sample, the size has a height and a width. But what if a width and height of 0 is permitted? How do you detect whether the value is newly initialized, or is saved there? Using the flag protects you from such pain.
Manage The Initialization Data - AIniConfig
The class AIniConfig is defined in inicfg.hpp and inicfg.cpp.
class AIniConfig : public AIniFile
{
public:
Boolean isPositionSaved () const
// is a value for the position in the
// profile?
{ return (containsKeyName (m_pszPosition)); }
POINTL getPosition () const; // retrieve the position
AIniConfig& setPosition (const POINTL pPosition);
// save the position
private:
static const char * const m_pszPosition;
// position key name
};
Figure 9: Declaration of AIniConfig.
Looking at figure 9 you can see that using AIniFile makes it quite easier to determine whether an initialization value exists or not. You just have to call IProfile::containsKeyName with the desired key name as parameter. If the initialization file contains the key, true is returned, otherwise false. In contrast to ACfgConfig you do not need an additional flag in the ini file.
// define the string for the position key
const char * const AIniConfig :: m_pszPosition = "Position";
Figure 10: Definition of AIniConfig::m_pszPosition.
As shown in Figure 9, this class has a private static const member variable called m_pszPosition. It is a pointer to the key name used to access the position element of the initialization file. Because the key name is the same for every instance of AIniConfig, the pointer to it can be shared by all AIniConfig objects. So it is declared as static. And to be sure it always points to the same string, it is declared as const too.
Maybe you will ask why you should declare a pointer to each key name. I think using a variable instead of typing the key name itself makes it easier to maintain the program. When you decide to rename a key during the development process, you just have to change the string one time: at the declaration of the pointer. And the compiler will check if you typed the variable name the right way. When you mistype the key name, you have to find it yourself.
//**********************************************************
// AIniConfig :: getPosition - return the position *
//**********************************************************
POINTL AIniConfig :: getPosition () const
{
POINTL pointRet = { 0, 0 };// initialize the return value;
if(!isPositionSaved ())
// was an element saved for the position?
return (pointRet); // no, return
// an element for the position exists, so retrieve it
IString strPosition (elementWithKey (m_pszPosition));
memcpy (&pointRet, (char *) strPosition, sizeof (POINTL));
// convert IString to POINTL
return (pointRet);
}
Figure 11: Retrieve a binary element of an initialization file.
IProfile::elementWithKey always returns an IString. If the value itself isn't a string, you have to convert it to the desired data type. Figure 11 shows you how the saved window position of the sample application (binary data) is retrieved and converted. If the saved data is numeric, you can use one of IString's type conversation methods to get the desired type.
//**********************************************************
// AIniConfig :: setPosition - set the position *
//**********************************************************
AIniConfig& AIniConfig::setPosition (const POINTL pPosition)
{
IString strPosition (&pPosition, sizeof (POINTL), 0x0);
addOrReplaceElementWithKey (m_pszPosition, strPosition);
return (*this);
}
Figure 12: Save a binary element of an initialization file.
Because IProfile::addOrReplaceElementWithKey only accepts key values of type long or IString, you have to convert binary data to IString before saving. This can be done by one of IString's constructors, which accepts a void pointer to a buffer, the buffer's size and a pad character as parameters. AIniConfig::setPosition uses this way to save the window's position of the sample application. After converting the POINTL structure to an IString, IProfile::addOrReplaceElementWithKey can be used to save the data.
Which File Shall I Use?
As you can see when you're looking at the sample code, using the initialization file ends up in more coding and marginal slower performance because of the many type conversations of binary data. But having a good concept of how to organize the keys and writing an easy to use initialization class can make it really easy for others to use the data and easy for you to maintain it. So play around with the sample to find out which you like most.
The Sample Application
The sample application initest shows you the use of both ways to handle your ini data. Of course this is not a typical sample, since you will almost never use both ini file classes in one application. But here it is done for demonstration purpose.
When you run the sample, be sure to close it using the F3 key or the menu 'File' 'Exit'. Any other way to end it doesn't saves the current position and size of the window. The position is saved in initest.ini, the size in initest.cfg.
When you build the sample application, you can use the makefiles included in the source zip. There is one makefile for IBM's C Set++ (initest.mac) and one for IBM's VisualAge C++ (initest.mav). If you decide to use your own makefile, make sure all switches especially for the use of templates are set correctly.
- IBM, Programming Guide Volume II, e.g. Developer Connection Disk 2, Directory Docs, pmv2base.inf, 1996
- Harald Wilhelm, Register me!, OS/2 Inside, 5/97, 1997, p. 65-69
- Stefan Ruck, A Progress-indicating Status Line in C++ (Part 2), EDM/2 Volume 4 Issue 6, 1996
- Bjarne Stroustrup, The C++ Programming Language, Second Edition, 1995, p. 580