![]() |
Platforms, Frameworks & Libraries »
Libraries »
General
Intermediate
Modal dialog creation without resource editors in DWinLibBy RandomMonkeyThis article describes how to create modal and modeless dialogs easily in DWinLib without a resource editor, as well as a "Hello World" program. |
C++, Windows, Visual Studio, MFC, Dev
|
||||||||
|
Advanced Search |
|
|
|
||||||||||||||||

2/27/06 - Updated code in zip file to fix a bug. Also slightly modified the 'Modeless dialogs' section outlining the bug and the fix.
This article will discuss one of DWinLib's benefits that I have not seen in other window wrappers. That is the elimination of dialogs. This solution makes Visual Studio Express much more usable for a straight C++ Windows programming environment, as it circumvents the lack of a resource editor in VSE in a fairly simple manner. Along the path we'll see what else kicks up. It should be an interesting ride, so tag along!
You might think that getting rid of dialogs is not that big of an issue. After all, you simply design them in your drag-n-drop editor, and use the thingy's. Right?
OK, OK. It isn't quite so simple, is it? You have to create a dialog window procedure, and you have to deal with control IDs. And, if you are not using a Visual Studio environment, you may not have access to a great dialog editor. In addition, I believe there are also some other rules you must keep in mind while you design that window procedure.
In my opinion, dialog boxes are an Achilles' heel in Windows programming, especially if you are not using Visual Studio. Cross-compiler compatibility and ease-of-use were prime considerations in DWinLib, and even though I did not have the time or the inclination to create a dialog editor, I was able to make it so you can create dialog-like windows without having the problems I have mentioned. (But if you really want to create dialogs the hard way, it is possible to define a dialog resource yourself, and call CreateDialog in DWinLib, just as you could with other programming environments.)
As I write this, I have succeeded in making both modal and modeless dialog equivalents in DWinLib, but there is one thing I haven't done, and that is returning a value from my equivalent window. We will think some more about this problem as we go through this article, and give a method that may work for you.
So, let's get started.
Even though I have shown how to build a basic DWinLib program, I have not shown you how to modify it for your own use. Let me rectify that situation with the most boring of intro programs, "Hello World", DWinLib style.
If you are going to follow along without using the above zip file, first create a subdirectory to work in, and extract the .h, .cpp, and .rc files from the basic DWinLib program zip file into this directory. You might also want to rename the 'BareApp.cpp' and 'BareApp.rc' files to 'ModalExample.cpp' and 'ModalExample.rc'. Then create a project in the subdirectory, as outlined in the build instructions that were given. Complete the instructions until the program will compile and run.
Actually, if you have already followed the basic DWinLib program instructions, the easiest way to get your subdirectory set up properly may be to copy the 'BareApp' directory, rename the copy to whatever you want, delete the .sln, .user, .ncb, .vcproj, and .suo files in the new directory, and create the project in the new directory as outlined in the instructions in the basic DWinLib program page. Again, you might want to change the 'BareApp' units to 'ModalExample' units.
The above zip file uses a "D:\Programs\DWinLibExamples\ModalExample" subdirectory to hold the project. The project is named "ModalExample", and the "Hello World" example will be a drive-by shot in this solution.
After you have succeeded in compiling the program and executing it in Debug mode, it is time to get busy.
For the stupidity of "Hello World", double-click the 'AppWindow.cpp' unit in the Solution Explorer. This will bring up the AppWindow unit in the editor. Find the 'wPaint' function. It currently looks like this:
LRESULT AppWindow::wPaint(WinDC & dc) {
RECT r;
GetClientRect(hwndC, &r);
FillRect(dc(), &r, (HBRUSH)(COLOR_WINDOW+1));
return 0;
}
Modify this to:
LRESULT AppWindow::wPaint(WinDC & dc) {
RECT r;
GetClientRect(hwndC, &r);
FillRect(dc(), &r, (HBRUSH)(COLOR_WINDOW+1));
std::string str = "Hello World!";
//Now, check check how big the text will be:
SIZE textSize;
GetTextExtentPoint32(dc(), str.c_str(), str.length(), &textSize);
TextOut(dc(), (clientWidth()-textSize.cx)/2, (clientHeight()-textSize.cy)/2, str.c_str(),
str.length());
return 0;
}
Build and execute the project, and when it runs, press 'Ctrl+N'. You will have a window that looks something like this:

