Introduction
Configuration files have now come full-circle in Microsoft's product roadplan.
Originally,
.ini files were used to store
application-specific data
(accessed by
GetPrivateProfileXXX()
functions) and
also
system data stored in
Win.ini (accessed
by
GetProfileXXX()
functions). With the
introduction of Win95, developers were encouraged to use the registry instead:
"This function is provided only
for compatibility with 16-bit Windows-based applications. Applications
should store initialization information in the registry."
There were
good reasons for doing this: applications were storing their .ini
files in the Windows directory, or - even worse - dumping application
data in Win.ini, and it was impossible to set up
multi-user system using a single .ini file.
This of course spawned a whole new cottage industry of
registry cleaners,
to clean up the registry after misbehaving applications. Soon people were
noticing that the registry was being filled up with huge amounts of
application data, and began to wonder if this stuff should be put in
stand-alone database.
There were also ease-of-use considerations. Even if an
application behaved
itself and stayed out of HKEY_LOCAL_MACHINE hive, it was still easier
to just grab .ini file when you wanted to move application to
another machine. Developers noticed that putting a separate .ini
file in each user's CSIDL_APPDATA directory
(C:\Documents and Settings\<username>\Application Data)
was equivalent to using registry HKEY_CURRENT_USER hive.
So some applications offered users choice: to use an
.ini file, or to use registry.
Now with .Net .config files we are back to a type of .ini
file, with a twist: the .config files are encoded in XML.
This is probably a good thing, since XML is more accessible in
cross-platform situations.
Motivation
Recently one of my clients discussed with me how to make his application
more interoperable in his enterprise environment - multiple versions of
Windows, Mac, and Linux. He was especially keen on using
XML files
as a transfer mechanism, but was concerned about the impact on code base,
especially some
MFC and
WTL Windows applications.
I quickly sketched out XMLProfile, which would be encoded as
XML and require minimal changes to legacy apps. Here is list of
features I showed him:
|
No use of MFC or STL - My client wanted to avoid any dependencies that
might cause future problems with cross-platform use, and had some
specific concerns about use of MFC and STL. Of course, because of
cross-platform considerations, this also meant that
MSXML
could not be used.
|
|
Use standard XML - This meant that the XML had to be accepted by all
major parsing engines, to meet the goal of cross-platform use.
|
|
Backwards compatibility with ini files - .ini files
have a two-tier structure, with sections containing keys
(unique within a section) and their values.
There is no limit (except practical ones) in XML files for nesting depth,
but I did not want to introduce complexity when it was not needed,
and obviously the legacy applications worked fine with only two levels.
Here are examples of INI and XML profiles:
INI Profile
[Section1]
key1=value1
[Section2]
key2=value2
key3=value3
[Section3]
key4=value4
key5=value5
key6=value6 | | XML Profile
<?xml version="1.0" encoding="utf-8"?>
<profile>
<section name="Section1">
<key name="key1" value="value1"/>
</section>
<section name="Section2">
<key name="key2" value="value2"/>
<key name="key3" value="value3"/>
</section>
<section name="Section3">
<key name="key4" value="value4"/>
<key name="key5" value="value5"/>
<key name="key6" value="value6"/>
</section>
</profile> |
|
|
Familiar API - This really is the complement to
backwards compatibility requirement. Just like data format should
be compatible, the API structure should also be intuitive to those
familiar with .ini files.
|
|
Read XML or ini files, write XML and/or ini files - Of course it was a requirement
to write XML, but to allow for any unexpected situations, I also wanted
to provide for ini input and output. (As it turned out, this was
very handy when converting old .ini files).
|
|
Support for CWinApp Profile functions - To make it as easy as
possible to migrate to XML profiles, I defined
CWinApp -derived class that replaced the CWinApp
profile functions with compatible XML profile functions. This involved
creating a CXMLWinApp class, which is demonstrated in
the XMLWinAppTest demo.
|
|
Support for both Unicode (UTF-16) and ANSI - Support for Unicode was
essential for the use of company's applications in international
market.
|
XMLProfile Concepts and Facilities
Loading and Saving Profiles
These are functions for loading and saving profile files:
Function | Description |
---|
BOOL Load(LPCTSTR lpszFileName, ENCODING eEncoding = Default) | Loads the XML profile |
BOOL LoadIni(LPCTSTR lpszFileName, ENCODING eEncoding = Default) | Loads INI profile |
BOOL Save(LPCTSTR lpszFileName, ENCODING eEncoding = Default) | Saves XML profile |
BOOL SaveIni(LPCTSTR lpszIniFile, ENCODING eEncoding = Default) | Saves profile as INI |
The Load()
function loads XML profile data from file,
using second parameter (eEncoding
) to determine
how the file is encoded, and what conversion (if any) should be
applied. The following table shows effect of various combinations
of eEncoding
, compiler preprocessor symbol
_UNICODE
, and
Byte Order Mark (BOM) that is read from the file:
eEncoding | _UNICODE Symbol | BOM | Conversion Applied |
---|
Ansi | Not defined | N/A | None; file assumed to be ANSI |
Ansi | Defined | N/A | Contents converted to Unicode |
Unicode | Not defined | N/A | Contents converted to ANSI |
Unicode | Defined | N/A | None; file assumed to be Unicode |
Default | Not defined | Not present | None; file assumed to be ANSI |
Default | Not defined | Present | Contents converted to ANSI |
Default | Defined | Not present | Contents converted to Unicode |
Default | Defined | Present | None; file assumed to be Unicode |
The Save()
function is somewhat simpler in its handling
of the Default
encoding option. When saving,
Default
simply means use Unicode encoding if
_UNICODE
is defined, otherwise use ANSI.
Low-Level Functions
These are low-level functions of
CXmlProfile
:
Function | Description |
---|
void Close(BOOL bSave) | Close profile file |
void DeleteContents() | Empties data contents of section and key lists, frees memory |
void Dump() | Dumps contents of profile via OutputDebugString() |
CXmlKeyListElement * FindKey(LPCTSTR lpszSectionName, LPCTSTR lpszName) | FindKey retrieves key named lpszName in section named lpszSectionName |
CXmlKeyListElement * FindKey(CXmlSectionListElement *pSection, LPCTSTR lpszName) | FindKey retrieves key named lpszName in section specified by pSection |
CXmlSectionListElement * FindSection(LPCTSTR lpszName) | FindSection retrieves section named lpszName |
CWTLString GetKey(LPCTSTR lpszSectionName, LPCTSTR lpszKeyName, LPCTSTR lpszDefault) | GetKey retrieves key named lpszName in section specified by pSection |
UINT GetKeys(LPCTSTR lpszSectionName, CXmlProfileKey **ppKeyBuffer) | GetKeys retrieves all the keys in a section |
UINT GetNumKeys(LPCTSTR lpszSectionName) | GetNumKeys retrieves the number of keys in a section |
UINT GetNumSections() | GetNumSections retrieves the number of sections in the profile |
BOOL GetReadOnly(CXmlKeyListElement *pKey) | GetReadOnly retrieves the readonly status of a key |
BOOL GetReadOnly(CXmlSectionListElement *pSection) | GetReadOnly retrieves the readonly status of a section |
UINT GetSections(CXmlProfileSectionEntry **ppSSectionArray) | GetSections retrieves all the sections in the profile |
BOOL IsFileOpen() | IsFileOpen checks if profile file is open |
BOOL IsKey(LPCTSTR lpszSectionName, LPCTSTR lpszKeyName) | IsKey checks if key exists |
BOOL IsSection(LPCTSTR lpszSection) | IsSection checks if section exists |
CXmlKeyListElement * SetKey(LPCTSTR lpszSectionName, LPCTSTR lpszKeyName, LPCTSTR lpszValue, BOOL bReadOnly = FALSE) | SetKey sets the value lpszValue in the key named lpszKeyName in section named lpszSectionName |
CXmlKeyListElement * SetKey(CXmlSectionListElement *pSection, LPCTSTR lpszKeyName, LPCTSTR lpszValue, BOOL bReadOnly = FALSE) | SetKey sets the value lpszValue in the key named lpszKeyName in section specified by pSection |
BOOL SetKeys(LPCTSTR lpszSectionName, CXmlProfileKey *pKeyBuffer, UINT nKeys) | SetKeys creates/updates keys in a section |
void SetReadOnly(CXmlSectionListElement *pSection, BOOL bReadOnly) | SetReadOnly sets the readonly status of a section |
void SetReadOnly(CXmlKeyListElement *pKey, BOOL bReadOnly) | SetReadOnly sets the readonly status of a key |
BOOL Validate() | Validates contents of profile |
MFC-Compatible Functions
These are MFC-Compatible functions:
Function | Description |
---|
BOOL GetProfileBinary(LPCTSTR lpszSection, LPCTSTR lpszEntry, BYTE** ppData, UINT* pBytes) | Retrieve binary data from an entry within a specified section of the profile |
BOOL WriteProfileBinary(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPBYTE pData, UINT nBytes) | Writes binary data into an entry within a specified section of the profile |
UINT GetProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault) | Retrieves an integer value from an entry within a specified section of the profile |
BOOL WriteProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nValue) | Writes an integer value into an entry within a specified section of the profile |
CWTLString GetProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault) | Retrieves a string from an entry within a specified section of the profile |
BOOL WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue) | Writes a string into an entry within a specified section of the profile |
XML Tags
The tags used in
XML profile files may be changed as required:
CXmlProfile String Variable | Default Value |
---|
m_strKeyTag | "key" |
m_strNameTag | "name" (used for both sections and keys) |
m_strProfileTag | "profile" |
m_strReadOnlyTag | "readonly" |
m_strSectionTag | "section" |
m_strValueTag | "value" |
Implementation Notes
XMLProfile may be compiled without any dependence on MFC, by simply
commenting out line
#include "stdafx.h"
in files
XMLProfile.cpp and
XMLParser.cpp. (If using
CXmlWinApp
class, then you should include this line).
Because XMLProfile makes heavy use of strings, I had to find
replacement for MFC's CString
class. I found such a class in
WTL, and renamed it to
CWTLString
to avoid any conflicts. When MFC is being used,
there is #define
to redefine CWTLString
to CString
.
The other major problem was what to do about list handling. I wanted
to avoid using both MFC's and STL's list classes, because of some
configuration issues with my client's code. A colleague suggested I look
at the
doubly-linked list class written by Sam Buss, which proved
to be a very good choice. I use this both in
CXmlProfile
and CXmlParser
.
XMLProfile Demo
Here is what the
XMLProfile demo looks like:
The Convert dialog allows you to convert back and forth between
INI files and XML files:
The Test dialog exercises XMLProfile functions
with automated tests:
How to use
Step 1 - Add Files
To integrate XMLProfile into your app, you first need to
add following files to your project:
- XMLProfile.cpp
- XMLProfile.h
- XMLParser.cpp
- XMLParser.h
- CLinkedList.h
- wtlstring.h
If you are not using MFC, you can set the .cpp files to
Not using precompiled header
in Visual Studio. Otherwise, you will get error
fatal error C1010: unexpected end of file while looking for precompiled header directive
If you want to use the
CWinApp
-based
XML profile
functions, you should also include:
- XMLWinApp.cpp
- XMLWinApp.h
Use of CXmlWinApp
requires that you enable precompiled headers
for XMLProfile.cpp and XMLParser.cpp.
Step 2 - Add Header File to Your Source Module
In the module where you want to use
XMLProfile,
include header file
XMLProfile.h.
Step 3 - Add Code
See
XMLProfileTestDlg.cpp and
Test.cpp for examples of how to use.
The XMLProfileTest demo program writes test files to the exe
directory when it is run. These files are not deleted, so that
they may be viewed after the tests are run.
Using XMLProfile with MFC
The code used in
XMLProfile may be compiled with or without MFC.
To compile
without MFC, make sure the line
#include "stdafx.h"
is
commented out in files
XmlParser.cpp and
XmlProfile.cpp, and set both of these files to
Not using precompiled header in Visual Studio.
To compile with MFC, make sure the line #include "stdafx.h"
is not commented out in files XmlParser.cpp and
XmlProfile.cpp, and set both of these files to
Use precompiled header in Visual Studio.
References
Useful Links
More Articles
Here are some other articles on CodeProject that deal with profile and
configuration files:
Revision History
Version 1.0 - 2008 July 2
Usage
This software is released into the public domain. You are free to use it
in any way you like, except that you may not sell this source code. If you
modify it or extend it, please to consider posting new code here for everyone
to share. This software is provided "as is" with no expressed or implied
warranty. I accept no liability for any damage or loss of business that
this software may cause.