visual-c++ - 是否可以将 CDialog RADIO 控件映射到枚举类对象而不是 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
orSelectMode
对象?因此,它不是映射到1
etc,而是映射到DisplayMode::Everyone
etc。这将简化此上下文中的代码并避免对所有to_underlying
调用的需要。
这是 MFC 的源代码DDX_Radio
:
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 支持数据(类成员)和 UI 状态之间的映射。标准机制称为对话数据交换(DDX),问题中的代码已经在使用 ( DDX_Radio
)。数据交换是双向的,由对 的调用触发UpdateData
,其中的参数TRUE
将 UI 状态转换为值,并FALSE
读取关联的值并适当地调整 UI。
MFC 已经提供了许多标准的对话数据交换例程,但是客户端可以提供自己的例程,以防它们都不适合直接用例。该问题属于这一类,并方便地提供实现DDX_Radio
作为起点。
这个实现看起来有点吓人,尽管一旦代码在此处和那里添加了一些注释,事情就开始变得有意义了:
自定义DDX.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
};
并将以下类成员添加到对话框类:
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 应用程序(基于对话框)在启动时会产生以下结果:
这很甜蜜,其实。特别注意,第二行单选按钮选中了第二项,它与对话框类的实现中设置的初始值相匹配 ( Color color_ { Color::Green }
)。
那么一切都好吗?
嗯,是的。我猜。反正有点。让我们谈谈不太酷的事情、需要注意的事情以及根本没有解决方案的问题。
上面提供的实现做了许多假设,没有一个可以在编译时验证,只有其中一些可以(并且)在运行时验证:
- 枚举值需要由整数值支持,从 0 开始,并且没有任何间隙地向上计数。据我所知,目前没有办法强制执行此操作(C++20),而确保这一点的最有效方法是代码注释。
- 枚举值的顺序必须与单选按钮控件的 tab 顺序相匹配。同样,这不是可以强制执行或验证的。
- 调用中指定的控件 ID
DDX_RadioEnum
必须是单选按钮组的开始。这是在运行时验证的(第一个ASSERT
)。 - 调用中指定的控件 ID
DDX_RadioEnum
必须标识一个单选按钮控件。同样,这是在运行时验证的(第二个ASSERT
)。 - 单选按钮组后面的第一个控件(按 Tab 键顺序)必须
WS_GROUP
设置样式。这部分在运行时得到验证。如果下面的控件不是单选按钮控件,则会发出警告。如果控件恰好是一个单选按钮,那么这不是可以验证的。
这些假设当然不是不可能匹配的。困难的部分是保持这些不变量随着时间的推移有效。如果可以,那么这个实现值得一试。
推荐阅读
- python - 替换列表中的值(列表重叠)
- javascript - Smoothie.js 图表不适用于 JSON 数据
- php - PHP | 模板 - 仅使用文件作为模板
- java - 如何找到产生 Spring InvalidResultSetAccessException 的名称
- django - pdf中的Internet公共IP地址和django静态文件
- c# - WPF ObservableCollection 更新 DataGrid MVVM
- jquery - JQMIGRATE:jQuery.fn.offset() 需要一个连接到文档的元素
- javascript - 如何在 Element 和 Infinity 之间获得 if 语句?
- python - WTForms render_field() 方法参考for循环变量
- javascript - HTML / JS - 切换显示/隐藏动画