Introduction
The default MFC CSettingsStore
class stores all settings (including UI customization) in the registry. It is also very read- and write-happy, leading to an amazing amount of traffic to the registry, and a large amount of data stored.
This contribution (written by me and placed in the public domain) allows you to replace that behavior with a class that stores settings in a single file in the user's Application Data folder, and keeps the settings in memory for optimal reading speed.
Using the Code
To activate it, just add the two files to your project and add the following line to
your CMyApplication::InitInstance()
function:
#include "LocalSettingsStore.h"
BOOL CMyApplication::InitInstance()
{
CSettingsStoreSP::SetRuntimeClass(RUNTIME_CLASS(CLocalSettingsStore));
Also, you need to change the APPLICATION_NAME
define in the file LocalSettingsStore.cpp
and remove the line that says #undef APPLICATION_NAME
, to store the settings in a file
that's specific to your application.
The code has been tested with MFC 9.0 and Visual Studio 2008 SP1, but ought to work with earlier/later versions that use the same basic CSettingsStore
class.
How Does it Work?
MFC uses a wrapper for accessing the registry, called the CSettingsStore
. In your application, you use the CSettingsStoreSP
class to actually access this wrapper, and then you get
/set
your preference items (window position, preferred color, etc.). The internal framework uses this a lot to store and read settings for toolbars, menu customization, etc. In fact, about 20 kilobytes of registry data goes into a single do-nothing MFC application!
There are a large number of virtual functions in this class, allowing you to override the way that storage is done. Unfortunately, there is no documentation for how to actually do this, so I went spelunking inside the source code that ships with Microsoft Visual C++ 2008 SP1.
virtual BOOL CreateKey(LPCTSTR lpszPath);
virtual BOOL Open(LPCTSTR lpszPath);
virtual void Close();
virtual BOOL DeleteValue(LPCTSTR lpszValue);
virtual BOOL DeleteKey(LPCTSTR lpszPath, BOOL bAdmin = FALSE);
virtual BOOL Write(LPCTSTR lpszValueName, int nValue);
virtual BOOL Write(LPCTSTR lpszValueName, DWORD dwVal);
virtual BOOL Write(LPCTSTR lpszValueName, LPCTSTR lpszVal);
virtual BOOL Write(LPCTSTR lpszValueName, const CRect& rect);
virtual BOOL Write(LPCTSTR lpszValueName, LPBYTE pData, UINT nBytes);
virtual BOOL Write(LPCTSTR lpszValueName, CObject& obj);
virtual BOOL Write(LPCTSTR lpszValueName, CObject* pObj);
virtual BOOL Read(LPCTSTR lpszValueName, int& nValue);
virtual BOOL Read(LPCTSTR lpszValueName, DWORD& dwValue);
virtual BOOL Read(LPCTSTR lpszValueName, CString& strValue);
virtual BOOL Read(LPCTSTR lpszValueName, CRect& rect);
virtual BOOL Read(LPCTSTR lpszValueName, BYTE** ppData, UINT* pcbData);
virtual BOOL Read(LPCTSTR lpszValueName, CObject& obj);
virtual BOOL Read(LPCTSTR lpszValueName, CObject*& pObj);
Where do you start? Well, it turns out that the only functions you need to override are the DWORD
, LPCTSTR
and BYTE
/UINT
read/write functions, plus the create/delete management functions:
virtual BOOL CreateKey(LPCTSTR lpszPath);
virtual BOOL Open(LPCTSTR lpszPath);
virtual void Close();
virtual BOOL DeleteValue(LPCTSTR lpszValue);
virtual BOOL DeleteKey(LPCTSTR lpszPath, BOOL bAdmin = FALSE);
virtual BOOL Write(LPCTSTR lpszValueName, DWORD dwVal);
virtual BOOL Write(LPCTSTR lpszValueName, LPCTSTR lpszVal);
virtual BOOL Write(LPCTSTR lpszValueName, LPBYTE pData, UINT nBytes);
virtual BOOL Read(LPCTSTR lpszValueName, DWORD& dwValue);
virtual BOOL Read(LPCTSTR lpszValueName, CString& strValue);
virtual BOOL Read(LPCTSTR lpszValueName, BYTE** ppData, UINT* pcbData);
Internally, the CSettingsStore
class will re-formulate all the other requests into one of these three requests. For example, the CSettingsStore
supports marshaling of CObject
objects into and out of the registry, but at the I/O level, that goes through BYTE**
, UINT*
.
In the implementation, everything is stored as a std::vector<char>
, which I find is a convenient "resizable buffer" class. Keys are stored as std::wstring
, downcased through the C locale to provide the case insensitive search that usually happens in the registry. This all goes into a std::map<std::wstring
, std::vector<char>>
which implements the full key/value mapping. (If you know your Win32, you'd know that registry keys have multiple named values; I implement this as a single long path without special handling. For example, the key FOO\bar\baz with the value 64 named "Bits" would be stored as foo\bar\baz\bits in the map, pointing to a vector of 4 bytes storing the DWORD
64.)
The implementation only loads the settings once, to avoid unnecessary I/O to the disk. After loading, it stays in memory until program termination, which means that performance is very good. To make sure that changes are committed to file when expected, the file is written each time the CLocalSettingsStore
object is destroyed. Unfortunately, this happens a lot (the MFC implementation is very object create/destroy happy), but to optimize this case, it keeps a dirty bit, and only actually writes to disk when something has been changed.
The file format itself is fairly straightforward, with an optimization to save disk space. Because many key paths have a common prefix, a key is stored as a count of characters to use from the previous key in the file, plus a string to append to get the next key path. This simple change actually made the default settings file go from about 30 kB to about 20 kB!
Points of Interest
The documentation for CSettingsStore
is pretty weak. It turns out that the marshaling of objects needs to use the particular format used by the base implementation, or it won't work right. Also, it turns out that you should only override three of the virtual functions for writing data, not all of them -- see the implementation for details.
History
- Version 1.0 -- February 4, 2009
- Version 1.1 -- February 7, 2009