Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

EnumBinder - Bind C++ enums to strings, combo-boxes, arbitrary data structures

4.83/5 (20 votes)
15 Aug 2005CPOL11 min read 1   1.4K  
An easy way to bind C++ enums to strings, combo-boxes, list-boxes, arbitrary data structures.

Introduction

This article presents a set of templates and macros that, with a minimal amount of code, will provide a way to:

  • Associate each enumerator in a C++ enumeration (enum) with strings, and convert between string and enumerator, given either form.
  • Associate each enumerator in an enum, with an arbitrary set of data (e.g. an int, and a CRect paired to each enumerator).
  • Iterate (i.e. loop) over the enumeration, in a type-safe and convenient manner (i.e. with a syntax similar to writing a loop for std::vector or CArray).
  • Bind an enumerator to MFC Comboboxes and Listboxes (with automatic handling of population, selection and DDX).
  • Automatically self-test the enum declaration, which helps catch copy/paste errors that may have been made when setting up your declaration.

Under the hood, the actual work is done by a combination of templates, static member functions (some of which are member templates) static class variables, and macros. I initially attempted to code everything with templates, but by the end of it all, I had to resort to the duct tape of C++ (i.e. macros) to clean up the declarations.

Motivation

Enumerations provide a clean, highly readable and type-safe way of dealing with a variable that can take on a well-defined set of possible states (days of the week, or the suits in a deck of cards are the typical examples). This MSDN article gives a good overview of enumerations, but also demonstrates the unfortunate downsides that come with using enums:

  1. Iterating over all of the enumerations with a loop (refer to the second code fragment in the MSDN article, with the for loop) requires:
    • Use of the names of the first and last enumerators. This isn't a great way to do things - imagine if you add a new enumerator to the beginning or end of the enumeration - you would have to search around and modify all of the loops.
    • Writing operator++: Writing this operator for each enumeration is mildly inconvenient, but it also requires a potentially unsafe cast. (the results are undefined if you accidentally cast an integer with a value that is not listed in the enumeration).
  2. If you're saving the value of an enumeration to a file (or registry, etc.) you inevitably end up writing a large switch statement (as in the MSDN article) or a chain of if statements to convert back and forth between the enumerators and a string representation of them.

Amongst other things, the code presented here tries to solve these problems. As an additional level of safety, no casting is performed from integer to enumerator anywhere in the code (all conversions are from enumerator to integer, which is automatic and safe).

Binding to a CCombobox

The following is an example of how to use CEnumBinder to associate each enumerator in the enumeration with two strings, and bind it to a ComboBox.

The first step in the following code is just a regular C++ enum declaration. The second step defines the association between each enumerator in the enum and the corresponding two strings. The first parameter in BIND_ENUM is the name of the enumeration. The second parameter declares the name of a new enum binder class (the BIND_ENUM macro expands to define a class named by the second parameter, so the name of the second parameter can be anything you want).

enum eWeekdays
{
   eSunday,
   eMonday,
   eTuesday,
   eWednesday,
   eThursday,
   eFriday,
   eSaturday,
};

BIND_ENUM(eWeekdays, EnumWeekdays)
{
   { eSunday   , "sun"  , "Sunday"    },
   { eMonday   , "mon"  , "Monday"    },
   { eTuesday  , "tues" , "Tuesday"   },
   { eWednesday, "Wed"  , "Wednesday" },
   { eThursday , "thurs", "Thursday"  },
   { eFriday   , "fri"  , "Friday"    },
   { eSaturday , "sat"  , "Saturday"  },
};

Now we add an enum member variable to the dialog class, and bind the enum to a CCombobox:

class CEnumBinderDlg : public CDialog 
{
   ...
   // a regular CCombobox (or derived class)
   CCombobox m_comboBox;
   // a regular enumeration variable   
   eWeekdays m_enumMember; 
}

CEnumBinderDlg::CEnumBinderDlg()
{
   m_enumMember = eWednesday; // set default selection
}

void CEnumBinderDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_COMBO_BOX, m_comboBox);
    
    // associate the combo box (m_comboBox) 
    // with the enum member (m_enumMember)
    EnumBinder::DDX_Control(pDX, m_comboBox, 
                                  m_enumMember);
}

This produces:

The DDX_Control function transfers both ways just like a regular DDX. The function takes care of population and selection of the CCombobox, so once the DDX_Control is used to bind the member variable to the control, all access is simply done through the m_enumMember variable.

This works in the same manner as when you bind a CString to a CEdit control - you just work with the CString, before or after calls to UpdateData(). The default value of m_enumMember would typically be set in the dialog constructor (or before CDialog::DoModal() has been called) and the final value would be retrieved as usual (typically, after CDialog::DoModal() has returned, or in the handler for IDOK).

Note on CListbox: CListbox is handled in the exact same manner. For this example, just change the m_comboBox member from a CComboBox to a CListbox, and change IDC_COMBO_BOX in the dialog editor to a ListBox.

Note on Sorting: in this example, the sort property of the ComboBox must be set to false in the dialog editor (or the weekdays will be in alphabetical order, instead of the proper chronological order). For both the CCombobox and the CListbox, the sorting is controlled by the settings in the dialog editor. If sorting is set to false the items appear in the same order as they are listed in the BIND_ENUM declaration.

Why two strings?

The first string is for storing the value to a file, or to the registry. The second string is for display to the user. We will refer to the first string as the code string and the second string as the user string.

Separating the two strings can be quite useful. The first string can be a don't change this value or it will break I/O string, while the second string can be freely changed at any time (e.g. by Documentation writers) without any impact on the file or registry I/O. For example:

Imagine dealing with typos. If a user reports "Wenesday" was spelled incorrectly in version 1.0 of your application, the correct spelling is simply updated in the BIND_ENUM declaration. Another example where having two strings would be useful is translation. The second string can be changed at runtime, to allow modification of the text in the CEnumBinder structure, while the first string can, again, be used for file I/O.

Note on UNICODE: Everything will work fine when building with UNICODE, just wrap all of your strings with the usual _T() macro. (see the example project for more details).

List of functions (part 1 - Element access)

class CEnumBinder
{
   ...
   // returns number of enumerators in the enum
   int GetSize()
   // returns const reference, lookup by enumerator             
   GetAt(Tenum enumItem)
   // returns const reference, lookup by zero-based index     
   GetAt(int index)
   // returns non-const reference, lookup by enumerator          
   ElementAt(Tenum enumItem)
   // returns non-const reference, lookup by zero-based index 
   ElementAt(int index)      
   ...
};

These functions allow you to loop through the enumeration by zero-based integer index, or to lookup the strings using the enumerator. For example:

for(int i=0; i < EnumWeekdays::GetSize(); ++i){
   TRACE("\n%s", EnumWeekdays::GetAt(i).m_stringUser);
}
TRACE("\n%s", EnumWeekdays::GetAt(eTuesday).m_stringCode);
TRACE("\n%s", EnumWeekdays::GetAt(eTuesday).m_stringUser);

produces the following output:

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
tues
Tuesday

Usage note: The GetAt() functions are preferred to the ElementAt() functions, as they return const references. The ElementAt functions only need to be used if you are changing the strings at runtime (i.e. infrequently).

List of functions (part 2 - String/enumerator conversion)

bool CodeStringToEnum(LPCTSTR stringToCheck, 
                                Tenum& enumValue_OUT)
const CString& EnumToCodeString(const Tenum enumItem)
const CString& EnumToUserString(const Tenum enumItem)
bool CodeStringEnumExchange(CStringOrSimilar& strINOUT, 
                       Tenum& enumINOUT, bool strToEnum)