As you can see, it has quite a lot more on it than a standard "Hello World" program. It already has a menu, a status bar, and a dockable toolbar. It is also already set up as a MDI (Multiple Document Interface) program - pressing 'Ctrl+N' again will create a new subwindow. It is these reasons and more that justify the existing AppWindow code. If you try to eliminate the existing code, some of these functions will be impaired.
I could have made it so that you had to derive AppWindows from a class that had this functionality in it, but the goals of DWinLib did not really include shielding you from what is really happening. If you delve into the code, you will realize that a lot of it is for recreating the MDI logic within DWinLib. Recreating this logic made it much easier to use a common base class for windows, and it also vastly simplified the program structure, in my opinion.
Let us give our wonderful little program another item, and make it flicker-free (almost). If you resize the child window and you pay attention, you will note that the "Hello World!" text will sometimes flicker. To eliminate that flicker, replace the wPaint code with this:
LRESULT AppWindow::wPaint(WinDC & dc) {
RECT r;
HDC mdc = CreateCompatibleDC(dc());
GetClientRect(hwndC, &r);
HBITMAP bmp = CreateCompatibleBitmap(dc(), r.right-r.left, r.bottom-r.top);
HBITMAP oldBmp = (HBITMAP)SelectObject(mdc, bmp);
FillRect(mdc, &r, (HBRUSH)(COLOR_WINDOW+1));
std::string str = "Hello World!";
SIZE textSize;
GetTextExtentPoint32(mdc, str.c_str(), str.length(), &textSize);
TextOut(mdc, (clientWidth()-textSize.cx)/2, (clientHeight()-textSize.cy)/2, str.c_str(),
str.length());
BitBlt(dc(), 0, 0, wWidthC, wHeightC, mdc, 0, 0, SRCCOPY);
SelectObject(mdc, oldBmp);
DeleteObject(bmp);
DeleteDC(mdc);
return 0;
}
Resize the AppWindow after rebuilding and you will notice that the text now moves smoothly. Search the web for 'double-buffering' and 'eliminate flicker' to learn more about what we just did.
And the reason I said that the program is 'almost' flicker-free is because the program is set up to use a standard Windows status bar. If you resize the main window using the top edge, you will notice that the "This is a status bar test." text flickers terribly. That is the joy of Windows Common Controls - you have to sometimes go to extraordinary lengths to overcome this flicker.
In this case, you can replace the references to the 'WinStatusBar' in the WinMainO header and .cpp file with a reference to a WinStatusBarA. (Perform a 'search and replace' in those units.) Remove the WinStatusBar from the project using the Solution Explorer, and add a WinStatusBarA object using the methods outlined in the "Creating a bare DWinLib Program" article.
I tried many things to get the Windows Common Control status bar to stop flickering, but in the end I simply resorted to creating a new window to put in its place. That unit needs some work in order to respond to custom user text height settings, but it should get you started. It is also missing 'zones', or whatever they are called, and does not have the angled hash gripper in the right corner, but it works for a flicker-free demo.
Oh! One last thing. You will notice that the application remembers its settings between executions. This is performed using .ini files. I prefer to use .ini files for this purpose, as I detest bloated registries, and .ini files bring a lot less overhead to a program than an XML parser. To change where this file is stored, find the WinMainO::appWorkDir() function, and change the TEXT in the "wString fileName = ..." line to a subdirectory name you desire. (That subdirectory will be placed in the computer's 'Application Data' directory.) Then search and replace "BareApp.ini" with whatever name you want to use. Perform that search across the entire 'Current Project'.
All righty, then! Lets get on to those modal thingy's, and see a couple more tricks in DWinLib!
First, undo the "Hello World" changes, so we have a nice white screen to play with again. Now let us put a button on the window that will open our 'dialog'.
In the AppWindow header, add a line above the class declaration somewhere consisting of "class WinButton;". In the private: section of the class, add a line with "rmw::grin_ptr<WinButton> buttonC;". (See Note 1.) Add another line in the public: section consisting of "virtual LRESULT wPosChanged(WINDOWPOS * wp);". Add yet another line in the public: section with "void buttonCallback(WinObject * obj);" in it.
Now, in the AppWindow.cpp file, add "#include "WinButton.h"" to the list of include files. Then, in the constructor, below the "if (!hwndC) throw..." line and before the ShowWindow(hwndC, SW_SHOW); line, add the following line, (and see note 2 for an explanation):
buttonC.reset(new WinButton(this, 10, 10, 130, 25, "Open Modal Dialog", CreateDelegate(AppWindow, buttonCallback, this, this)));
Then add the following two function definitions:
void AppWindow::buttonCallback(WinObject * ) { ModalBaseForm * w = new ModalBaseForm(200, 200, 200, 200); //You can perform 'ShowWindow' from the ModalBaseForm ctor, or from here. //I usually do it here, so I can use values this window may know about to center //the window where I want it to be centered. Doing it from here also //allows you to derive several layers deep from ModalBaseForm, and //not worry about flashing the window in a partial state on the screen before //the final constructors finish. ShowWindow(w->hwnd(), SW_SHOW); } LRESULT AppWindow::wPosChanged(WINDOWPOS * wp) { buttonC->moveWindow((clientWidth()-buttonC->width())/2, (clientHeight()-buttonC->height())/2, buttonC->width(), buttonC->height()); return 0; }
Build and run the executable. 'Ctrl+N' a new AppWindow, click the button, and you will have something that looks like this:

You will note that there does not appear to be any way to close the window, but pressing the 'Escape' key will do the trick. (For some interesting accelerator table information behind this feat, see note 3.) To overcome this, we need to override the ModalBaseForm unit.
Before we do this, though, let us make a non-note comment about the ModalBaseForm we have created. This is that for all effects and purposes, this window acts as if it is a modal window. If you click outside the window, on another portion of your application, you receive the nice 'flashy' animation, indicating that you are not allowed to work with the other portions of your application. This is obtained via the 'EnableWindow' calls within the ModalBaseForm constructor and destructor. Pretty slick. The only thing that remains is true halting of your application logic and the return of a dialog result.
In order to really obtain that aspect of the functioning of modal dialog boxes, it would be necessary to either re-write DWinLib to enable it to create windows in separate threads, or create your dialog the hard way, and create a window procedure for it as well as a resource.
The reasons I say you would need to enable DWinLib to create windows in separate threads are because 1) I believe that is what CreateDialog is doing, and 2) I don't know of a way to return a value from a destructor. After you created the window in a separate thread, you would need to either perform a SendMessage to the threaded window, and return the value in response to the SendMessage somehow, or you would have to create the window and then perform a WaitForSingleObject on the thread handle from your main thread, and somehow get the return value in your main thread as the result of the return value of the threaded window's message handling procedure. It is a complicated problem, and I have not needed a solution to this problem for my own code (yet), but let us create a solution that will work for us if we need it.
Let us start by adding a header and .cpp file to the 'aaa' filter in the Solution Explorer. I usually just take a .cpp / .h file combination that is close to the class I need, copy them to a new name, drag that into the Solution Explorer, and then modify the files in the IDE. But to my knowledge, I don't have a file that is close for the purposes at hand, and I should probably show this method at least once. (I sure wish VCE had Borland's 'Create New Unit' command, that added a .h / .cpp combination, and even sets up the include guards in the .h file!)
So, right-click on the 'aaa' filter, select 'Add -> New Item' twice, selecting 'Visual C++ -> Header File (.h) and 'C++ File (.cpp)', using a filename of 'ModalReturnTest' for them. Then modify them to:
//ModalReturnTest.h: #ifndef ModalReturnTestH #define ModalReturnTestH #include "ModalBaseForm.h" class ModalReturnTest : public ModalBaseForm { public: ModalReturnTest(); }; #endif //ModalReturnTest.cpp: #include "PrecompiledHeaders.h" #pragma hdrstop #include "ModalReturnTest.h" ModalReturnTest::ModalReturnTest() : ModalBaseForm(200, 200, 200, 200) { }
Then change the #include "ModalBaseForm.h"; line in AppWindow.cpp to #include "ModalReturnTest.h";, and change the AppWindow::buttonCallback, ModalBaseForm line to
ModalReturnTest * w = new ModalReturnTest();
You can compile now, and have the same thing we had earlier. Now it's time to flush the functionality out in the derived unit, so let me get to work. I'll post the completed unit below, but here is the order I'm taking to get there as I work...
And, after completing the above, the following is the result:
//ModalReturnTest.h, excluding include guards: #include "ModalBaseForm.h" #include "WinButton.h" class AppWindow; class ModalReturnTest : public ModalBaseForm { public: //Step 3: Set up a public enum for the return value: enum MRT { OptionA, OptionB, OptionC }; private: rmw::grin_ptr<WinButton> buttonAC; rmw::grin_ptr<WinButton> buttonBC; rmw::grin_ptr<WinButton> buttonCC; rmw::grin_ptr<WinButton> cancelButtonC; MRT returnValueC; AppWindow * appWindowC; public: ModalReturnTest(AppWindow * win); void buttonCallback(WinObject * obj); };
//ModalReturnTest.cpp: #include "PrecompiledHeaders.h" #pragma hdrstop #include "ModalReturnTest.h" #include "AppWindow.h" ModalReturnTest::ModalReturnTest(AppWindow * win) : ModalBaseForm(200, 200, 155, 165), appWindowC(win) { //Step 1: Modify the title bar: 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, "Modal Return Test"); //Step 2: Set up the buttons: buttonAC.reset(new WinButton(this, 10, 10, 130, 25, "Option A", CreateDelegate(ModalReturnTest, buttonCallback, this, this))); buttonAC->WinControlWin::user = OptionA; buttonBC.reset(new WinButton(this, 10, 40, 130, 25, "Option B", CreateDelegate(ModalReturnTest, buttonCallback, this, this))); buttonBC->WinControlWin::user = OptionB; buttonCC.reset(new WinButton(this, 10, 70, 130, 25, "Option C", CreateDelegate(ModalReturnTest, buttonCallback, this, this))); buttonCC->WinControlWin::user = OptionC; cancelButtonC.reset(new WinButton(this, 10, 100, 130, 25, "Cancel", CreateDelegate(ModalBaseForm, cancel, this, this))); } //Step 4: Set up the callback: void ModalReturnTest::buttonCallback(WinObject * obj) { WinButton * button = d_cast<WinButton*>(obj); if (!button) return; MRT option = button->user.cast<MRT>(); appWindowC->optionCallback(option); wClose(); }
Pretty simple, all told.
You will note using this technique that the callback is called before the 'modal' window is destroyed. I don't see any reason that would ever be a problem, but decided to point it out anyway.
You will also note that the ModalBaseForm has an 'accept' and 'setSettings' function included. The manner in which the ModalBaseForm is coded allows you place an 'OK' (or 'Accept', or whatever you want to call it) button on your derived form, and call the ModalBaseForm's 'accept' function for your delegate. That 'accept' function can then call an overridden 'setSettings' in your derived form, and if it returns false, the form stays open and allows the user to fix whatever you have flagged as being wrong.
Of course, it is better to disallow incompatible items to be selected on the form in the first place, but the above method allows flexibility on your end. You can also disregard accept and setSettings entirely, and do whatever you want using your own methods in the derived classes. These methods saved me some work in the past, so I left them in if you want to use them yourself.
And that's about it for modal dialog boxes.
I just had a thought as I was writing this. Previously, I had created modeless dialogs by simply creating a window with no parent, and doing the work in that. If you try out MEdit 3.03, an example is the frequency finder window. That method has the drawback that it creates another icon in the Windows Start Bar.
This can be circumvented by modifying the definition and implementation of the ModalBaseForm. Now I'll have to go back and fix up the previous, so you won't see the changes, but let us get to it.
As I am doing this, I will note that I made the decision that only modal dialogs will create a new accelerator table state. This keeps things simple, as it otherwise becomes necessary to add logic to enable and disable the accelerator table state whenever the modeless window becomes enabled and disabled.
I also made the decision to make the ModalBaseForm default to modal. To make it modeless, add a fifth 'false' argument to the line calling the ModalBaseForm constructor.
Lucky me! That change did not change a single item in the above exposition.
...
Darn! I want the example project to show one of each form, therefore I'm going to modify the ModalReturnTest so that it can be either modal or non-modal. Then I'll place another button on the AppWindow, and make it open the modeless version of the ModalReturnTest. That would modify the above code, but I've decided not to change this example, as the above code will still compile if you follow the steps. To see the example code, look in the zip file.
This solution to modal dialogs in DWinLib raises two more items that should be mentioned before leaving. The first is that if you test the example program, you will find that it is possible to open a modeless dialog and then a modal dialog, and it will be possible to switch to the modeless dialog while the modal dialog is displayed. A true modal dialog would not allow that to happen. A solution to that problem is to hold a pointer to the modeless dialog when it is created, have the modeless dialog call back to the AppWindow upon destruction and NULL the pointer, and perform an if (modelessPtr) EnableWindow(modelessWindowHwnd, FALSE); call to disable the modeless dialog from the AppWindow unit when it creates the modal dialog. Then undo that when the modeless dialog is destroyed.
The second item is that if you must make a response to the user's input, as I have in the demo, using a MessageBox or other floating window, you must be sure to make the modal dialog the parent of the window making the response. If you don't, the user will be able to repeatedly press the buttons in the modal window, and when the user presses the second MessageBox's 'OK' button, the application will crash while trying to access the destroyed modal window's memory.
To make the modal window the parent of the MessageBox (or other type of window), it is necessary to make the calling unit aware of the modal window's HWND in some manner. I simply passed the 'this' pointer back in the 'optionCallback' function, and called hwnd() on that. (Thanks go to Marijn Stevens for pointing out the bug in my original code regarding this.)
That feels like a good place to end this article. I hope the information in here is is useful to you, and was as enjoyable to read as it was to write.
Note 1 - The use of the rmw::grin_ptr is used to keep the unit interdependencies down, as well as to provide automatic cleanup of the held pointer. You can read more about this in another article I wrote: Two-thirds of a pimpl and a grin.
Note 2 - Windows can be declared either on the stack or in the heap in DWinLib. DWinLib does not impose any limitations, but the logic will quite often dictate that the window must be in the heap, as in this instance. The button cannot be stack-based here, as the button requires a valid HWND as a parent during its construction. Making the button stack-based would fail, as the AppWindow does not have a valid HWND before the end of the AppWindow's initializer list, so it is impossible to construct the button in the initializer list.
If, on the other hand, we were working with a window derived from AppWindow, we could place a proper button in the class definition, and not work with a smart pointer. This is because the full AppWindow constructor is guaranteed to be called before the rest of the derived window initializer list is called, so the button can be a 'real' object, and constructed using the existing HWND of the AppWindow in that case.
(I don't often do this, simply out of the habit of keeping the number of #includes in my header files to an absolute minimum.)
Note 3 - You will see that you cannot remove the WinAccelerator unit from the bare DWinLib application without performing major surgery on a few units. It is this unit that takes care of creating the accelerator table that handles the 'Ctrl+N' keystrokes, among others. Part of the magic of the ModalBaseForm unit is that it swaps the existing accelerator table for a table that includes a handler for 'Escape'. You can add your own accelerators by mirroring the following declaration with appropriate identifiers in the constructor of your ModalBaseForm derived units:
gWinApp->accelC.addAccelerator(FVIRTKEY, VK_ESCAPE, (short)cancelCallbackC->id());
When the ModalBaseForm is destroyed, the old accelerator table is restored. "Pretty spiffy", as the old saying goes.
As noted at the end of the article, the accelerator table is only changed for modal dialog windows. Modeless windows will not have their own accelerator.
2/27/06 - Updated code in zip file to fix a bug. Also slightly modified the 'Modeless dialogs' section by outlining the bug and the fix.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 23 Feb 2006 Editor: |
Copyright 2006 by RandomMonkey Everything else Copyright © CodeProject, 1999-2009 Web18 | Advertise on the Code Project |