Windows dialog design in C++ without dialog templates.

Windows dialog design in C++ without dialog templates.

 

Image 1

Background,   Introduction
Fundamentals of coded layout design
Building a dialog class
Reducing the verbosity of the layout definition with your own macros
Control methods and notifications
Controlling colors and fonts
Beyond notifications
Summary of event handling and more about aesthetic metrics structs,
Painted controls
A more complex example,   Tabbed dialogs
Display text in multiple languages
Quick reference,  How it works
Points of InterestHistory

Code update 5 June 2015 Language files would cause Link errors with multiple compilation units  - now fixed by changes to autodlg.h only.

Background

The Visual Dialog Editor, class wizard and MFC have been the entry point for many people (including myself) to C++ Windows programming. If all the wizards work correctly it is a comfortable way of starting to write code in an event driven and component supplied environment. However the wisdom of remaining with this as the only way to design and define a dialog is questionable. In particular:

  • Dragging and positioning boxes is still labour, especially if high standards of alignment and presentation are required. A lot of labour if you have to respond to frequently changing aestheitic directives.
  • The visual design requires a laborious assignment of names for control IDs which has to be done manually through the IDE.
  • The visual design requires a lot of code to be written or generated to make it do anything and much of this is not at all programmer friendly. The wizards that compensate for this can break, and may be unavailable or not tuned to the class library you are using..
  • Reusing a Dialog in different applications requires an awkward merging of its dialog template into the host applications .rc file. Code libraries cannot provide dialogs without imposing a requirement to carry out this merging process

I decided that a new approach is needed with the fundamental requirement being that:

  • the entire dialog including layout be defined by code which does not require the assistance of IDE tools to be written nor the presence of IDE generated resources to be executed.

Presented here is a way of achieving this encapsulated as a C++ base class for windows dialogs. It is 'Win 32' in that it requires neither MFC nor ATL/WTL. However it will sit comfortable with either class library and supports incorporating thier control class wrappers in its dialog definitions.

Introduction

The fundamental difference with the approach presented here is that your code does all the work of creating the dialog rather than have Windows create a dialog from a dialog template resource that you then attach code to. There is no need for dialog or control ID's because there is no run-time mapping of code to a dialog template. Instead there is compile time mapping of each control to a C++ variable. Furthermore each of those variables has its own unique data type. Such rich typing is perhaps radical but it brings many benefits and is key to the design. It allows the C++language to resolve many issues at compile time through type resolution and this reduces and simplifies the code that you have to write. In practice this means that for every variable representing a control, say btnCancel, a unique data type will be generated with the same name prefixed by an underscore _btnCancel.As you will see, there are times when you will need to refer to a control by its data type _btnCancel rather than its variable name btnCancel.

The unfamiliarity of coding a layout instead of dragging and dropping it is dealt with in the next section and after that, most things are simpler, cleaner and more concise than you are probabaly used to.

There are some other innovations including:

  • all control variables carry a dynamic text dynamic text buffer as_text through which the controls window text may be read and written, and which persists when the dialog is closed.
  • support for non-windows controls that are simply painted onto the dialog and respond to mouse events. (examples are provided for some cases where this makes more sense).
  • support for imposing aesthetic metrics (button sizes, spacing, colours, control styles etc.) at an application level on all dialogs hosted.
  • replacement of the notion of 'Anchors' with Expand_X and Expand_Y styles for controls that can benefit from being enlarged if the dialog window is enlarged.
  • some modest but effective streamlining of Win 32 programming with controls.

Fundamentals of coded layout design

This is what could be described as the hard bit because it replaces the use of a visual dialog editor. By the time you have your dialog working you will have written less code but the coded layout instructions require a bit more mindfullness than dragging boxes until it looks right. There will be rewards for that mindfullness though and you will not get them if you use visual editing. This is how it works:

Each control is fully defined, declared, and positioned with the following macro:

N.B. The verbose AUTODLG moniker prefixes all macros so they don't clash with anything else in the global namespace. 

C++
AUTODLG_CONTROL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles)

or if you want it to have an accompanying label:

C++
AUTODLG_CONTROL_WITH_LABEL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles,
     label_locator, label_height, label_type,label_style)

The position of each control is determined by the parameters locator_verb, Dx, Dy, width, height and associated labels by the parameters label_locator, label_height. 

The locator_verb can be one of:

  • at - where Dx, and Dy are the absolute position from the top left corner of the dialog.
    Image 2
  • to_right_of<_control> - where Dx, and Dy are offsets from that position
    Image 3
  • under<_control> - where Dx, and Dy are offsets from that position
    Image 4

and the label_locator can be any one of:

  • label_left<by> - where 'by' is the amount by which the label extends to the left of the control.
    Image 5
  • label_left_align<_control> - where the left edge of the label aligns with the left edge of another control.
    Image 6
  • label_above<int by>- where 'by' is the amount by which the label hangs above the control.
    Image 7
  • label_top_align<_control>- where the top edge of the label aligns with the top edge of another control.
    Image 8

Note that when referring to previously declared controls, we refer to them by their data type name _btnActivate rather than the the variable name btnActivate

The data type name can also be used to access the calculated (at compile time) position and size of the control

e.g. _btnActivate::left

toprightbottomwidth, and height are also available. It can be useful to use these with the at verb where to_right_of or under aren't exactly what you want.

For instance; There are no verbs for 'to left of' or 'above'. This is because the size of the newly created control is needed to calculate the position. However you, the programmer, do know the size of the control you are adding and can position a control to the left of another as follows:

C++
AUTODLG_CONTROL( btnOK, at , _btnCancel::left-BWidth-hGap, _btbCancel::top, BWidth, BHeight, ..... 

and to position a control above a previously declared control:

C++
AUTODLG_CONTROL( btnOK, at , _btnCancel::left, _btbCancel::top-BHeight-vGap, BWidth, BHeight, ..... 

The width and height parameters will typically be filled with standard widths and heights but they also may make reference to other controls. Two macros are provided exclusively for these arguments:

  • AUTODLG_WIDTH_TO(x_coord)
  • AUTODLG_HEIGHT_TO(y_coord)

They are useful for instructing a control to use up the available space up to the edge of another control.  

C++
AUTODLG_WIDTH_TO(_btnCancel::right)

The remaining parameters: control_type, control_styles, extended_stylesdescribe the control itself. The control type can be a raw control specified by its windows class name (BUTTON, EDIT, LISTBOX etc.). or it can be a control wrapper class such as the CButton, CEdit and CListBox of MFC and WTL

Building a dialog class

It is a good idea to start with a visual plan. You can hold it in your head, sketch it with pen and paper, use Paint.exe or even a visual dialog editor (any old one will do). I created this with Paint for a dialog that captures an entry code from the user.

Image 9 

There is always a way of coding any layout, indeed many ways, but what you want is a way that maintains its integrity when parameters such as button width change and also that is amenable to being altered. The key to this is to identify which groupings and alignments you care about and the dependancy chains involved. In this example we only have one group but we do care about some alignments as illustrated in the following diagram.

Image 10

We want the Cancel button to line up under the Reset button, the OK button to the right of the Cancel button by a standard spacing and we want the right edge of the edit to line up with the right of the OK button with its label left aligning with the Reset button. The more arbritrary exact position of the left edge of the edit can then be adjusted without affecting anything else.

The dialog class definition takes a metrics struct as a template parameter and uses autodlg::def_metrics as a default. For now you need to know that autodlg::def_metrics defines the following enums

C++
enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };

It is a good idea to code your layout with reference to these because you can tweak these values by passing in a different metrics struct.

We can now go ahead and code the dialog layout with reference to the parameters hGapvGapBWidth and BHeight.

C++
template <class metrics = autodlg::def_metrics>
class EnterCodeDlg : public autodlg::dialog < metrics >
{
   
public:
    AUTODLG_DECLARE_CONTROLS_FOR(EnterCodeDlg)

        AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap,
        BWidth, BHeight, 
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnOK, to_right_of<_btnCancel>, hGap, 0, BWidth, BHeight,
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtCode, under<_btnReset>, BWidth / 2, vGap,
        AUTODLG_WIDTH_TO(_btnOK::right), BHeight,
           EDIT, WS_TABSTOP, 0,
        label_left_align<_btnReset>, BHeight, STATIC, SS_CENTER)

    AUTODLG_END_DECLARE_CONTROLS
  
    AUTODLG_BEGIN_TABLIST
        &btnCancel, &btnReset, &edtCode, &btnOK
    AUTODLG_END_TABLIST

}

It has some resemblance to the text in an .rc file. That is because it is providing the same information. The difference is that this is not text that is read in from a file and parsed at run-time. It is simply code that is compiled. As such it follows the syntactical rules of the C++ compiler and preprocessor that we are familiar with. 

All controls must be declared between the AUTODLG_DECLARE_CONTROLS_FOR and AUTODLG_END_DECLARE_CONTROLS macros and nothing else should appear in this space except the publicprivate and protected keywords which you are free to apply according to your design needs.

Note that btnCancel is declared as under btnReset but with a further y offset (Dy) of BHeight + 2*vGap. This is to leave space for the edit control that will nestle between them. Also edtCode is declared as under btnReset but with a futher x offset (Dx) of half a button width (to leave room for the label), its width is declared as extended to align with right edge of btnOK and its label to align its left edge with the left edge of btnReset.

Finally the slightly circular manner in which we have defined these controls doesn't represent the tab order we want so we specIfy that explicitly using AUTODLG_BEGIN_TABLIST and AUTODLG_END_TABLIST.

N.B Some older compilers may not compile this and will have to use an alternative slightly more clunky way of setting the tab order:

C++
AUTODLG_BEGIN_SET_TABS //alternative for older compilers
   AUTODLG_SET_TAB(btnCancel)
   AUTODLG_SET_TAB(btnReset)
   AUTODLG_SET_TAB(edtCode)
   AUTODLG_SET_TAB(btnOK)
