
Programming Sail Using Visual C++
Introduction
For the graphical user interface (GUI) to the SAIL
project, Visual C++ was chosen to be the programming platform. While a few
of the SAIL control modules had been programmed using
the Microsoft Foundation Class (MFC), many others were written using the
C language or within another integrated programming environment. The variety
of code used caused some problems when I attempted to put together a sensible,
user friendly interface. Using Visual C++ and the MFC to do the majority
of the interface work greatly eased my job. I did, however, have to write
code using Win32 API calls in the cases where MFC did not directly provide
enough flexibility to incorporate the variety of code already written.
The opening screen of the SAIL user interface
looks very similar to the prototype which Laura Blackwood had begun developing
the previous year, although it's functionality is entirely different. If
all of the researchers in the lab would have used Laura's basic prototype
for modeling their individual modules, the entire SAIL
package would have been fairly easy to assemble. As it was, much of the
code in use has been built on code and functions developed in previous years,
and none of it was designed in such a manner to be consistent with Laura's
vision. Given the wide variety of software which needed to function together
as one virtual package, I approached the problem from a slightly different
angle.
Having never before worked with an integrated programming environment
such as Visual C++, there was quite a steep learning curve for me when I
began my tasks at the MSU PRIP
Lab. Luckily, there were many fine resources as well as online help
and tutorials available in the lab to help me become quickly familiar with
the basic functionality of the Visual C++ environment and the MFC. One book
in particular, Charles Petzold's, Programming Windows95,
proved to be very helpful when I needed to go beyond MFC and use API
calls directly. According to the author, "[his] book shows the hard
way to do Windows programming--but it is also the most basic fundamental,
and powerful way. Moreover, by learning classical Windows programming using
C and the raw APIs, you can more clearly understand how Windows and your
application interact...."
Most of the books talk about a 6 month period for really learning and
understanding Windows programming, and from my experience, this may well
be true. In the 3 months which I had to accomplish my project, was barely
enough time to get it done. The programming experience I gained with Windows,
however, will be very beneficial to me forever, and I feel very fortunate
to have had this chance to develop my programming skills.
The Project
The project I was involved with was to create a sensible manual
user interface for the SAIL robot. In order to
tie in the variety of programs without having to rewrite each one individually,
I developed an interface which calls up the other modules rather than directly
incorporating them into the interface. My interface displays each module
on its own tab and appears to work as a single unit to the user, even though
in effect many separate programs may be running simultaneously. There are
two advantages to this approach. The first is, when SAIL
is upgraded to a multiprocessor machine, the modules will be better able
to take full advantage of the processing power since in effect the program
runs as a multi-threaded process since each module is its own process rather
than part of my program (some of the individual modules are multi-threaded
themselves when deemed necessary). The second advantage of this approach
is that it is very easy to add additional functionality to the robot by
adding additional modules, which are really just frameless dialog boxes
along with any code needed to make them functional.
To date, there are seven modules -- two head modules which control the
robots vision, one a manual control and
the other an automatic convergence control;
two speech modules, one which provides for text
reading and the other which builds words and sentences phonetically;
one Reach module which controls the robots arm;
one Listen module which controls the robots 'ears';
and one Move module which controls the mobile base
unit.
Additional modules can be added as needed. The bulk of this paper will
show how to incorporate another module into the project and in doing so
will give a little insight into how the SAIL GUI
is currently put together.
The SAIL GUI program is aptly named Sail.exe.
When this executable is run a number of thing happen. First, the user interface
framework is drawn on the screen with the client area being filled with
a tab control. This part of the interface was implemented using the MFC
classes and was fairly easy to code since Visual C++ and MFC did the majority
of the work for me. The second thing that happens is a blank dialog box
is called up by the framework initialization routine which covers the entire
client area except for the tabs of the tab control. The reason this blank
dialog box is implemented, is so that when the user opens another tab by
choosing a module from the menu, the previous modules dialog box gets blanked
over while the new module is being loaded into memory. This gives the user
an instant visual indication that the program is indeed responding to their
mouse action. Finally, the framework initialization routine creates a tab
page on the tab control labeled "Main", and then calls the program
Main.exe which draws a dialog window over top of the SAIL interface window
so that it appears to be part of the Main tab though it is actually another
process running with its window brought to the top of the Z-order. When
the initial splash screen (which is nice although really only for decoration)
disappears, either after a time-out period or when the user clicks the left
mouse button on the SAIL interface window, the resulting screen looks like
this:

