Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Win32

DWinLib - Creating Modal Dialogs

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
14 Feb 2013CPOL8 min read 15.6K   364   5  
Need to make a simple, or not-so-simple dialog? The following approach may be just the ticket, even without DWinLib!

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:

C++
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.

 

Image 1

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:

C++
//For play:
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():

C++
bool MainAppWin::instantiate() {
   ui::MainMenu * theMenu = new ui::MainMenu(this);
   //In the following, MainMenu has a virtual destructor, so everything will be deleted
   //properly.  I also noted in the past that gMainWin must be set before the menu
   //instantiation.  That is done in MainWin's constructor, which is called beforehand.
   mainMenuC.reset(theMenu);
   fullMenuC = s_cast<ui::MainMenu*>(mainMenuC.get());
   MainWin::instantiate(mainMenuC.get()); //The MDI client is created in this call

   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:

C++
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

Image 1

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 winProcs (or WindowProcs, 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 Buttons 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:

C++
//...
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:

C++
//In 'ModalBaseForm::instantiate':
if (modalC) {
   EnableWindow(parentC->hwnd(), FALSE);
   //...
   }

//In both the destructor and <code>wClose</code>:
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:

C++
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);

      //The ones that were left to the user to set if needed were:
      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,

C++
ModalReturnTest::ModalReturnTest(AppWindow * win, bool modal) :
            dwl::ModalBaseForm(modal),
            appWindowC(win) { //This line sets up an <code>AppWindow</code> pointer
                              //for later use

   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"));

   //Set up the buttons to call back to the ModalReturnTest::buttonCallback:
   buttonAC.reset(new dwl::Button(this,
               CreateDwlDelegate(ModalReturnTest, buttonCallback, this, this)));
   //And set the 'user' to the appropriate option:
   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)));
   }

//The 'buttonCallback is' defined as:
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();
   }

//And, finally, the AppWindow::optionCallback, where the final logic occurs, is:
void AppWindow::optionCallback(ModalReturnTest * win, MRT option) {
   //Take action here, based upon the option selected in the modal dialog box.
   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:

C++
//This is a plain windows callback function, unassociated with any class.
//The AppWindow * must be sent to it in the form of a LPARAM variable. This means the
//CreateDialogParam version of CreateDialog must be used. Also note that this means
//only 1 non-modal window could use this at a time as-is, because of the 'static' variable:

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer www.randommonkeyworks.com
United States United States
I am the author of Laughing at the Devil: One Man’s Religious Discoveries. If you want to understand the astronomic investigations of our priests 3,000 years ago, LATD is the book to turn to. It opens up the thoughts that pushed them away from their earlier polytheism and towards our current definition of God.

Trained as a mechanical engineer, I have been involved with design, supervision, and project management. I taught myself C++ programming in order to play around with binaural beats more easily. I've also created various databases to help with project management and personal tasks.

Databases are cool and extremely useful! Happy coding, everybody!

Comments and Discussions

 
-- There are no messages in this forum --