AUTODLG_END_SET_TABS

The following code will display the dialog.

C++
EnterCodeDlg<> dlg;
dlg.DoModal();

dlg has been declared as a EnterCodeDlg<> type with empty braces <>. This means that it will use the default metrics struct with which it was defined. A different metrics struct (but with the same parameter structure) can be forced on it by passing it in as the template parameter. e.g.

C++
EnterCodeDlg<MyAppMetrics> dlg;
dlg.DoModal();

here is the dialog that it displays:

Image 11

At this point the dialog doesn't do anything - just like one freshly created in the visual editor. However quite a bit more work has already been done than is done by the visual editor. Every control is already bound to a named variable, is subclassed so you have accesss to everything that happens to it and is synchronised with a built in text buffer which you can intialise before the control is created and persists after it has been destroyed. You might have also noticed that the variable names (stripped of their btn and edt prefixes) have appeared as the display text of the controls. We will see the benefits of the other work done as we add code to make it fully functional.

Reducing the verbosity of the layout definition with your own macros

The AUTODLG_CONTROL macro is technically optimal in that it captures the required information in the most concise way possible (apart from the verbose macro name) but nine parameters, some of which can be lengthy expressions (your empowerment as a programmer) is not easy on the eye.  The nine parameters give you total flexibility in defining the control but you may not always need all of that flexibility. For instance it may well be the case that all your buttons are the same size and they almost certainly all need the BS_NOTIFY and WS_TABSTOP styles. Rather than enter these same parameters each time, you can define a INHOUSE_BUTTON macro which calls AUTODLG_CONTROL with some parameters ready filled out::

C++
#define INHOUSE_BUTTON( name, locator, Dx, Dy) \
AUTODLG_CONTROL( name, locator, Dx, Dy, BWidth, BHeight,\
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

and then buttons can be defined and declared more concisely, leaving their layout much more readable.

C++
AUTODLG_DECLARE_CONTROLS_FOR(EnterCodeDlg)

        INHOUSE_BUTTON(btnReset, at, hGap, vGap)
        INHOUSE_BUTTON(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap)
        INHOUSE_BUTTON(btnOK, to_right_of<_btnCancel>, hGap, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtCode, under<_btnReset>, BWidth / 2, vGap,
        AUTODLG_WIDTH_TO(_btnOK::right), BHeight,
           EDIT, WS_TABSTOP, 0,
        label_left_align<_btnReset>, BHeight, STATIC, SS_CENTER)

 AUTODLG_END_DECLARE_CONTROLS

I have not included such macros in the library because they are very easy to create according to your in-house requirements and I can't reasonably anticipate what that may be. They may also be dependant on the names you have chosen for your metrics parameters which is beyond the scope of the library. Nor have I defined or used any in the example code that follows because that would hide the direct use of the library macros which need to be the focus of a tutorial introduction.

Control methods and notifications

First of all if the Reset and OK buttons can't do anything if no text has been entered. So lets start with them disabled. 

C++
void OnInitDialog(HWND hWnd)
{
    btnOK.enable(FALSE);
    btnReset.enable(FALSE);
}

OnInitDialog is called after the dialog and all its controls have been created. Exactly as you will be accustomed to.

The controls are disabled using their enable method. This is one of a generic group of methods that can be called on all controls. A full list of generic methods provided for all controls can be found in the quick reference section below.

Now lets deal with the controls as they become enabled starting with the cancel button that is always enabled and always does the same thing. We just create a nofication handler for btnCancelThere is no need to create a message map entry or any code to call this handler. It will be called automatically simply by being there.

C++
void OnNotificationsFrom(_btnCancel*, UINT NotifyCode, LPARAM lParam)
{
     if (BN_CLICKED == NotifyCode)
         EndDialog(IDCANCEL);
}

Note the first argument to OnNotificationsFrom is typed as a pointer to btnCancel's data type _btnCancel*. This is what determines that only btnCancel will call the handler. We aren't interested in the pointer passed in because we know it just points to btnCancel.

EndDialog() does what youi are accustomed to but can be used to end both modal and modeless dialogs so there is no need to code them differently. Typically its argument will be IDOK or IDCANCEL which are always defined. You will not be able to pass control IDs because you don't have any. You can of course pass your own numbers that mean things to you.

Now the response to text being entered into the edit control:

C++
void OnNotificationsFrom(_edtCode*, UINT NotifyCode, LPARAM lParam)
{
        if(EN_CHANGE==NotifyCode)
        {
            btnOK.enable(TRUE);
            btnReset.enable(TRUE);
        }
}

and a response to the now enabled Reset button:

C++
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if(BN_CLICKED==NotifyCode)
        {
            edtCode.as_text = _T("");
            btnOK.enable(FALSE);
            edtCode.set_focus();
        }
}

This shows use of the as_text member supported for all controls. It is a dynamic text buffer that is gauranteed to remain synchronised with the primary display text of the control (as determined by Windows). That is:

  • If it is initialised with text before the control is created then that will be the initial display text of the control.
  • If read, it will always give the display text of the control.
  • If written to, it will update the display text of the control.
  • After the control has been destroyed it will hold the last display text as long as the control has a label. Controls whose text is data of interest usually are accompanied by a seperate label. For controls that carry no label (e.g. buttons), the as_text buffer will remain empty unless you explicitly use it.

This works as a replacement for the traditional do data exchange mechanism. More details of what you can do with auto_string the type of  as_text can be found in the Quick reference section.

and finally the OK button

C++
void OnNotificationsFrom(_btnOK*, UINT NotifyCode, LPARAM lParam)
{
     if (BN_CLICKED == NotifyCode)
            EndDialog(IDOK);
}

The dialog is now ready for use as follows:

C++
EnterCodeDlg<> dlg;
if (IDOK == dlg.DoModal())
{
     TCHAR* szCode=dlg.edtCode.as_text;
}

The code that was entered is found in dlg.edtCode.as_text even though the control itself has been destroyed.

Controlling colors and fonts

We now have a dialog that allows you to enter a code, that is any code. Now lets make the OK button reject codes that don't comply with a criteria and produce a suitable visual response. First we need to add a boolean to the class definition to flag the error condition.

C++
bool bErrorInCode;

and initialise it in the constructor:

C++
EnterCodeDlg()
{
      bErrorInCode = false;
}

and now we can change the code handling the OK button

C++
void OnNotificationsFrom(_btnOK*, UINT NotifyCode, LPARAM lParam)
{
        if (BN_CLICKED == NotifyCode)
        {
            if (_tcsstr(edtCode.as_text, _T("1234")) == edtCode.as_text)
            {
                EndDialog(IDOK);
            }
            else
            {
                btnOK.enable(FALSE);
                bErrorInCode = true; 
                edtCode.set_focus();
                edtCode.invalidate();
            }
        }
}

the edtCode.invalidate(); is needed because we are going to change how edtCode is displayed when it holds a non-conforming code. And this is how we do it:

C++
LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
{
        if (bErrorInCode)
        {
            ::SetTextColor(hDC, RGB(255, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            SelectFont(hDC, GetStockObject(SYSTEM_FONT));
            return (LRESULT)GetStockBrush(LTGRAY_BRUSH);
        }
        return NULL;
}

To complete the new design we also need to make some changes to the behaviour of edtCode and btnReset:

C++
void OnNotificationsFrom(_edtCode*, UINT NotifyCode, LPARAM lParam)
{
        if(EN_CHANGE==NotifyCode)
        {
            if (false == bErrorInCode)
                btnOK.enable(TRUE);
            btnReset.enable(TRUE);
            if (wcslen(edtCode.as_text) < 1)
            {
                bErrorInCode = false;
                edtCode.invalidate();
            }
        }
}
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if (BN_CLICKED == NotifyCode)
        {
            edtCode.as_text = _T("");
            btnOK.enable(FALSE);
            edtCode.set_focus();
            bErrorInCode = false;
            edtCode.invalidate();
        }
}

Here is how the dialog looks when you enter a non conforming code:

Image 12

Beyond notifications

Controls are designed to notify a dialog of specific events that you are likely to want to respond to and they do this through notifications that we can conveniently hande. Sometimes our need to know or need to intefere goes beyond what was anticipated in the design of the control but can be satisfied by intercepting raw Windows messages as they arrive at the control. Here I will contrive an example with this requirement and show how easily it can be done.  

On some web pages, the edit field in which you are supposed enter text will be initialised by a geyed out prompt (e.g. "Enter text here") but when you click on it, the prompt dissappears and you are entering text into an empty edit field. I am not saying I like this effect but it is something you could be asked to do.

We already know how to get it to display the initial greyed out text: 

add another boolean to indicate if the edit has been touched

C++
bool bErrorInCode;
bool bEditTouched;

initialise it and set the initial text for edtCode

C++
EnterCodeDlg()
{
      bErrorInCode = false;
      bEditTouched = false;
      edtCode.as_text = _T("Enter code here");
}

and add further conditional code to edtCode`s ctlcolor handler

C++
LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
{
        if (bErrorInCode)
        {
            ::SetTextColor(hDC, RGB(255, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            SelectFont(hDC, GetStockObject(SYSTEM_FONT));
            return (LRESULT)GetStockBrush(LTGRAY_BRUSH);
        }
        if (false==bEditTouched)
        {
            ::SetTextColor(hDC, RGB(0, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            return (LRESULT)GetStockBrush(WHITE_BRUSH);
        }
        return NULL;
}

The problem we have is that edtCode is an EDIT and doesn't issue any notification if it is clicked on. It isn't something that you would normally want from it. However everything recieves WM_LBUTTONDOWN and WM_LBUTTONUP messages when clicked on. So we handle messages arriving at edtCode to get our click event.

C++
LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        if (WM_LBUTTONUP==message)
        {
            if (false == bEditTouched)
            {
                bEditTouched = true;
                edtCode.as_text = _T("");
                btnOK.enable(FALSE);
            }
        }
        return 0;
}

The zero retrurn allows the WM_LBUTTONUP message to fall through and do what it would have done anyway. In this case we don't want to interfere with that message, we just want to know about it. 

All that is needed now is to change btnReset`s BN_CLICKED handler to reset the new prompt

C++
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if(BN_CLICKED==NotifyCode)
        {
            edtCode.as_text = _T("Click here to enter code");
            btnOK.enable(FALSE);
            bErrorInCode = false;
            bEditTouched = false;
            edtCode.invalidate();
        }
}

