65.9K
CodeProject 正在变化。 阅读更多。
Home

EnumBinder - 将 C++ 枚举绑定到字符串、组合框、任意数据结构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (20投票s)

2005 年 8 月 15 日

CPOL

11分钟阅读

viewsIcon

137850

downloadIcon

1438

一种将 C++ 枚举绑定到字符串、组合框、列表框、任意数据结构的简便方法。

引言

本文介绍了一组模板和宏,它们只需少量代码,就能提供一种方法来

  • 将 C++ 枚举(enum)中的每个枚举器与字符串关联,并在给定任一形式的情况下,在 string 和枚举器之间进行转换。
  • enum 中的每个枚举器与任意一组数据(例如,与每个枚举器配对的 intCRect)关联。
  • 以一种类型安全且方便的方式(即具有类似于编写 std::vectorCArray 的循环语法)迭代(即循环)枚举。
  • 将枚举器绑定到 MFC 组合框和列表框(具有自动处理填充、选择和 DDX)。
  • 自动自测 enum 声明,这有助于捕获在设置声明时可能发生的复制/粘贴错误。

底层实现方面,实际工作是通过模板、静态成员函数(其中一些是成员模板)、静态类变量和宏的组合完成的。我最初尝试使用模板来编写所有内容,但在最后,我不得不借助 C++ 的“万能胶”(即宏)来清理声明。

动机

枚举提供了一种清晰、高度可读且类型安全的方式来处理可以处于一组明确定义的可能状态(星期几、扑克牌的花色是典型示例)的变量。这篇 MSDN 文章 很好地概述了枚举,但也展示了使用 enums 的不幸缺点

  1. 在枚举的所有枚举器上使用循环进行迭代(参见 MSDN 文章中的第二个代码片段,带有 for 循环)需要
    • 使用第一个和最后一个枚举器的名称。这不是一种好的做法——想象一下,如果您在新枚举器添加到枚举的开头或结尾,您将不得不搜索并修改所有循环。
    • 编写 operator++:为每个枚举编写此运算符虽然有些不方便,但还需要一个可能不安全的强制类型转换。(如果您意外地强制转换为枚举中未列出的值的整数,则结果是未定义的)。
  2. 如果您将枚举的值保存到文件(或注册表等)中,您将不可避免地编写一个大型 switch 语句(如 MSDN 文章所示)或一系列 if 语句来在枚举器及其字符串表示形式之间进行转换。

除其他优点外,此处提供的代码试图解决这些问题。作为额外的安全层,代码中不会执行从整数到枚举器的任何强制类型转换(所有转换都是从枚举器到整数,这是自动且安全的)。

绑定到 CCombobox

以下是如何使用 CEnumBinder 将枚举中的每个枚举器与两个字符串关联,并将其绑定到组合框的示例。

以下代码的第一步只是一个常规的 C++ enum 声明。第二步定义了 enum 中每个枚举器与相应的 两个字符串 之间的关联。BIND_ENUM 中的第一个参数是枚举的名称。第二个参数声明了一个新的 enum 绑定器类的名称(BIND_ENUM 宏展开以定义一个由第二个参数命名的类,因此第二个参数的名称可以是您想要的任何名称)。

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"  },
};

现在,我们在对话框类中添加一个 enum 成员变量,并将 enum 绑定到 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);
}

这将产生

DDX_Control 函数双向传输,就像常规 DDX 一样。该函数负责组合框的填充和选择,因此一旦使用 DDX_Control 将成员变量绑定到控件,所有访问都通过 m_enumMember 变量进行。

这与将 CString 绑定到 CEdit 控件的方式相同——您只需在调用 UpdateData() 之前或之后处理 CStringm_enumMember 的默认值通常在对话框构造函数中设置(或在调用 CDialog::DoModal() 之前),并且最终值将按常规检索(通常在 CDialog::DoModal() 返回后,或在 IDOK 的处理程序中)。

关于 CListbox 的注意事项CListbox 的处理方式完全相同。对于此示例,只需将 m_comboBox 成员从 CComboBox 更改为 CListbox,并将对话框编辑器中的 IDC_COMBO_BOX 更改为 ListBox。

