Step 7: Using common dialog boxes
In this step, you'll implement the event-handling functions you added in
Step 6. The CmFileOpen function, the CmFileSave function, and the CmFileSaveAs function
use the ObjectWindows classes TFileOpenDialog and TFileSaveDialog. These classes
encapsulate the Windows Open and Save common dialog boxes to prompt the user for file
names. You can find the source for Step 7 in the files STEP07.CPP and STEP07.RC in the
directory EXAMPLES\OWL\TUTORIAL.
You'll make the CanClose function check whether the drawing in the
window has changed before the drawing is discarded. If the drawing has changed, the user
is given a chance to either save the file, continue without saving the file, or abort the
close operation entirely.
Also, to implement the CmFileOpen function, the CmFileSave function,
and the CmFileSaveAs function, you need to add two more protected functions,
OpenFile and SaveFile, to the window class. These functions are discussed a little later
in this step.
Changes to TDrawWindow
To implement the menu commands, add some new data members to the
TDrawWindow class: FileData, IsDirty, and IsNewFile.
FileData
The FileData member is a pointer to a TOpenSaveDialog::TData object.
The TOpenSaveDialog class is the direct base class of both the TFileOpenDialog class and
the TFileSaveDialog class. Both of these classes use the TOpenSaveDialog::TData class to
contain information about the current file or file operation, such as the file name, the
initial directory to search, file name filters, and so on.
FileData is initialized in the TDrawWindow constructor to a newed
TOpenSaveDialog::TData object. Because FileData is a pointer to an object, a delete
statement must be added to the TDrawWindow destructor to ensure that the object is removed
from memory when the application terminates.
IsDirty
The IsDirty flag indicates whether the current drawing is
"dirty," that is, whether the drawing has been saved since it was last modified
by the user. If the drawing hasn't been modified, or if the user hasn't drawn anything on
an empty window, IsDirty is set to false. Otherwise, it is set to true. IsDirty is set to
false in the TDrawWindow constructor because the drawing hasn't been modified yet.
Outside of the constructor, the IsDirty flag is set in a number of
functions:
IsNewFile
The IsNewFile flag indicates whether the file has a name. A file has
a name if it was loaded from an existing file or has been saved to disk to some file name.
If the file has a name (that is, if it's been saved previously or was loaded from an
existing file), the IsNewFile flag is set to false. IsNewFile is set to true in the
TDrawWindow constructor because the drawing hasn't yet been saved with a name.
Outside the constructor, the IsNewFile flag is set in a number of
functions:
Improving CanClose
The CanClose function that you've been using since Step 2 of this
tutorial has a couple of flaws. First, whenever it's called, it prompts the user to save
the drawing. This isn't necessary if the drawing hasn't been changed since it was loaded,
saved, or the window was cleared. Second, a simple yes or no answer to this question isn't
sufficient. For example, if the user didn't intend to close the window, the desired
response is to cancel the whole operation.
Checking the IsDirty flag tells the CanClose function whether it's
even necessary to prompt the user for approval of the closing operation. If the drawing
isn't dirty, there's no need to ask whether it's OK to close. The user can simply reload
the file.
If the file is dirty, then the CanClose function pops up a message
box. Using the MB_YESNOCANCEL flag in the message box call gives the user three possible
choices instead of two:
The code for the new
CanClose function looks something like this:
bool
TDrawWindow::CanClose()
{
if (IsDirty)
switch(MessageBox("Do you want to save?", "Drawing has changed",
MB_YESNOCANCEL | MB_ICONQUESTION))
{
case IDCANCEL:
// Choosing Cancel means to abort the close -- return false.
return false;
case IDYES:
// Choosing Yes means to save the drawing.
CmFileSave();
}
return true;
}
Note that the CmFileNew function is
modified in this step to take advantage of the new CanClose function.
CmFileSave function
The CmFileSave function is relatively simple. It checks whether the
drawing is new by testing IsNewFile. If IsNewFile is true, CmFileSave calls CmFileSaveAs,
which prompts the user for a file in which to save the drawing. Otherwise, it calls
SaveFile, which does the actual work of saving the drawing.
The CmFileSave function should look something like this:
void
TDrawWindow::CmFileSave()
{
if (IsNewFile)
CmFileSaveAs();
else
SaveFile();
}
CmFileOpen function
The CmFileOpen function is also fairly simple. It first checks
CanClose to make sure it's OK to close the current drawing and open a new file. If the
CanClose function returns false, CmFileOpen aborts.
After ensuring that it's OK to proceed, CmFileOpen creates a
TFileOpenDialog object. The TFileOpenDialog constructor can take up to five parameters,
but for this application you need to use only two. The last three parameters all have
default values. The two parameters you need to provide are a pointer to the parent window
and a reference to a TOpenSaveDialog::TData object. In this case, the pointer to the
parent window is the this pointer. The TOpenSaveDialog::TData object is provided by
FileData.
Once the dialog box object is constructed, it is executed by calling
the TFileOpenDialog::Execute function. There are only two possible return values for the
TFileOpenDialog, IDOK and IDCANCEL. The value that is returned depends on whether the user
presses the OK or Cancel button in the File Open dialog box.
If the return value is IDOK, CmFileOpen then calls the OpenFile
function, which does the actual work of opening the file. The Execute function also stores
the name of the file the user selected into the FileName member of FileData. If the return
value is not IDOK (that is, if the return value is IDCANCEL), no further action is taken
and the function returns.
The CmFileOpen function should look something like this:
void
TDrawWindow::CmFileOpen()
{
if (CanClose())
if (TFileOpenDialog(this, *FileData).Execute() == IDOK)
OpenFile();
}
CmFileSaveAs function
The CmFileSaveAs function can be used in two ways: to save a new
drawing under a new name and to save an existing drawing under a name different from its
present name.
To determine which of these the user is doing, CmFileSaveAs first
checks the IsNewFile flag. If the file is new, CmFileSaveAs copies a null string into the
FileName member of FileData. If the file is not new, FileName is left as it is.
The distinction between these two is quite important. If FileName
contains a null string, the default name in the File Name box of the File Open dialog box
is set to the name filter found in the FileData object, in this case, *.pts. But if
FileName already contains a name, that name plus its directory path is inserted in the
File Name box.
Once this has been done, TFileSaveDialog is created and executed.
This works exactly the same as TFileOpenDialog does in the CmFileOpen function. If the
Execute function returns IDOK, CmFileSaveAs then calls the SaveFile function.
The CmFileSaveAs function should look something like this:
void
TDrawWindow::CmFileSaveAs()
{
if (IsNewFile)
strcpy(FileData->FileName, "");
if ((new TFileSaveDialog(this, *FileData))->Execute() == IDOK)
SaveFile();
}
Opening and saving drawings
The CmFileOpen, CmFileSave, and CmFileSaveAs functions only provide
the interface to let the user open and save drawings. The actual work of opening and
saving files is done by the OpenFile and SaveFile functions. This section describes how
these functions perform these actions, but it doesn't provide technical explanations of
the entire functions.
OpenFile function
The OpenFile function opens the file named in the FileName member of
the FileData object as an ifstream, one of the standard C++ iostreams. If the file can't
be opened for some reason, OpenFile pops up a message box informing the user that it
couldn't open the file and then returns.
Once the file is successfully opened, the Line array is flushed.
OpenFile then reads in the number of points saved in the file, which is the first data
item stored in the file. It then sets up a for loop that reads each point into a
temporary TPoint object. That object is then added to the Line array.
Once all the points have been read in, OpenFile calls Invalidate.
This invalidates the window region, causing a WM_PAINT message to be sent and the new
drawing to be painted in the window.
Lastly, OpenFile sets IsDirty and IsNewFile both to false. The
OpenFile function should look something like this:
void
TDrawWindow::OpenFile()
{
ifstream is(FileData->FileName);
if (!is)
MessageBox("Unable to open file", "File Error",
MB_OK | MB_ICONEXCLAMATION);
else {
Line->Flush();
unsigned numPoints;
is >> numPoints;
while (numPoints--) {
TPoint point;
is >> point;
Line->Add(point);
}
}
IsNewFile = IsDirty = false;
Invalidate();
}
SaveFile function
The SaveFile function opens the file named in the FileName member of
FileData as an ofstream, one of the standard C++ iostreams. If the file can't be opened
for some reason, SaveFile pops up a message box informing the user that it couldn't open
the file and then returns.
Once the file has been opened, the function Line->GetItemsInContainer
is called. The result is inserted into the file. This number is read in by the OpenFile
function to determine how many points are stored in the file.
After that, SaveFile sets up an iterator called i from Line. This
iterator goes through all the points contained in the Line array. Each point is then
inserted into the stream until there are no points left.
Lastly, IsNewFile and IsDirty are set to false. Here is how the
SaveFile function should look:
void
TDrawWindow::SaveFile()
{
ofstream os(FileData->FileName);
if (!os)
MessageBox("Unable to open file", "File Error",
MB_OK | MB_ICONEXCLAMATION);
else {
os << Line->GetItemsInContainer();
TPointsIterator i(*Line);
while (i)
os << i++;
IsNewFile = IsDirty = false;
}
}
CmAbout function
The CmAbout function demonstrates how easy it is to use custom
dialog boxes in ObjectWindows. This function contains only one line of code. It uses the
TDialog class and the IDD_ABOUT dialog box resource to pop up an information dialog box.
TDialog can take up to three parameters:
Note: Usually you
don't actually pass in a TResId reference. Instead you pass a resource identifier number
or string, just as you would for a dialog box created using regular Windows API calls.
Conversion operators in the TResId class resolve the parameter into the proper type. The third parameter, a TModule *, usually uses its default value. Once the dialog box object is constructed, all that needs to be done is to call
the Execute function. Once the user closes the dialog box and execution is complete,
CmAbout returns. The temporary TDialog object goes out of scope and disappears.
The code for CmAbout should look like this:
void
TDrawWindow::CmAbout()
{
TDialog(this, IDD_ABOUT).Execute();
}
Where to find more information
Here's a guide to where you can find more information on the topics
introduced in this step:
|