Summary of event handling and more about aesthetic metrics structs

So far we have seen three event handlers that can be defined for any individual control:

C++
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)

LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

Each of these also has a version that will handle events from all controls of a particular type (such as BUTTON or EDIT) rather than individual control.

C++
void OnNotificationsByControlType(BUTTON* pC, UINT NotifyCode, LPARAM lParam)
//You may want to detect if any button has been pressed

LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)
//You may want to set the font or background for all edits

LRESULT OnMesageByControlType(EDIT* pC, UINT message, WPARAM wParam, LPARAM lParam)
//You may want to detect if any edit has been clicked on

There is also a similar pair for handling the owner draw family of messages

C++
LRESULT OnItemMsgFrom(_edtCode*, DRAWITEMSTRUCT* pInfo, bool bMouseOver)

LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

With these, the type of pInfo determines the message being handled. For instance DRAWITEMSTRUCT* pInfo determines that it handles WM_DRAWITEM and  MEASUREITEMSTRUCT* pInfo determines that it handles WM_MEASUREITEM

Finally there is a handler for messages arriving at the dialog

C++
LRESULT OnDialogMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

Before describing the calling sequence for these handlers we need to know more about aesthetic metrics structs

Here is the definition of the def_metrics struct. This is the minimum that a metrics struct can be.

C++
struct def_metrics
{
    AUTODLG_METRICS_DEF_HANDLERS

    enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };
    static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        return 0;
    }
};

Metrics structs can be passed in as template parameters to dialog declarations and are designed to provide centralised control of aesthetic aspects of the dialogs hosted by an application. As well as the sizing and spacing parameters that we have already used it can also be used to impose styles on controls, control their colours and fonts and also to handle owner drawing.

You will see that it already has an OnCtlColor handler set to do nothing and be ignored. In addition we can add handlers for the following to a metrics struct and they will be called.

C++
LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver) 

LRESULT OnItemMsgByControlType(BUTTON* pC, DRAWITEMSTRUCT* pInfo, bool bMouseOver)

Now we can look at the calling sequences that each type of handler follows. They are not all the same.

When a control issues a notification, all notifications defined to handle it will be called. First it will call any OnNotificationsByControlType handler that matches its control type then it will call any OnNotificationsFrom handlers for the particular control. The control type handler gets the first shout and the specific control handler gets the last word. Control notifications can only be handled by the dialog that contains them.

When a control sends a CtlColor message the handling requirements are differernt. CtlColor handlers may change the font , text  colour,and the background brush but they typically only want to control one or two of these and leave others as they were. For this reason CtlColor handling is built up layer by layer. First it is passed to Windows to get a default brush, font and text colours. Then it calls any OnCtlColorByControlType handler provided by the metrics struct passed in. If there isn't a matching control type handler then it will call the bare OnCtlColor handler of the metrics struct. Next it calls any matching OnCtlColorByControlType handler provided by the dialog and finally any matching OnCtlColorFrom handler in the dialog. On each call, it will adopt the brush returned if it is non zero, otherwise it will keep the brush it has. Operations on the device context passed in are effective regardless of whether a non zero brush is returned.

When a control sends a DrawItem message or any other of the Item message family only the most specific handler will be called and any others will be ignored. The search for a handler follows the following sequence and finishes as soon as a handler return non-zero: First a OnItemMsgFrom in the dialog, then a OnItemMsgByControlType in the dialog and finally a OnItemMsgByControlType in the metrics struct. In the case of a button control, a default handler is provided for owner draw if you don't provide one. It draws plain 3D edged buttons that will respond to CtlColor - standard buttons don't.

If you intercept messages arriving at a control with a OnMessageAt or OnMessageByControlType handler in your dialog then you are working at a lower level with the power to intervene impolitely. These handlers are called first. First of all OnMessageByControlType and then OnMessageAt. Each one has the power to return a non-zero and not allow any other handlers, including windows default handlers, to be called. With these handlers you have the first shout and can insist on the last word. 

If you intercept messages arriving at the dialog with OnDialogMessage you also have the first shout and can insist on the last word. 

To summarise:

  • All notification handlers will be called.
  • CtlColor handlers are all called starting with windows defaults and finishing with the most specific.
  • Only the most specific DrawItem handler is called.
  • CtlColor handlers and DrawItem handlers defined in the metrics struct will also be called.
  • Raw window message handlers have the power to terminate message handling.

In our example we added an OnCtlColorFrom handler for the edit control. This was to give a dynamic visual response to the user. That is to say its purpose is functional rather than aesthetic. Anything of an aestheitic nature is better handled by the metrics struct passed in.

Here is a more developed metrics struct that we will use for the examples that follow;

C++
struct my_app_metrics 
       : public autodlg::styles //This gives access to special autodlg styles
{
    AUTODLG_METRICS_DEF_HANDLERS
    enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };

    AUTODLG_IMPOSE_STYLE_FOR(EDIT, WS_BORDER, 0) //#1
    AUTODLG_IMPOSE_STYLE_FOR(BUTTON, BS_OWNERDRAW | MOUSE_OVER_STYLE, 0) //#2

    static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        if (WM_CTLCOLORLISTBOX == nCtlColor) //#3
        {
            ::SetTextColor(hDC, RGB(255, 255, 255));
            ::SetBkMode(hDC, TRANSPARENT);
            return GetStockBrush(DKGRAY_BRUSH);
        }
        if (WM_CTLCOLORBTN == nCtlColor) //#4
        {
            ::SetTextColor(hDC, RGB(0, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            if (bMouseOver)
                return GetStockBrush(WHITE_BRUSH);
            return 0;// GetStockBrush(LTGRAY_BRUSH);
        }
        return NULL;
    }
    //#5
    static LRESULT OnCtlColorByControlType(BannerLabel* pCtrl, UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        ::SetTextColor(hDC, RGB(255, 255, 255));
        ::SetBkMode(hDC, TRANSPARENT);
        SelectFont(hDC, GetStockObject(SYSTEM_FONT));
        return (LRESULT)GetStockBrush(GRAY_BRUSH);
    }
    static LRESULT OnCtlColorByControlType(STATIC_BANNER* pCtrl, UINT nCtlColor, HDC hDC, 
            bool bMouseOver)
    {
        ::SetTextColor(hDC, RGB(255, 255, 255));
        ::SetBkMode(hDC, TRANSPARENT);
        SelectFont(hDC, GetStockObject(SYSTEM_FONT));
        return (LRESULT)GetStockBrush(GRAY_BRUSH);
    }
};

Here we decide at an application level, sub-application level or whatever you like, that: 

  1. All edits have a border
  2. All buttons have owner draw and detect mouse over styles - no OwnerDraw handler is supplied so the default CtlColor sensitive button is drawn.
  3. Listboxes have white text and a dark backround
  4. Buttons are painted with a white brush when the mouse moves over them
  5. The specific control types BannerLabel and STATIC_BANNER have white text and a dark backround

The prefered way to handle CtlColor in the metrics struct  is by switching on the nCtlColor parameter in its general OnCtlColor handler as is done  with LISTBOX and BUTTON. This puts it there for all to see including derived controls that want to adopt the colours of specific control types. However this method of handling is limited in discrimination to the control types enumerated by nCtlColor. It is when you want a handler for a very specific control type that you need to define OnCtlColorByControlType as with BannerLabel and STATIC_BANNER which are control types invented here (see below) and not part of the nCtlColor enumeration.

If yu wish to work with a different set of names for your metrics parameters the you should register them in autodlg_metrics_config.h

Painted controls

I have always felt a bit uncomfortable about creating a STATIC control window to sit on a dialog and give an impression of a label painted straight onto the dialog. Why not just paint a label straight onto the dialog?

For various reasons I've been doing this for years. Labels are easy, buttons aren't too much trouble and I have done nice list boxes. Not edit boxes though, at that point the edit control window is packed with functionality that you don't want to replicate and as it is already sitting there as part of Windows, the overhead of creating a window is more than justified. Nevertheless a typical dialog is plastered with many control windows that could be just painted onto the dialog and in some cases it would make more sense.

Such painted (windowless) controls are supported and it is relatively easy to define them. Here is the all of the code for a PaintedLabel control that can be used to display labels instead of using STATIC controls. It doesn't do everything a STATIC control can do but it does exactly the same job of painting labels. 

C++
class PaintedLabel : public autodlg::painted_control
{
protected:
    bool OnPaint(HDC& hDC, RECT& r, bool bMouseOver)
    {
        if ((style & WS_VISIBLE) == 0)
            return false;
        
        HBRUSH hBrush = GetCtlColor(WM_CTLCOLORSTATIC, hDC);
        
        if (hBrush)
            ::FillRect(hDC, &r, hBrush);

        if (style & WS_DISABLED)
        {
            RECT rr = r;
            int i = 1;
            ::SetTextColor(hDC, RGB(240, 240, 240));
            ::DrawText(hDC, as_text, wcslen(as_text), &rr, style);
            rr.top += i; rr.left += i;
            rr.right += i; rr.bottom += i;
            ::SetTextColor(hDC, RGB(180, 180, 180));
            ::DrawText(hDC, as_text, wcslen(as_text), &rr, style);
        }
        else
            ::DrawText(hDC, as_text, _tcslen(as_text), &r, style);
        return true;
    }
};

This PaintedLabel control will be used instead of STATIC controls in the examples that follow. Also used is a class derived from PaintedLabel:

C++
class BannerLabel : public PaintedLabel
{};

BannerLabel is distinguished from PaintedLabel only by having a different type name and this is simply so that it can be identified for special handling by the OnCtlColorByControlType(BannerLabel*, ...) handler as defined in the my_app_metrics struct defined earlier. If you decide to remain with STATIC controls for labels then you can use STATIC_BANNER and that also will recieve the same special handling from the OnCtlColorByControlType(STATIC_BANNER*, ...) handler provided in my_app_metrics

Other painted controls included in the library are SpacerControl which is a  hidden reference frame on which other controls can be hung (it has no code), GroupBox which is a replacement for the Windows group box and PaintedTabFrame which you have to use instead of a Windows tab control. The code for these can be found at the foot of autodlg.h. Further anecdotal examples StopButtonStartupButton and PaintedSmiler can be found in misc_painted_controls.h

If a painted control only displays and needs no mouse interaction then it should be derived from painted_control and needs only to implement bool OnPaint(HDC& hDC, RECT& r, bool bMouseOver) to handle paint messages. These are the only messages it will recieve unless you send some to it using do_msg. The RECT r passed in is in dialog coordiates because the hDC is that of the dialog.

If mouse interaction is required then it must derive from painted_mouse_control and will also have to provide the handler void OnNonPaintMessage(UINT message, WPARAM wParam, LPARAM lParam). This will recieve mouse messages which can be decoded using the mouse_parms struct. The x,y coordinates in this case are with respect to your control, not the dialog. 

C++
mouse_parms mouse(wParam, lParam);
mouse.x; //coordinates with respect to the control
mouse.y;
mouse.vKeys;

It will also recieve keyboard messages when it has the keyboard focus and any messages you may choose to send using do_msg.

For most painted controls, implementing those two handlers is enough. However there are more resources available in the design of painted controls with more complexity such as PaintedTabFrame:

You may also implement void OnControlCreated() which will be called after the control has been created and OnDialogCreated() which will be called after all controls on the dialog have been created. PaintedTabFrame needs the latter to adjust to its content which may expand as it is created.

You also have access to and can override the generic methods provided for all controls. For instance PaintedTabFrame  overrides invalidatesizemove and show.

That is all there is to know about designing painted controls as far as library support is concerned. To get an idea of how to go about it, study the source code of the examples listed above.

A more complex example 

Here is a more complex dialog that gives more of a feel for what can be done:

Image 13

You enter your access code, select a day of the week and a time of day, view the report and then launch an imaginary process in either a modal or modeless dialog. This section will show all of its code.

N.B. Comments are used to clarify the working of the control and bold is used here to highlight anything introduced here for the first time and explained in more detail.

The dialog definition and layout 

C++
template <class metrics = autodlg::def_metrics>
class SelectDataDlg
    : public autodlg::dialog
        < metrics, autodlg::auto_size, WS_OVERLAPPEDWINDOW > //#1
{
public:
    auto_string sTimePeriod; //#2
    LaunchDlg<def_sizes3> dlgLaunch; //used for modeless dialog

    AUTODLG_DECLARE_CONTROLS_FOR(SelectDataDlg)

        AUTODLG_CONTROL_WITH_LABEL(dlgEnter_code, at, hGap, BHeight+vGap,
        BWidth * 2 + hGap * 3, BHeight * 2 + vGap * 4,
        EnterCodeDlg<metrics>, WS_TABSTOP, 0,
        label_above<BHeight>, BHeight, BannerLabel, SS_LEFT | SS_CENTER) //#3

        AUTODLG_CONTROL_WITH_LABEL(edtUser, to_right_of<_dlgEnter_code>,
        hGap, 0, BWidth, BHeight,
        PaintedLabel, SS_CENTER , 0,
        label_above<BHeight>, BHeight, PaintedLabel, SS_CENTER) //#4

        AUTODLG_CONTROL(spacer1, to_right_of<_edtUser>, 0, 0,
        hGap, BHeight,
        SpacerControl, EXPAND_X_STYLE, 0) //#5

        AUTODLG_CONTROL_WITH_LABEL(lbSelect_day_of_week,
        to_right_of<_spacer1>, hGap * 2, 0,
        BWidth * 3 / 2 + hGap,
        AUTODLG_HEIGHT_TO(this_dlg_type::_dlgEnter_code::bottom),
        LISTBOX, LBS_NOTIFY | WS_TABSTOP, 0,
        label_above<BHeight>, BHeight, BannerLabel, SS_CENTER)

        AUTODLG_CONTROL(lblDay_of_week, under<_dlgEnter_code>, 0, vGap,
        AUTODLG_WIDTH_TO(_lbSelect_day_of_week::right), BHeight,
        BannerLabel, SS_CENTER | EXPAND_X_STYLE, 0) //#6

        AUTODLG_CONTROL(btnMorning, under<_lblDay_of_week>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, WS_GROUP | BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnAfternoon, under<_btnMorning>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnEvening, under<_btnAfternoon>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtReport, to_right_of<_btnMorning>,
        BWidth * 2 / 3, 0,
        AUTODLG_WIDTH_TO(_lbSelect_day_of_week::left),
        AUTODLG_HEIGHT_TO(_btnEvening::bottom),
        EDIT, WS_GROUP | WS_TABSTOP | ES_READONLY | EXPAND_X_STYLE
            | EXPAND_Y_STYLE, 0,
        label_left<BWidth * 2 / 3>, BHeight, PaintedLabel, SS_CENTER) //#7

        AUTODLG_CONTROL(btnModal, to_right_of<_edtReport>,
        BWidth/2, vGap, BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnModeless, under<_btnModal>, 0, vGap,
        BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(groupProcess_control, at,
        _btnModal::left - hGap, _btnModal::top - BHeight,
        AUTODLG_WIDTH_TO(_btnModal::right + hGap),
        AUTODLG_HEIGHT_TO(_btnModeless::bottom + vGap),
        GroupBox, 0, 0) //#8

    AUTODLG_END_DECLARE_CONTROLS

    AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT
        OKToLaunch //significant event that parent may want to know about //#9
    AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT
    .....
}

1. In this case the class definition passes three template parameters into the autodlg::dialog base class. This is the full list of template parameters it can take:

  • metrics struct - default: autodlg::def_metrics, you may pass in any conforming metrics struct
  • initial size policy - default: autodlg::auto_size (sizes to accomodate the layout), alternately you may pass in autodlg::explict_size<Width, Height> in you want to fix the initial size explicitly
  • window style - default: WS_POPUPWINDOW | WS_CAPTIONIn this case WS_OVERLAPPEDWINDOW has been passed in instead to make the dialog resizable by the user.

2. A member variable is declared of type autodlg::auto_string. We just need a string to store some text. You would normally use the CString of MFC or WTL or std::string but I didn't want any other library dependancies in the example so I used the very basic dynamic string that comes with this library.

3. The first control is of type EnterCodeDlg<metrics>. That is, it is the dialog that we defined earlier. The dialog will display at the size specified in the layout or its own initial size, whichever is greater. This means you don't have to be exactly precise with the size you specify in the layout. Although this means that you could specify a zero size and it will still display, that would not allow you to place any controls to the right or underneath it in your layout. You should specify a layout size that is somewhere near the dialogs own initial display size and use that as a basis for how many controls can fit to its right or under it. The first control also has a label of type BannerLabel, the variant of PaintedLabel mentioned in the section on Painted Controls.

4. The second control is called edtUser because it originally was an edit with a label but the edit is never edited so it might as well be a label as well. So there you have it; a label (displaying a value) with an accompanying label. 

5. The third control is a SpacerControl. This is invisible and has two functions: a reference for other controls and it can be made expandable so controls to the right or below are pushed to the right or down. It is used here to push lbSelect_day_of_week to the right when the dialog expands. 

6. The long banner label lblDay_of_week that runs right across the dialog is given the EXPAND_X_STYLE. This means it will expand horizontally as the dialog is resized so it continues to run right across the dialog. 

7. The multiline edit edtReport is given EXPAND_X_STYLE | EXPAND_Y_STYLE so that it can make the most of the dialog being resized to a larger size.

8. The last control is of type GroupBox. This is a PaintedControl that performs the same function as a windows Group Box. If you prefer to stay with the windows Group Box then the type is GROUP_BOX.

9. Sandwiched between the AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT and AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT macros is a notify code  OKToLaunch that this dialog can use to notify its parent (should it have one) of an important event.

OnInitDialog

C++
void OnInitDialog(HWND hWnd)
{
        //Disable all controls in this dialog except dlgEnter_code
        control_cursor cursor;
        if (cursor.set_first_ctrl(*this))
        do
        {
            if (cursor.get_control() != &dlgEnter_code 
                    && cursor.get_control() != &dlgEnter_code_label)
                cursor.get_control()->enable(FALSE);
        } while (cursor.move_to_next_ctrl());  //#1

        //Populate list box
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Monday")); //#2
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Tuesday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Wednesday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Thursday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Friday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Saturday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Sunday"));
        
        //A child dialog's Cancel button won't do anything so hide it
        dlgEnter_code().btnCancel.show(SW_HIDE); //#3
        
        edtUser.as_text = _T("---");
 }

1. In OnInitDialog, a control_cursor is created to enumerate all of the controls on the dialog. 

2. The listbox is populated using do_msg which is no more than a wrapper for  SendMessage but relieves you from having write ugly casts or fill out parameters you are not using. It is only a small detail but it can make Win 32 programming almost as readable as using the methods of control class wrappers.

3. If you remember, the OK and Cancel button of the EnterCodeDlg call EndDialog to close the dialog. Well, if the dialog is control within another dialog (it has the WS_CHILD style), then EndDialog will not close the dialog but instead will send a notification to its parent with the code pased to EndDialog  As the only purpose of Cancel is to close the dialog and that won't be allowed to happen, there is really no point in ity being there. So we hide it.

Note that following the control variable with brackets dlg_Enter_code() is necessary to access its members as a dialog.

OnNotificationsFrom(_dlgEnter_code*, ...

C++
void OnNotificationsFrom(_dlgEnter_code*, 
        UINT NotifyCode, LPARAM lParam)  //#1
    {
        if(IDOK==NotifyCode)
        {
            //User code is entry code without the secret first 4 digits
            edtUser.as_text = dlgEnter_code().edtCode.as_text.from(4);
            
            //Obscure first 4 digits showing in edit because dlgEnter_code
            //will not close and will still be visible
            dlgEnter_code().edtCode.as_text.overwrite(0, _T("****"));
            
            //Enable all controls in this dialog
            control_cursor cursor;
            if (cursor.set_first_ctrl(*this))
            do{
                cursor.get_control()->enable(TRUE);
            } while (cursor.move_to_next_ctrl());  //#2

            //Disable all controls in dlgEnter_code
            if (cursor.set_first_ctrl(dlgEnter_code()))
            do{
                cursor.get_control()->enable(FALSE);
            } while (cursor.move_to_next_ctrl());  //#3
            
            //These two should not be available until day and time period are set
            btnModal.enable(FALSE);
            btnModeless.enable(FALSE);
            
            UpdateReport();
            lbSelect_day_of_week.set_focus();
        }
    }

1. This is the handler for any notification from the embedded EnterCodeDlg. No parent notifications were designed into EnterCodeDlg but because it is embedded as a child, the EndDialog calls in the OK and Cancel buttons will send notifications instead of closing the dialog. The IDCANCEL notification will never arrive because we hid the Cancel button but the IDOK notification interests us because it tells us a conforming code has been sucessfully entered.

2. With a correct code now entered, a control_cursor is created to enumerate the controls of the host dialog and enable them.

3. The same control_cursor is set to EnterCodeDlg to enumerate its controls and disable them

OnNotificationsFrom(_lbSelect_day_of_week*, ...

C++
void OnNotificationsFrom(_lbSelect_day_of_week*, 
        UINT NotifyCode, LPARAM lParam)
    {
        if(LBN_SELCHANGE==NotifyCode)
        {
            int iSel = lbSelect_day_of_week.do_msg(LB_GETCURSEL);
            if (iSel > -1)
            {
                //read selected text straight into lblDay_of_week
                lbSelect_day_of_week.do_msg(LB_GETTEXT, //action
                    iSel,                                //selection
                    //buffer
                    lblDay_of_week.as_text.get_buf_set_length(
                        lbSelect_day_of_week.do_msg(LB_GETTEXTLEN, iSel))//buffer len //#1
                    );
            }
            UpdateReport();
        }
    }

1. When someone clicks on a day of the week in the listbox we simply want it to display in lblDay_of_week, the banner label that stretches across the screen.

The section marked in bold will not be completely unfamiliar to many programmers:

The LB_GETTEXTLEN message is sent first and the result is used to initialise the buffer of as_text to the required length using get_buf_set_length which returns a buffer which is passed with LB_GETTEXT to be filled. 

What will be a suprise to the same programmers is that the control that  lblDay_of_week represents will update automatically to display the new text in its as_text member. This is due to some magic in get_buf_set_length that will be expained later.

The rest of the code

C++
void OnNotificationsFrom(_btnMorning*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("morning");
            UpdateReport();
        }
    }
    void OnNotificationsFrom(_btnAfternoon*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("afternoon");
            UpdateReport();
        }
    }
    void OnNotificationsFrom(_btnEvening*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("evening");
            UpdateReport();
        }
    }
    
    void OnNotificationsFrom(_btnModal*, UINT NotifyCode, LPARAM lParam)
    {
        if(BN_CLICKED==NotifyCode)
        {
            LaunchDlg<def_sizes3> dlg;
            dlg.DoModal();
        }
    }
    void OnNotificationsFrom(_btnModeless*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            dlgLaunch.Create();
        }
    }
    void UpdateReport()
    {
        //Update edtReport with day and time period
        //Return early if either is not set
        if (lbSelect_day_of_week.do_msg(LB_GETCURSEL) < 0)
        {
            edtReport.as_text = _T("SELECT DAY AND TIME PERIOD");
            return;
        }
        edtReport.as_text = lblDay_of_week.as_text;
        edtReport.as_text.append(_T(" - "));
        if (sTimePeriod)
            edtReport.as_text.append(sTimePeriod);
        else
        {
            edtReport.as_text.append(_T("SELECT TIME PERIOD"));
            return;
        }

        //We only want this called once so use disabled state of 
        //btnModal as a condition
        if (btnModal.get_style() & WS_DISABLED)
        {
            //dialog may be used as a child and this is a significant event
            NotifyParent(notify::OKToLaunch, 0);  //#1
            //ready to launch
            btnModal.enable(TRUE);//this block won't get called again
            btnModeless.enable(TRUE);
        }
    }

1. UpdateReport is called whenever the day of week of time of day selection changes. It updates what is displayed in edtReport and also detrmines if btnModal and btnModeless should be enabled. It only executes the code to enable them once and when it does it also calls NotifyParent(notify::OKToLaunch, 0);

This is not part of the functioning of this dialog. It is there in polite anticipation of it being embedded within another dialog. It is the event the host dialog most likely might need.

The dialog launched by btnModal and btnModeless uses a variety of painted controls as well as a windows progress control:

Image 14

The code for the dialog can be found in example_dialogs.h and the code for the painted controls in misc_painted_controls.h. In this case, painted controls were the most straightforword way of getting the behaviour I wanted. The Go button has to be pressed in a very deliberate way, the Stop button goes off as soon as you touch it and the smiley face has its own special behaviour.

Tabbed dialogs

One thing you won't be able to do with these dialogs is use them in standard windows tab controls. This is because standard windows tab controls use dialog templates and these dialogs don't have any. Therefore a windowless PaintedTabFrame control is provided. It is called PaintedTabFrame because it is not a window that owns the tab content. It simply frames the tab content and controls which is visible. The content is directly owned by the host dialog.  Its use is as follows:

C++
template <class metrics = def_sizes>
class TabbedDlg : public autodlg::dialog 
    < metrics, autodlg::auto_size, WS_OVERLAPPEDWINDOW >
{
    AUTODLG_DECLARE_CONTROLS_FOR(TabbedDlg)
private:
        AUTODLG_CONTROL_WITH_LABEL(tabSelect, at, 6, hGap + BHeight,
        350, 200,
        PaintedTabFrame<5>, EXPAND_X_STYLE | EXPAND_Y_STYLE, NO_STYLE,
        label_above<BHeight>, BHeight, BannerLabel, SS_CENTER)
public:
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgEnter_code, EnterCodeDlg<metrics>, WS_TABSTOP, 0)
        
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgSelectDataDlg, SelectDataDlg<metrics>, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgLaunch, LaunchDlg<metrics>, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, ctrlCalendar, SysMonthCal32, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, edtRichEdit, RICHEDIT, 
                        WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN, 0)
    AUTODLG_END_DECLARE_CONTROLS

}

The PaintedTabFrame control takes 5 as a template parameter to indicate that ther will be 5 tabs and the next five controls declared must be those tabs, each declared using AUTODLG_TAB_BAR_MEMBER. Here is it showing the most populous tab:

Image 15

In this case the host dialog TabbedDlg is dedicated to displaying tabSelect (the PaintedTabFrame) and its contents because that is what you usually want, but it doesn't have to be. tabSelect could equally be one among many controls on the same dialog. Unlike window tab controls, all of the tabs are created together as members of the host dialog, rather than created as each tab is selected. As we will see, this greatly facilitates any coding the host dialog may require.

As it stands the tab frame allows you to tab wherever you like quite (usually just what you want) regardless of whether you have entered a valid code or selected a day and time period. So lets start to tie things down so you have to follow some sort of proper procedure starting with OnInitDialog:

C++
void OnInitDialog(HWND hWnd)
{
        tabSelect.lock(true); //#1
        dlgEnter_code().btnCancel.show(false); //#2
        tabSelect.set_tab_text(4, _T("Rich edit field")); //#3
}
  1. By default it will select the first tab which is our EnterCodeDlg. We don't want other tabs selected until a valid code has been enterered so we lock the tab frame. This disables all other tabs so they cannot be selected.
  2. As in our SelectDataDlg, the Cancel button of EnterCodeDlg will not do anything so we hide it. Note the brackets postfix dlgEnterCode(), required to access members of the embedded dialog.
  3. The text displayed on the tab buttons is by default the window text of the dialog or control they display. For the most part this gives us what we want but in the case of the Rich Edit control this will cause its tab button text to attempt to display its content. So we explicitly set the tab text for that tab.

When a code has successfuly been entered dlgEnterCode will send a notification with a code of IDOK, so we handle it.

C++
void OnNotificationsFrom(_dlgEnter_code*, UINT NotifyCode, LPARAM lParam)
{
        if (IDOK == NotifyCode)
        {
            dlgSelectData().dlgEnter_code().edtCode.as_text = dlgEnter_code().edtCode.as_text; //#1
            tabSelect.select_tab(1);  //#2
            dlgEnter_code.enable(false);  //#3
            dlgSelectData().dlgEnter_code().btnOK.notify(BN_CLICKED);  //#4
        }
}