As you can see, the result looks entirely like a normally implemented
tab control in Windows with only one tab being available, although in reality
at this time there are already four processes running -- the Main tab dialog
window over top of the blank dialog window over top of the SAIL framework
window in addition to a dynamically linked library which provides for error
and status messages to be displayed in the main window. This may at first
seem unnecessary since the main tab is always available and cannot be removed
from the tab control. However, because of the implementation of this program,
this is a very necessary step. If this entire framework as seen above were
one window, then every time the user would choose an item from the main
menu, the entire window would come to the top of the Z-order and the main
tab would become the visible one regardless of what was previously the currently
selected tab. Obviously, the main tab being brought to the front every time
the mouse choose an item from the menu would be completely unacceptable
behavior. Therefore, even the Main tab is implemented as a separate process.
Although I won't go into detail here about every item on each tab (see
the Robot Manual Mode for detailed information),
there is one thing I should mention here. The main window on the main tab
shown above is a text window implemented through a direct link library (dll)
named Message.dll. This is a shared dll and any of the modules can write
text to this window simply by calling the Message function in their module
(of course they have to include the message library in the linking of their
code). This message window can be saved to a file or printed out and used
for diagnostic purposes when the robot is running in automatic mode. That
is why the main tab is always loaded with the Sail executable program so
that the message window is always available when the other modules wish
to send error or status messages to this common destination.
One of the reasons that this interface works so well is that it is of
a fixed unmovable size. The size was fixed at 640 x 480 so that it would
display properly on the LCD monitor which is used with the SAIL robot. In
order for it to display properly on larger monitors, the window interface
places itself in the upper left corner of the monitor where it remains as
long as it is running. This way all the modules windows properly overlay
one another as they are accessed.
The Main Function Calls
The heart of the Sail program are a number of functions which perform
the following tasks:
- load new program modules as the user requests them
- brings the window of the current task to the forefront of the Z-order
when the user selects a different tab
- shut down the the corresponding process when the user closes a tab
- shut down all running processes and clean up when the sail program
exits
The functions which perform these tasks are listed below:
- The Windows API functions:
- EnumThreadWindows
- CreateProcess
- Application Specific functions:
- WindowToTop(hWnd, lParam )
- WindowToBottom(hWnd, lParam )
- WindowTerminate(hWnd, lParam )
Starting A Process
The application specific functions are callback functions and are used
in conjunction with the Windows API EnumThreadWindows function. The Windows
API CreateProcess function is called when a user starts using one of the
program modules by choosing an item from the menu bar. The CreateProcess
function does a number of things. First, if the function is successful,
a Boolean value of TRUE is returned. I use this value
to be sure that the requested module was found and could be loaded into
memory. Secondly, the CreateProcess function takes as an argument a PROCESS_INFORMATION structure which is updated when the
function returns. From this structure, I can retrieve information about
the process created. The two values I store and use store are a handle to
the process (hProcess) and the thread identification (threadID) of the process.
If the CreateProcess function is successful, I then create a new tab
in the tab control with an appropriate title to identify the process associated
with that tab. For example, in the graphic above, the main window is associated
with the "Main" tab. When the user clicks the "Main"
tab, the main process window will come to the front of the Z-order regardless
of which other processes are currently running.
EnumThreadWindows
This brings us to the EnumThreadWindows function which is provided as
part of the Win32 API. This function takes three arguments. The first argument
is the threadID which was retrieved from the PROCESS_INFORMATION
structure when the CreateProcess function was called. The second
argument is the name of the callback function which the EnumThreadWindows
function is going to use and the last argument can be additional information
that you need to pass to the callback function specified in the second argument.
The EnumThreadWindows function then begins to enumerate or step through
all of the windows which are associated with the processId which was passed
to it as the first argument. For each window of the process, EnumThreadWindows
makes a call to the specified callback function with the handle of the window
and the additional information which was supplied to it as a third argument.
The callback function then does whatever I programmed it to due and returns
control to the EnumThreadWindows function which gets the handle of the next
window of the process and again invokes the callback function which was
specified. This continues until there are no more windows owned by the threadID
specified or until a value of FALSE is returned from
the callback function.
The Callback Functions
The three callback functions used in the SAIL project provide a means
for altering the Z-order of the windows displayed on screen as well as a
way for terminating the various modules of the robot when no longer needed
or when the program shuts down. As specified above, these three functions
are WindowToTop, WindowToBottom, and WindowTerminate. The use of each function
should be rather obvious from the names I choose, but we will view them
in some detail here to see how they work within the SAIL framework. The
WindowToTop function is specified like this:
BOOL CALLBACK CSAILView::WindowToTop(HWND hWnd,
LPARAM lParam)
{
if(g_pCurrentWnd = CWnd::FromHandle(hWnd))
g_pCurrentWnd->SetWindowPos(&wndTopMost, 0, 0, 0, 0, SWP_NOMOVE |
SWP_NOSIZE);
return FALSE;
}
When EnumThreadWindows first invokes this callback function it sends
the handle of the main window of the process to the function. I do not need
the additional information which could be specified in the lParam argument
and so make no use of it in this function. What I do use is the handle of
the window. From this, I get a pointer to the window using the CWnd::FromHandle
function which is then stored in the global variable g_pCurrentWnd (g for
global, p for pointer and CurrentWnd to indicate this is a pointer to a
window). Using this pointer, I can then set the position of the window on
screen. In the SetWindowPos function, we send the address of the top most
window and our window is then placed above this in the Z-order. The return
value of FALSE means that I do not want the EnumThreadWindows
function to call us back again since I have already performed the only step
that is required by the SAIL program. Notice the SWP_NOMOVE
and SWP_NOSIZE parameters which SetWindowPos takes
as the last argument. These insure that the window is not moved or resized
with this call. It is only brought to the top of the Z-order.
The next callback function that I make use of is this:
BOOL CALLBACK CSAILView::WindowToBottom(HWND hWnd,
LPARAM lParam)
{
if(g_pCurrentWnd = CWnd::FromHandle(hWnd))
g_pCurrentWnd->SetWindowPos(&wndNoTopMost, 0, 0, 0, 0, SWP_NOMOVE
| SWP_NOSIZE);
return FALSE;
}
It provides basically the same functionality as the previous function
only it moves a window to the bottom of the Z-order rather that to the top.
The final callback function used is this:
BOOL CALLBACK CSAILView::WindowTerminate(HWND
hWnd, LPARAM lParam)
{
if(g_pCurrentWnd = CWnd::FromHandle(hWnd))
g_pCurrentWnd->SendMessage(WM_CLOSE);
return TRUE;
}
This function again gets a pointer to the main window of the process
used in the EnumThreadWindows call which invoked this function. I use the
WM_CLOSE message to terminate the process specified rather than using a
brute force method so that the process can ask the user to save information
before closing if it needs to do this.
Creating a New Module for use in SAIL
Now lets take a step by step look at adding a new module to the SAIL
project. In doing so, we can accomplish two things. First, we'll see the
steps needed to add another module and secondly, we can gain some insight
into the functionality of the program.
Lets pretend that we have a new module called Smell to add to the manual
user interface for the SAIL project. Furthermore, assume that the name of
the program is smell.exe and that it is a frameless dialog window of no
greater than 625 x 390 pixels with its origin at (7,45). The size restriction
is because of the screen size of the LCD display used with the SAIL robot.
It is easy to create a dialog based interface using Visual C++ by choosing
the dialog-based option when stepping through the AppWizard creation process.
To make the dialog frameless, change the styles in the dialog properties
to 'none' for border and uncheck the titlebar box which will remove the
titlebar form the dialog. Also the following lines must be commented out
in the SmellDlg.cpp file OnInitDialog() function.
/*
// Add "About..." menu item to system
menu.
// IDM_ABOUTBOX must be in the system command
range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
// Set the icon for this dialog. The framework
does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
*/
This is because we aren't using a system menu in our dialog since the
titlebar is not present and trying to add one will cause an error in the
program.
If you would rather create a multiple-tabbed dialog box using the CPropertyPage
class, the procedure is a little more involved. after creating the property
pages and the property sheet, you must change a little code to get the frameless
look that we need To make the property sheet frameless, remove (or comment
out) the following code which was generated by the AppWizard in Visual C++
and is in the CSmellDialog class InitInstance function:
/*
CSmellDialog dlg("Smell Control", NULL,
1);
m_pMainWnd = dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
// Since the dialog has been closed, return FALSE
so that we exit the
// application, rather than start the application's message pump.
return FALSE;
*/
Add this code instead:
CSmellDialog* dlg = new CSmellDialog();
dlg->Create(NULL, WS_POPUP , 0);
dlg->SetWindowPos(NULL, 5, 68, 0, 0, SWP_NOSIZE);
dlg->ShowWindow(SW_SHOW);
m_pMainWnd = dlg;
return TRUE;
Of course in place of CSmellDialog you would use whatever class the
AppWizard created for you. If left to App Wizard, this would be C[NAME_OF_PROGRAM]Dialog. In other words, in place of Smell
there would be whatever you named the program when you first create it with
the AppWizard. For a closer look at a frameless property sheet example,
examine the code for the Move module which incorporates the changes as shown
above.
In either case, remember to keep the size of the dialog box smaller
than 625 x 390 pixels with its origin at (7,45) and you will be all set
to use this 'Smell' module in the SAIL project.
Here's an example of a simple smell window ready for use in the SAIL
project:

