Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

QuickDialogs - A Library for Creating Dialogs Quickly and Elegantly

0.00/5 (No votes)
23 May 2011 1  
A library for creating simple dialogs declaratively with minimal overhead
QuickDialogs is a framework which aims to provide a much greater level of flexibility to building a dialog while keeping as much of the simple elegant syntax of the MessageBox API call as possible.

An options dialog created with quickdialogs

Introduction

Consider the ubiquitous Windows message box. It's possibly one of the most used and familiar pieces of UI in Windows, in no small part due to how easy it is to create one:

MessageBox(hwnd, "Hello, World", "Message", MB_OK);

Just take a moment to consider how wonderfully simple that is. You can create an entire window in one beautifully clean line of code. The downside becomes apparent after using it for a while - flexibility. With a messagebox, you have one blob of text, an optional icon, and a predefined set of buttons to choose from. And that's it. No edit boxes, no checkboxes, no other feedback. Now, there are quite a number of dialogs which require a little but more than the messagebox offers, however the compelling simplicity has led the messagebox to be used where a custom dialog would be more appropriate (square peg in round hole syndrome). A common example is rephrasing what you are asking so that it fits "Yes", "No", or "Cancel", or the even more horrific "Click OK to do x, or Cancel to do y".

QuickDialogs is a framework which aims to provide a much greater level of flexibility to building a dialog while keeping as much of the simple elegant syntax of the MessageBox API call as possible. The strengths of QuickDialogs are threefold:

  1. It's quick. Producing a simple dialog takes a bit more code than a message box, but much less code than a custom dialog.
  2. It's consistent. Because the dialog code handles all the layout for you, dialog boxes look and feel consistent without any effort.
  3. It's customizable. The library is based on templated classes, and many parts - the base window class, and the layout engine can be swapped out with your own custom classes. You can add your own controls to the dialogs.

It's important to note that it's not a general purpose window framework. It's designed to do one specific task (dialogs) well. If you want a good general window framework, look at WFC, MFC, or Win32++, or any of the many other excellent frameworks out there.

Background

The inspiration for this library came about from a similar library we have at work which does a very similar thing. However, that library is written in pure C and uses varargs macros to accomplish dialog definition and instantiation in a single call. This means that there is no type safety and you have to know the parameters off by heart - crashes are frequent when constructing a dialog. It's also completely undocumented, and the code is not the most clear and concise in the world (I found a char**** in the depths of the code once, which is still the most indirection I've ever seen on a production code pointer).

I've wanted to rework it in my own image for a while, but couldn't think of a way to keep the concise syntax whilst making it readable and type-safe. It wasn't until I started playing with boost and saw some of the remarkable things with operator overloading done by Boost::Format and Boost::Spirit that I came up with the approach presented here.

How It Works

QuickDialogs uses operator overloading to provide a type safe mechanism to insert an arbitrary number of controls onto a dialog. Controls are inserted using the stream insertion (<<) operator. All of the control's properties can be set through constructors, allowing dialogs to be written declaratively in only a couple of lines of code.

Layout is qualitative - rather than manually positioning controls, you just add the controls in, specifying this such as "I'd like the controls on the same line as each other", "centre this control", or "put these controls in a new column". Layout control is provided by the | operator, and by special controls columnbreak and sectionbreak, and alignment control is provided by the + and * unary operators. In order to accomplish this, controls are sized automatically.

Most of the classes are templated in order to handle Unicode flexibly and to provide customisation and extensibility - you can swap out the layout engine, change the base dialog window, and write your own controls without needing to modify the library*.

*Having said that, someone will immediately come up with a perfectly good idea which it doesn't handle. We'll cross that bridge when we come to it.

Using the Code

First, you need to copy the header files into your include path. There are no CPP files required - the library is header only. Then #include <quickdialogs.h> in your project and you're good to go! Well, not quite. You will need to link to user32.lib, gdi32.lib, and comctl32.lib as well.

The best way to show how it works is by giving examples and then analysing them step by step. Let's start with a basic dialog.

dialog d("My first dialog");
d << "Hello, World!"
  << spacer()
  << *button("OK");
d.show();

Which gives us a basic message box:

A simple messagebox lookalike

This is about twice the amount of code that we would need if we used the MessageBox API call, but it's still pretty small. Now let's look at each part of that code in turn:

  • dialog d("My first dialog");. This creates a new dialog with a caption.
  • d << "Hello, World!". This adds a new paragraph (static control) with the text "Hello", World".
  • << spacer(). The adds some vertical space between the last control and the next.
  • << *button("OK");. This creates an OK button. The * operator is an alignment operator (centre). The default alignment is stretched to fit the width. You can specify any text you want for the caption of the button. By default, the return value is equivalent to "OK".
  • d.show();. I'll leave working out the meaning of this one as an exercise for the reader ;).

Now let's try something a bit more functional:

bool check = false;
HICON ico = (HICON)LoadImage(NULL, "ChickenEgg.ico", 
             IMAGE_ICON, 0, 0, LR_LOADFROMFILE);

dialog d("My second dialog");
d << image(ico)
  << columnbreak()
  << spacer(20)
  << "Which came first, the chicken or the egg?"
  << sectionbreak()
  << spacer()
  << ~*(button("&Chicken", qdYes) | button("&Egg", qdNo) | button("&Don't Care", qdCancel))
  << spacer()
  << check_box("Don't ask me pointless questions again", check);
if (d.show() == qdYes)
{ /*...*/ }

This looks like:

Chicken or Egg dialog

This is where the flexibility of QuickDialogs shows through over classic message boxes. We have added custom buttons and a checkbox. Let's look at them in more detail:

  • We've added an image in - loaded from a custom icon file. Anything that can be expressed as a HBITMAP or HICON is allowed. All the standard icons you see in message boxes are available using the LoadIcon function (check MSDN for details).
  • Layout - notice the use of two new controls - columnbreak() and sectionbreak(). These are layout controls. The layout engine in QuickDialogs divides controls into columns and sections. If you want to add a new column, add a columnbreak, and the layout engine will create one in a sensible place. Similarly, if you want to start again on the left hand side below all the current columns, add a sectionbreak. Here, we're using it to line the question up next to the icon, and centre the buttons beneath both of them.
  • We've added a group of buttons, denoted by ~*(button("&Chicken", qdYes) | button("&Egg", qdNo) | ...). The grouping operator (|) is used to create a group of controls that appears on the same line. As before, we have centred the group with the * operator. The ~ operator is a special operator specific to groups - the uniform grouping operator. It makes all the group controls the same width and height (the width/height of the largest controls). This was designed with buttons like this in mind - it gives the buttons a uniform size, as you would expect to see in a dialog box. The buttons are fairly self-explanatory, but note that we specify the return value of the buttons (qdYes/qdNo/qdCancel).
  • The final point of note is the checkbox - check_box("Don't ask me pointless questions again", check). We pass in a bool by reference (check) - this serves two purposes. Firstly, it specifies the initial state of the checkbox (true for checked, false for unchecked). Secondly, it will contain the state of the checkbox when the dialog box returns. This simple "pass by reference" binding is used with all the controls that expect input/provide output.

Finally, something more complex: a tabbed options dialog:

int fontsize = 2; 
std::string autosaveinterval( "10 ");
char* fontsizecaptions[] = {  "Very Small ",  "Small ", 
      "Normal ",  "Large ",  "Very Large " };
bool bold = false, italic = false, underline = false, strikethough = false;
bool superscript = false, subscript = false, smallcaps = false, normal = true;
bool reloadonstartup = false, multiinstance = false, 
     splashscreen = true, lockfiles = false;
bool linenums = false, autosave = true;

dialog d( "Options ");
d  << (tab_control(0) + (tab("Font ")
       << (paragraph( "Font Size: ") | 
             combo_box(fontsize, fontsizecaptions, fontsizecaptions + 5))
                   << (groupbox( "Styles ")  << check_box("Bold", bold)
                         << check_box("Italic", italic)
                         << check_box("Underline", underline)
                         << check_box("Strikethrough", strikethrough)
                         << columnbreak()
                         << radio_button("Normal", normal)
                         << radio_button("Subscript", subscript)
                         << radio_button("Superscript", superscript)))
                + (tab("Other options ")  
                    << check_box("Reload last document at startup", reloadonstartup)
                        << check_box("Allow multiple instances to run", multiinstance)
                        << check_box("Display Splash screen", splashscreen)
                        << check_box("Keep files locked while editing", lockfiles)
                        << check_box("Show line numbers", linenums)
                        << (check_box("Autosave every", autosave) | 
                             edit(autosaveinterval, esNumber, 25) | paragraph("minutes"))))
                        << ~+(button( "&Apply", apply_changes, qdNone) 
                            | button( "&OK", qdOK, true) 
                            | button( "&Cancel", qdCancel));

The result of all this code looks like this:

Options Dialog - First Page Options Dialog - Second page

Whilst it looks more complex than the previous dialogs, most of this is exactly the same as we've seen before. The key new concept is the container controls.

  • There are two container controls used here - group boxes and tab controls. Groupboxes work the same way as dialogs - you can insert controls using the same set of operators. Tab controls are slightly different: a tab control contains only tabs (added with the "+" operator). A tab works as per dialogs and group boxes.
  • We use an event in the Apply button - it calls the apply_changes function to apply any changes to the dialog. You can use function objects as well, or lambdas if you're using C++0x.