These functions convert between the enumerator and one of the two strings (the user string, and the code string ). Converting from an enumerator to either string should not fail (unless you pass an enumerator not defined in the BIND_ENUM declaration), so a string reference is returned directly. Converting from a code string to an enumerator can fail (e.g. trying to match strings coming out of a file, validating data the user typed, etc...), so this function returns a bool to indicate if a match was found, and returns the enumerator by reference.

A couple of different forms of the functions are provided, for convenience. Refer to the function: CEnumBinderDlg::RegistryExchange (in EnumBinderDlg.cpp in the example project) for an example of how these functions are used.

List of functions (part 3 - CCombobox/CListBox binding)

bool DDX_Control(bool saveAndValidate, 
      CListBox_or_CComboBox& listOrComboBox, 
      Tenum& memberVariable)
bool DDX_Control(CDataExchange* pDX, 
      CListBox_or_CComboBox& listOrComboBox, 
      Tenum& memberVariable)
bool Populate(CListBox_or_CComboBox& listOrComboBox)
bool SetSel(CListBox_or_CComboBox& listOrComboBox, 
                         const Tenum selectThisItem)
bool PopulateSetSel(CListBox_or_CComboBox& listOrComboBox, 
                               const Tenum selectThisItem)
bool GetCurSel(CListBox_or_CComboBox& listOrComboBox, 
                                  Tenum& itemSelected)

These functions are used for population, control of selection state, and DDX with CComboBoxes and CListboxes. Refer to the functions: DoDataExchange and OnLbnSelchangeListCanada (in EnumBinderDlg.cpp in the example project) for an example of how these functions are used.

Note on member templates: These functions are all C++ member templates. (i.e. the template parameter for the function is figured out automatically by the compiler). This is what allows CCombobox and CListbox to be used interchangeably. The really interesting implication of using member templates is that you could use any other class that provides the same set of functions as CComboBox/CListBox (AddString, SetCurSel, etc.), as you will see that there is no use of the class names CCombobox or CListbox anywhere in the code.

Automatic self-testing

The class provides automatic self-testing, which ensures there is a one-to-one mapping between the enumerators and the code strings. This is essential because the code strings are written to files or the registry in string form, and when they are read back they must match up to the same enumerator.

