首页 > 解决方案 > 是否可以将 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);
}

我想知道的是是否可以将这些无线电控件直接映射到DisplayModeorSelectMode对象?因此,它不是映射到1etc,而是映射到DisplayMode::Everyoneetc。这将简化此上下文中的代码并避免对所有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));
}

我正在尝试使用答案中的代码,但收到此错误:

在此处输入图像描述

标签: visual-c++enumsmfcradio-button

解决方案


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)以下列表提供了有关编号代码注释的更多信息:

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

RadioEnumDlg 屏幕截图

这很甜蜜,其实。特别注意,第二行单选按钮选中了第二项,它与对话框类的实现中设置的初始值相匹配 ( Color color_ { Color::Green })。


那么一切都好吗?

嗯,是的。我猜。反正有点。让我们谈谈不太酷的事情、需要注意的事情以及根本没有解决方案的问题。

上面提供的实现做了许多假设,没有一个可以在编译时验证,只有其中一些可以(并且)在运行时验证:

  • 枚举值需要由整数值支持,从 0 开始,并且没有任何间隙地向上计数。据我所知,目前没有办法强制执行此操作(C++20),而确保这一点的最有效方法是代码注释。
  • 枚举值的顺序必须与单选按钮控件的 tab 顺序相匹配。同样,这不是可以强制执行或验证的。
  • 调用中指定的控件 IDDDX_RadioEnum必须是单选按钮组的开始。这是在运行时验证的(第一个ASSERT)。
  • 调用中指定的控件 IDDDX_RadioEnum必须标识一个单选按钮控件。同样,这是在运行时验证的(第二个ASSERT)。
  • 单选按钮组后面的第一个控件(按 Tab 键顺序)必须WS_GROUP设置样式。这部分在运行时得到验证。如果下面的控件不是单选按钮控件,则会发出警告。如果控件恰好是一个单选按钮,那么这不是可以验证的。

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


推荐阅读