Having got a correct code we now want to display the next tab, dlgSelectData, but that in turn displays its own dlgEnterCode. We don't want the user to have to fill out this one too, so:

  1. Copy the code from dlgEnterCode (the first tab) into the dlgEnterCode in dlgSelectData. Note the requirement for empty brackets ().  to access the members of each embedded dialog.
  2. Select the next tab. The lock does not prevent programatic selection, just selection by the user. The user will now find it locked on the new tab.
  3. Disable the first tab. It is no longer visible and this prevents it being seen again.
  4. Fire the BN_CLICKED notification of the OK button in the dlgEnterCode within dlgSelectData. Note again the need for empty brackets ().  to access the members of embedded dialogs at each level.

If you remember we provided SelectDataDlg with a OKToLaunch notification in anticipation of it finding itself embeded in another dialog, as it is here. So we handle it here and unlock the tab frame. This will give access to all the tabs except the first one dlgEnterCode which remains disabled.

C++
void OnNotificationsFrom(_dlgSelectData*, UINT NotifyCode, LPARAM lParam)
{
        if (_dlgSelectData::notify::OKToLaunch == NotifyCode)
            tabSelect.lock(false);
}

Note that the controls type name _dlgSelectData and the qualifier ::notify are required to access its custom notifications.

Finally the LaunchDlg politely issues a ProcessStarted notification when it begins processing and a ProcessStopped notification when it is stopped. If we want, we can use them to lock the tab frame during processing.

C++
 void OnNotificationsFrom(_dlgLaunch*, UINT NotifyCode, LPARAM lParam)
 {
        switch (NotifyCode)
        {
        case _dlgLaunch::notify::ProcessStarted:
            tabSelect.lock(true);
            break;
        case _dlgLaunch::notify::ProcessStopped:
            tabSelect.lock(false);
            break;
        }
}

I hope this has demonstrated that there is a lot of scope for building a dialog from existing dialogs with slightly modified behaviour but if you want this to work well then it is polite to make your controls public and have the dialogs define and issue some useful notifications.

Display text in multiple languages

Text displayed by controls

You may have noticed that you don't have to specify the text of controls and labels in the dialog. Instead it is extracted automatically from your variable names - removing any lower case prefix and converting underscores to spaces.  This is just a convenient default to avoid unwanted labour when prototyping. You can also explicitly set the text for each control using its as_text member and can do this either in the dialogs constructor or its OnInitDialog handler. 

You may also specify the text for each control in a separate file for which versions can be available in multiple languages with the following format:

C++
AUTODLG_CTRL_TEXT(EnterCodeDlg, btnOK, Aceptar)
AUTODLG_CTRL_TEXT(EnterCodeDlg, btnReset, Resetear)
AUTODLG_CTRL_TEXT(EnterCodeDlg, btnCancel, Cancelar)
AUTODLG_CTRL_TEXT(EnterCodeDlg, edtCode_label, Codigo:)

AUTODLG_CTRL_TEXT(SelectDataDlg, dlgEnter_code_label, Entrega codigo)
AUTODLG_CTRL_TEXT(SelectDataDlg, edtUser_label, Usuario:)
AUTODLG_CTRL_TEXT(SelectDataDlg, lbSelect_day_of_week_label, Dia de semana)
AUTODLG_CTRL_TEXT(SelectDataDlg, lblDay_of_week, Dia de semana)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnMorning, mañana)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnAfternoon, tarde)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnEvening, noche)
AUTODLG_CTRL_TEXT(SelectDataDlg, edtReport_label, Informe:)
AUTODLG_CTRL_TEXT(SelectDataDlg, groupProcess_control, Gestión processo)

AUTODLG_CTRL_TEXT(LaunchDlg, btnGo, Arranque)
AUTODLG_CTRL_TEXT(LaunchDlg, btnStop, STOP)
AUTODLG_CTRL_TEXT(LaunchDlg, pbarProcessing, Procesando)
AUTODLG_CTRL_TEXT(LaunchDlg, ctrlMeanwhile_stroke_me_label, Mientras tocarme!)

and activate it by including the following AFTER the dialog definitions to which it refers.

C++
#define AUTODLG_APP_METRICS app_metrics 
#include "Spanish_for_controls.h" //the file above
#undef AUTODLG_APP_METRICS

Such a file can be created for different languages and also for native language text that is better prepared than that derived by the variable names chosen by the developer. It is not necessary to include a language file for your controls (they will just extract defaults from the variable names) and a language file does not have to specify every control, defaults will be used for those not listed.

Strings you use in your code

As well as wanting to control the text of individual controls, you will also want to control the text of strings you use in your code. You do this by replacing literal text:

C++
pbarProcessing_label.as_text = _T("Press firmly on Go to resume process");

with an underscore linked version of it within the AUTODLG_TRANSLATE macro

C++
pbarProcessing_label.as_text =
                AUTODLG_TRANSLATE(Press_firmly_on_Go_to_resume_process);

You can do this right from the start without any obligation to include a string translation file with entries to match it. If no string translation file is included then it will simply give you the original string back with the underscores converted back to spaces.

To activate string translation you have to provide another file with following format:

C++
AUTODLG_SET_TRANSLATE(Monday, lunes)
AUTODLG_SET_TRANSLATE(Tuesday, martes)
AUTODLG_SET_TRANSLATE(Wednesday, miercoles)
AUTODLG_SET_TRANSLATE(Thursday, jueves)
AUTODLG_SET_TRANSLATE(Friday, viernes)
AUTODLG_SET_TRANSLATE(Saturday, sabado)
AUTODLG_SET_TRANSLATE(Sunday, domingo)
AUTODLG_SET_TRANSLATE(Click_here_to_enter_code, Click aqui para entrega codigo)
AUTODLG_SET_TRANSLATE(Press_firmly_on_Go_to_start_process, Apreta en Arranque con deliberación)
AUTODLG_SET_TRANSLATE(Processing, En processo)
AUTODLG_SET_TRANSLATE(Press_firmly_on_Go_to_resume_process, Apreta en Arranque para resumir);
AUTODLG_SET_TRANSLATE(SELECT_DAY_AND_TIME_PERIOD, Selecciona DIA y TEMPORADA)
AUTODLG_SET_TRANSLATE(SELECT_TIME_PERIOD, Selecciona TEMPORADA)

and include the following BEFORE  the dialog definitions that will be using it:

C++
#undef AUTODLG_TRANSLATE
#define AUTODLG_TRANSLATE(name) autodlg_string_##name
#include "Spanish_translate_table.h" //the file above

You do not have to include a string translation table but if you do then it must be complete. That is, it must contain an entry for every use of the AUTODLG_TRANSLATE macro in your code. If not, it won't compile.

Quick reference

FilesNamespaceDialog definition headerDialog layout definitionStyles, 
Naming conventionsDialog public membersDialog protected members, 
Event handlers that may be defined in the dialog definition
Events handlers that may be defined in metrics struct
Control methodsControl enumerationauto_stringList of windows controls supported
List of painted controls supplied with the libraryPaintedTabFrame
Library configuration filesMultiple languages

Files 

To use the the library you will need autodlg.hautodlg_controls.h and autodlg_metrics_config.h to reside in the same directory. Only autodlg.h needs to be included for compilation.

To use the example code you will also need  example_dialogs.h,  misc_painted_controls.h and compile example dialogs.cpp .

N.B. For older compilers you will have to use example_dialogs_alt.h instead of example_dialogs.h. It uses a more clunky way of defining tab order and fills out all of the parameters in do_msg calls;

Namespace

The library code is enclosed within the autodlg namespace specifcally to avoid name clashes so don't do

C++
using namespace autodlg; //DON'T DO THIS

You will only need to use the namespace in the header of class definitions as in the dialog definition below.

Dialog definition header

C++
template <class metrics = autodlg::def_metrics>
class SelectDataDlg : public autodlg::dialog < metr​ics, autodlg::auto_size, WS_OVERLAPPEDWINDOW >

Your class must derive from autodlg::dialog which takes the following template parameters

  • metrics struct - default: autodlg::def_metrics, you may pass in any conforming metrics struct

  • initial size policy - default: autodlg::auto_size (sizes to accomodate the layout), alternately you may pass in autodlg::explict_size<Width, Height> in you want to fix the initial size explicitly

  • window style - default: WS_POPUPWINDOW | WS_CAPTIONIn this case WS_OVERLAPPEDWINDOW has been passed in instead to make the dialog resizable by the user.

It is your design choice if you want to be able to pass those parameters into your derived class as is the case with metrics in the example above.

Dialog layout definition

All of this should be written in the declaration section of your derived dialog definition

The layout must begin with AUTODLG_DECLARE_CONTROLS_FOR and end with AUTODLG_END_DECLARE_CONTROLS and nothing must appear between these macros other than control declaration and the public and private keywords which you are free to use as your design demands.

Controls are declared with the following macros

C++
AUTODLG_CONTROL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles)

or if you want it to have an accompanying label:

C++
AUTODLG_CONTROL_WITH_LABEL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles,
     label_locator, label_height, label_type,label_style)
See the section Fundamentals of coded layout design for details of the locator verbs available
 
