Step 7: Using common dialog boxes

Back Home Up Next

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:

In the EvLButtonDown function, IsDirty is set to true to reflect the change made to the drawing.
In the CmFileNew function, IsDirty is set to false when the window is cleared.
In the OpenFile and SaveFile functions, IsDirty is set to false to reflect that the drawing hasn't been modified since last saved or loaded.

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:

In the CmFileNew function, IsNewFile is set to true when the window is cleared.
In the OpenFile and SaveFile functions, IsNewFile is set to false to reflect that the drawing has been saved to disk.

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:

Choosing Cancel means the user wants to abort the entire close operation. In this case, when MessageBox returns IDCANCEL, the CanClose function returns false, signaling to the calling function that it's not all right to proceed.
Choosing Yes means that the user wants to save the file before proceeding. When MessageBox returns IDYES, the CanClose function calls the CmFileSave function (CmFileSave is explained later in this section). After calling CmFileSave, CanClose returns true, signaling to the calling function that it's all right to proceed.
Choosing No means that the user doesn't want to save the file before proceeding. In this case, CanClose takes no further action and returns true.

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:

The first parameter is a pointer to the dialog box's parent window. Just as with the TFileOpenDialog and TFileSaveDialog constructors, you can use the this pointer, setting the parent window to the TDrawWindow object.
The second parameter is a reference to a TResId object. This should be the resource identifier of the dialog box resource.

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:

The CanClose function is discussed in "Application and module objects" in the ObjectWindows Programmer's Guide.
Dialog boxes, including the TFileOpenDialog and the TFileOpenDialog classes, are discussed in "Dialog box objects" in the ObjectWindows Programmer's Guide.
 


Copyright © 1998-2001 Yura Bidus. All rights reserved.