In my first article about dialog boxes, I introduced how dialogs work and provided an overview dialog resources. In the article, we’ll dive into three dialog procedures and some common controls employed in a simple survey program.
Friday, April 13. 2018
LDC #80: Dialog Boxes Part II — A Simple Dialog Box
Introduction
To explore some procedures and controls, I created a little mock-up media survey program (don’t worry so much about the content or application of the data). The concepts discussed here apply to all dialogs. The survey’s dialog looks like this:
For this dialog, I opted to try the group style boxes, which I have not used for many years. It turns out that the box style is very lightly colored which does not display well in Windows 10. So I set the background to white. Given a little extra time, I would change it to etched frames.
The point behind this exercise is to demonstrate the load, action, and validate dialog procedures. Along the way we will show one method of storing parameters and also discuss functions related to a few common controls.
Let’s look at our code:
// // Dialog Example -- Media Survey // ------------------------------ // // // //////////////////////////////////////// // Resources #beginresource #define ES_NAME 201 #define ES_FEMALE 202 #define ES_MALE 203 #define ES_SELF_ID 204 #define ES_SELF_ID_TITLE 205 #define ES_SELF_ID_TEXT 206 #define ES_AGE_GROUP 207 #define ES_AYS_TERRESTRIAL 211 #define ES_AYS_STREAMING 212 #define ES_AYS_CABLE 213 #define ES_AYS_SATELLITE 214 #define ES_AYS_DVR 215 #define ES_OM_DVD 221 #define ES_OM_BLUE_RAY 222 #define ES_OM_COMPUTER 223 #define ES_OM_PLEX 224 #define ES_OM_OTHER 225 #define ES_OM_OTHER_TEXT 226 #define ES_SPORTS 231 #define ES_POLICE 232 #define ES_DRAMA 233 #define ES_COOKING 234 #define ES_HEALTH 235 #define ES_NEWS 236 #define ES_REALITY 237 #define ES_MUSIC 238 #define ES_CARTOONS 239 #define ES_FANTASY 240 #define ES_SCI_FI 241 #define ES_EDUCATIONAL 242 EntertainSurveyDialog DIALOGEX 0, 0, 330, 205 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Home Entertainment" FONT 8, "MS Shell Dlg" { CONTROL "About You", -1, "button", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 6, 4, 260, 64, 0 CONTROL "First &Name:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 18, 42, 8, 0 CONTROL "", ES_NAME, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 60, 16, 100, 12, 0 CONTROL "Gender:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 34, 42, 8, 0 CONTROL "&Female", ES_FEMALE, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 60, 32, 40, 12, 0 CONTROL "&Male", ES_MALE, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 110, 32, 40, 12, 0 CONTROL "Self-&Identify", ES_SELF_ID, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 160, 32, 60, 12, 0 CONTROL "as:", ES_SELF_ID_TITLE, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 180, 50, 15, 8, 0 CONTROL "", ES_SELF_ID_TEXT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 196, 48, 60, 12, 0 CONTROL "&Age Group:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 50, 42, 8, 0 CONTROL "", ES_AGE_GROUP, "combobox", CBS_DROPDOWNLIST | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 60, 48, 50, 35, 0 CONTROL "About Your Service", -1, "button", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 6, 72, 155, 60, 0 CONTROL "&Terrestrial TV", ES_AYS_TERRESTRIAL, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 12, 84, 60, 12, 0 CONTROL "Streamin&g", ES_AYS_STREAMING, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 84, 60, 12, 0 CONTROL "Cable &Box", ES_AYS_CABLE, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 12, 98, 60, 12, 0 CONTROL "Sate&llite", ES_AYS_SATELLITE, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 98, 60, 12, 0 CONTROL "&DVR Capable", ES_AYS_DVR, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 29, 112, 60, 12, 0 CONTROL "Other Media", -1, "button", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 169, 72, 155, 60, 0 CONTROL "&DVD", ES_OM_DVD, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 84, 60, 12, 0 CONTROL "Blue Ra&y", ES_OM_BLUE_RAY, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 250, 84, 60, 12, 0 CONTROL "&Computer", ES_OM_COMPUTER, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 98, 60, 12, 0 CONTROL "&Plex", ES_OM_PLEX, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 250, 98, 60, 12, 0 CONTROL "&Other", ES_OM_OTHER, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 112, 40, 12, 0 CONTROL "", ES_OM_OTHER_TEXT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 223, 112, 81, 12, 0 CONTROL "What You Watch", -1, "button", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 6, 137, 318, 60, 0 CONTROL "S&ports", ES_SPORTS, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 12, 148, 60, 12, 0 CONTROL "&Police Drama", ES_POLICE, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 12, 162, 60, 12, 0 CONTROL "D&rama", ES_DRAMA, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 12, 176, 60, 12, 0 CONTROL "Coo&king", ES_COOKING, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 148, 60, 12, 0 CONTROL "&Health", ES_HEALTH, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 162, 60, 12, 0 CONTROL "Ne&ws", ES_NEWS, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 82, 176, 60, 12, 0 CONTROL "&Reality TV", ES_REALITY, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 152, 148, 60, 12, 0 CONTROL "M&usic", ES_MUSIC, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 152, 162, 60, 12, 0 CONTROL "Cartoons", ES_CARTOONS, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 152, 176, 60, 12, 0 CONTROL "Fantasy T&V ", ES_FANTASY, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 222, 148, 60, 12, 0 CONTROL "&Sci F&i", ES_SCI_FI, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 222, 161, 60, 12, 0 CONTROL "&Educational", ES_EDUCATIONAL, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 222, 175, 60, 12, 0 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 273, 8, 50, 14, 0 CONTROL "Cancel", IDCANCEL, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 273, 27, 50, 14, 0 } #endresource //////////////////////////////////////// #define AGE_GROUPS "18 – 24,25 – 34,35 – 44,45 – 54,55 – 64,65+" #define FILE_NAME "Legato Practice Survey.txt" //////////////////////////////////////// // Data string data[]; //////////////////////////////////////// int main() { // Program Entry string s1, s2; s1 = GetTempFileFolder() + FILE_NAME; s2 = FileToString(s1); data = ParametersToArray(s2); if (DialogBox("EntertainSurveyDialog", "es_") == ERROR_NONE) { s2 = ArrayToParameters(data, "\r\n"); StringToFile(s2, s1); } return ERROR_NONE; } //////////////////////////////////////// void set_state() { // Set Control State if (CheckboxGetState(ES_SELF_ID)) { // Gender Controls ControlEnable(ES_SELF_ID_TITLE); ControlEnable(ES_SELF_ID_TEXT); } else { ControlDisable(ES_SELF_ID_TITLE); ControlDisable(ES_SELF_ID_TEXT); } if (CheckboxGetState(ES_AYS_CABLE)) { // Cable DVR ControlEnable(ES_AYS_DVR); } else { ControlDisable(ES_AYS_DVR); } if (CheckboxGetState(ES_OM_OTHER)) { // Media Other ControlEnable(ES_OM_OTHER_TEXT); } else { ControlDisable(ES_OM_OTHER_TEXT); } } //////////////////////////////////////// int es_load() { // Load Dialog DialogSetPageColor("white"); // About You EditSetText(ES_NAME, data["Name"]); switch (data["Gender"]) { case "": break; case "Female": CheckboxSetState(ES_FEMALE); break; case "Male": CheckboxSetState(ES_MALE); break; default: CheckboxSetState(ES_SELF_ID); EditSetText(ES_SELF_ID_TEXT, data["Gender"]); break; } ComboBoxLoadList(ES_AGE_GROUP, AGE_GROUPS); ComboBoxSelectItem(ES_AGE_GROUP, data["Age Group"]); // About Your Service if (IsTrue(data["Service Terrestrial"])) { CheckboxSetState(ES_AYS_TERRESTRIAL); } if (IsTrue(data["Service Streaming"])) { CheckboxSetState(ES_AYS_STREAMING); } if (IsTrue(data["Service Cable"])) { CheckboxSetState(ES_AYS_CABLE); } if (IsTrue(data["Service Satellite"])) { CheckboxSetState(ES_AYS_SATELLITE); } if (IsTrue(data["Service DVR"])) { CheckboxSetState(ES_AYS_DVR); } // Media if (IsTrue(data["Media DVD"])) { CheckboxSetState(ES_OM_DVD); } if (IsTrue(data["Media BlueRay"])) { CheckboxSetState(ES_OM_BLUE_RAY); } if (IsTrue(data["Media Computer"])) { CheckboxSetState(ES_OM_COMPUTER); } if (IsTrue(data["Media Plex"])) { CheckboxSetState(ES_OM_PLEX); } if (IsTrue(data["Media Other"])) { CheckboxSetState(ES_OM_OTHER); } EditSetText(ES_OM_OTHER_TEXT, data["Media Other Description"]); // What You Watch if (IsTrue(data["Media Sports"])) { CheckboxSetState(ES_SPORTS); } if (IsTrue(data["Media Police"])) { CheckboxSetState(ES_POLICE); } if (IsTrue(data["Media Drama"])) { CheckboxSetState(ES_DRAMA); } if (IsTrue(data["Media Cooking"])) { CheckboxSetState(ES_COOKING); } if (IsTrue(data["Media Health"])) { CheckboxSetState(ES_HEALTH); } if (IsTrue(data["Media News"])) { CheckboxSetState(ES_NEWS); } if (IsTrue(data["Media Reality"])) { CheckboxSetState(ES_REALITY); } if (IsTrue(data["Media Music"])) { CheckboxSetState(ES_MUSIC); } if (IsTrue(data["Media Cartoons"])) { CheckboxSetState(ES_CARTOONS); } if (IsTrue(data["Media Fantasy"])) { CheckboxSetState(ES_FANTASY); } if (IsTrue(data["Media Sci Fi"])) { CheckboxSetState(ES_SCI_FI); } if (IsTrue(data["Media Educational"])) { CheckboxSetState(ES_EDUCATIONAL); } set_state(); return ERROR_NONE; } //////////////////////////////////////// void es_action(int c_id, int c_code) { // Control Actions if ((c_id == ES_FEMALE) || (c_id == ES_MALE) || // Gender (c_id == ES_SELF_ID)) { if (CheckboxGetState(ES_SELF_ID) == FALSE) { EditSetText(ES_SELF_ID_TEXT, ""); } set_state(); return ; } if (c_id == ES_AYS_CABLE) { // Cable Can have DVR if (CheckboxGetState(ES_AYS_CABLE) == FALSE) { CheckboxSetState(ES_AYS_DVR, FALSE); } set_state(); return ; } if (c_id == ES_OM_OTHER) { // Media Other if (CheckboxGetState(ES_OM_OTHER) == FALSE) { EditSetText(ES_OM_OTHER_TEXT, ""); } set_state(); return ; } } //////////////////////////////////////// int es_validate() { // Get Controls (Validate) string s, a[]; int flag; // About You a["Name"] = EditGetText(ES_NAME); if (a["Name"] == "") { MessageBox('x', "Please enter your first name."); return ERROR_SOFT | ES_NAME; } s = ""; if (CheckboxGetState(ES_FEMALE)) { s = "Female"; } if (CheckboxGetState(ES_MALE)) { s = "Male"; } if (CheckboxGetState(ES_SELF_ID)) { s = EditGetText(ES_SELF_ID_TEXT); if (s == "") { MessageBox('x', "Please enter your self-identified gender."); return ERROR_SOFT | ES_SELF_ID_TEXT; } } if (s == "") { MessageBox('x', "Please select or enter a gender."); return ERROR_SOFT | ES_FEMALE; } a["Gender"] = s; a["Age Group"] = ComboBoxGetSelectString(ES_AGE_GROUP); if (a["Age Group"] == "") { MessageBox('x', "Please enter your first name."); return ERROR_SOFT | ES_AGE_GROUP; } // About Your Service flag = FALSE; if (CheckboxGetState(ES_AYS_TERRESTRIAL)) { a["Service Terrestrial"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_STREAMING)) { a["Service Streaming"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_CABLE)) { a["Service Cable"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_SATELLITE)) { a["Service Satellite"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_DVR)) { a["Service DVR"] = "True"; flag = TRUE; } if (flag == FALSE) { MessageBox('x', "Please select one or more boxes under 'About Your Service'."); return ERROR_SOFT | ES_AYS_TERRESTRIAL; } flag = FALSE; if (CheckboxGetState(ES_OM_DVD)) { a["Media DVD"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_OM_BLUE_RAY)) { a["Media BlueRay"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_OM_COMPUTER)) { a["Media Computer"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_OM_PLEX)) { a["Media Plex"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_OM_OTHER)) { a["Media Other"] = "True"; flag = TRUE; a["Media Other Description"] = EditGetText(ES_OM_OTHER_TEXT); } if (flag == FALSE) { MessageBox('x', "Please select one or more boxes under 'Other Media'."); return ERROR_SOFT | ES_OM_DVD; } // What You Watch flag = FALSE; if (CheckboxGetState(ES_SPORTS)) { a["Media Sports"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_POLICE)) { a["Media Police"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_DRAMA)) { a["Media Drama"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_COOKING)) { a["Media Cooking"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_HEALTH)) { a["Media Health"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_NEWS)) { a["Media News"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_REALITY)) { a["Media Reality"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_MUSIC)) { a["Media Music"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_CARTOONS)) { a["Media Cartoons"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_FANTASY)) { a["Media Fantasy"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_SCI_FI)) { a["Media Sci Fi"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_EDUCATIONAL)) { a["Media Educational"] = "True"; flag = TRUE; } if (flag == FALSE) { MessageBox('x', "Please select one or more boxes under 'What You Watch'."); return ERROR_SOFT | ES_SPORTS; } data = a; return ERROR_NONE; }
This program contains quite a bit of code, about 400 lines. Why? Well, for each field we need: (1) a control defined in the resource, (2) a loader and (3) a retriever/validator. We actually saved a bunch of code by using an array with key names to store the data.
Let’s graphically break this down. Our code appears in the green boxes:
Our main() function loads the information to the data array, calls the DialogBox SDK function, and then saves the information, assuming the user did not press the Cancel button.
While there are many dialog procedures that our script can hook, this time we will be focusing on load, action and validate. For most basic dialogs, this is all you need.
Passing Data In and Out
The easiest way to pass data in and out of a dialog box is via global variables. If you have a several dialogs, you can share variables or have a set for each dialog, perhaps prefixed with the same sequence as the serving procedure names. In this case, we are using a global string array called data. For illustration purposes, the ParametersToArray and ArrayToParameters functions are used to load and save the data. Our sample will create a file called “Legato Practice Survey.txt” in your temporary file area. It is named with the define FILE_NAME. We are not being careful about errors; as you can see, failure to load the data results in a dialog with no fields set. Not checking for errors would obviously not be optimal in a real-world environment, but in this case it’s alright.
Procedures
When the DialogBox function is called, our resource name is passed along with a procedure prefix. This prefix is used to determine which functions are called. For example, if the prefix was “mydialog” the DialogBox function would call mydialogload for the load procedure. Adding an underscore to your prefix will make the function names easier to read. The procedures used are as follows:
load (es_load with the example’s prefix)
This procedure is called on the initial loading of a page. It is called only once and only when a page becomes visible. As such, within a Property Sheet style dialog (tabbed dialog), a script should not rely on this procedure to be called if the page is not the first to be displayed.
The load procedure is commonly used to setup controls, load data, and set the initial state of the dialog page. For example, combo boxes can be loaded and certain controls disabled or hidden depending on conditions. We will go into detail on this below.
For a basic dialog, the return value can be TRUE or FALSE (ERROR_NONE). If it’s FALSE, keyboard focus is set to the specified control using the ControlSetFocus function, or by default, to the first control that can accept keyboard focus in the resource order. If TRUE, focus is set to the first control in the resource order that will accept input without regard to any other focus changes.
For property sheet dialogs, focus is always set to the first control in the resource order that will accept input without regard to any other focus changes.
Returning a formatted error code will abort the dialog load. This is not considered best practice for property sheets since the load procedure is not called until a tab’s page is displayed. If a page tab is clicked and the called load procedure returns an error, the whole property sheet dialog will immediately close and return the error code used to exit the load procedure. For the first tab, a returned error code will cause the page to briefly appear and then close.
action (es_action with the example’s prefix)
This procedure is called for any control action that generates a message, such as a button press (except OK, Cancel, Help, etc.) or selection change. Note this procedure is called frequently, including control focus changes. This means that complex processing should be kept out of this procedure unless it is tied to a specific action such as a button press.
The procedure definition should include two int values: control id and action code for the control’s ID and the submessage, respectively. See each control for submessage values. By convention, submessage values from command actions are positive and messages passed from a notification are negative.
You will notice that there is a notify procedure that is grayed out in the above diagram. This procedure can be used for more advanced common controls and operates in a similar manner to action. We will discuss this in a later article.
The return value for the action procedure is not used.
validate (es_validate with the example’s prefix)
This procedure is called when a page is ready to be validated prior to exit. Returning ERROR_NONE allows the dialog to continue to close and eventually proceed to the ok procedure. Returning any other value prevents the dialog from closing. If an ERROR_ mode is not set, then the lower word of the value is used as the control ID on which to set focus (in other words, the control with the offending value). The high word of the value can be used to change the active page for the control. We will discuss this in a later article.
The validate procedure is not called when a user presses the Cancel button. In that case, the default operation is to close the dialog and return ERROR_CANCEL from the DialogBox function.
Loading Controls
The load procedure allows the programmer to set up a dialog page. Controls can be loaded, items can be enabled or disabled, or any other relevant tasks can be performed. The actual page is not displayed during the load procedure. Only after the procedure exits does the page appear with all of its controls.
Let’s look at some of our dialog load procedure code:
int es_load() { // Load Dialog DialogSetPageColor("white"); // About You EditSetText(ES_NAME, data["Name"]); switch (data["Gender"]) { case "": break; case "Female": CheckboxSetState(ES_FEMALE); break; case "Male": CheckboxSetState(ES_MALE); break; default: CheckboxSetState(ES_SELF_ID); EditSetText(ES_SELF_ID_TEXT, data["Gender"]); break; } ComboBoxLoadList(ES_AGE_GROUP, AGE_GROUPS); ComboBoxSelectItem(ES_AGE_GROUP, data["Age Group"]); // About Your Service if (IsTrue(data["Service Terrestrial"])) { CheckboxSetState(ES_AYS_TERRESTRIAL); } if (IsTrue(data["Service Streaming"])) { CheckboxSetState(ES_AYS_STREAMING); } if (IsTrue(data["Service Cable"])) { CheckboxSetState(ES_AYS_CABLE); } if (IsTrue(data["Service Satellite"])) { CheckboxSetState(ES_AYS_SATELLITE); } if (IsTrue(data["Service DVR"])) { CheckboxSetState(ES_AYS_DVR); } // Media if (IsTrue(data["Media DVD"])) { CheckboxSetState(ES_OM_DVD); } if (IsTrue(data["Media BlueRay"])) { CheckboxSetState(ES_OM_BLUE_RAY); } if (IsTrue(data["Media Computer"])) { CheckboxSetState(ES_OM_COMPUTER); } if (IsTrue(data["Media Plex"])) { CheckboxSetState(ES_OM_PLEX); } if (IsTrue(data["Media Other"])) { CheckboxSetState(ES_OM_OTHER); } EditSetText(ES_OM_OTHER_TEXT, data["Media Other Description"]); // What You Watch if (IsTrue(data["Media Sports"])) { CheckboxSetState(ES_SPORTS); } if (IsTrue(data["Media Police"])) { CheckboxSetState(ES_POLICE); } if (IsTrue(data["Media Drama"])) { CheckboxSetState(ES_DRAMA); } if (IsTrue(data["Media Cooking"])) { CheckboxSetState(ES_COOKING); } if (IsTrue(data["Media Health"])) { CheckboxSetState(ES_HEALTH); } if (IsTrue(data["Media News"])) { CheckboxSetState(ES_NEWS); } if (IsTrue(data["Media Reality"])) { CheckboxSetState(ES_REALITY); } if (IsTrue(data["Media Music"])) { CheckboxSetState(ES_MUSIC); } if (IsTrue(data["Media Cartoons"])) { CheckboxSetState(ES_CARTOONS); } if (IsTrue(data["Media Fantasy"])) { CheckboxSetState(ES_FANTASY); } if (IsTrue(data["Media Sci Fi"])) { CheckboxSetState(ES_SCI_FI); } if (IsTrue(data["Media Educational"])) { CheckboxSetState(ES_EDUCATIONAL); } set_state(); return ERROR_NONE; }
The procedure is broken into five major sections: initialization, ‘about you’, ‘about your service’, ‘media’, ‘what you watch’, and wrap-up processing. In this case, the only initialization for the overall dialog is to set the background color. Next we address the ‘about you’ section. This is a good time to introduce a little information about the API for common controls. Legato features a series of API functions to access specific type of controls. We will be introducing edit, checkbox and the combobox controls.
The first thing we will do is set text into a control:
EditSetText(ES_NAME, data["Name"]);
The EditSetText function takes a control ID and a string. This function can be used to set text into any control that supports displaying text, including static controls.
The dialog box above also makes use of checkboxes. For example, the participant selecting his or her gender lends itself to a checkbox control (as a radio button style):
switch (data["Gender"]) { case "": break; case "Female": CheckboxSetState(ES_FEMALE); break; case "Male": CheckboxSetState(ES_MALE); break; default: CheckboxSetState(ES_SELF_ID); EditSetText(ES_SELF_ID_TEXT, data["Gender"]); break; }
Some Legato API functions control both checkboxes and radio buttons. Checkboxes and radio buttons are both styles of the button control class and are differentiated by the BS_AUTOCHECKBOX and BS_AUTORADIOBUTTON styles (or the BS_CHECKBOX and BS_RADIOBUTTON for manual versions). Checkboxes are independent of each other while radio buttons flip among controls in a group, and only one button in the group may be checked.
In our example, there are four allowed states: (i) empty, nothing set; (ii) female; (iii) male; and, (iv) self-identified. Depending on the state, one of the radio buttons is set using the following function:
int = CheckboxSetState ( int id, [int state] );
The CheckboxSetState function sets the state of buttons. If not specified, the default value of state is BST_CHECKED (1 or TRUE). The groups that control which radio buttons are linked are controlled by the order of the buttons in the resource data and the WS_GROUP style. Using a group box does not control the scope of the automatic radio buttons. When more than one group of radio buttons is being deployed a control with WS_GROUP style flags delineates the groups. As highlighted below the three radio buttons are all part of a group since they are all after the “About You” box with the WS_GROUP style.
CONTROL "About You", -1, "button", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 6, 4, 260, 64, 0 CONTROL "First &Name:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 18, 42, 8, 0 CONTROL "", ES_NAME, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 60, 16, 100, 12, 0 CONTROL "Gender:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 34, 42, 8, 0 CONTROL "&Female", ES_FEMALE, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 60, 32, 40, 12, 0 CONTROL "&Male", ES_MALE, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 110, 32, 40, 12, 0 CONTROL "Self-&Identify", ES_SELF_ID, "button", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 160, 32, 60, 12, 0 CONTROL "as:", ES_SELF_ID_TITLE, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 180, 50, 15, 8, 0 CONTROL "", ES_SELF_ID_TEXT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 196, 48, 60, 12, 0
During the load procedure it is important to note that the “auto” functionality of radio buttons is ignored. This means if you use the CheckboxSetState function on multiple radio buttons they will all be checked regardless of their grouping and style. Finally, on default the self-identify ‘as:’ edit control is loaded.
On a side note, you may have noticed ‘&‘ characters like ‘First &Name’ on some resource text. These help the dialog processor highlight and process Alt quick keys. So ‘First Name’ can be accessed via Alt+N. When pressed, the processor moves focus to that control or the first control that will accept keyboard input. If there are conflicts, it rotates the highlighting though each match.
The last part of loading controls worth noting here is the combobox used for the age of the participant:
ComboBoxLoadList(ES_AGE_GROUP, AGE_GROUPS);
ComboBoxSelectItem(ES_AGE_GROUP, data["Age Group"]);
The ComboBoxLoadList function loads the combo control with the list. It is loading a define called AGE_GROUPS:
#define AGE_GROUPS "18 – 24,25 – 34,35 – 44,45 – 54,55 – 64,65+"
There are a number of methods to load data to such a control. In a later article, I will cover the combobox control in detail with each method explored. The data is selected using the ComboBoxSelectItem function.
The remaining controls are loaded using the same methods as just described. When we are done, the set_state function is called, which sets the state of all controls that change state depending on selection context. We discuss that next.
User Interaction
For more complex, well-designed dialogs, user interaction is interpreted on the fly and the controls should adapt to the context. For our dialog we have three operational contexts defined:
– ‘Gender’ allows the user to enter a self-identified value.
– ‘Cable Box’ allows a ‘DVR Capable’ checkbox to be checked (Satellite probably should, too)
– Media has an ‘Other’ checkbox with an optional description.
Ideally these controls should become available as the context of the data entered demands them. This is possible through the action procedure:
void es_action(int c_id, int code) { ... }
As the user interacts with the dialog, numerous calls are made to the action procedure. It is important to note that hundreds or even thousands of calls may be made to the action procedure during the life of a dialog. Each control type sends actions/messages specific to that control and its settings. For example, by default, button controls send a single message on click, but if the BS_NOTIFY style is set, then messages are sent for focus changes, highlights, and more. For an edit control, Legato will always send notifications focus change, content change, etc. The code indicates the specific actions as defined for the control.
The codes (c_code parameter) meanings can overlap. For example, button notification BN_SETFOCUS and CBN_EDITUPDATE both have the value 6. The meaning of this value depends on the control class as identified by the c_id parameter.
Let’s look closer at our action procedure:
void es_action(int c_id, int c_code) { // Control Actions if ((c_id == ES_FEMALE) || (c_id == ES_MALE) || // Gender (c_id == ES_SELF_ID)) { if (CheckboxGetState(ES_SELF_ID) == FALSE) { EditSetText(ES_SELF_ID_TEXT, ""); } set_state(); return ; } if (c_id == ES_AYS_CABLE) { // Cable Can have DVR if (CheckboxGetState(ES_AYS_CABLE) == FALSE) { CheckboxSetState(ES_AYS_DVR, FALSE); } set_state(); return ; } if (c_id == ES_OM_OTHER) { // Media Other if (CheckboxGetState(ES_OM_OTHER) == FALSE) { EditSetText(ES_OM_OTHER_TEXT, ""); } set_state(); return ; } }
The first action condition concerns changes to the ‘gender’ controls. Three ids are watched. Any change to these will cause two things to happen: the self id (ES_SELF_ID_TEXT) text is cleared and the state of the dialog is updated. Note that we only need check the state of ES_SELF_ID to see if that control is unchecked. Because our radio buttons were created with the BS_AUTORADIOBUTTON when one is checked the others automatically uncheck. If we did not use this style we would need to handle that behavior ourselves.
The second action checks the ‘cable’ checkbox, which can clear the ‘DVR’ checkbox and update the dialog state.
Finally, the last action of interest is ‘Media Other’, which also clears the associated text control.
All other actions (messages) are ignored.
Updating the Dialog State
We have a simple routine that updates all controls in the dialog at once. Yes, this is a little inefficient but most computers have tens of millions of CPU cycles available, so for ease of coding and debugging, we will do it all at once every time something relevant changes. This also makes it easier to change how controls interact since it is all in one place. The function:
void set_state() { // Set Control State if (CheckboxGetState(ES_SELF_ID)) { // Gender Controls ControlEnable(ES_SELF_ID_TITLE); ControlEnable(ES_SELF_ID_TEXT); } else { ControlDisable(ES_SELF_ID_TITLE); ControlDisable(ES_SELF_ID_TEXT); } if (CheckboxGetState(ES_AYS_CABLE)) { // Cable DVR ControlEnable(ES_AYS_DVR); } else { ControlDisable(ES_AYS_DVR); } if (CheckboxGetState(ES_OM_OTHER)) { // Media Other ControlEnable(ES_OM_OTHER_TEXT); } else { ControlDisable(ES_OM_OTHER_TEXT); } }
The function checks the state of each conditional context as I described them above and enables or disables controls based on user selection. In a real script, this routine could become very complex. It can hide and show controls, swap out entire sets of overlapping controls, and more. Because of it’s complexity it should only be called when a relevant change is made, including the loading of the dialog.
A function that sets the state also aids in validation because you can prevent the user from entering conflicting data.
Collecting and Validating Data
The last procedure we will cover is validation. The validate procedure is called when the user presses OK or the DialogPostOk function is called. Two primary tasks usually occur here: gathering user input and validating it for correctness. Let us look at the first section of our validation:
int es_validate() { // Get Controls (Validate) string s, a[]; int flag; // About You a["Name"] = EditGetText(ES_NAME); if (a["Name"] == "") { MessageBox('x', "Please enter your first name."); return ERROR_SOFT | ES_NAME; } s = ""; if (CheckboxGetState(ES_FEMALE)) { s = "Female"; } if (CheckboxGetState(ES_MALE)) { s = "Male"; } if (CheckboxGetState(ES_SELF_ID)) { s = EditGetText(ES_SELF_ID_TEXT); if (s == "") { MessageBox('x', "Please enter your self-identified gender."); return ERROR_SOFT | ES_SELF_ID_TEXT; } } if (s == "") { MessageBox('x', "Please select or enter a gender."); return ERROR_SOFT | ES_FEMALE; } a["Gender"] = s; a["Age Group"] = ComboBoxGetSelectString(ES_AGE_GROUP); if (a["Age Group"] == "") { MessageBox('x', "Please enter your first name."); return ERROR_SOFT | ES_AGE_GROUP; }
First, notice we are placing the data into a local array a to later be copied into our global data variable. This is important. Without an alternate version of our working data, if the user presses OK but the validation does not complete because of an error, we will have destroyed the contents of data. If the user then decides to cancel, we have now corrupted our original information. Pressing Cancel should always leave original information unaltered. The last thing we’ll do in our validation procedure is copy the array a over data.
We start by capturing the ‘About You’ group. The first field captured is the ‘First Name’. It is required so we test the data. The EditGetText function trims any spaces so we don’t have to worry about a field full of spaces. The EditGetText function also has some validation built in. Here is the prototype:
string = EditGetText ( int id, [string name], [int flags], [int size] );
By adding a name, the function can report errors back to the user. Setting bits within flags allows for a variety of tests to be performed:
Definition | Bits | Description | |||||
General | |||||||
EGT_FLAG_REQUIRED | 0x80000000 | Field Is Required | |||||
EGT_FLAG_DO_NOT_TRIM | 0x40000000 | Don’t Trim Spaces | |||||
Field Types | |||||||
EGT_FLAG_TYPE_MASK | 0x0F000000 | Mask for Type | |||||
EGT_FLAG_STRING | 0x00000000 | General String (no test) | |||||
EGT_FLAG_NUMERIC | 0x01000000 | Numeric Field | |||||
EGT_FLAG_CURRENCY | 0x02000000 | Currency Field | |||||
EGT_FLAG_HEX | 0x07000000 | Hex Field (0x00000000) | |||||
Numeric Options | |||||||
EGT_FLAG_NO_ZERO | 0x10000000 | Zero Not Allowed | |||||
EGT_FLAG_UPPER_LIMIT | 0x20000000 | Upper Limit on Value (embed or void) | |||||
EGT_FLAG_NEGATIVE | 0x40000000 | Negative Allowed | |||||
EGT_FLAG_UPPER_LIMIT_MASK | 0x00FFFFFF | Upper Limit Value Mask | |||||
String Options | |||||||
EGT_FLAG_FILE_MUST_EXIST | 0x00000001 | Check File for Existing File | |||||
EGT_FLAG_FILE_CAN_OPEN_READ | 0x00000002 | Check File Can Open as Read (tests for exists as well) | |||||
EGT_FLAG_FILE_CAN_OPEN_WRITE | 0x00000004 | Check File Can Open as Write | |||||
EGT_FLAG_FILE_QUERY_OVERWRITE | 0x00000008 | Query Overwrite |
For this demo, we will just check the string ourselves. On failure, we display a message box and return an error code with the control ID. This prevents the dialog from closing and sets the keyboard focus on the offending field.
The second part of ‘About You’ is the ‘Gender’ section. In this example, I use a little program trick by having the variable s be both the selection and the ‘as’ data. If s comes up empty, they did not enter anything. If not, I can store the data.
The last item is the ‘Age Group’ where I capture the selected string from the combobox using the ComboBoxGetSelectString function. If nothing has been selected, the returned value is an empty string.
Each of the following sections is a series of check boxes. Since we are requiring that the user check at least one box in each group, a we will use the flag to store whether a box was checked and the value will be stored in the array.
flag = FALSE; if (CheckboxGetState(ES_AYS_TERRESTRIAL)) { a["Service Terrestrial"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_STREAMING)) { a["Service Streaming"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_CABLE)) { a["Service Cable"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_SATELLITE)) { a["Service Satellite"] = "True"; flag = TRUE; } if (CheckboxGetState(ES_AYS_DVR)) { a["Service DVR"] = "True"; } if (flag == FALSE) { MessageBox('x', "Please select one or more boxes under 'About Your Service'."); return ERROR_SOFT | ES_AYS_TERRESTRIAL; }
At the end of the group, if flag has not been set, we report an error and stop. After repeating this process, we save the data and exit:
data = a; return ERROR_NONE; }
Cross validation for additional requirements or conflicts can also be performed after all the data has been collected.
Validation in property sheets can sometimes get a little more complicated. This is particularly true when data from one page can conflict with data on another page. In addition, if a page is never displayed, Legato (Windows) does not load or validate the page. Again, conflicts are difficult to detect in this situation. There are options to cope with these complexities which I will cover in a later article.
Debugging Techniques
Unfortunately, the IDE does not allow debug stepping inside of a dialog procedure. The best bet is to use debug trace, either on the entire file or using the trace log option in the IDE.
Legato Execution Trace Dump: Survey.ls ---- -- :: ====== SCRIPT ENTRY ====== 107 1 :: s1 = GetTempFileFolder() + FILE_NAME; 108 1 :: s2 = FileToString(s1); 109 1 :: data = ParametersToArray(s2); 111 1 :: if (DialogBox("EntertainSurveyDialog", "es_") == ERROR_NONE) { 148 2 :: DialogSetPageColor("white"); 150 2 :: EditSetText(ES_NAME, data["Name"]); 251 2 :: (c_id == ES_SELF_ID)) { 259 2 :: if (c_id == ES_AYS_CABLE) { 267 2 :: if (c_id == ES_OM_OTHER) { 274 2 :: } ^ 251 2 :: (c_id == ES_SELF_ID)) { 259 2 :: if (c_id == ES_AYS_CABLE) { 267 2 :: if (c_id == ES_OM_OTHER) { 274 2 :: }
Strategically placed message boxes can also help to inspect the program. However, use caution when placing message boxes (or displaying message boxes as part of the program) within the action procedure, For example, let’s say a message box was added which displays the action code for a control getting focus. You can get stuck in a loop with the message box being displayed and the control regaining focus every time you press OK on the message box. At that point, you are pretty much cooked and will have to terminate the application.
Another option is to add messages to the default log to examine what is happening:
void es_action(int c_id, int c_code) { AddMessage("Action %3s - Code %d", c_id, c_code); ...
Just firing up the dialog and pressing Cancel will yield this in the log:
Action 201 - Code 1024 Action 201 - Code 768 Action 226 - Code 1024 Action 226 - Code 768 Action 201 - Code 256 Action 201 - Code 512
A basic translation of the IDs and codes:
Action 201 - Code 1024 First Name -- EN_UPDATE Action 201 - Code 768 First Name -- EN_CHANGE Action 226 - Code 1024 Other Media -- EN_UPDATE Action 226 - Code 768 Other Media -- EN_CHANGE Action 201 - Code 256 First Name -- EN_SETFOCUS Action 201 - Code 512 First Name -- EN_KILLFOCUS
Six actions occurred during open and cancel. Remember, the codes are specific to the controls and are also defined in the SDK.
Conclusion
I find that, depending on the application, I may spend far more time building a dialog interface than writing code to perform the actual functions associated with the dialog. A well designed interface makes for a better user experience. In later articles I will cover the details of specific common controls and some of the other dialog procedures.
Hopefully this information will help you build a more effective user interface and reduce development time.
Scott Theis is the President of Novaworks and the principal developer of the Legato scripting language. He has extensive expertise with EDGAR, HTML, XBRL, and other programming languages. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato