Design Patterns for Implementing Application Preferences, Anyone?
Configuration, preferences, settings, options, properties—whatever you call it—all but the simplest applications allow the user to customize some of their functionality (and often appearance). But how do you implement this? Is there a best practices for programming application preferences in a clean, easily maintainable and well-structured way?
I recently began working on my latest hobby project, a desktop application for viewing photographs using (hardware-accelerated) 3D rendering, implemented in Qt and OpenGL. It’s been a while since using Qt, so I started small with a basic skeleton application, adding a menu bar, a toolbar and a status bar. I also implemented menu items to toggle the toolbar, the status bar and a full screen viewing mode.
The application doesn’t do anything interesting yet, but even with this simple logic there are already several variables that could (and, in my opinion, should) be stored between sessions:
- Show/hide the toolbar?
- Show/hide the status bar?
- Show application in full screen, normal or maximized mode?
- Size and position of main window.
The Configuration Object
Most application frameworks (and some programming languages) provide utility classes for reading and writing persistent variables from and to configuration files or the system registry, like QSettings, wxConfig and java.util.Properties. The simplest way of providing persistence for application settings is to use these classes directly whenever needed. However, using them directly will often lead to duplication of code and possibly troublesome maintenance if access is spread across many modules and classes. Because of this—and other reasons I will mention later—I prefer to collect all variables and the code to read/write them in a separate configuration object.
For my simple Qt application, the code might look something like this:
class Config {
public:
bool maximized;
bool fullScreen;
bool showToolBar;
bool showStatusBar;
QString windowPos;
void read() {
QSettings settings("phex3d", "phex3d");
maximized = settings.value("maximized" , false).toBool());
fullScreen = settings.value("full_screen" , false).toBool());
showToolBar = settings.value("show_toolbar" , false).toBool());
showStatusBar = settings.value("show_statusbar", true ).toBool());
windowPos = settings.value("window_pos" , "" ).toString());
}
void write() {
QSettings settings("phex3d", "phex3d");
settings.setValue("maximized" , maximized);
settings.setValue("full_screen" , fullScreen);
settings.setValue("show_toolbar" , showToolBar);
settings.setValue("show_statusbar", showStatusBar);
settings.setValue("window_pos" , windowPos);
settings.sync();
}
};
By making an instance of the configuration object available as a global variable (or a singleton, if that makes you sleep better), I can now easily reference persistent settings from anywhere in the application. For example, the event handler for toggling the toolbar could be something like this:
void Window::toggleToolbar(void)
{
bool visible = toolbar->isVisible();
config.showToolBar = !visible;
if (visible)
toolbar->hide();
else
toolbar->show();
}
As long as I remember to call Config::read()
on startup and Config::write()
on exit, the settings will be saved and restored without any extra work needed.
Centralized Information
Although simple, the above solution will get somewhat messy if many variables are involved. For every new variable, extra code must be added to read()
and write()
. If we also think ahead a little, and take into account that these options will need to be exposed in a configuration dialog, allowing the user to change their values, we can recognize the need to associate some more information with each of them:
- The name of the option, typically used to identify it in a configuration file or the registry.
- A short description of what the option means and what part of the application is affected by changing it. This text will typically be used as a label for the check box, edit field or other widget used to change the variable in the configuration dialog.
- A more elaborate help text, suitable for use as a tool tip or in a separate help dialog.
- The default value, useful if you want to allow the user to reset something to “factory defaults”.
- The data type of the option.
You may wonder why the variable names and data types are relevant in this context—they could stay hardcoded, like before—but if you consider more advanced configuration interfaces, like the Firefox about:config feature, they can be very useful.
To collect all information about an option in one place, you might define an Option
class looking something like this:
class Option {
public:
enum OptionType { INT, STRING };
private:
QString name;
QString desc;
QString help;
int defInt;
QString defString;
OptionType type;
void *value;
public:
Option(int *var, const QString &name, int def, const QString desc = "", const QString &help = "" ) {
this->type = INT;
this->value = var;
this->name = name;
this->defInt = def;
this->desc = desc;
this->help = help;
}
Option(QString *var, const QString &name, const QString &def, const QString desc = "", const QString &help = "" ) {
this->type = STRING;
this->value = var;
this->name = name;
this->defString = def;
this->desc = desc;
this->help = help;
}
const QString &getName() { return name; }
const QString &getDescription() { return desc; }
const QString &getHelpText() { return help; }
OptionType getType(void) { return type; }
int getInt(void) { return *((int *) value); }
int getDefaultInt(void) { return defInt; }
const QString &getString(void) { return *((QString *) value); }
const QString &getDefaultString(void) { return defString; }
void setInt(int value) { *((int *) this->value) = value; }
void setString(const QString &value) { *((QString *) this->value) = value; }
};
If you are wondering why the option value is stored as a pointer and not a local variable inside the Option
class, I did this because I wanted them to reference the corresponding class variables in the configuration object, thus allowing me to continue accessing hem directly elsewhere in my application. It’s kind of a hack, I know, but it works. If you don’t like it, you can always use the get*()
functions instead. Also, if you know a better solution, or how to solve the type info situation with templates, please share.
Now that we have all the information wee need about each option wrapped in a class, we can add a list of Option
instances in our Config
class to simplify and generalize the read()
and write()
implementations. The new Config
class might look something like this:
class Config {
public:
int maximized;
int fullScreen;
int showToolBar;
int showStatusBar;
QString windowPos;
Option *_maximized;
Option *_fullScreen;
Option *_showToolBar;
Option *_showStatusBar;
Option *_windowPos;
QList option_list;
Config() {
settings = new QSettings("phex3d", "phex3d");
_maximized = addOption ("maximized" , &maximized , true );
_fullScreen = addOption ("fullscreen" , &fullScreen , false);
_showToolBar = addOption ("show_toolbar" , &showToolBar , false);
_showStatusBar = addOption ("show_statusbar", &showStatusBar, true );
_windowPos = addOption("window_pos" , &windowPos , "" );
}
~Config() {
for (QList::Iterator i = option_list.begin(); i < option_list.end(); i++)
delete *i;
delete settings;
}
template
Option *addOption(const QString &name, T *var, T value, const QString &desc = "", const QString &help = "" ) {
Option *option = new Option(var, name, value, desc, help);
option_list.append(option);
return option;
}
void read() {
for (QList::Iterator i = option_list.begin(); i < option_list.end(); i++) {
Option *option = *i;
if (option->getType() == Option::INT) {
QVariant value = settings->value(option->getName(), option->getDefaultInt());
option->setInt(value.toInt());
}
else {
QVariant value = settings->value(option->getName(), option->getDefaultString());
option->setString(value.toString());
}
}
}
void write() {
for (QList::Iterator i = option_list.begin(); i < option_list.end(); i++) {
Option *option = *i;
if (option->getType() == Option::INT)
settings->setValue(option->getName(), option->getInt());
else
settings->setValue(option->getName(), option->getString());
}
settings->sync();
}
private:
QSettings *settings;
};
Outstanding Issues
The above solution works fine for my current needs in the application, but as we all know, needs change over time. I can already think of several outstanding issues that are not covered by this design, which might be needed in the future.
It would be useful to provide a list of valid values for each option. For example, if an integer option can only be between 1 and 100, this information should also be stored in the Option
object. The same with text to display in a drop-down list or for auto-completing commonly used values as they are being entered in an edit field. If advanced validation or many different validation algorithms are used, this might be better solved by adding a reference to a validation object responsible for validating values for a given option.
As time passes, code is typically rewritten and programs restructured. This can eventually lead to the need, or simply the desire, to also rename options. For example, if the option window_pos
describes the main window position, you may want to rename it to main.window_pos
when more windows are added to the application. The issue can also arise when option names are automatically generated from class names (which is quite common for Java properties). When the name of the class changes, so will the option name. For this reason, it could be useful if the Option
class was extended to provide a list of name aliases. The read()
function could then be updated to use the value from an alias if found, but write()
would only save it under the new name.
Another disadvantage of my simple design is that the Config
class must know about all the options in the application, and therefore could get a tighter coupling with the various application modules than you might prefer. If your application supports custom extensions via plug-ins, it might be useful to implement functionality to add options to the Config
object at run-time. For options that are added this way it will also be useful to provide a lookup function for retrieving the Option
object based on name, i.e. by storing the objects in a map keyed by the option name as it was provided when the option was added.
I am likely to discover even more issues once I start implementing the configuration dialogs, but at least now I have an application that remembers where I left the window.
Please share your thoughts on this. Any feedback is appreciated.