The self-test is automatically performed by the class the first time any of the functions are called (in _DEBUG mode). The self-test can also be run manually by calling the function UnitTest(), but this is only necessary if you change the code strings at run-time. The user strings are not tested for uniqueness, as this will not cause any errors (although I can't imagine a scenario where this would be a good UI design ?!?).

For example, in the following block of code, any of the last three lines will cause the self-test code to ASSERT:

enum eFruit {
   eApple,
   eOrange,
   ePear,
   ePlum,
   eBanana,
};

BIND_ENUM(eFruit, EnumFruit)
{
   { eApple , "apple" , "Apples"  },
   { eOrange, "orange", "Oranges" },
   { ePear  , "pear"  , "Pears"   },
   // error 1: forgot to change "orange" to "plum"
   { ePlum  , "orange", "Plums"   },
   // error 2: forgot to change "eApple" to "eBanana"  
   { eApple , "banana", "Banana"  },  
   // error 3: total duplicate of another line
   { eOrange, "orange", "Oranges" },  
};

Extending beyond two strings...

...but I want to associate three strings with my enumerator! (one for the registry, one for the CComboBox text, and one for a custom tooltip I'm adding to my CCombobox). As the following example will illustrate, almost any data structure can be associated with an enumerator. We will begin by modifying our weekdays example, and associating a newly defined class to each enumerator.

Adding more than the usual two strings is slightly more complicated, as we have to define a new class (whatever arbitrary data structure you want to associate with the enumeration) and bind it using a new macro called BIND_ENUM_CUSTOM. We begin, again, by declaring a standard C++ enumeration:

enum eWeekdays
{
   eSunday,
   eMonday,
   eTuesday,
   eWednesday,
   eThursday,
   eFriday,
   eSaturday,
};

To add the custom data, we define a new custom-data class (the name of the class is arbitrary). In the following example we will associate a resource ID and an integer with each enumerator. Refer to the function: CEnumBinderDlg::<CODE>OnCbnSelchangeComboWeekdays (in EnumBinderDlg.cpp in the example project) to see how this is used.

To hook into the CEnumBinder framework, the custom-data class must contain the macro: INSERT_ENUM_BINDER_ITEMS() with the parameter to this macro being the name of the enumeration. Typically, the macro is the first listing in the class (i.e. before any other variables), but this ordering can be changed.

class CWeekdaysData
{
public:
   INSERT_ENUM_BINDER_ITEMS(eWeekdays);
   UINT m_iconID;      // add a resource ID
   int m_offsetX;      // add an integer
   // or anything else ...
};

We need to use BIND_ENUM_CUSTOM instead of BIND_ENUM, as we now have to specify the name of the custom data class we just created. The first two parameters to the BIND_ENUM_CUSTOM macro are the same as the regular BIND_ENUM macro (name of the enumeration, followed by the name of a new enum binder class, as explained above). The third parameter is the name of the custom data class, we created in the last code fragment.

We then add the data we want to associate to each enumerator, to the end of each line:

BIND_ENUM_CUSTOM(eWeekdays, EnumWeekdaysCustom, CWeekdaysData)
{
   { eSunday   , "sun"  , "Sunday"    , IDI_WEEKEND,  15 },
   { eMonday   , "mon"  , "Monday"    , IDI_WEEKDAY,  30 },
   { eTuesday  , "tues" , "Tuesday"   , IDI_WEEKDAY,  45 },
   { eWednesday, "Wed"  , "Wednesday" , IDI_WEEKDAY,  60 },
   { eThursday , "thurs", "Thursday"  , IDI_WEEKDAY,  75 },
   { eFriday   , "fri"  , "Friday"    , IDI_WEEKDAY,  90 },
   { eSaturday , "sat"  , "Saturday"  , IDI_WEEKEND, 105 },
};

Note on ordering: The ordering of the entries in the BIND_ENUM_CUSTOM declaration must correspond to the ordering of variables in the custom-data class. Changing the order of the variables/macro is fine, as long as the class and BIND_ENUM_CUSTOM declaration are consistent.

In this example, the custom-data class lists the INSERT_ENUM_BINDER_ITEMS() macro first, followed by the resource ID, and then the integer. This means the enumerator, code string and user string must be the first three entries in each line of the BIND_ENUM_CUSTOM declaration, followed by the resource ID and then the integer.

Restrictions on enum values

The only absolute restriction on the value assigned to each enumerator in the enumeration is that each one must be unique. However:

  • the enumerator values do not have to start at zero (e.g. using =4 on the first enumerator is fine).
  • the enumerator values do not have to be in any particular order (e.g. 2,5,1,6,8 is fine).
  • the enumerator values can contain gaps (e.g. 0,1,2,7,8,9 is fine).
  • Note enumerator values that have automatic ordering (start at 0, increment by 1) will have faster lookups from enumerator to strings/data (constant time vs. O(n) time), when writing code like:
    CString fruitName = EnumFruit::GetAt(eApple).m_stringUser;
    ASSERT(fruitName == "Apples");

Modification and versioning of the enumeration

Adding new entries, or re-ordering the enumerators in the BIND_ENUM declaration should not cause a problem for reading previously-written files or registry entries, as they are stored in string form. This makes it easy to add new items without causing backwards compatibility problems. Similarly, changing the value of an enumerator should not cause any problems.

Removing entries in the BIND_ENUM declaration will cause the CodeStringToEnum function to fail, and the enumerator will be left with whatever value it previously had (the default value?) which is probably what is desired in this situation.

Any of the items elements (The user string, code string, the enumeration, arbitrary data) can be changed at runtime, using the ElementAt() functions. Refer to the function: OnInitDialog (in EnumBinderDlg.cpp in the example project) for an example of changing the user string at runtime. Changing any of the other elements are done in a similar fashion.

History

  • 2005-08-15: Version 1.0 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)