【问题标题】:Is it possible to map CDialog RADIO controls to an enum class object instead of int?是否可以将 CDialog RADIO 控件映射到枚举类对象而不是 int?
【发布时间】:2021-12-01 07:46:15
【问题描述】:

我有一个标准对话框资源,上面有一些无线电控件。

目前一切都以正常方式完成,因此第一个无线电被映射到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));
}

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

【问题讨论】:

  • 我很确定Stan, sorry, I mean Loretta 不会喜欢这段代码。无论如何......最简单的解决方案可能是根本不将选择存储在成员变量中。相反,实现一个查询 UI 状态的函数,并将选择转换为一个作用域枚举。也可以在 DoDataExchange 方法或其他一些数据交换机制中实现枚举和 UI 状态之间的自定义映射。
  • @IInspectable 如果我查询 UI 状态,那么您如何知道选择了哪个收音机? DoDateExchange 听起来很有趣。一种替代方法是我添加自己的枚举变量(未映射)并在所有单击的处理程序中手动更新该枚举。
  • 该框架提供了standard DDX routines,用于在数据和 UI 之间执行双向转换。正如here 所暗示的那样,您可以实现自己的。您可以查看提供的实现以了解如何编写自定义 DDX 例程。它可能归结为执行转换的大量强制转换,但它仅限于单个函数。
  • 通过使其成为枚举类来实现,如果您的 UI 需要更改,即单选按钮顺序需要更改(技术上应该与枚举类顺序“无关”),您需要无论如何要“映射”它?这是将 UI 与枚举值紧密耦合的缺点之一(除非它们可以更改,并且它们的值永远不会在对话框之外使用,但如果不是,您可能会遇到这个问题)

标签: visual-c++ enums mfc radio-button


【解决方案1】:

MFC 支持数据(类成员)和 UI 状态之间的映射。标准机制称为Dialog Data Exchange(DDX),问题中的代码已经在使用它(DDX_Radio)。数据交换是双向的,由调用 UpdateData 触发,其中 TRUE 的参数将 UI 状态转换为值,FALSE 读取关联的值并适当地调整 UI。

MFC 已经提供了许多standard dialog data exchange routines,但客户可以提供自己的,以防它们都不适合直接用例。题属于此类,方便提供DDX_Radio的实现作为起点。

实现看起来有点吓人,不过一旦代码在各处添加了一些 cmets,事情就开始变得有意义了:

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) 的代码 cmets 的更多信息:

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

并将以下类成员添加到对话框类中:

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 顺序相匹配。同样,这不是可以强制执行或验证的。
  • DDX_RadioEnum 调用中指定的控件 ID 必须是单选按钮组的开头。这是在运行时验证的(第一个 ASSERT)。
  • DDX_RadioEnum 调用中指定的控件 ID 必须标识单选按钮控件。同样,这是在运行时验证的(第二个 ASSERT)。
  • 单选按钮组后面的第一个控件(按 Tab 键顺序)必须设置 WS_GROUP 样式。这部分在运行时得到验证。如果下面的控件不是单选按钮控件,则会发出警告。如果控件恰好是一个单选按钮,那么这不是可以验证的。

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

【讨论】:

  • 非常感谢您的回答、代码和解释。我今晚要早点试一试。 :)
  • 我似乎遗漏了一些东西。我创建了一个新的头文件并粘贴了您的代码。但我在void AFXAPI DDX_RadioEnum(CDataExchange* pDX, int nIDC, E&amp; value) 线上遇到错误。它要我在AFXAPI 后面加一个分号。我错过了一步吗?
  • 该错误是因为AFXAPI在实例化时是未知的。它指定调用约定,并在afxver_.h 中定义。我不确定应该在声明类模板的标头之前包含哪个标头,可能是包罗万象的标头之一,例如afx.h
  • DDX_Radio 实现了相同的逻辑。如果单选按钮组后面有一个不具有 WS_GROUP 样式且不是单选按钮控件的控件,您将看到跟踪消息 ("Warning: skipping non-radio button in group.")。这就是单选按钮组的工作方式:第一个单选按钮需要WS_GROUP 样式,并且该组运行到但不包括具有WS_GROUP 样式的下一个控件。如果您忘记了,您仍然会收到跟踪消息作为提醒。
  • 该代码很旧,可能超过 25 年。它是在静态代码分析工具大多仅限于 C 代码的时候编写的,并且带有 5 位数的价格标签。它可能早于 PREFast,甚至是最初的 MISRA C 草案。那时的世界不同,密码是那个时代的时间胶囊。这里没有惊喜,真的。如果您想强化您的代码(通过添加空指针检查或使用_In_ 注释),那很好。只是不要更改 MFC 代码。相反,将其从代码分析中排除。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-05
  • 1970-01-01
  • 2014-07-29
  • 1970-01-01
  • 2021-07-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多