Implementing the new Module in SAIL
Now that we have a module to use, we must modify the SAIL program to
allow the user to use the new module. This requires several modifications,
but all are within the CSailView class program and header files so it is
fairly easy to accomplish. First we must add some new variables to the CSailView
header file and initialize them in the CSailView constructor. the variables
we need are as follows:
- UINT m_nSmellTab;
- BOOL m_bSmellCtrlExists;
- HANDLE m_hSmellHandle;
- DWORD m_dwSmellThrdID;
The m_nSmellTab variable stores the position of the smell tab in the
tab list when the Smell module is activated by the user from the menu and
is initialized to Zero. It is used to determine when the smell tab is clicked-on
so that the Smell dialog box can brought to the front of the Z-order. The
m_bSmellCtrlExists variable keeps track of whether or not the Smell module
is in use so that the menu can properly reflect the status of the module
(menu item is checked when in use). It also is used to be sure that only
one instance of the smell module can be activated. It is initially set to
FALSE. The m_hSmellHandle is a handle to the Smell
process which is useful in terminating the process when necessary and it
is initialized to NULL. The m_dwSmellThrdID is the
threadID of the Smell process to be used in conjunction with the EnumThreadProcess
function and it is also initially set to NULL.
We also must add a menu item for the smell control and this is easy
to do using the the resource editor which is part of the Visual C++ package.
Now that the necessary variables are created and initialized, and we
have a menu item for the Smell module, we need to add functions to be called
when the user chooses the Smell module from the menu and also to update
the status of the menu. These functions we will name OnSmellControl
and OnUpdateSmellControl. Since these functions
must be called in response to user input, they must be included in the message
map for the CSailView class. Therefore we need to add the following lines
to the CSailView header file in the message map section:
afx_msg void OnSmellControl();
afx_msg void OnUpdateSmellControl(CCmdUI* pCmdUI);
In addition, these lines must be added to the CSailView.cpp file in
the message map section:
ON_COMMAND(ID_SMELL_CONTROL, OnSmellControl)
ON_UPDATE_COMMAND_UI(ID_SMELL_CONTROL, OnUpdateSmellControl)
This gives Windows the instruction on which functions to call and in
which case to call them. When the user chooses the Control item from the
Smell menu, Windows adds a call to OnSmellControl to the message cue and
that function will be called in our CSailView class.
The easiest way to create these two functions, is to copy existing functions
and then to modify them for the new module. The OnReachControl
function looks as follows and is easily modified to become an OnSmellControl
function.
void CSAILView::OnReachControl()
{
if(m_bReachCtrlExists) {
// Remove the Reach tab, set the focus to another tab, decrement the number
of tabs,
// reset the m_bReachCtrlExists to FALSE and renumber the tabs as needed.
// NOTE: There needs to an if statement included for every possible tab.
if(m_hReachHandle) {
EnumThreadWindows(m_dwReachThrdID, (WNDENUMPROC)WindowTerminate,
(LPARAM)(LPTSTR)szWindowName );
m_dwReachThrdID = 0;
m_hReachHandle = NULL;
}
m_nCurTab = m_pTabCtrl->GetCurSel();
m_pTabCtrl->DeleteItem(m_nReachTab);
m_bReachCtrlExists = FALSE;
m_nNumTabs--;
if(m_nListenTab > m_nReachTab)
m_nListenTab--;
if(m_nSpeakTab > m_nReachTab)
m_nSpeakTab--;
if(m_nSpeakLpcTab > m_nReachTab)
m_nSpeakLpcTab--;
if(m_nMoveTab > m_nReachTab)
m_nMoveTab--;
if(m_nHeadTab > m_nReachTab)
m_nHeadTab--;
if(m_nAutoHeadTab > m_nReachTab)
m_nAutoHeadTab--;
if(m_nCurTab == m_nReachTab) { // m_nReachTab
was the active tab
m_pTabCtrl->SetCurSel(m_nCurTab - 1);
TabChanged(m_nCurTab - 1);
}
} else {
// Create a Reach tab, set the tab number and make the new tab have the
focus
// Also change the m_bReachCtrlExists to TRUE to control the menu.
ZeroMemory(&m_StartupInfo, sizeof(m_StartupInfo));
ZeroMemory(&m_ProcessInfo, sizeof(m_ProcessInfo));
m_StartupInfo.cb = sizeof(m_StartupInfo);
m_StartupInfo.dwFlags = STARTF_FORCEONFEEDBACK;
if(::CreateProcess(".\\ReachDlg\\Debug\\MainCtrl.exe",NULL,
NULL, NULL, NULL, 0, NULL, NULL,
&m_StartupInfo, &m_ProcessInfo)) {
m_dwReachThrdID = m_ProcessInfo.dwThreadId;
m_hReachHandle = m_ProcessInfo.hProcess;
m_TabCtrlItem.mask = TCIF_TEXT;
m_TabCtrlItem.pszText = "Reach";
m_pTabCtrl->InsertItem(m_nNumTabs, &m_TabCtrlItem);
m_pTabCtrl->SetCurSel(m_nNumTabs);
m_nReachTab = m_nNumTabs;
m_nNumTabs++;
m_bReachCtrlExists = TRUE;
EnumThreadWindows(m_dwBlankThrdID, (WNDENUMPROC)WindowToTop,(LPARAM)(LPTSTR)szWindowName
);
Sleep(1500);
EnumThreadWindows(m_dwReachThrdID, (WNDENUMPROC)WindowToTop,(LPARAM)(LPTSTR)szWindowName
);
}
}
}
Before we modify the code, however, lets take a good look at it as
this is where the greatest functionality of the SAIL interface is implemented.
Deleting a Tab in SAIL
The first statement in the OnReachControl
function is a check to see is the control already exists. If it does, then
we know that the user has chosen to terminate use of this control and we
go ahead and do this. The next statement checks to be sure that the threadID
pointer is valid (non-NULL), and if so, the call to
EnumThreadWindows proceeds and the Reach module is then terminated. The
remainder of the statements in the first half of the function are used to
update the Reach variables, remove the Reach tab, and, to rearrange and
reassign the remaining tabs in order to maintain a valid tab count and ordering.
There are three possibilities when the Reach tab is removed. One, is that
the Reach tab is the currently selected tab. The second possibility is that
a lower number tab than the Reach tab is the current tab, and the last possibility
is that the currently selected tab is greater than the Reach tab. The reason
we need to know this is because we would like the currently selected tab
to remain selected, or in the case that the reach tab was the current tab,
we need to select another tab when the Reach tab is removed.
In order to make the programing easier, I choose to select the tab previous
to the Reach tab to become the selected tab in the case that the Reach tab
is removed it is the selected tab.This is because the Reach tab can never
be the first tab (the main tab is always available) and so it always has
a predecessor which makes for only on case rather than testing for two possible
cases.
Before we delete any of the tabs, we must get the currently selected
tab in order to make some choices about which tab will become active. After
we have saved the current tab (in the m_nCurTab variable), we go ahead and
delete the Reach tab and decrement the total tab count ( the variable m_nNumTabs).
Next we decrement the tab number of any tabs that followed the Reach tab.
Now that the tabs are all properly ordered, we must set the proper tab
as the selected tab. The only case we have to worry about is when the Reach
tab was the current tab. In this case, we set the current tab to one less
than it was and activate that tab. That is the purpose of these statements:
if(m_nCurTab == m_nReachTab) { // m_nReachTab
was the active tab
m_pTabCtrl->SetCurSel(m_nCurTab - 1);
TabChanged(m_nCurTab - 1);
}
In all of the other cases, the current tab will remain the current tab
and the software module pertaining to that tab will remain where it was
at the top of the Z-order. Also note that all of the variables corresponding
to the reach module have been reset to their initialized default values.
Adding a Tab in SAIL
The second half of the OnReachControl example from above takes care
of creating a process module and adding the corresponding tab to the tab
control. If the Reach tab does not exist (the first check in the function),
then we create it. If the Reach module is found and loaded, then we go ahead
and create a tab for it in the tab control and bring it's window to the
top of the Z-order. We also increment the tab count (m_nNumTabs) and initialize
the variables corresponding to the reach module ( the m_hReachHandle,
the m_dwReachThrdID, the value of the m_nReachTab and the m_bReachCtrlExists
variable).
The last two function calls within the OnReachControl function are of
some interest. The first call to EnumThreadWindows with the arguments m_dwBlankThrdID and WindowToTop,
calls up a blank dialog box to the top of the Z-order which effectively
blanks out the previous screen and gives the user an instantaneous visual
feedback that the program is working while the process that was created
gets its window created. Once the widow is created, we call EnumThreadWindows
again to bring that new window to the top of the Z-order. To the end user,
the effect is one of a single program in action rather than a multitude
of programs working together.
Adding an OnSmellControl Function to SAIL
The easiest way to add this function to the CSailView class as stated
before, is to copy the body of an existing function (such as OnReachControl
as above), and to modify it somewhat. The first modification is just a matter
of changing the function name form OnReachControl to OnSmellControl. Next
we change all of the variable names from the old names to the new ones like
this:
- m_nReachTab becomes m_nSmellTab
- m_bReachCtrlExists becomes m_bSmellCtrlExists
- m_hReachHandle becomes m_hSmellHandle
- m_dwReachThrdID becomes m_dwSmellThrdID
If you keep the names consistent as above so that only one part of the
name changes, as in Reach becomes Smell, it is very easy to make these changes
in any kind of text editor that will change all occurrences of the string
Reach to the string Smell. This way you can just copy the OnReachControl
function into the editor, changes all the occurrences of Reach to Smell
and then paste the whole thing back into the CSailView file. This way, you
won't accidentally miss any occurrences that should have been changed and
the bulk of the work is done automatically for you by the text editor.
The next item that needs to be changed, is the path to the Smell program
has to be put into the CreateProcess portion of the OnSmellControl function.
In our example, the path is ".\\Smell\\Debug\\Smell.exe"
which is a relative path rather than a full path name. Usually it is better
to use the relative path in case we need to move the files to a new drive
or machine. As long as the files are all moved as a group so that relative
paths are maintained, then we do not have to go back and update the paths
to the executable modules if such a move should ever occur.
Now that we have the possibility of a new tab, all of the functions
that modify the tabs must be updated to test for the new tab when repositioning
the tabs. In the SAIL project at this time, there are seven modules and
seven corresponding functions that need to have a test (if statement) added.
They are OnReachControl, OnListenControl,
OnSpeakControl, OnSpeakLpc,
OnMoveControl, OnHeadControl
and OnHeadAutomatic. For an example, we'll
just modify OnReachControl although all of the other functions will need
to have a similar update as well. For the OnReachControl function, we just
add the following lines to the first half of the function where we are testing
to see if the tabs exist or not. Her are the lines to be inserted:
if(m_nSmellTab > m_nReachTab)
m_nSmellTab--;
What we are testing for, is to see if the smell tab came after the Reach
tab. If it did, then its tab number needs to be decremented when the reach
tab is removed. After we add similar lines to all seven functions, we need
to make one more change to the OnSmellControl function. Since we copied
the OnReachControl function to create the OnSmellControl function, the OnSmellControl
function does not contain a test to check for the Reach tab. so we'll add
one as follows:
if(m_nReachTab > m_nSmellTab)
m_nReachTab--;
Now that all of the tabs function properly we have to add the body of
the function that updates the Smell menu item, that is OnUpdateSmellControl(CCmdUI*
pCmdUI). This function was already declared above and added to the message
map for the CSailView class. The body of the function looks like this:
void CSAILView::OnUpdateSmellControl(CCmdUI* pCmdUI)
{
if(m_bSmellCtrlExists)
pCmdUI->SetCheck(1);
else
pCmdUI->SetCheck(0);
}
This function is called by Windows when the user chooses a menu item.
What we need to do, is check that menu item when the module is running and
uncheck it if it is not been loaded. To so this we simply check the variable
m_bSmellCtrlExists which is a boolean value
set to TRUE or FALSE in the OnSmellCommand function discussed earlier. If it
is TRUE then we set the checkmark and if not, we don't
set the checkmark.
One Last Change
The only thing we need to add now is a statement in the CSailView destructor
to test to see if our new module is in service when the user terminates
the SAIL program. Since we don't want to leave any loose modules lying around
in memory when we exit SAIL, we add a statement to test to see if the Smell
module has been loaded. If it has, then we use our EnumThreadWindows
function once again with the Smell threadID and the TerminateWindows
callback functions as arguments to terminate the Smell process. Here's the
only required code:
if(m_dwSmellThrdID)
EnumThreadWindows(m_dwSmellThrdID, (WNDENUMPROC)WindowTerminate, (LPARAM)(LPTSTR)szWindowName
);
Simply stated, if the m_dwSmellThrdID is valid, terminate the process.
So here's a view of our Smell module when called from the SAIL interface.

