是否可以将 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);
}

我想知道是否可以将这些无线电控件直接映射到 DisplayModeSelectMode 对象?因此,它不是映射到 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) 代码注释的更多信息:

  1. 这会初始化框架使用的内部状态。只要调用了正确的函数,精确的细节并不是很重要。有 3 种实现,一种用于 OLE 控件,一种用于编辑控件,另一种用于所有其他内容。我们属于 “其他” 类别。
  2. 执行完整性检查。这验证了由 nIDC 标识的控件是单选按钮组 (WS_GROUP) 中的第一个控件,并且它确实是一个单选按钮控件。这有助于在 运行 进行调试构建时尽早清除错误。
  3. 初始化单选按钮索引计数器 (rdbtn_index),并开始迭代单选按钮。
  4. 确保我们在本次迭代中操作的控件是单选按钮控件(如果不是,请参见 7.)。
  5. 将UI状态转换回成员变量时,验证当前控件是否被选中,并将其索引作为作用域枚举值存储在组中。
  6. 否则(即在将数据转换为 UI 状态时)如果枚举的数值与控件索引匹配,则设置复选标记,否则取消选中。后者在使用 BS_AUTORADIOBUTTON 控件时不是严格要求的,但它也无害。
  7. 如果我们遇到不是单选按钮控件的控件,请发出警告。仔细观察此消息的调试输出;它指定对话框模板中的错误。确保在此单选按钮组之后的第一个控件上设置 WS_GROUP 样式(按 Tab 键顺序)。
  8. 按 Tab 键顺序移至下一个控件。
  9. 如果没有尾随控件,或者控件开始一个由 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 样式。这在 运行 时间得到了部分验证。如果后面的控件不是单选按钮控件,则会发出警告。如果控件恰好是单选按钮,那么这个就不是可以验证的了。

这些假设当然不是不可能匹配的。困难的部分是随着时间的推移保持这些不变量有效。如果可以,那么这个实现值得一试。