The basic idea
First, what does S.I.V mean? It simply means - "Simplicity Is Virtue". You might ask yourself "What the heck is that?". Well, it goes like this - I tend to keep things simple, in my personal life, and likewise in programming. Simple things work better, faster, and are generally - simpler. :-)
You have probably seen many solutions for making your settings persistent, and I would not say that any of them is "bad". This article will describe how I do it in my projects, and it seems to work just great, so I thought it would be nice to share it with the rest of the world.
When I first started to think about it, I wanted to create a settings "system" that would be simple to update with new settings, and of course, to keep it as simple as possible. As you will see soon, even a complete newbie could implement this in a matter of minutes. However, this article assumes that you've done some MFC programming, and you do know how to create a dialog based project, add new class, functions, variables. ;-)
There are few things you could learn in this article, if you are a newbie:
- how to create a serializable class
- how to use
CFile
and CArchive
in a dialog based app
- basic exception handling
The birth of a new class
Question was: where do I keep my settings? In a bunch of public vars? No - that's bad. In my main dialog class? No - how would I pass it to the rest of the program classes - one by one? Bad! A new class? Right, Let us see: if I want to pass ALL settings to someone, I just pass a pointer to a class. If I want to add a new setting, I just add a new variable to the class and update the system to be aware of it. Seems okay. But how would I save it? Let us be consistent with the crowd, and use some built-in MFC features - serialization!
For the sake of readability, we'll name our class CSettings
( you didn't see this coming, right? :-) ) and make it inherit stuff from CObject
, so that our serialization works. In order to make our class serialize itself we need to do a few things:
- use
DECLARE_SERIAL
macro
- use
IMPLEMENT_SERIAL
macro
- override
CObject
's Serialize
virtual function
This is the skeleton of our new class:
class CSettings : public CObject
{
DECLARE_SERIAL( CSettings );
public:
CSettings();
virtual ~CSettings();
virtual void Serialize( CArchive &ar );
};
IMPLEMENT_SERIAL( CSettings, CObject, 1 );
CSettings::CSettings()
{
}
CSettings::~CSettings()
{
}
void CSettings::Serialize( CArchive &ar )
{
CObject::Serialize( ar );
}
Ok, that's the skeleton, but what do we do next.? Well, first we need to take care of settings versioning system. As most of you probably know, VERSIONABLE_SCHEMA
isn't quite reliable and practical to use, so we'll kick that out right here. Here is what we do:
- create a private variable for the class, which will hold the class version
- implement it into the
IMPLEMENT_SERIAL
macro
- save that version into the settings file itself
- check for version upon loading the settings
The code now looks like this:
class CSettings : public CObject
{
DECLARE_SERIAL( CSettings );
public:
CSettings();
virtual ~CSettings();
virtual void Serialize( CArchive &ar );
private:
static const UINT m_uiClassVersion;
};
const UINT CSettings::m_uiClassVersion = 1;
IMPLEMENT_SERIAL( CSettings, CObject, CSettings::m_uiClassVersion );
void CSettings::Serialize( CArchive &ar )
{
CObject::Serialize( ar );
if ( ar.IsStoring() )
{
ar << m_uiClassVersion;
}
else
{
UINT uiFileVersion;
ar >> uiFileVersion;
if ( uiFileVersion == m_uiClassVersion )
{
}
else
{
AfxMessageBox( "Wrong settings file version!", MB_OK );
}
}
}
Simple, right?:-) Of course, you decide what to do when you have version mismatch, you can load the old version, display a message box, abort, launch a nuclear missile.
Right, now let us do some settings! For each setting we want to save, create a private variable. Since we want to manipulate it, we will create some inline functions for each one of them.
class CSettings : public CObject
{
DECLARE_SERIAL( CSettings );
public:
CSettings();
virtual ~CSettings();
virtual void Serialize( CArchive &ar );
int GetInt() { return m_int; }
float GetFloat() { return m_float; }
long GetLong() { return m_long; }
UINT GetUint() { return m_uint; }
BOOL GetBool() { return m_bool; }
DWORD GetDword() { return m_dword; }
CString GetString() { return m_string; }
void SetInt( int data ) { m_int = data; }
void SetFloat( float data ) { m_float = data; }
void SetLong( long data ) { m_long = data; }
void SetUint( UINT data ) { m_uint = data; }
void SetBool( BOOL data ) { m_bool = data; }
void SetDword( DWORD data ) { m_dword = data; }
void SetString( CString data ) { m_string = data; }
private:
static const UINT m_uiClassVersion;
int m_int;
float m_float;
long m_long;
UINT m_uint;
BOOL m_bool;
DWORD m_dword;
CString m_string;
};
Of course, in a real life project, your variables would not be called anything like m_float
. :-) Now, our class has the ability to hold custom data. Now, we need to tell our serialization function to save and load the data. Remember, saving and loading must be done in the same order!
void CSettings::Serialize( CArchive &ar )
{
CObject::Serialize( ar );
if ( ar.IsStoring() )
{
ar << m_uiClassVersion;
ar << m_bool << m_float << m_int << m_long;
ar << m_string << m_uint << m_dword;
}
else
{
UINT uiFileVersion;
ar >> uiFileVersion;
if ( uiFileVersion == m_uiClassVersion )
{
ar >> m_bool >> m_float >> m_int >> m_long;
ar >> m_string >> m_uint >> m_dword;
}
else
{
AfxMessageBox( "Wrong settings file version!", MB_OK );
}
}
}
There is just one more thing we might want to do: set the default values for each variable. We can do that quite well in the class constructor:
CSettings::CSettings()
{
m_bool = TRUE;
m_float = 3.14f;
m_int = -3827;
m_long = 2203675808;
m_string = "Eddie lives!";
m_uint = 3827;
m_dword = 0x3827;
}
Make it work!
Well, that's all we need to do in CSettings
class! Now we create a simple interface for our user. When you are finished drawing controls, you must create variables for each one of them, as done in the demo app.
And after that, we need to make it work. We need to create two functions:
...and it is obvious what they do. ;-) I will post only
INIFileSave
, to make the article shorter, but basically, two of these do
nearly the same thing. :-) Here goes:
BOOL CSettingsDemoDlg::INIFileSave(CSettings *settings)
{
if ( settings == NULL )
return FALSE;
int result;
CFile file;
CFileException exception;
result = file.Open( "settings.ini", CFile::modeCreate |
CFile::modeWrite, &exception );
if ( result == 0 )
{
exception.ReportError();
return FALSE;
}
CArchive archive( &file, CArchive::store );
try {
settings->Serialize( archive );
}
catch ( CException *err )
{
err->ReportError();
err->Delete();
archive.Close();
file.Close();
return FALSE;
}
archive.Close();
file.Close();
return TRUE;
}
To explain a couple of things: I made the function return BOOL
, because you might want to know what the heck happened and take appropriate action. I also made it accept a CSettings*
parameter, because I might want to have a few predefined settings ("profiles"), and this way I can implement it quite simply. Last but not least, you don't have to inform your end-user that an error occured, you can simply ignore errors and cleanup. This is most appropriate at loading, since the application will complain that "settings.ini" could not be found when you run it for the first time (and then create the default file).
Also, you don't have to put INIFileSave
& INIFileLoad
into your dialog class, or you could even make them private, for complete control over your settings.
Now it is time to connect the interface with the rest of the code. Here we won't reinvent the wheel, and simply use the WM_CLICK
event on our Save and Load buttons. Again, for the sake of the article size, this is only OnBtnsave()
.
void CSettingsDemoDlg::OnBtnsave()
{
UpdateData( TRUE );
m_pSettings.SetBool( m_bool );
m_pSettings.SetDword( m_dword );
m_pSettings.SetFloat( m_float );
m_pSettings.SetInt( m_int );
m_pSettings.SetLong( m_long );
m_pSettings.SetString( m_string );
m_pSettings.SetUint( m_uint );
BOOL result = INIFileSave( &m_pSettings );
if ( result == FALSE )
AfxMessageBox( "Could not save yer settings!", MB_OK );
}
Last thing I did was to add some code to OnInitDialog()
to load settings and, if the file doesn't exist, create the default one.
BOOL result = INIFileLoad( &m_pSettings );
if ( result == FALSE )
INIFileSave( &m_pSettings );
And that's it! Note that this concept could be easily extended to support different "settings profiles", to save to custom "settings.ini" file, to load old versions of settings file, etc.
But remember - simplicity is virtue. ;-)
Copyright & Conclusion
This is my first article at CodeProject, so I apologize for any typos and stuff. If you have any comments, ideas or anything else, feel free to contact me (rhyme!), I'd be glad to hear from you. (especially if you like the article:-))
As for the copyright, all of this is copyrighted by me (T1TAN) and my pseudo-company SprdSoft Inc. That means that you can use this code in any app (private or commercial) without any restrictions whatsoever, since our company supports open source and free software. However, if you are going to use (parts of) the code, I'd like to hear from you, just to keep track of things.
Greets & good programming!
History
- 02/11/2003 - First release