Windows dialog design in C++ without dialog templates.
Windows dialog design in C++ without dialog templates.
- Download autodlg.zip - 37.2 KB - code only
- Download VS2013Express_Example.zip - 39.7 KB
- Download VS2005_Example.zip - 41.7 KB
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 Interest, History
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.
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:
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
- whereDx
, andDy
are the absolute position from the top left corner of the dialog.to_right_of<_control>
- whereDx
, andDy
are offsets from that positionunder<_control>
- whereDx
, andDy
are offsets from that position
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.- label_left_align<_control> - where the left edge of the label aligns with the left edge of another control.
- label_above<int by>- where 'by' is the amount by which the label hangs above the control.
label_top_align<_control>
- where the top edge of the label aligns with the top edge of another control.
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
top
, right
, bottom
, width
, 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:
AUTODLG_CONTROL( btnOK, at , _btnCancel::left-BWidth-hGap, _btbCancel::top, BWidth, BHeight, .....
and to position a control above a previously declared control:
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.
AUTODLG_WIDTH_TO(_btnCancel::right)
The remaining parameters: control_type, control_styles, extended_styles,
describe 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.
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.
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.
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
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 hGap
, vGap
, BWidth
and BHeight
.
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 public
, private
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:
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.
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.
EnterCodeDlg<MyAppMetrics> dlg;
dlg.DoModal();
here is the dialog that it displays:
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::
#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.
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.
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 btnCancel
. There 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.
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:
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:
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
void OnNotificationsFrom(_btnOK*, UINT NotifyCode, LPARAM lParam)
{
if (BN_CLICKED == NotifyCode)
EndDialog(IDOK);
}
The dialog is now ready for use as follows:
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.
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.
bool bErrorInCode;
and initialise it in the constructor:
EnterCodeDlg()
{
bErrorInCode = false;
}
and now we can change the code handling the OK button
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:
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
:
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:
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
bool bErrorInCode;
bool bEditTouched;
initialise it and set the initial text for edtCode
EnterCodeDlg()
{
bErrorInCode = false;
bEditTouched = false;
edtCode.as_text = _T("Enter code here");
}
and add further conditional code to edtCode
`s ctlcolor handler
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.
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
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:
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.
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
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
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.
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.
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;
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:
- All edits have a border
- All buttons have owner draw and detect mouse over styles - no OwnerDraw handler is supplied so the default CtlColor sensitive button is drawn.
- Listboxes have white text and a dark backround
- Buttons are painted with a white brush when the mouse moves over them
- The specific control types
BannerLabel
andSTATIC_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
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.
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
:
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 StopButton
, StartupButton
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.
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 invalidate
, size
, move
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.
Here is a more complex dialog that gives more of a feel for what can be done:
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
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 inautodlg::explict_size<Width, Height>
in you want to fix the initial size explicitly - window style - default:
WS_POPUPWINDOW | WS_CAPTION
. In this caseWS_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 SpacerContro
l. 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
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*, ...
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*, ...
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
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:
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.
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:
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:
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
:
void OnInitDialog(HWND hWnd)
{
tabSelect.lock(true); //#1
dlgEnter_code().btnCancel.show(false); //#2
tabSelect.set_tab_text(4, _T("Rich edit field")); //#3
}
- 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. - As in our
SelectDataDlg
, the Cancel button ofEnterCodeDlg
will not do anything so we hide it. Note the brackets postfixdlgEnterCode()
, required to access members of the embedded dialog. - 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.
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:
- Copy the code from
dlgEnterCode
(the first tab) into thedlgEnterCode
indlgSelectData
. Note the requirement for empty brackets().
to access the members of each embedded dialog. - 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.
- Disable the first tab. It is no longer visible and this prevents it being seen again.
- Fire the
BN_CLICKED
notification of theOK
button in thedlgEnterCode
withindlgSelectData
. 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.
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.
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:
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.
#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:
pbarProcessing_label.as_text = _T("Press firmly on Go to resume process");
with an underscore linked version of it within the AUTODLG_TRANSLATE
macro
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:
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:
#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.
Files, Namespace, Dialog definition header, Dialog layout definition, Styles,
Naming conventions, Dialog public members, Dialog protected members,
Event handlers that may be defined in the dialog definition,
Events handlers that may be defined in metrics struct,
Control methods, Control enumeration, auto_string, List of windows controls supported,
List of painted controls supplied with the library, PaintedTabFrame,
Library configuration files, Multiple languages
To use the the library you will need autodlg.h, autodlg_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;
The library code is enclosed within the autodlg
namespace specifcally to avoid name clashes so don't do
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.
template <class metrics = autodlg::def_metrics>
class SelectDataDlg : public autodlg::dialog < metrics, 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 inautodlg::explict_size<Width, Height>
in you want to fix the initial size explicitly -
window style - default:
WS_POPUPWINDOW | WS_CAPTION
. In this caseWS_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.
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
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:
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)
AUTODLG_WIDTH_TO(_btnCancel::right) AUTODLH_HEIGHT_TO(_btnCancel::bottom
AUTODLG_BEGIN_TABLIST &btnCancel, &btnReset, &edtCode, &btnOK AUTODLG_END_TABLIST
or with older compilers
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
AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT
OKToLaunch //significant event that parent may want to know about
AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT
The autodlg library defines three extra styles
MOUSE_OVER_STYLE
which causes controls to invalidate when the mouse enters and leves it rectangleEXPAND_X_STYLE
andEXPAND_Y_STYLE
which mark a control for expansion if the dialog is resized to larger than its design sise.
Naming conventions, these 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 ofbtnOK
is_btnOK
- The name of a control's label is the name of the control postfixed by
_label
e.g. the label foredtCode
is callededtCode_label
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
void NotifyParent(int NotifyCode, LPARAM lParam = NULL)
static bool DrawCtlColorButton(LPDRAWITEMSTRUCT lpDrawItemStruct)
Event handlers that may be defined in the dialog definition
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
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.
//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:
HWND m_hWnd; operator HWND() const;
//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
cursor.set_first_ctrl(dlgEnter_code())
or a particular control
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
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:
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
BUTTON
, CHECKBOX
, RADIOBUTTON
, GROUPBOX
, EDIT
, RICHEDIT
, LISTBOX
, COMBOBOX
, SCROLLBAR
, STATIC
, STATIC_BANNER
, SysMonthCal32
, PROGRESSBAR
, SysListView32
, SysTreeView32
List of painted controls supplied with the library
PaintedLabel
, PaintedBanner
, SpacerControl
, GroupBox
, PaintedTabFrame
For more informatiom about these and also how to design painted controls see the Painted controls section
A PaintedTabFrame
should be declared in your layout as a control, specifying how many tabs it has as a template parameter.
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
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:
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
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.
Include string translation table BEFORE dialog definitions
#undef AUTODLG_TRANSLATE
#define AUTODLG_TRANSLATE(name) autodlg_string_##name
#include "Spanish_translate_table.h"
Include control text specification AFTER dialog definitions
#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
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:
AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
BUTTON, BS_NOTIFY | WS_TABSTOP, 0)
whose expansion is shown here:
//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 classcontrol_dlglink
. - The position and size information
Locator, X, Y, W, H
is used to initialse the class enumsleft, top, width, height, right
andbottom
. Thecontrol_type
typedef is defined as theCtrlType
passed in. - The
_style
and_extStyle
enums are initialised in a more complex way depending of theCtrlType
as well as the styles passed in. - The
label_of_type
andlabel_type
typedefs are defined asdialog_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:
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.
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:
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):
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.
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 BUTTON
, EDIT
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:
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:
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.
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.
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:
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.
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.
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