The following macros can be be used for the width and height parameters respectively
C++
AUTODLG_WIDTH_TO(_btnCancel::right)
AUTODLH_HEIGHT_TO(_btnCancel::bottom
C++
AUTODLG_BEGIN_TABLIST
        &btnCancel, &btnReset, &edtCode, &btnOK
AUTODLG_END_TABLIST

or with older compilers 

C++
AUTODLG_BEGIN_SET_TABS //alternative for older compilers
   AUTODLG_SET_TAB(btnCancel)
   AUTODLG_SET_TAB(btnReset)
   AUTODLG_SET_TAB(edtCode)
   AUTODLG_SET_TAB(btnOK)
AUTODLG_END_SET_TABS

Notifications codes may be defined for the NotifyParent call

C++
AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT
        OKToLaunch //significant event that parent may want to know about
AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT

Styles

The autodlg library defines three extra styles

  • MOUSE_OVER_STYLE which causes controls to invalidate when the mouse enters and leves it rectangle
  • EXPAND_X_STYLE and EXPAND_Y_STYLE which mark a control for expansion if the dialog is resized to larger than its design sise.

 

Naming conventionsthese names are generated automatically

  • The name of data type of a control on your dialog is the control name prefixed by underscore 
    e.g. the data type of btnOK is _btnOK
  • The name of a control's label is the name of the control postfixed by _label 
    e.g. the label for edtCode is called edtCode_label

Dialog public members

C++
HWND m_hWnd;
UINT DoModal(TCHAR* szTitle = 0, DWORD Style = 0)
bool Create(HWND hWnd, TCHAR* szTitle = 0, DWORD Style = 0, RECT* pRect = 0)
void CenterWindow()
void EndDialog(UINT id)
UINT GetEndDialogValue()
static void RunAppMsgLoop(HACCEL hAccelTable)

Dialog protected members - available within your derived dialog class definition along with the public members 

C++
void NotifyParent(int NotifyCode, LPARAM lParam = NULL)
static bool DrawCtlColorButton(LPDRAWITEMSTRUCT lpDrawItemStruct)

Event handlers that may be defined in the dialog definition

C++
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
void OnNotificationsByControlType(BUTTON* pC, UINT NotifyCode, LPARAM lParam)

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)

LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
LRESULT OnMessageByControlType(EDIT* pC, UINT message, WPARAM wParam, LPARAM lParam)

LRESULT OnItemMsgFrom(_edtCode*, DRAWITEMSTRUCT* pInfo, bool bMouseOver)
LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

LRESULT OnDialogMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

For more information on how event handlers are called see Summary of event handling and more about aesthetic metrics structs,

Events handlers that may be defined in metrics struct they must be declared static

C++
static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)

static LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)
static LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

Control methods - You will also have the methods of any control wrapper class you have chosen to use but these methods are available for all controls regardless of how they are wrapped.

C++
//methods that can be called on all controls//
BOOL invalidate()         
BOOL enable(BOOL bEnable) // when called on a control will also act on its label
BOOL show(int nShow)      // when called on a control will also act on its label
HWND set_focus()
HWND set_capture(bool bSet)
DWORD get_style()
DWORD set_style(DWORD _style)
RECT get_rect()
void move(int left, int top)
void move_by(int x, int y)
void size(int width, int height)
void size_by(int x, int y)
template <class W = WPARAM, class L = LPARAM> LRESULT do_msg(UINT message, W wParam = 0, L lParam = 0)

The public members of basic_window, the base class of the in-built wrapper for raw controls such as BUTTON and EDIT are:

C++
HWND m_hWnd;
operator HWND() const;

Control enumeration

C++
//Enable all controls in this dialog
control_cursor cursor(*this);
if (cursor.set_first_ctrl(*this))
do{
    cursor.get_control()->enable(TRUE);
} while (cursor.move_to_next_ctrl());

set_first_ctrl may be passed a reference to the dialog in whih it is written as in the example above, or it can be passed another dialog 

C++
cursor.set_first_ctrl(dlgEnter_code()) 

or a particular control

C++
cursor.set_first_ctrl(_btnOK) 

if it is initialised to a dialog then it will enumerate all of its controls beginning with the first. If its initialised to a control then it wil enumerate from that control to the end of the list. The control* returned by get_control() can be used to access all of the control generic methods and the control_cursor also hold a public data member Createinfo filled out with further information queried from the derived class.

auto_string 

auto_string is the dynamic text buffer type that is used for the as_text variable supplied with every control. Its special feature is that writing to it also writes to the control and reading from it reads from the control. Otherwise it is a very basic dynamic text buffer. You can do the following with it:

C++
as_text = _T("some text"); //assign zero terminated string
as_text = btnOK.as_text //assign from another auto_string
TCHAR* pzText = as_text; //read into a zero terminated string
as_text.append(_T("some more text")); //append extra text
as_text.overwrite(5, _T("***"));  //overwrite existing text from a given position
TCHAR* pzText = as_text.from(5); //copy all the text after a giving position

//get a buffer of the required length and write into it - thi still updates the control
lbSelect_day_of_week.do_msg(LB_GETTEXT, iSel,  
               lblDay_of_week.as_text.get_buf_set_length(
                                     lbSelect_day_of_week.do_msg(LB_GETTEXTLEN, iSel))

More information about the nature of auto_string can be found here.

List of windows controls supported - more can be added by modying autodlg_controls.h

BUTTONCHECKBOXRADIOBUTTONGROUPBOXEDITRICHEDITLISTBOXCOMBOBOX
SCROLLBARSTATICSTATIC_BANNERSysMonthCal32PROGRESSBAR
SysListView32SysTreeView32 

List of painted controls supplied with the library

PaintedLabelPaintedBannerSpacerControlGroupBoxPaintedTabFrame

For more informatiom about these and also how to design painted controls see the Painted controls section

PaintedTabFrame

PaintedTabFrame should be declared in your layout as a control, specifying how many tabs it has as a template parameter.

C++
AUTODLG_CONTROL(tabSelect, at, 6, hGap + BHeight,
        350, 200,
        PaintedTabFrame<5>, EXPAND_X_STYLE | EXPAND_Y_STYLE, NO_STYLE)

It should then be followed by its content defined as controls using its special AUTODLG_TAB_BAR_MEMBER macro

C++
AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgEnter_code,
    EnterCodeDlg<metrics>, WS_TABSTOP, 0)
AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgSelectData,
    SelectDataDlg<metrics>, WS_TABSTOP, 0)
AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgLaunch,
    LaunchDlg<metrics>, WS_TABSTOP, 0)
AUTODLG_TAB_BAR_MEMBER(_tabSelect, ctrlCalendar,
    SysMonthCal32, WS_TABSTOP, 0)
AUTODLG_TAB_BAR_MEMBER(_tabSelect, edtRichEdit,
    RICHEDIT, WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN, 0)

The content will typically be embedded dialogs but may also include ordinary controls as with the last two items.

PaintedTabFrame public methods in addition the generic control methods are:

C++
bool select_tab(int NewTab)
void set_tab_text(int i, TCHAR* pText) //only needed when you don't get what you want automatically
void lock(bool _bLock) //prevents user from changing the tab displayed

Tabbed dialogs provides a more developed examble of what you can do with PaintedTabFrame

Library configuration files

The library reads two configuration files that you may modify;

  • autodlg_metrics_config.h - where you define the names of the parameters used by aesthetic metrics structs
  • autodlg_controls.h - where you can very consisely register standard controls and common controls for use and also specify the name of the hWnd member and creation requirements of control wapper libraries that you wish to use. These are already provided for the control wappers of MFC and WTL.

Multiple languages

Include string translation table BEFORE dialog definitions

C++
#undef AUTODLG_TRANSLATE
#define AUTODLG_TRANSLATE(name) autodlg_string_##name
#include "Spanish_translate_table.h"

Include control text specification AFTER dialog definitions

C++
#define AUTODLG_APP_METRICS app_metrics
#include "Spanish_for_controls.h"
#undef AUTODLG_APP_METRICS

Neither are necessary but the string translation table must be complete if included.

For information on the format of string translation and control text specification files see Display text in multiple languages

How it works

The key is rich typing. That is, a unique data type for each control. This enables all of the unique data of the control (type of control, position, size, and styles) to be captured in the control's data type during declaration, as is achieved with the AUTODLG_CONTROL macro:

C++
AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

whose expansion is shown here:

C++
//macro to define and declare a control within a layout definition 
#define AUTODLG_CONTROL(name, Locator, X,Y, W,H, CtrlType, Style, ExtStyle)\
struct _##name##_label;\
    /***define the unique type for this control***/\
struct _##name : public control_dlglink\
    <this_dlg_type, typename wrap_selector<CtrlType>::type >\
{\
    template <class C, class T> friend static C* ctrlq::get_label(T* pT);\
    enum {left=Locator::x+X, top=Locator::y+Y, width=W, height=H, \
        right=left+width, bottom=top+height, \
        _style=Style | autodlg::control_specification<CtrlType>::defstyle \
            | metrics::add_style<CtrlType>::style, \
        _extstyle=ExtStyle\
            | autodlg::control_specification<CtrlType>::defextstyle \
            | metrics::add_style<CtrlType>::extstyle};\
        typedef CtrlType control_type;\
        typedef dialog_element label_of_type;\
private:\
    typedef dialog_element label_type;\
    /***virtual method implementation - generates run-time code***/\
    void autodlg_on_control_query(ctrlq::query_base& query)\
    {\
        if(ctrlq::INFO==query.use)\
            static_cast<ctrlq::createinfo&>(query).DoQuery\
              <_##name, CtrlType, dialog_element, metrics>(this, _T(#name));\
        else\
            DoTypedQuery(this, query);\
    }\
};\
    /***declare the control variable***/\
_##name name;\

The macro is passed all of the information that will be needed to create and display the control and bind it to a variable.

First a new struct is defined whose name is formed from the variable name passed in by prepending it with underscore using the prepocessor ## concatenator.

  • The CtrlType passed in is used indirectly as a template parameter to its base class control_dlglink.
  • The position and size information Locator, X, Y, W, H is used to initialse the class enums left, top, width, height, right and bottom. The control_type typedef is defined as the CtrlType passed in.
  • The _style and _extStyle enums are initialised in a more complex way depending of the CtrlType as well as the styles passed in.
  • The label_of_type and label_type typedefs are defined as dialog_element (nothing in particular) to indicate that is not a label nor has a label.

All of this so far is no more than compile time book keeping, however the member function

  • autodlg_on_control_query is a virtual function implementation and does actualy generate run-time code that will be called.

Finally the variable is declared to be of the newly defined type.

An immediate advantage of declaring the layout as static const class info  is that having defined and declared one control:

C++
AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
          BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

you can define and declare the next control with reference to its type.