关于排序的注意事项:在此示例中,组合框的 sort 属性必须在对话框编辑器中设置为 false(否则,工作日将按字母顺序排列,而不是按正确的顺序)。对于 CComboboxCListbox,排序由对话框编辑器中的设置控制。如果设置为 false,则项的出现顺序与它们在 BIND_ENUM 声明中的列出顺序相同。

为什么是两个字符串?

第一个字符串用于将值存储到文件或注册表。第二个字符串用于向用户显示。我们将第一个字符串称为代码字符串,第二个字符串称为用户字符串

将这两个字符串分开可能非常有用。第一个字符串可以是一个“不要更改此值,否则将破坏 I/O”字符串,而第二个字符串可以随时自由更改(例如,由文档编写者)而不影响文件或注册表 I/O。例如:

想象一下处理拼写错误。如果用户报告您的应用程序 1.0 版中的“Wenesday”拼写不正确,则可以在 BIND_ENUM 声明中更新正确的拼写。另一个需要两个字符串的有用场景是翻译。第二个字符串可以在运行时更改,以允许修改 CEnumBinder 结构中的文本,而第一个字符串可以再次用于文件 I/O。

关于 UNICODE 的注意事项:使用 UNICODE 构建时,一切都会正常工作,只需将所有字符串用常规的 _T() 宏括起来即可。(有关详细信息,请参阅示例项目)。

函数列表(第一部分 - 元素访问)

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)      
   ...
};

这些函数允许您通过零基整数索引遍历枚举,或使用枚举器查找字符串。例如:

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);

产生以下输出:

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
tues
Tuesday

使用说明:首选 GetAt() 函数而不是 ElementAt() 函数,因为它们返回 const 引用。仅当您在运行时(即不频繁地)更改字符串时,才需要使用 ElementAt 函数。

函数列表(第二部分 - 字符串/枚举器转换)

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)

这些函数在枚举器和 两个字符串用户字符串代码字符串)之间进行转换。从枚举器到任一字符串的转换不应失败(除非您传递了一个未在 BIND_ENUM 声明中定义的枚举器),因此直接返回字符串引用。从代码字符串到枚举器的转换*可能*会失败(例如,尝试匹配来自文件的字符串,验证用户输入的字符串等),因此此函数返回一个 bool 来指示是否找到匹配项,并通过引用返回枚举器。

为了方便起见,提供了该函数的几种不同形式。请参阅函数:CEnumBinderDlg::RegistryExchange(在示例项目中的EnumBinderDlg.cpp 中)以了解如何使用这些函数的示例。

函数列表(第三部分 - CCombobox/CListBox 绑定)

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)

这些函数用于 CComboBoxCListbox 的填充、选择状态控制和 DDX。请参阅函数:DoDataExchangeOnLbnSelchangeListCanada(在示例项目中的EnumBinderDlg.cpp 中)以了解如何使用这些函数的示例。

关于成员模板的注意事项:这些函数都是C++ 成员模板。(即,函数模板参数由编译器自动推导)。这使得 CComboboxCListbox 可以互换使用。使用成员模板真正有趣的影响是,您可以使用任何提供与 CComboBox/CListBox 相同函数的类(AddString, SetCurSel 等),因为您会发现代码中*没有*使用类名 CComboboxCListbox

自动自测

该类提供自动自测,确保枚举器和代码字符串之间存在一对一的映射。这很重要,因为代码字符串以字符串形式写入文件或注册表,并且在读回时必须与同一个枚举器匹配。

自测在任何函数(在 _DEBUG 模式下)被调用时都会由类自动执行。您也可以通过调用 UnitTest() 函数手动运行自测,但这仅在您在运行时更改代码字符串时才需要。用户字符串不进行唯一性测试,因为这不会导致任何错误(尽管我无法想象这种情况会是好的 UI 设计?!)。

例如,在以下代码块中,最后三行中的任何一行都会导致自测代码 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" },  
};

扩展到两个字符串以上...

...但我希望与我的枚举器关联三个字符串!(一个用于注册表,一个用于 CComboBox 文本,一个用于我添加到 CCombobox 的自定义工具提示)。正如以下示例所示,几乎任何数据结构都可以与枚举器关联。我们将从修改我们的工作日示例开始,并将一个新定义的类与每个枚举器关联。

添加超过通常的两个字符串会稍微复杂一些,因为我们必须定义一个新类(您想要与枚举关联的任何任意数据结构),并使用一个名为 BIND_ENUM_CUSTOM 的新宏来绑定它。我们再次从声明一个标准的 C++ 枚举开始

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

为了添加自定义数据,我们定义了一个新的自定义数据类(类的名称是任意的)。在以下示例中,我们将把资源 ID 和一个整数与每个枚举器关联。请参阅函数:CEnumBinderDlg::<CODE>OnCbnSelchangeComboWeekdays(在示例项目中的EnumBinderDlg.cpp 中)以了解如何使用它。

为了接入 CEnumBinder 框架,自定义数据类必须包含宏:INSERT_ENUM_BINDER_ITEMS(),该宏的参数是枚举的名称。通常,该宏是类中的第一个列表项(即在任何其他变量之前),但这种 排序 可以更改。

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

我们需要使用 BIND_ENUM_CUSTOM 而不是 BIND_ENUM,因为我们现在需要指定我们刚刚创建的自定义数据类的名称。BIND_ENUM_CUSTOM 宏的前两个参数与常规 BIND_ENUM 宏相同(枚举的名称,后跟一个新 enum 绑定器类的名称,如上所述)。第三个参数是我们在上一个代码片段中创建的自定义数据类的名称。

然后,我们在每行末尾添加我们想要与每个枚举器关联的数据

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 },
};

关于排序的注意事项: BIND_ENUM_CUSTOM 声明中条目的顺序必须与自定义数据类中变量的顺序相对应。更改变量/宏的顺序是可以的,只要类和 BIND_ENUM_CUSTOM 声明一致即可。

在此示例中,自定义数据类首先列出了 INSERT_ENUM_BINDER_ITEMS() 宏,然后是资源 ID,然后是整数。这意味着每个 BIND_ENUM_CUSTOM 声明行中的枚举器、代码字符串和用户字符串必须是前三个条目,然后是资源 ID,然后是整数。

枚举值的限制

对枚举中每个枚举器分配的值的唯一绝对限制是它们必须是唯一的。但是:

  • 枚举器的值*不必*从零开始(例如,第一个枚举器使用 =4 是可以的)。
  • 枚举器的值*不必*按任何特定顺序排列(例如,2,5,1,6,8 是可以的)。
  • 枚举器的值可以包含间隙(例如,0,1,2,7,8,9 是可以的)。
  • 注意:当编写类似以下代码时,自动排序的枚举器值(从 0 开始,每次增 1)将具有更快的从枚举器到字符串/数据的查找速度(常数时间 vs. O(n) 时间):
    CString fruitName = EnumFruit::GetAt(eApple).m_stringUser;
    ASSERT(fruitName == "Apples");

枚举的修改和版本控制

BIND_ENUM 声明中添加新条目或重新排序枚举器不应给读取先前写入的文件或注册表条目带来问题,因为它们以字符串形式存储。这使得添加新项而不会导致向后兼容性问题变得容易。类似地,更改枚举器的值也不应引起任何问题。

删除 BIND_ENUM 声明中的条目将导致 CodeStringToEnum 函数失败,并且枚举器将保留其先前的值(默认值?),这在这种情况下可能是期望的。

任何项元素(用户字符串代码字符串、枚举、任意数据)都可以使用 ElementAt() 函数在运行时更改。请参阅函数:OnInitDialog(在示例项目中的EnumBinderDlg.cpp 中)以了解在运行时更改用户字符串的示例。更改其他元素也以类似方式进行。

历史

  • 2005-08-15:版本 1.0 - 初始发布。
© . All rights reserved.