MFC supports mapping between data (class members) and UI state. The standard mechanism is called 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
, where an argument of TRUE
translates the UI state into values, and FALSE
reads the associated values and adjusts the UI appropriately.
There are a number of standard dialog data exchange routines provided by MFC already, but clients can provide their own in case none of them fit the immediate use case. The question falls into this category, and conveniently provides the implementation of DDX_Radio
to serve as a starting point.
The implementation looks a fair bit intimidating, though things start to make sense once the code has been augmented with a few comments here and there:
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));
}
This declares a function template that can be instantiated for arbitrary scoped enum types, and implements the logic to translate between UI state and enum value. The integral underlying value of the enum serves as the zero-based index into the radio button group selection.
The implementation needs a bit of explanation, though. The following list provides a bit more information regarding the numbered // (n)
code comments:
- This initializes internal state used by the framework. The precise details aren't very important, as long as the correct function is called. There are 3 implementations, one for OLE controls, one for edit controls, and one for every thing else. We're in the "everything else" category.
- Perform sanity checks. This verifies that the control identified by
nIDC
is the first control in a radio button group (WS_GROUP
), and that it is indeed a radio button control. This helps weed out bugs early when running a debug build.
- Initialize the radio button index counter (
rdbtn_index
), and start iterating over radio buttons.
- Make sure the control we're operating on in this iteration is a radio button control (if not, see 7.).
- When translating UI state back to member variables, verify whether the current control is checked, and store its index in the group as a scoped enum value.
- Otherwise (i.e. when translating data to UI state) set the check mark if the numeric value of the enum matches the control index, and uncheck it otherwise. The latter is not strictly required when using
BS_AUTORADIOBUTTON
controls, but it's not harmful either.
- If we encounter a control that isn't a radio button control, issue a warning. Closely watch the debug output for this message; it designates a bug in the dialog template. Make sure to set the
WS_GROUP
style on the first control following this radio button group (in tab order).
- Move on to the next control in tab order.
- Terminate the loop if either there is no trailing control, or the control starts a new group, designated by the
WS_GROUP
style.
That's a fair bit to digest. Luckily, use of this function template is far less cumbersome. For purposes of illustration, let's use the following scoped enums:
enum class Season {
Spring,
Summer,
Fall,
Winter
};
enum class Color {
Red,
Green,
Blue
};
and add the following class members to the dialog class:
private:
Season season_ {};
Color color_ { Color::Green };
All that's left is setting up the DDX associations, i.e.:
void CRadioEnumDlg::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_RadioEnum(pDX, IDC_RADIO_SPRING, season_);
DDX_RadioEnum(pDX, IDC_RADIO_RED, color_);
}
(with CRadioEnumDlg
deriving from CDialogEx
). All the template machinery is neatly hidden, with the template type argument getting inferred from the final argument.
For completeness, here is the dialog template used:
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
as well as its accompanying 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
Adjusting a default-generated MFC application (dialog-based) with the above produces the following result when launched:

That's sweet, actually. Note in particular that the second row of radio buttons has the second item checked, which matches the initial value set in the dialog class' implementation (Color color_ { Color::Green }
).
So all's good then?
Well, yeah. I guess. Sort of, anyway. Let's talk about the things that aren't quite as cool, things to watch out for, and problems that simply don't have a solution.
The implementation provided above makes a number of assumptions, none of which can be verified at compile time, and only some of them can (and are) verified at run time:
- Enum values need to be backed by integral values, starting at 0, and counting up without any gaps. To my knowledge, there's no way to enforce this today (C++20), and the most effective way to ensure this is a code comment.
- The order of enum values must match the tab order of radio button controls. Again, this is nothing that can be enforced nor verified.
- The control ID specified in the
DDX_RadioEnum
call must be the start of a radio button group. This is verified at run time (the first ASSERT
).
- The control ID specified in the
DDX_RadioEnum
call must identify a radio button control. Again, this is verified at run time (the second ASSERT
).
- The first control following the radio button group (in tab order) must have the
WS_GROUP
style set. This is verified at run time, in part. If the control following is not a radio button control, a warning is issued. If the control happens to be a radio button, then this is not something that can be verified.
Those assumptions certainly aren't impossible to match. The hard part is keeping those invariants valid over time. If you can, then this implementation is worth a try.