C++
AUTODLG_CONTROL(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap,
       BWidth, BHeight,
          BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

which facilitates the chaining together of controls by relative positioning.

Here is the definition of the under verb:

C++
template<class C> struct under
{
        enum { x = C::left, y = C::top + C::height };
};

and here is a reminder of how it is used in the expansion of the AUTODLG_CONTROL macro
(N.B. under is the Locator in this case):

C++
enum {left=Locator::x+X, top=Locator::y+Y,.....

What happens here is that you express your layout as much as possible in more friendly terms of verbs, relative positioning and small parametrised displacements and that gets turned into raw x,y coordinates by the compiler. 

 

The following schematic illustrates the class hierarchy that lies beneath a control defined by the AUTODLG_CONTROL macro.

Image 16

As we have already seen in the expansion of AUTDLG_CONTROL macro, a new type, struct _##name, is created and most of the parameter info is stored (at compile time) as its const static class info. If you have the class name then you can refer to its class info anywhere and the complier will simply write it into your code.  

struct _##name is derived from control_dlglink which takes two template parameters. One is the exact type of the parent dialog which is provided by the AUTODLG_DECLARE_CONTROLS_FOR macro which must always preceded any list of control declarations. The other is a control type which must derive from the internal control type and the wrap_selector<Ctrl> struct is there to ensure that this is the case. Painted controls such as PaintedLabel and PaintedTabFrame are created for this system and therefore already inherit from the internal control type so the wrap_selector does nothing and selects Ctrl. However any control wrapper classes you may want to use from MFC or WTL will not derive from the internal control type and you can't make them do so. The raw controls provided with this library BUTTONEDIT etc follow a similar pattern, deriving from an internal basic_window. Therefore in these cases the wrap_selector wraps the control with win_control<CtrlType>.

In the case of a windows control ( wrap_selector yeilds win_control<CtrlType> ) we need win_control to derive from control so that this system knows what to do with it but we also need it to derive from Ctrl so the the methods of Ctrl are inherited and available for convenient coding. Therefore we use multiple inheritance. It inherits from Ctrl which could be a BUTTON or EDIT or a CButton or CEdit and also inherits from win_control_base which in turn inherits from control.

In the case of a painted control. the Ctrl itself already inherits from paint_control either directly or via mouse_control (if it processes mouse events) and paint_control in turn inherits from control.

It follows from this that if you are working with a control say btnOK defined as CButton the public methods of btnOK will include the generic methods implemented in win_control and the specific methods provided by CButton.

control is the most developed common enumerable base class. That is it is the only class that says that it is a control without being at all specific about what type of control. control is in turn derived from dialog_element which is less than a control and serves two purposes; to provide the virtual function autodlg_on_control_query which which can be called by everything deriving from dialog_element and to provide a null control marker when it stands alone. 

Controls are declared as adjacent variables of varying sizes. There is no list or array of pointers to traverse so another way has to be found. The method used is to move from one control to the next by incrementing a pointer by its class size. The only thing is that the full class size is not immediately available to the base class control. It can only be found by calling a virtual function that is implemented in the complete derived class. The one and only virtual function is autodlg_on_control_query which is used for all such queries including the class info registered by the AUTODLG_CONTROL macro. A query is a class with a code, appropriate data member and a function to execute the query. It can request an action or information. The query is initialised, passed to autodlg_on_control_query and is executed by the autodlg_on_control_query in the most derived class. The most important query has code INFO and this fill itself out with all of the class info:

C++
struct ctrlq //struct that embraces all queries
{
        enum { INFO, CREATE, NOTIFICATION, CTLCOLOR, ITEM_MSG, MESSAGE, MOUSEOVER, CTRLMETHOD, SHOW, 
               ENABLE, PAINT, MOUSE, MOVE, CREATED, DLG_CREATED };
        struct query_base //base class for all queries
        {
            int use;
            query_base() : use(INFO) {}
        };
        struct createinfo : public ctrlq::query_base
        {
        private:
            createinfo(createinfo const& c){}
        public:
            createinfo(){} //inherits INFO as the value of the use member
            DWORD class_size;
            RECT rect;
            DWORD style;
            DWORD extstyle;
            TCHAR* pzInitText;
            DWORD label_class_size;
            DWORD label_of_class_size;
            DWORD control_family;
            dialog_base* m_pParent;
            HWND* pHWnd;
            template <class T, class Ctrl, class LabelType, class m> void DoQuery(T* pT, TCHAR* pzText)
            {
                class_size = sizeof(T);
                rect.left = pT->left;
                rect.top = pT->top;
                rect.right = pT->right;
                rect.bottom = pT->bottom;
                style = pT->_style;
                extstyle = pT->_extstyle;
                pzInitText = pzText;
                label_class_size = sizeof(LabelType);
                label_of_class_size = sizeof(T::label_of_type);
                control_family = control_family_selector<T>::family;
                pHWnd = hWnd_func_selecter<Ctrl>::selected::get_hWnd_pointer(pT);
            }
            void set_to_create(dialog_base* pParent)
            {
                m_pParent = pParent;
                use = CREATE; //once filled out, the same struct is used as a CREATE query
            }
        };
........
}

control_cursor is used to enumerate controls and it uses INFO queries to get the class size. Because the information returned in the INFO query is so useful, control_cursor retains the INFO query as a public member that can be used during the enumeration. It is by using that information during an enumeration of all controls that the controls are created with the correct type, size, position and style.

Queries are also used to allow the base class control to support the methods implemented in paint_control or win_control<Ctrl>. For instance if you call the enable method of control it will pass a query to autodlg_on_control_query which will cause the most derived class to call its enable method, which will either be that in paint_control or that in win_control<Ctrl> depending on what it is derived from.

Here is the ENABLE query:

C++
struct enable : public ctrlq::query_base
{
            int bEnable;
            enable(bool _bEnable) : bEnable(_bEnable)
            {
                use = ENABLE;
            }
            template <class T> void DoQuery(T* pT)
            {
                pT->OnEnable(bEnable);
                control* pLabel = get_label<control>(pT);
                if (pLabel)
                    pLabel->enable(bEnable);
            }
};

In this case the query also acts on the control`'s label if it has one.

With this we can define a layout, get it created and call methods on controls. All we need now is to handle events, for instance notifications. Control notifications arrive at the dialog and are reflected straight back to the control which calls autodlg_on_control_query with a Notify query.

Here is the DoQuery method of the Notify query.

C++
template<class T> void DoQuery(T* pT)
            {
                pT->GetDlg()->OnNotificationsByControlType(
                    (typename T::control_type*)pT, *this, lParam);

                pT->GetDlg()->OnNotificationsFrom(pT, *this, lParam);
            }

T is the most derived class passed in by its autodlg_on_control_query implementation. Two methods are called on the dialog. In the general case, they will be handled by the following sinks provided by the AUTODLG_DECLARE_CONTROLS_FOR macro which do nothing and compile nothing.

C++
template <class C>    inline void OnNotificationsByControlType\
    (C* pC, UINT NotifyCode, LPARAM lParam){}\
template <class C>    inline void OnNotificationsFrom\
    (C* pC, UINT NotifyCode, LPARAM lParam){}\

They are indifferent to the type of the first parameter. Any type will be accepted. However if you provide the dialog with a version that types the first parameter as the control you called it from:

C++
void OnNotificationsFrom(_btnCancel*, UINT NotifyCode, LPARAM lParam)
{ ....

then that will get called instead. So all you have to do is provide a handler with the first parameter typed for the control and it will get called. All event handling uses this mechanism.

Those are the main arteries of how it works. To know more you have to examine the code according to your curiosity. Most of it is found inside the class dialog_base. It has to be encapsulated somwhere and putting control class definitions inside the dialog base class eliminates many problems of precedence between the two and makes for cleaner coding.

Points of Interest

I already knew from working on a previous project Units of measurement types in C++ that rich typing can allow the compiler to check many things and make better decisions without any run-time cost (because it has already been done during compilation) but it still surprised me that it could bring so many seemingly miraculous benefits to dialog design. It really is no miracle. It seems so because as designers we see the dialog layout as something variable because it is our job to vary it. This causes us to overlook the fact that it is a constant fully known at compile time for any given compilation. The use of dialog resource templates reinforces this oversight because it seems (and probably is) a variable loaded up at run-time. Expressing the layout as pure C++ code provides the opportunity for the constness of a dialog layout to be properly recognised by the language and providing an informative type for each control is the way to do it. The apparent miracles result from full information about the layout being available at compile time. 

Most of the code has a very clear logic to follow and my hope is that this will work under what ever circumstances you want to throw at it. The least developed part is the resizing and rearranging of controls as a dialog is resized. Nevertheless you will probably find that it works well for you and that it is not difficult to avoid the corner cases that catch it out. I have decided to publish it as is and refine this later at my leisure.

For me the biggest achievement is that coding your layout empowers you as a programmer, the skill you are good and clever with, and it releases you from having to put aside those fine skills to labour as a draughtsman and IDE operative.

I have done this work and published it because I would like to see others using it. I enjoy writing libraries more than using them and in some ways I wrote it as a remedy for the years I have suffered with tradional dialog design. I wanted to put an end to that suffering for everyone. I would be very interested in any feedback on the joys and frustrations experienced by anyone who uses what is offered here.

Finally I must mention the excellent series of articles Custom Controls in Win32 API by Martin Mitáš which I have used as a Bible while working on this project. It provided clarity on many  issues that I could find nowhere else.

History

First publication and release 29 May 2015
Code update 3 June 2015 - Added "Display text in multiple languages" and fixed small errors.
Code update 5 June 2015 - Language files would cause Link errors with multiple compilation units  - now fixed by changes to autodlg.h only

License

posted @ 2022-12-13 14:24  小风风的博客  阅读(23)  评论(0编辑  收藏  举报