Conclusion
That's all there is to it. Once a new module is needed, it is a simple
matter to add it to the SAIL program. Since the modules are separate entities,
the researches can work on them freely without worrying about corrupting
the Sail program itself. The only restriction is to keep the dialog boxes
for user interaction, small enough so as not to obscure the rest of the
SAIL interface(ie. no larger than 625 x 390 pixels with an origin at (7,45)).
Although I doubt this type of program has much value in the commercial
world (it is of fixed unmovable window size), it appears to be able to work
nicely for our research purposes since it isolates the SAIL program from
corruption by the individual modules as they are developed and refined.
In addition, this manual user interface is only for visual feedback
when trying out new functions and procedures with the Sail robot. In the
end, when SAIL becomes an Autonomous Learning entity, the manual interface
will be of little value, and therefore should not require a lot of energy
to maintain. In the meantime, it should be easily expanded to accommodate
additional modules as they are developed.
So this is what I came up with. Is it the best implementation to meet
the requirements of the SAIL project?
I doubt it.
Would I do it the same way again?
Depending on the amount of programming experience I get in the next
few years, I'll probably look back and laugh at this first effort. And no
doubt I'll find an easier and more efficient way to implement a program
of this sort. However, this first contact with Windows programming and with
the Visual C++ Integrated Environment has given me a glimpse of the power
and possibilities of programming for Windows. The knowledge I take with
me from this summer job has given me a great start on becoming a real programmer
in a real world. And I am thankful for that opportunity.
THANKS
There are many great books on Windows programming that have helped me
along and I'd like to list a few of the more important ones here:
- Petzold, Charles. Programming Windows95. Redmond: Microsoft
Press 1996
- Telles, Matthew and Andrew Cooke. Windows95 API How-To. Waite Group
Press: Corte Madera 1996
- Stanfield, Scott and Ralph Arvesen. Visual C++ 4. Waite Group Press:
Corte Madera 1996
- Telles, Mathew. Beginning Visual C++ Components Wrox Press Ltd. Chicago
1996
In addition, I'd like to thank John Weng for the opportunity to work
in his lab this summer. And Laura Blackwood for the documentation she left
on her interface design (I didn't have to start completely from scratch.).
That's about it for now. I've got to get back to the books. Things are
changing so rapidly, that there's no time to rest or I'll be falling behind
just as I've begun to make headway.