VCPP 6 - TUTORIAL - LESSON #3* DHTML (c) Artur Marques, 08 August 1999
*You are supposed to have read Lesson #1 ; Lesson #2
Programming a GAME LAUNCHER!
Associating member variables to controls
Closing a dialog box, on request from any button
Displaying a message, from an edit box, when requested from a button
Clearing a message, in an edit box, from a button request
Activating / de-activating controls on run-time
Showing / NOT showing controls on run-time
Running an external program, from a combo-box, from a push-button
Today we are going to build a VCPP6 application full of the usual controls: text fields, edit boxes, buttons, drop-down-list-boxes, and check boxes.
Text fields are read only strings, that the user cannot edit, but that a program can change in runtime.
Edit boxes are zones where the user can input text, without format.
Buttons are... buttons that you can press to trigger actions.
Drop-down-list-boxes or combo-boxes are controls that you use to select from several optional values, and that sometimes may serve to enter a previously non-existent value.
Check boxes are squares that you can mark / unmark with a X. They are kind of an on / off switch.
To start with, just create a NEW project, exactly as you did in the previous lesson. You should be creating a MFC dialog based application.When the editor area shows the application's window [a dialog box], go ahead and edit it, until it looks something like this. If you can't see the window with all its default resources, make sure to select "resources" from the workspace pane and start your work.
The application we are creating in this lesson is very interesting and will show you how to:
- associate resources to actions.
- handle some kinds of user input.
- run external programs.
For example, this application has the functionality needed to do a magazine's CDROM interface. The user selects a demo, then your program activates the install procedure... However, I will exemplify the program as a game launcher, testing it with some of my favorite games: Aliens versus Predator, Kingpin and Simcity 3000.
After you've designed your dialog box, you should check the TAB ORDER of the controls you've inserted. The TAB ORDER determines the sequence by which, each control gets selected, when a user presses the TAB key. If you've never noticed it, just launch any Windows application with some dialog box and experience by yourself, the effect of using the TAB key.
In order to watch your design's TAB ORDER, you should select TAB ORDER from the LAYOUT menu, or use the \accelerator CTRL^D.
Probably, the TAB ORDER won't be the best one. For example, after my first design, I ended up with this unwanted TAB ORDER.
In order to fix the TAB ORDER, after doing CTRL^D, you should click, one-by-one on your controls, by the order you intend. Do not click your last control, or it will become the 1st one; if that happens, just repeat the whole process...
Here is my final TAB ORDER.
The strong point of this lesson's program, is the ability to run external software, from a combo-box.
Chose the properties for the combo-box, then chose "DATA", and write the names of the software you'll run from the list. In order to enter a new name, you must press CTRL^ENTER. If you just press ENTER, you'll close the properties for the combo-box.
When you wan a control to have a keyboard shortcut, when editing its name, make sure you use a "&" somewhere. For example, IF you wanted ALT+E to be the same as pressing the EXIT button, you should type &EXIT, as the button's name.
Beware with repeating shortcuts. Shortcuts do accelerate your interface, but technically they are not accelerators. The "&" char is called the "ampersand". The shortcut is also known as the control's mnemonic.
VCPP6 has a way of avoiding duplicate mnemonics. Just choose the dialog box / window you are editing, then secondary-click, then "check mnemonics". If everything is ok, you should get a "no duplicate mnemonics have been found" message.
Associating member variables to controls
To finish your design, check the properties of every control and choose appropriate IDs and names. I did change most of the IDs, just keeping the prefix. For example, I called the "Run program" button, IDC_BUTTON_run.
After you've really finished the design, you must associate variables with your controls!
Do as follows:
#1 - open the ClassWizard, by secondary-clicking on the dialog box you are designing. Choose ClassWizard / Member Variables. For the IDs I've selected, here is the picture of what you should see.
#2 - add variables to the controls that need variables, ie, the controls that will hold some kind of user input, such as ALL the check boxes, edit boxes and the combo box. You add the variables, by pushing the "Add Variable" button.
VCPP6 advises you to follow an identifier naming notation, where certain prefixes are better used than others. For example, because all the variables you will be adding will be related to members of a [class] dialog box, their name should start with "m_"; then, use "str" for strings [from edit boxes] and "b" for booleans [check boxes].
This naming scheme is called the "hungarian postfix notation".
For example, for my IDC_CHECK_message_action control, I chose the m_bmessage_action variable - view the picture here. Combo boxes also "return" strings. Check the picture of the names I chose for my variables.
#3 - don't forget the variable initialization! In order to handle variable initialization of class members of a dialog box, you should know that the dialog box you are creating - like all the dialog boxes - answers a WM_INITDIALOG message, by calling its OnInitDialog method. It is within the code for such method that the initialization will happen.
Use the ClassWizard / Message Maps and look for the OnInitDialog method. Edit its code.
The default code signals where you should write extra init code, with a "TODO" comment. Write your init code, below that comment line. I wrote this extra code:
//init all the check boxes to a check ON state
m_bmessage_action=TRUE;
m_bprogram_action=TRUE;
m_bshow_message_action=TRUE;
m_bshow_program_action=TRUE;
//init the edit box's variable
m_strmessage="My first VCPP6 program launcher";
//init the combo box's variable
m_strprogram2run="";
//the real important part
UpdateData (FALSE); //put the variables' values into the controls
The UpdateData (FALSE); instruction is the most important line of code we have yet seen.
That function updates the controls with the variable's values when its argument is FALSE; but it updates the variables with the controls' values, when its argument is TRUE.
So UpdateData is a 2-way function: it reads from the controls to the variables, when being passed TRUE; and it writes from the variables to the contros, when being passed FALSE.
Closing a dialog box, on request from any button
And how to EXIT from the application? This one should be the easiest implementation.
Use the ClassWizard / Message Maps to look for the ID of your EXIT button and for the message BN_CLICKED. Then edit the code for the function that will handle such pair. In my case it was method OnBUTTONexit.
And the code was just:
void CMy003_controlsDlg::OnBUTTONexit (){
OnOK();
};
The OnOK(); instruction is the default for a dialog box's OK button, and it closes that window. The names you are reading here correspond to my choice of identifiers. You can have a perfect program, with very different names.
Every class that has the CDialog class as its parent, inherits several stuff, including the methods OnOK and OnCancel. Inheritance means "using without the need to reinvent", and it is a very strong feature of OOP languages, such as C++. Related to inheritance, there is the overriding concept: imagine you want this particular dialog box to answer differently to the OK button?- you could override the default implementation of the OnOK method!
Displaying a message, from an edit box, when requested from a button.
In Lesson #2, we built an application that showed a message box. There we learned how to use the MessageBox function. How about to use it again, for when the user presses the "Display message" button? Easy.
Use the ClassWizard / Message Maps, with the button for displaying the message selected - check my picture. Then edit the code and type something like:
void CMy003_controlsDlg::OnBUTTONdisplay(){
UpdateData (TRUE); //put the controls' values into the variables
MessageBox (m_strmessage, "The message you requested...");
};
Just watch the major importance of doing an UpdateData (TRUE); right now, in order to READ / UPDATE the controls' values into the member variables. IF you needed to update the other way, ie, to change what the controls were showing, from the members' values, you should use the FALSE argument. Don't forget how to use this UpdateData funcion!
Notice that you ask now for the display of a string variable and not a string literal. The program will show whatever is written in the [my] IDC_EDIT_message control.
Clearing the message should be obvious... we just set the associated string to nothing ("") and then we update the dialog box's data.
void CMy003_controlsDlg::OnBUTTONclear() {
m_strmessage="";
UpdateData (FALSE);
};
Activating / de-activating controls on run-time
How will we activate / de-activate the controls that handle the message part of our program, from the check boxes?
Follow these steps:
#1 - use the Class Wizard / Message Maps to associate some method with the check box named "Enable message action". I called the method OnCHECKmessageaction.
#2 - code it:
void CMy003_controlsDlg::OnCHECKmessageaction() {
UpdateData (TRUE); //get controls' values into the variables
if (m_bmessage_action == TRUE){ //check if the ID we set to the check box of the message action is ON
//if it is ON, allow the display of the controls that handle the message part
GetDlgItem (IDC_EDIT_message)->EnableWindow (TRUE); //enable the edit box //enable the buttons
GetDlgItem (IDC_BUTTON_display)->EnableWindow (TRUE);
GetDlgItem (IDC_BUTTON_default)->EnableWindow (TRUE);
GetDlgItem (IDC_BUTTON_clear)->EnableWindow (TRUE);
//enable the static text that asks for a message
GetDlgItem (IDC_STATIC_type_message)->EnableWindow (TRUE);
} else {
GetDlgItem (IDC_EDIT_message)->EnableWindow (FALSE); //disable the edit box
//disable the buttons
GetDlgItem (IDC_BUTTON_display)->EnableWindow (FALSE);
GetDlgItem (IDC_BUTTON_default)->EnableWindow (FALSE);
GetDlgItem (IDC_BUTTON_clear)->EnableWindow (FALSE);
//disable the static text that asks for a message
GetDlgItem (IDC_STATIC_type_message)->EnableWindow (FALSE);
}; //else ends
}; //method ends
NOTICE that this code is written with NO hierarchy, because HTML doesn't allow TABs, but you should nest your code, to make it much more readable.
#3 - this code is very understandable, but it uses a mighty new function - GetDlgItem.
The method's signature is:
HWND GetDlgItem (HWND hDlg, int nIDDlgItem);
This signature refers to many new data types. You'll use such data types, as if you understood them 100%. Fully understanding these data types will happen as you practice. HWND is a "window handle", ie something that allows you to communicate with a window. Beware that every control is considered a window.
So GetDlgItem returns the handle of the specified dialog box's control. Through that handle you can access methods that the controls knows how to execute, such as the EnableWindow function.
Showing / NOT showing controls on run-time
You can do more than just graying out controls. The step ahead is to NOT showing controls at all. The EnableWindow function is useless for that, but there is this ShowWindow alternative :)
OnCHECKshowmessageaction is the method that will handle such task, on my solution. Use CodeWizard / Message Maps to choose the control that corresponds to the check box that turns on / off your own show / do not show settings. Choose the message BN_CLICKED, then "add function" [you can accept the proposed name] and then "edit code", ie, do exactly what you've done for the previous problem.
Type down your equivalent of the following code, which should be much more easy to understand at this point of the lesson: [NOTICE that the only changes, relative to the previous code, are the name of the method and the name of the variable to investigate: m_bmessage_action to m_bshow_message_action]
void CMy003_controlsDlg::OnCHECKshowmessageaction() {
UpdateData (TRUE); //get controls' values into the variables
if (m_bmessage_action == TRUE){ //check if the ID we set to the check box of the message action is ON
//if it is ON, allow the display of the controls that handle the message part
GetDlgItem (IDC_EDIT_message)->ShowWindow (TRUE); //enable the edit box //enable the buttons
GetDlgItem (IDC_BUTTON_display)->ShowWindow (TRUE);
GetDlgItem (IDC_BUTTON_default)->ShowWindow (TRUE);
GetDlgItem (IDC_BUTTON_clear)->ShowWindow (TRUE);
//enable the static text that asks for a message
GetDlgItem (IDC_STATIC_type_message)->ShowWindow (TRUE);
} else {
GetDlgItem (IDC_EDIT_message)->ShowWindow (FALSE); //disable the edit box
//disable the buttons
GetDlgItem (IDC_BUTTON_display)->ShowWindow (FALSE);
GetDlgItem (IDC_BUTTON_default)->ShowWindow (FALSE);
GetDlgItem (IDC_BUTTON_clear)->ShowWindow (FALSE);
//disable the static text that asks for a message
GetDlgItem (IDC_STATIC_type_message)->ShowWindow (FALSE);
}; //else ends
}; //method ends
Enabling / disabling and showing / NOT showing the "program action" part of the dialog box works exactly in the same way as it does for the message part, but only affecting different controls. Check your IDs and repeat the previous steps in order to handle the two check boxes that are still uncoded.
If you can't figure out how to do it, download my final source code.
Running an external program, from a combo-box, from a push-button
At last, we are only missing the final functionality we intended: running an external program!
Running an external program can be a very easy task, thanks to the WinExec function. We should take care with upper and lowercase chars on strings, and because of that we will be using the MakeUpper method from class CString. That method converts a string to all uppercase chars and helps our comparisons.
Applying your previous knowledges, get to edit the code of the function, that will handle the "Run program" button.
I suggest the following source code... If you have little C/C++ experience, you'll think that you could replace the 3 ifs by one switch statement, but that is not true, since switch can't accept expressions that evaluate to CString.
Please NOTICE that the "\" char, starts what is called an "escape sequence". If you want to refer the "\" char alone, you must type "\\". Check how the paths were specified that way. Those paths are only valid for my PC, of course.
WinExec could have many other second arguments, besides SW_SHOW, such as SW_SHOWMAXIMIZE and others, documented on the win32 SDK from Microsoft. SW_SHOW activates the window and displays it in its current size and position.
void CMy003_controlsDlg::OnBUTTONrun() {
//declare a CString object, to hold the name of the program to run
CString str_prg_name="";
//make sure we are reading the correct selection from the combo box
UpdateData (TRUE);
str_prg_name=m_strprogram2run; //get the name, from the variable associated to the combo control str_prg_name.MakeUpper(); //convert string to all uppercase chars
if (str_prg_name=="ALIENS VERSUS PREDATOR") WinExec ("R:\\avp\\AvP.exe", SW_SHOW);
if (str_prg_name=="KINGPIN") WinExec ("T:\\kingpin\\kingpin.exe", SW_SHOW);
if (str_prg_name=="SIM CITY 3000") WinExec ("K:\\sc3000\\Game\\SC3.EXE", SW_SHOW);
};//method ends;
I am glad you kept reading. There are some minor problems when programming a Game Launcher. The most basic problem, and the one I will address right now, is the "current directory". When you try to launch Kingpin, the current directory will be the game laucher's directory... and Kinpin won't be able to find its files.
In order to fix this problem, make sure you use _chdir. For example, for Kingpin, the correct VCPP6 code should be:
if (str_prg_name=="KINGPIN") {
_chdir ("T:\\kingpin");
WinExec ("T:\\kingpin\\kingpin.exe", SW_SHOW);
};
So, two instructions must happen per game: one for changing the current directory; another one for launching the game.
In order to build the program with the _chdir function, you must include the "direct" header file. Right at the beginning of your source code, type #include "direct.h"
And if you have been paying real attention, you did notice that I did NOT implement the "Use default message" button. It is a very simple thing to do, and you should try it alone.
In case you can't figure something out by yourself, jump to the downloads...
all the source files [ZIP - 10K]
the debug release win32 executable [ZIP - 32K]