是否可以将 CDialog RADIO 控件映射到枚举 class 对象而不是 int?
Is it possible to map CDialog RADIO controls to an enum class object instead of int?
我有一个标准对话资源,上面有一些无线电控件。
目前这一切都以正常方式完成,因此第一个无线电被映射到一个 int
变量。
DDX_Radio(pDX, IDC_RADIO_DISPLAY_EVERYONE, m_iDisplayMode);
DDX_Radio(pDX, IDC_RADIO_SELECT_EVERYONE, m_iSelectMode);
事情是这样的……我有这些相关的枚举:
enum class DisplayMode { Everyone = 0, Brother, Sister };
enum class SelectMode { Everyone = 0, Elders, MinisterialServants, Appointed, Custom, None };
因此,每当我需要对映射变量进行一些比较时,我都必须这样做:
示例 1:
m_iDisplayMode = to_underlying(DisplayMode::Everyone);
m_iSelectMode = to_underlying(SelectMode::None);
示例 2:
if (m_iDisplayMode == to_underlying(DisplayMode::Everyone))
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Brother) && mapPublisher.second.eGender == Gender::Male)
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Sister) && mapPublisher.second.eGender == Gender::Female)
bInclude = true;
to_underlying
函数是一个辅助函数,之前已经在 SO 上向我推荐过并且非常宝贵:
template <typename E>
constexpr auto to_underlying(E e) noexcept
{
return static_cast<std::underlying_type_t<E>>(e);
}
我想知道是否可以将这些无线电控件直接映射到 DisplayMode
或 SelectMode
对象?因此,它不是映射到 1
等,而是映射到 DisplayMode::Everyone
等。这将简化此上下文中的代码并避免所有 to_underlying
调用的需要。
这是 DDX_Radio
的 MFC 源代码:
void AFXAPI DDX_Radio(CDataExchange* pDX, int nIDC, int& value)
// must be first in a group of auto radio buttons
{
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
if (pDX->m_bSaveAndValidate)
value = -1; // value if none found
// walk all children in group
int iButton = 0;
do
{
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON)
{
// control in group is a radio button
if (pDX->m_bSaveAndValidate)
{
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0)
{
ASSERT(value == -1); // only set once
value = iButton;
}
}
else
{
// select button
::SendMessage(hWndCtrl, BM_SETCHECK, (iButton == value), 0L);
}
iButton++;
}
else
{
TRACE(traceAppMsg, 0, "Warning: skipping non-radio button in group.\n");
}
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
} while (hWndCtrl != NULL &&
!(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
我正在尝试使用答案中的代码,但出现此错误:
MFC 支持数据(class 成员)和UI 状态之间的映射。标准机制称为 Dialog Data Exchange (DDX) which the code in the question is using already (DDX_Radio
). The data exchange is two-way, triggered by a call to UpdateData
,其中 TRUE
的参数将 UI 状态转换为值,并且 FALSE
读取关联值并适当调整 UI .
MFC 已经提供了一些 standard dialog data exchange routines,但客户可以提供自己的,以防 none 适合直接用例。该问题属于此类,方便地提供 DDX_Radio
的实现作为起点。
该实现看起来有点吓人,但一旦代码在各处添加了一些注释后,事情就开始变得有意义了:
CustomDDX.h:
template<typename E>
void AFXAPI DDX_RadioEnum(CDataExchange* pDX, int nIDC, E& value)
{
// (1) Prepare the control for data exchange
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
// (2) Make sure this routine is associated with the first
// radio button in a radio button group
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
// And verify, that it is indeed a radio button
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
// (3) Iterate over all radio buttons in this group
using value_t = std::underlying_type_t<E>;
value_t rdbtn_index {};
do {
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON) {
// (4) Control is a radio button
if (pDX->m_bSaveAndValidate) {
// (5) Transfer data from UI to class member
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0) {
value = static_cast<E>(rdbtn_index);
}
} else {
// (6) Transfer data from class member to UI
::SendMessage(hWndCtrl, BM_SETCHECK,
(static_cast<E>(rdbtn_index) == value), 0L);
}
++rdbtn_index;
} else {
// (7) Not a radio button -> Issue warning
TRACE(traceAppMsg, 0,
"Warning: skipping non-radio button in group.\n");
}
// (8) Move to next control in tab order
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
}
// (9) Until there are no more, or we moved to the next group
while (hWndCtrl != NULL && !(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
这声明了一个可以为任意范围的枚举类型实例化的函数模板,并实现了在 UI 状态和枚举值之间转换的逻辑。枚举的整数基础值作为单选按钮组选择的从零开始的索引。
尽管如此,实施需要一些解释。以下列表提供了有关编号 // (n)
代码注释的更多信息:
- 这会初始化框架使用的内部状态。只要调用了正确的函数,精确的细节并不是很重要。有 3 种实现,一种用于 OLE 控件,一种用于编辑控件,另一种用于所有其他内容。我们属于 “其他” 类别。
- 执行完整性检查。这验证了由
nIDC
标识的控件是单选按钮组 (WS_GROUP
) 中的第一个控件,并且它确实是一个单选按钮控件。这有助于在 运行 进行调试构建时尽早清除错误。
- 初始化单选按钮索引计数器 (
rdbtn_index
),并开始迭代单选按钮。
- 确保我们在本次迭代中操作的控件是单选按钮控件(如果不是,请参见 7.)。
- 将UI状态转换回成员变量时,验证当前控件是否被选中,并将其索引作为作用域枚举值存储在组中。
- 否则(即在将数据转换为 UI 状态时)如果枚举的数值与控件索引匹配,则设置复选标记,否则取消选中。后者在使用
BS_AUTORADIOBUTTON
控件时不是严格要求的,但它也无害。
- 如果我们遇到不是单选按钮控件的控件,请发出警告。仔细观察此消息的调试输出;它指定对话框模板中的错误。确保在此单选按钮组之后的第一个控件上设置
WS_GROUP
样式(按 Tab 键顺序)。
- 按 Tab 键顺序移至下一个控件。
- 如果没有尾随控件,或者控件开始一个由
WS_GROUP
样式指定的新组,则终止循环。
这有点需要消化。幸运的是,这个函数模板的使用远没有那么麻烦。为了便于说明,让我们使用以下作用域枚举:
enum class Season {
Spring,
Summer,
Fall,
Winter
};
enum class Color {
Red,
Green,
Blue
};
并将以下 class 成员添加到对话框 class:
private:
Season season_ {};
Color color_ { Color::Green };
剩下的就是设置 DDX 关联,即:
void CRadioEnumDlg::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_RadioEnum(pDX, IDC_RADIO_SPRING, season_);
DDX_RadioEnum(pDX, IDC_RADIO_RED, color_);
}
(CRadioEnumDlg
派生自 CDialogEx
)。所有模板机制都被巧妙地隐藏起来,模板类型参数从最终参数中推断出来。
为了完整起见,这里是使用的对话框模板:
IDD_RADIOENUM_DIALOG DIALOGEX 0, 0, 178, 107
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,59,86,50,14
PUSHBUTTON "Cancel",IDCANCEL,121,86,50,14
CONTROL "Spring",IDC_RADIO_SPRING,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,7,7,39,10
CONTROL "Summer",IDC_RADIO_SUMMER,"Button",BS_AUTORADIOBUTTON,7,20,39,10
CONTROL "Fall",IDC_RADIO_FALL,"Button",BS_AUTORADIOBUTTON,7,33,39,10
CONTROL "Winter",IDC_RADIO_WINTER,"Button",BS_AUTORADIOBUTTON,7,46,39,10
CONTROL "Red",IDC_RADIO_RED,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,54,7,39,10
CONTROL "Green",IDC_RADIO_GREEN,"Button",BS_AUTORADIOBUTTON,54,20,39,10
CONTROL "Blue",IDC_RADIO_BLUE,"Button",BS_AUTORADIOBUTTON,54,33,39,10
END
及其附带的 resource.h:
#define IDD_RADIOENUM_DIALOG 102
#define IDC_RADIO_SPRING 1000
#define IDC_RADIO_SUMMER 1001
#define IDC_RADIO_FALL 1002
#define IDC_RADIO_WINTER 1003
#define IDC_RADIO_RED 1004
#define IDC_RADIO_GREEN 1005
#define IDC_RADIO_BLUE 1006
使用上述方法调整默认生成的 MFC 应用程序(基于对话框)会在启动时产生以下结果:
实际上,这很甜蜜。请特别注意,第二行单选按钮选中了第二项,这与对话框 class' 实现中设置的初始值匹配 (Color color_ { Color::Green }
).
那么一切都好吗?
嗯,是的。我猜。无论如何,有点。让我们谈谈不太酷的事情、需要注意的事情以及根本没有解决方案的问题。
上面提供的实现做了一些假设,none 可以在编译时验证,只有其中一些可以(并且正在)在 运行 时验证:
- 枚举值需要由整数值支持,从 0 开始,无间隙地向上计数。据我所知,今天(C++20)没有办法强制执行这一点,确保这一点的最有效方法是代码注释。
- 枚举值的顺序必须与单选按钮控件的 Tab 键顺序相匹配。同样,这不是可以强制执行或验证的内容。
- 在
DDX_RadioEnum
调用中指定的控件 ID 必须是单选按钮组的开头。这在 运行 次(第一个 ASSERT
)得到验证。
- 在
DDX_RadioEnum
调用中指定的控件 ID 必须标识单选按钮控件。同样,这在 运行 时间(第二个 ASSERT
)得到验证。
- 单选按钮组之后的第一个控件(按 Tab 键顺序)必须设置
WS_GROUP
样式。这在 运行 时间得到了部分验证。如果后面的控件不是单选按钮控件,则会发出警告。如果控件恰好是单选按钮,那么这个就不是可以验证的了。
这些假设当然不是不可能匹配的。困难的部分是随着时间的推移保持这些不变量有效。如果可以,那么这个实现值得一试。
我有一个标准对话资源,上面有一些无线电控件。
目前这一切都以正常方式完成,因此第一个无线电被映射到一个 int
变量。
DDX_Radio(pDX, IDC_RADIO_DISPLAY_EVERYONE, m_iDisplayMode);
DDX_Radio(pDX, IDC_RADIO_SELECT_EVERYONE, m_iSelectMode);
事情是这样的……我有这些相关的枚举:
enum class DisplayMode { Everyone = 0, Brother, Sister };
enum class SelectMode { Everyone = 0, Elders, MinisterialServants, Appointed, Custom, None };
因此,每当我需要对映射变量进行一些比较时,我都必须这样做:
示例 1:
m_iDisplayMode = to_underlying(DisplayMode::Everyone);
m_iSelectMode = to_underlying(SelectMode::None);
示例 2:
if (m_iDisplayMode == to_underlying(DisplayMode::Everyone))
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Brother) && mapPublisher.second.eGender == Gender::Male)
bInclude = true;
else if (m_iDisplayMode == to_underlying(DisplayMode::Sister) && mapPublisher.second.eGender == Gender::Female)
bInclude = true;
to_underlying
函数是一个辅助函数,之前已经在 SO 上向我推荐过并且非常宝贵:
template <typename E>
constexpr auto to_underlying(E e) noexcept
{
return static_cast<std::underlying_type_t<E>>(e);
}
我想知道是否可以将这些无线电控件直接映射到 DisplayMode
或 SelectMode
对象?因此,它不是映射到 1
等,而是映射到 DisplayMode::Everyone
等。这将简化此上下文中的代码并避免所有 to_underlying
调用的需要。
这是 DDX_Radio
的 MFC 源代码:
void AFXAPI DDX_Radio(CDataExchange* pDX, int nIDC, int& value)
// must be first in a group of auto radio buttons
{
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
if (pDX->m_bSaveAndValidate)
value = -1; // value if none found
// walk all children in group
int iButton = 0;
do
{
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON)
{
// control in group is a radio button
if (pDX->m_bSaveAndValidate)
{
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0)
{
ASSERT(value == -1); // only set once
value = iButton;
}
}
else
{
// select button
::SendMessage(hWndCtrl, BM_SETCHECK, (iButton == value), 0L);
}
iButton++;
}
else
{
TRACE(traceAppMsg, 0, "Warning: skipping non-radio button in group.\n");
}
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
} while (hWndCtrl != NULL &&
!(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
我正在尝试使用答案中的代码,但出现此错误:
MFC 支持数据(class 成员)和UI 状态之间的映射。标准机制称为 Dialog Data Exchange (DDX) which the code in the question is using already (DDX_Radio
). The data exchange is two-way, triggered by a call to UpdateData
,其中 TRUE
的参数将 UI 状态转换为值,并且 FALSE
读取关联值并适当调整 UI .
MFC 已经提供了一些 standard dialog data exchange routines,但客户可以提供自己的,以防 none 适合直接用例。该问题属于此类,方便地提供 DDX_Radio
的实现作为起点。
该实现看起来有点吓人,但一旦代码在各处添加了一些注释后,事情就开始变得有意义了:
CustomDDX.h:
template<typename E>
void AFXAPI DDX_RadioEnum(CDataExchange* pDX, int nIDC, E& value)
{
// (1) Prepare the control for data exchange
pDX->PrepareCtrl(nIDC);
HWND hWndCtrl;
pDX->m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl);
// (2) Make sure this routine is associated with the first
// radio button in a radio button group
ASSERT(::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP);
// And verify, that it is indeed a radio button
ASSERT(::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON);
// (3) Iterate over all radio buttons in this group
using value_t = std::underlying_type_t<E>;
value_t rdbtn_index {};
do {
if (::SendMessage(hWndCtrl, WM_GETDLGCODE, 0, 0L) & DLGC_RADIOBUTTON) {
// (4) Control is a radio button
if (pDX->m_bSaveAndValidate) {
// (5) Transfer data from UI to class member
if (::SendMessage(hWndCtrl, BM_GETCHECK, 0, 0L) != 0) {
value = static_cast<E>(rdbtn_index);
}
} else {
// (6) Transfer data from class member to UI
::SendMessage(hWndCtrl, BM_SETCHECK,
(static_cast<E>(rdbtn_index) == value), 0L);
}
++rdbtn_index;
} else {
// (7) Not a radio button -> Issue warning
TRACE(traceAppMsg, 0,
"Warning: skipping non-radio button in group.\n");
}
// (8) Move to next control in tab order
hWndCtrl = ::GetWindow(hWndCtrl, GW_HWNDNEXT);
}
// (9) Until there are no more, or we moved to the next group
while (hWndCtrl != NULL && !(GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP));
}
这声明了一个可以为任意范围的枚举类型实例化的函数模板,并实现了在 UI 状态和枚举值之间转换的逻辑。枚举的整数基础值作为单选按钮组选择的从零开始的索引。
尽管如此,实施需要一些解释。以下列表提供了有关编号 // (n)
代码注释的更多信息:
- 这会初始化框架使用的内部状态。只要调用了正确的函数,精确的细节并不是很重要。有 3 种实现,一种用于 OLE 控件,一种用于编辑控件,另一种用于所有其他内容。我们属于 “其他” 类别。
- 执行完整性检查。这验证了由
nIDC
标识的控件是单选按钮组 (WS_GROUP
) 中的第一个控件,并且它确实是一个单选按钮控件。这有助于在 运行 进行调试构建时尽早清除错误。 - 初始化单选按钮索引计数器 (
rdbtn_index
),并开始迭代单选按钮。 - 确保我们在本次迭代中操作的控件是单选按钮控件(如果不是,请参见 7.)。
- 将UI状态转换回成员变量时,验证当前控件是否被选中,并将其索引作为作用域枚举值存储在组中。
- 否则(即在将数据转换为 UI 状态时)如果枚举的数值与控件索引匹配,则设置复选标记,否则取消选中。后者在使用
BS_AUTORADIOBUTTON
控件时不是严格要求的,但它也无害。 - 如果我们遇到不是单选按钮控件的控件,请发出警告。仔细观察此消息的调试输出;它指定对话框模板中的错误。确保在此单选按钮组之后的第一个控件上设置
WS_GROUP
样式(按 Tab 键顺序)。 - 按 Tab 键顺序移至下一个控件。
- 如果没有尾随控件,或者控件开始一个由
WS_GROUP
样式指定的新组,则终止循环。
这有点需要消化。幸运的是,这个函数模板的使用远没有那么麻烦。为了便于说明,让我们使用以下作用域枚举:
enum class Season {
Spring,
Summer,
Fall,
Winter
};
enum class Color {
Red,
Green,
Blue
};
并将以下 class 成员添加到对话框 class:
private:
Season season_ {};
Color color_ { Color::Green };
剩下的就是设置 DDX 关联,即:
void CRadioEnumDlg::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_RadioEnum(pDX, IDC_RADIO_SPRING, season_);
DDX_RadioEnum(pDX, IDC_RADIO_RED, color_);
}
(CRadioEnumDlg
派生自 CDialogEx
)。所有模板机制都被巧妙地隐藏起来,模板类型参数从最终参数中推断出来。
为了完整起见,这里是使用的对话框模板:
IDD_RADIOENUM_DIALOG DIALOGEX 0, 0, 178, 107
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,59,86,50,14
PUSHBUTTON "Cancel",IDCANCEL,121,86,50,14
CONTROL "Spring",IDC_RADIO_SPRING,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,7,7,39,10
CONTROL "Summer",IDC_RADIO_SUMMER,"Button",BS_AUTORADIOBUTTON,7,20,39,10
CONTROL "Fall",IDC_RADIO_FALL,"Button",BS_AUTORADIOBUTTON,7,33,39,10
CONTROL "Winter",IDC_RADIO_WINTER,"Button",BS_AUTORADIOBUTTON,7,46,39,10
CONTROL "Red",IDC_RADIO_RED,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,54,7,39,10
CONTROL "Green",IDC_RADIO_GREEN,"Button",BS_AUTORADIOBUTTON,54,20,39,10
CONTROL "Blue",IDC_RADIO_BLUE,"Button",BS_AUTORADIOBUTTON,54,33,39,10
END
及其附带的 resource.h:
#define IDD_RADIOENUM_DIALOG 102
#define IDC_RADIO_SPRING 1000
#define IDC_RADIO_SUMMER 1001
#define IDC_RADIO_FALL 1002
#define IDC_RADIO_WINTER 1003
#define IDC_RADIO_RED 1004
#define IDC_RADIO_GREEN 1005
#define IDC_RADIO_BLUE 1006
使用上述方法调整默认生成的 MFC 应用程序(基于对话框)会在启动时产生以下结果:
实际上,这很甜蜜。请特别注意,第二行单选按钮选中了第二项,这与对话框 class' 实现中设置的初始值匹配 (Color color_ { Color::Green }
).
那么一切都好吗?
嗯,是的。我猜。无论如何,有点。让我们谈谈不太酷的事情、需要注意的事情以及根本没有解决方案的问题。
上面提供的实现做了一些假设,none 可以在编译时验证,只有其中一些可以(并且正在)在 运行 时验证:
- 枚举值需要由整数值支持,从 0 开始,无间隙地向上计数。据我所知,今天(C++20)没有办法强制执行这一点,确保这一点的最有效方法是代码注释。
- 枚举值的顺序必须与单选按钮控件的 Tab 键顺序相匹配。同样,这不是可以强制执行或验证的内容。
- 在
DDX_RadioEnum
调用中指定的控件 ID 必须是单选按钮组的开头。这在 运行 次(第一个ASSERT
)得到验证。 - 在
DDX_RadioEnum
调用中指定的控件 ID 必须标识单选按钮控件。同样,这在 运行 时间(第二个ASSERT
)得到验证。 - 单选按钮组之后的第一个控件(按 Tab 键顺序)必须设置
WS_GROUP
样式。这在 运行 时间得到了部分验证。如果后面的控件不是单选按钮控件,则会发出警告。如果控件恰好是单选按钮,那么这个就不是可以验证的了。
这些假设当然不是不可能匹配的。困难的部分是随着时间的推移保持这些不变量有效。如果可以,那么这个实现值得一试。