All these dialogs are included in quickdialogs.cpp with the project so you can see how it works. I've also included a test dialog which uses every control so you can see an example of how they all work.

Points of Interest

  1. When looking for the best way to link a C++ class instance with a window class instance, I thought a nice straightforward way would be to use the User data that is stored with every window class (i.e., using SetWindowLongPtr(hwnd, GWLP_USERDATA, ptr)). However I found the hard way that, as Raymond Chen points out in his blog The Old New Thing, this data is actually reserved for the window class to use as private storage, and not for the user of the class to do (and the SysLink control makes use of it). A better solution is to use GetProp/SetProp, together with a global string atom to increase the lookup speed. Apparently, this is what .NET uses.
  2. Unicode. Although I have not discussed Unicode, the library is fully Unicode compliant. Each control has a Unicode equivalent (usually the same name preceded by a 'w'). You can use Unicode controls on a non-Unicode dialog, and vice versa. I can't think of why you would ever want to, but it was quite cool to implement.
  3. When wondering how to implement something, look at the .NET Framework. The .NET Framework has been astoundingly useful as a reference on how to do things, in particular how to auto-size controls. In these cases, .NET Reflector is your friend. Or it was, until they removed the free version. Look for ILSpy instead, which is a free, Open Source alternative with very similar functionality.
  4. Themes. Themes are a real pain in the arse to get working properly. It took some time and effort to get the various different button and static controls drawing correctly on themed tab controls. For anyone trying to get the same thing working, look at EnableThemeDialogBackground in the Windows API. Note that it only works on dialog boxes. Windows that have the same window class don't seem to work. You seem to need to put it in WM_INITDIALOG as well - it didn't seem to work anywhere else. You have no idea how annoying this was to fix.

Compiler Compatibility

The code is C++03 compliant (apart from a small lambda expression in the sample code...), but requires TR1. I've got clean, warning free compiles with the following platforms:

  • Microsoft Visual Studio 2010
  • Microsoft Visual Studio 2008 SP1 (SP1 is required for TR1)
  • Intel C++ 2011
  • GCC 4.5.2 (MinGW) with boost TR1 libraries.

I tried getting it to work under CLang using boost, but received a few errors compiling the type traits in boost, and after the monumental effort to convert a clean compile on VS2010 to a clean compile on GCC, I didn't feel like trying to find the right combination of CLang and boost. But it's not far off, so I would imagine that it will "just work" in a few versions' time.

It compiles as 32-bit in all compilers, and 64-bit in VS2010 and VS2008. I haven't tested 64-bit in any of the other compilers.

OS Compatibility

This has been tested on Windows XP, Windows Vista, and Windows 7.

In theory, it will work with Windows 2000, but I haven't tested this. If anyone feels the urge to use an 11 year old unsupported OS, let me know if it works.

Linux lovers, if you've got this far, well done, but this is Windows only. I have no Linux programming experience so you're out of luck, although I've just installed Ubuntu at home, so maybe in the future... The approach would in theory still work, so if anyone implements an equivalent library for their favourite Linux or cross platform widget toolkit, I'd love to hear about it.

Alternatives

  1. The venerable Windows messagebox is quick, simple, and entirely inflexible. But it is a part of every version of Windows and it is simple to use.
  2. In Windows Vista and above, you can use the TaskDialog API. This was introduced to solve much the same problem domain that this library covers. Advantages over QuickDialogs is that it is a standard OS part, and offers progress bars and command link buttons, which QuickDialogs currently doesn't. QuickDialogs offers a wider range of controls, much more flexibility of layout, and can be used on Windows XP as well. Depending on your goals, either is a good choice.

Future Plans

  1. Documentation - this article is currently it. I'd like to write something more substantial. I'm considering writing articles on how to customise the various parts of the framework, so thoughts and suggestions would be welcome.
  2. Resizable dialogs. It would be quite cool if the dialogs resized. Don't think it would be a lot of work, given the whole framework is built around automatic layout.
  3. More controls - Progress bars, ListViews, and the Vista command link controls. Requests for others (e.g., ComboBoxEx, Trackbars, Scrollbox) considered.
  4. TaskDialogs - I'd like to make the functionality a superset of the TaskDialog API, and have the ability to produce dialogs that look similar to those from the TaskDialog API. I haven't decided on an approach to this yet, so any suggestions would be appreciated.
  5. There are a few places where you can specify minimum sizes for different controls - currently, these are all in pixels. I'd like to change this to use dialog units, which will make things resolution independent.
  6. There are still a few minor layout bugs. I'm tracking them down one by one...

History

  • 8th May, 2011: First version!
  • 22nd May, 2011: Fixed issues compiling with UNICODE defined and added "t" typedefs of the controls (thanks to _flix01_). Fixed embarrassing copy-paste error reported by marl.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here