This article discusses creating modal dialog boxes in DWinLib, from a pure Windows Application Programming Interface (API) perspective, and a semi-equivalent DWinLib wrapper perspective.
While creating a MIDI program with binaural beat capabilities I needed simple ways to adjust MIDI event properties. One of the methods that naturally suggests itself is to double-click on an item to bring up a properties window.
When I was creating DWinLib I did not have much experience with MFC or the Windows' API. All of my work had been concentrated on Borland Builder and its Visual Class Library, so I tried to keep my framework as simple as that one.
My experience with dialog boxes had been pretty much limited to just Windows' MessageBox
. If you have played with that function you know it blocks your code flow until the user presses 'OK' or 'Cancel.' In other words, given:
int result = MessageBox(hwndC, _T("Press Yes"), _T("Do It"), MB_YESNO);
if (result == IDYES) doSomething();
else if (result == IDNO) giveUserHardTime();
The program won't execute the if (result == IDYES) doSomething();
line until the user presses the MessageBox
button, which closes the box and continues the program at that point.
When I needed similar functionality in my program I didn't know what to do. I had never played with CreateDialog
or DialogBox
before, and the examples I saw seemed to be all MFC oriented. So I attacked the problem from what I knew. And that was creating Windows: I wanted to create dialog boxes the same way. After a lot of investigation I figured out how.
The following project contains the result of my efforts. It has a complete copy of DWinLib 6.04 (Jan, 2021), so if you extract the zip you can skip those subdirectories if you already have them from another article. The .sln was compiled with Visual Studio 2019, so older editions may require manual project recreation.
Hello World!
Before getting into modal dialogs let me present a little 'Hello World' program, because it's came to my attention I have not done so. Some esoteric DWinLib knowledge will be shared along the journey!
Assuming that you extract the zip file and successfully open and build the .sln file in Visual Studio, close the executable. Navigate to the MainAppWin.h file and add the following lines at the end of the MainAppWin class:
private:
dwl::Button * helloButtonC;
public:
LRESULT hello(Button b, WPARAM flags, Point p);
};
And in the .cpp file add the following bolded text to MainAppWin::instantiate()
:
bool MainAppWin::instantiate() {
ui::MainMenu * theMenu = new ui::MainMenu(this);
mainMenuC.reset(theMenu);
fullMenuC = s_cast<ui::MainMenu*>(mainMenuC.get());
MainWin::instantiate(mainMenuC.get());
helloButtonC = new dwl::Button(mdiClientC.get(), [&](dwl::Object*) { hello(Button::Left, 0, {0, 0}); } );
helloButtonC->instantiate(50, 50, 120, 25, _T("Press Me!"));
helloButtonC->onButtonDown([&](Button b, WPARAM flags, Point p) {
return hello(b, flags, p);});
Rect winRect;
And add the hello()
function to the file:
LRESULT MainAppWin::hello(Button b, WPARAM flags, Point p) {
dwl::msgBox(_T("Hello there!"));
return 0;
}
Compile and execute the project and you will be greeted with a button that, once clicked, will present you with a classic 'Hello' message
Now for the esoteric knowledge.
This button is special. It is parented to the application's main Multiple Document Interface (MDI) client window, and if you view the design illustration in my "DWinLib - The Guts" article, you will see that the MdiClient
derives from Control
. Also reading that article, you will find that ControlWin
and BaseWin
classes have winProc
s (or WindowProc
s, if you prefer that nomenclature). This means that the button DOES NOT HAVE a window procedure to tie into! The MDI client cannot capture WM_COMMAND
messages and route them to the application's delegate handler in order to forward them to your own functions.
That is why the above code sets the button's onChange
handler. Since Button
s derive from WinControl
, they have a built-in winProc
handler, and in that handler there is a std::function
that is triggered when the left mouse button is pressed.
This also means that our button is quite limited in comparison to other buttons. There is no way to tab in to it, nor can pressing the 'Enter' key trigger the button. Only the left mouse button press will make the 'Hello' message appear.
Another limitation is seen if you open a new document by pressing the 'New Document' button, or 'Ctrl + N,' and then immediately 'restoring' the document to non-maximized. The button 'bleeds through' onto the document for some reason, until the document is forcibly repainted by resizing the window.
The simplest solution to these problems is to not parent controls to the main MDI client window. But it was a good example for learning purposes.
(I suspect you could get over these limitations if the MdiClient
was given a window procedure that called back into DefMDIChildProc
at the appropriate places. But is it worth the hassle? I believe this is a case of YAGNI - You Ain't Gonna Need It, which is why I never went down that route.)
Moving On!
If you wish, remove the previous additions before continuing. Then run the program again, add a new document to it via 'Ctrl + N' or pressing the 'New Document' button (or select the main menu 'File' item), and press the top button. A DWinLib modal dialog will appear. Functionally, it is almost equivalent to a true Windows dialog box. But not quite, if you examine the box's characteristics more closely.
As I said, I hadn't played with true modal windows from an API standpoint when I came across this problem. So I searched and searched. Eventually I came across the key to a solution: EnableWindow
.
Playing with it, I found I could fake modal window behaviors. And after more work I got everything to work.
The solution requires two parts. The first is creating a window that looks modal. This is accomplished during the creation step. Call CreateWindow
with a CREATESTRUCT
having the following attributes:
cs.dwExStyle = WS_EX_TOOLWINDOW;
cs.style = WS_POPUP | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
And finally, the second part: using EnableWindow
during construction and destruction of the modal window:
if (modalC) {
EnableWindow(parentC->hwnd(), FALSE);
}
EnableWindow(parentC->hwnd(), TRUE);
With that, if the window is specified to be true modal the main window becomes disabled and the modal window takes control. If it isn't fully modal the main window can again gain focus if it is clicked on, but the modeless window won't disappear.
Of course, there is a little more to it if you want to handle accelerators. The responsible codes are the instantiate
method and the destructor:
ModalBaseForm::~ModalBaseForm() {
EnableWindow(parentC->hwnd(), TRUE);
if (modalC) gDwlGlobals->dwlApp->accel().pop();
gDwlGlobals->dwlApp->removeWindow(this);
}
void ModalBaseForm::instantiate(int x, int y, int width, int height) {
static bool alreadyRegistered = false;
wpWidthC = width;
wpHeightC = height;
if (!alreadyRegistered) {
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(wc));
wc.cbSize = sizeof (WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = dwl::Application::winProc;
wc.hInstance = gDwlGlobals->dwlApp->instance();
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hIcon = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = &winClassName[0];
wc.hIconSm = NULL;
if (!RegisterClassEx (&wc))
throw dwl::Exception(_T("Unable to register ModalBaseForm"));
alreadyRegistered = true;
}
CREATESTRUCT cs;
cs.dwExStyle = WS_EX_TOOLWINDOW;
cs.lpszClass = &winClassName[0];
cs.lpszName = winCaption;
cs.style = WS_POPUP | WS_CAPTION | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
cs.x = x;
cs.y = y;
cs.cx = width;
cs.cy = height;
cs.hwndParent = gDwlGlobals->dwlMainWin->hwnd();
cs.hMenu = NULL;
cs.hInstance = gDwlGlobals->dwlApp->instance();
cs.lpCreateParams = NULL;
hwndC = gDwlGlobals->dwlApp->createWindow(this, cs);
if (!hwndC) throw dwl::Exception(_T("Unable to create window"));
if (modalC) {
EnableWindow(parentC->hwnd(), FALSE);
gDwlGlobals->dwlApp->accel().push();
gDwlGlobals->dwlApp->accel().addAccelerator(FVIRTKEY, VK_ESCAPE, (short)cancelCallbackC->id());
gDwlGlobals->dwlApp->changeAccelTable();
}
}
The second part of the solution was 'faking' return values from the dialog box. This is done in response to the dialog box button presses.
Because DWinLib 'dialog boxes' are a thin wrapper around a regular window, a pointer to the parent window, or controlling class, can easilly be passed into the constructor. (You can do the same to WinAPI dialog boxes as well, with a little more difficulty. You just have to cast an 'LPARAM
variable to the real type.)
Likewise, the buttons, and other controls in the 'dialog box' are full DWinLib controls, and can be used as such. This is unlike a resource-created dialog box, in which you can only use Windows API-like functionality. That functionality limits you to using 'define
's for the button identifiers, which means you must then somehow map those 'define
's to the corresponding option or control flow you want.
In DWinLIb, every ControlWin
-derived control contains a user
variable in it, which can hold 'anything.' This is just Christopher Diggin's 'any
' utility object. For this example, just place an enum
value into that user
and then take direct action based on that value. Putting this into code,
ModalReturnTest::ModalReturnTest(AppWindow * win, bool modal) :
dwl::ModalBaseForm(modal),
appWindowC(win) {
DWORD style = ((WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW) &
~WS_SIZEBOX & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
SetWindowLong(hwndC, GWL_STYLE, style);
SetWindowPos(hwndC, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOZORDER | SWP_NOSIZE |
SWP_NOACTIVATE | SWP_FRAMECHANGED);
SetWindowText(hwndC, _T("Modal Return Test"));
buttonAC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
buttonAC->user = MRT::OptionA;
buttonBC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
buttonBC->user = MRT::OptionB;
buttonCC.reset(new dwl::Button(this,
CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
buttonCC->user = MRT::OptionC;
cancelButtonC.reset(new dwl::Button(this,
CreateDwlDelegate(dwl::ModalBaseForm, cancel, this, this)));
}
void ModalReturnTest::buttonCallback(dwl::Object * obj) {
dwl::Button * button = d_cast<dwl::Button*>(obj);
if (!button) return;
MRT option = button->user.cast<MRT>();
appWindowC->optionCallback(this, option);
wClose();
}
void AppWindow::optionCallback(ModalReturnTest * win, MRT option) {
if (option == MRT::OptionA)
MessageBox(win->hwnd(), _T("Option A selected"), _T("Result"), MB_OK);
else if (option == MRT::OptionB)
MessageBox(win->hwnd(), _T("Option B selected"), _T("Result"), MB_OK);
else if (option == MRT::OptionC)
MessageBox(win->hwnd(), _T("Option C selected"), _T("Result"), MB_OK);
}
Pretty simple and straightforward C++ code. Note that the ModalReturnTest::buttonCallback
is where the user
is casted back to its option.
As I was writing the January 2021 version of this, I wanted to finally understand the Windows API way of creating dialog boxes. I had perused a little bit about creating and using resources through Visual Studio's resource editor, and wondered how easily a DWinLib program could interface with one created that way. I discovered it wasn't too hard. The third and fourth buttons on the form create a resource dialog box modally and modelessly.
The first thing to do is to create the box. In Visual Studio you must right-click in the Solution Explorer on a Filter and 'Add' a dialog resource to it. You can then add controls to that resource through the Toolbox ('View -> Toolbox' menu items). Right-click on an added button and select 'Properties' and you can modify the ID of the button to a logical mnenomic. From there you must add a callback function to your program for the dialog box to use. And in that callback you must handle the WM_COMMAND
message for those IDs, and take appropriate action at that point.
In order to show the dialog box you must invoke either CreateDialog
or DialogBox
functions. For the first, you must also call ShowWindow
. Putting this to code:
INT_PTR CALLBACK DialogProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
static AppWindow * win = nullptr;
switch (msg) {
case WM_INITDIALOG:
win = r_cast<AppWindow*>(lParam);
return TRUE;
break;
case WM_COMMAND:
switch(LOWORD(wParam)) {
case ID_OPTIONA:
if (win) win->winDialogCallback(ID_OPTIONA);
EndDialog(hwnd, ID_OPTIONA);
break;
case ID_OPTIONB:
if (win) win->winDialogCallback(ID_OPTIONB);
EndDialog(hwnd, ID_OPTIONB);
break;
case ID_OPTIONC:
if (win) win->winDialogCallback(ID_OPTIONC);
EndDialog(hwnd, ID_OPTIONC);
break;
case IDCANCEL:
EndDialog(hwnd, IDCANCEL);
break;
}
break;
default:
return FALSE;
}
return TRUE;
}
void AppWindow::modalResourceButtonPress(dwl::Object * obj) {
int option = DialogBox(gDwlGlobals->dwlApp->instance(), MAKEINTRESOURCE(IDD_DIALOG1),
hwndC, (DLGPROC)DialogProc);
if (option == ID_OPTIONA) dwl::msgBox(_T("Option A Selected"));
if (option == ID_OPTIONB) dwl::msgBox(_T("Option B Selected"));
if (option == ID_OPTIONC) dwl::msgBox(_T("Option C Selected"));
}
void AppWindow::nonModalResourceButtonPress(dwl::Object * obj) {
HWND hwnd = CreateDialogParam(gDwlGlobals->dwlApp->instance(), MAKEINTRESOURCE(IDD_DIALOG1),
hwndC, (DLGPROC)DialogProc, (LRESULT)this);
if (hwnd) ShowWindow(hwnd, SW_SHOW);
int a = 0;
}
void AppWindow::winDialogCallback(int option) {
if (option == ID_OPTIONA) dwl::msgBox(_T("Option A Selected"));
if (option == ID_OPTIONB) dwl::msgBox(_T("Option B Selected"));
if (option == ID_OPTIONC) dwl::msgBox(_T("Option C Selected"));
}
Of course those 'options' (ID_OPTIONA, etc.) are defined in a header file (called 'resource.h' by default) that auto-generates and updates when you change the ID mnenomics through the resource editor.
And that is that. I can't think of any more words to take up space, so I will wrap up this writing by hoping you found it helpful for something, even if that was intellectual curiousity about how the Windows' API works behind the scenes of frameworks like MFC.
Happy Programming!
Update history
- 1/16/2021 - Updated article to reflect DWinLib 6.04. This included the addition of resource-created dialog boxes, to see how DWinLib and the Windows' API can work together.
- 2/13/13 - Updated article to reflect DWinLib 3.01.
- 2/27/06 - Fixed some creepy-crawlies.