CFile is a relatively simple class that encapsulates the portion of the Win32 API that deals with file I/O. Among its 25-plus member functions are functions for opening and closing files, reading and writing file data, deleting and renaming files, and retrieving file information. Its one public data member, m_hFile, holds the handle of the file associated with a CFile object. A protected CString data member named m_strFileName holds the file name. The member functions GetFilePath, GetFileName, and GetFileTitle can be used to extract the file name, in whole or in part. For example, if the full file name, path name included, is C:\Personal\File.txt, GetFilePath returns the entire string, GetFileName returns "File.txt," and GetFileTitle returns "File."
But to dwell on these functions is to disregard the features of CFile that are the most important to programmers—that is, the functions used to write data to disk and read it back. The next several sections offer a brief tutorial in the use of CFile and its rather peculiar way of letting you know when an error occurs. (Hint: If you've never used C++ exception handling, now is a good time to dust off the manual and brush up on it.)
Files can be opened with CFile in either of two ways. The first option is to construct an uninitialized CFile object and call CFile::Open. The following code fragment uses this technique to open a file named File.txt with read/write access. Because no path name is provided in the function's first parameter, Open will fail unless the file is located in the current directory:
CFile file; file.Open (_T ("File.txt"), CFile::modeReadWrite); |
CFile::Open returns a BOOL indicating whether the operation was successful. The following example uses that return value to verify that the file was successfully opened:
CFile file; if (file.Open (_T ("File.txt"), CFile::modeReadWrite)) { // It worked! } |
A nonzero return value means the file was opened; 0 means it wasn't. If CFile::Open returns 0 and you want to know why the call failed, create a CFileException object and pass its address to Open in the third parameter:
CFile file; CFileException e; if (file.Open (_T ("File.txt"), CFile::modeReadWrite, &e)) { // It worked! } else { // Open failed. Tell the user why. e.ReportError (); } |
If Open fails, it initializes the CFileException object with information describing the nature of the failure. ReportError displays an error message based on that information. You can find out what caused the failure by examining the CFileException's public m_cause data member. The documentation for CFileException contains a complete list of error codes.
The second option is to open the file using CFile's constructor. Rather than construct an empty CFile object and call Open, you can create a CFile object and open a file in one step like this:
CFile file (_T ("File.txt"), CFile::modeReadWrite); |
If the file can't be opened, CFile's constructor throws a CFileException. Therefore, code that opens files using CFile::CFile normally uses try and catch blocks to trap errors:
try { CFile file (_T ("File.txt"), CFile::modeReadWrite); } catch (CFileException* e) { // Something went wrong. e->ReportError (); e->Delete (); } |
It's up to you to delete the CFileException objects MFC throws to you. That's why this example calls Delete on the exception object after processing the exception. The only time you don't want to call Delete is the rare occasion when you use throw to rethrow the exception.
To create a new file rather than open an existing one, include a CFile::modeCreate flag in the second parameter to CFile::Open or the CFile constructor:
CFile file (_T ("File.txt"), CFile::modeReadWrite ¦ CFile::modeCreate); |
If a file created this way already exists, its length is truncated to 0. To create the file if it doesn't exist or to open it without truncating it if it does exist, include a CFile::modeNoTruncate flag as well:
CFile file (_T ("File.txt"), CFile::modeReadWrite ¦ CFile::modeCreate ¦ CFile::modeNoTruncate); |
An open performed this way almost always succeeds because the file is automatically created for you if it doesn't already exist.
By default, a file opened with CFile::Open or CFile::CFile is opened for exclusive access, which means that no one else can open the file. If desired, you can specify a sharing mode when opening the file to explicitly grant others permission to access the file, too. Here are the four sharing modes that you can choose from:
Sharing Mode | Description |
---|---|
CFile::shareDenyNone | Opens the file nonexclusively |
CFile::shareDenyRead | Denies read access to other parties |
CFile::shareDenyWrite | Denies write access to other parties |
CFile::shareExclusive | Denies both read and write access to other parties (default) |
In addition, you can specify any one of the following three types of read/write access:
Access Mode | Description |
---|---|
CFile::modeReadWrite | Requests read and write access |
CFile::modeRead | Requests read access only |
CFile::modeWrite | Requests write access only |
A common use for these options is to allow any number of clients to open a file for reading but to deny any client the ability to write to it:
CFile file (_T ("File.txt"), CFile::modeRead ¦ CFile::shareDenyWrite); |
If the file is already open for writing when this statement is executed, the call will fail and CFile will throw a CFileException with m_cause equal to CFileException::sharingViolation.
An open file can be closed in two ways. To close a file explicitly, call CFile::Close on the corresponding CFile object:
file.Close (); |
If you'd prefer, you can let CFile's destructor close the file for you. The class destructor calls Close if the file hasn't been closed already. This means that a CFile object created on the stack will be closed automatically when it goes out of scope. In the following example, the file is closed the moment the brace marking the end of the try block is reached:
try { CFile file (_T ("File.txt"), CFile::modeReadWrite); // CFile::~CFile closes the file. } |
One reason programmers sometimes call Close explicitly is to close the file that is currently open so that they can open another file using the same CFile object.
A file opened with read access can be read using CFile::Read. A file opened with write access can be written with CFile::Write. The following example allocates a 4-KB file I/O buffer and reads the file 4 KB at a time. Error checking is omitted for clarity.
BYTE buffer[0x1000]; CFile file (_T ("File.txt"), CFile::modeRead); DWORD dwBytesRemaining = file.GetLength (); while (dwBytesRemaining) { UINT nBytesRead = file.Read (buffer, sizeof (buffer)); dwBytesRemaining -= nBytesRead; } |
A count of bytes remaining to be read is maintained in dwBytesRemaining, which is initialized with the file size returned by CFile::GetLength. After each call to Read, the number of bytes read from the file (nBytesRead) is subtracted from dwBytesRemaining. The while loop executes until dwBytesRemaining reaches 0.
The following example builds on the code in the previous paragraph by using ::CharLowerBuff to convert all the uppercase characters read from the file to lowercase and using CFile::Write to write the converted text back to the file. Once again, error checking is omitted for clarity.
BYTE buffer[0x1000]; CFile file (_T ("File.txt"), CFile::modeReadWrite); DWORD dwBytesRemaining = file.GetLength (); while (dwBytesRemaining) { DWORD dwPosition = file.GetPosition (); UINT nBytesRead = file.Read (buffer, sizeof (buffer)); ::CharLowerBuff ((LPTSTR)buffer, nBytesRead); file.Seek (dwPosition, CFile::begin); file.Write (buffer, nBytesRead); dwBytesRemaining -= nBytesRead; } |
This example uses the CFile functions GetPosition and Seek to manipulate the file pointer—the offset into the file at which the next read or write is performed—so that the modified data is written over the top of the original. Seek's second parameter specifies whether the byte offset passed in the first parameter is relative to the beginning of the file (CFile::begin), the end of the file (CFile::end), or the current position (CFile::current). To quickly seek to the beginning or end of a file, use CFile::SeekToBegin or CFile::SeekToEnd.
Read, Write, and other CFile functions throw a CFileException if an error occurs during a file I/O operation. CFileException::m_cause tells you why the error occurred. For example, attempting to write to a disk that is full throws a CFileException with m_cause equal to CFileException::diskFull. Attempting to read beyond the end of a file throws a CFileException with m_cause equal to CFileException::endOfFile. Here's how the routine that converts all the lowercase text in a file to uppercase might look with error checking code included:
BYTE buffer[0x1000]; try { CFile file (_T ("File.txt"), CFile::modeReadWrite); DWORD dwBytesRemaining = file.GetLength (); while (dwBytesRemaining) { DWORD dwPosition = file.GetPosition (); UINT nBytesRead = file.Read (buffer, sizeof (buffer)); ::CharLowerBuff ((LPTSTR)buffer, nBytesRead); file.Seek (dwPosition, CFile::begin); file.Write (buffer, nBytesRead); dwBytesRemaining -= nBytesRead; } } catch (CFileException* e) { e->ReportError (); e->Delete (); } |
If you don't catch exceptions thrown by CFile member functions, MFC will catch them for you. MFC's default handler for unprocessed exceptions uses ReportError to display a descriptive error message. Normally, however, it's in your best interest to catch file I/O exceptions to prevent critical sections of code from being skipped.
CFile is the root class for an entire family of MFC classes. The members of this family and the relationships that they share with one another are shown in Figure 6-1.
Figure 6-1. The CFile family.
Some members of the CFile family exist solely to provide filelike interfaces to nonfile media. For example, CMemFile and CSharedFile let blocks of memory be read and written as if they were files. MFC's COleDataObject::GetFileData function, which is discussed in Chapter 19, uses this handy abstraction to allow OLE drop targets and users of the OLE clipboard to retrieve data from memory with CFile::Read. CSocketFile provides a similar abstraction for TCP/IP sockets. MFC programmers sometimes place a CSocketFile object between a CSocket object and a CArchive object so that C++'s insertion and extraction operators can be used to write to and read from an open socket. COleStreamFile makes a stream object—a COM object that represents a byte stream—look like an ordinary file. It plays an important role in MFC applications that support object linking and embedding (OLE).
CStdioFile simplifies the programmatic interface to text files. It adds just two member functions to those it inherits from CFile: a ReadString function for reading lines of text and a WriteString function for outputting lines of text. In CStdioFile-speak, a line of text is a string of characters delimited by a carriage return and line feed pair (0x0D and 0x0A). ReadString reads everything from the current file position up to, and optionally including, the next carriage return. WriteString outputs a text string and writes a carriage return and line feed to the file, too. The following code fragment opens a text file named File.txt and dumps its contents to the debug output window:
try { CString string; CStdioFile file (_T ("File.txt"), CFile::modeRead); while (file.ReadString (string)) TRACE (_T ("%s\n"), string); } catch (CFileException* e) { e->ReportError (); e->Delete (); } |
Like Read and Write, ReadString and WriteString throw exceptions if an error prevents them from carrying out their missions.
CFile includes a pair of static member functions named Rename and Remove that can be used to rename and delete files. It doesn't, however, include functions for enumerating files and folders. For that, you must resort to the Windows API.
The key to enumerating files and folders is a pair of API functions named ::FindFirstFile and ::FindNextFile. Given an absolute or relative file name specification (for example, "C:\\*.*" or "*.*"), ::FindFirstFile opens a find handle and returns it to the caller. ::FindNextFile uses that handle to enumerate file system objects. The general strategy is to call ::FindFirstFile once to begin an enumeration and then to call ::FindNextFile repeatedly until the enumeration is exhausted. Each successful call to ::FindFirstFile or ::FindNextFile—that is, a call to ::FindFirstFile that returns any value other than INVALID_HANDLE_VALUE or a call to ::FindNextFile that returns a non-NULL value—fills a WIN32_FIND_DATA structure with information about one file or directory. WIN32_FIND_DATA is defined this way in ANSI code builds:
typedef struct _WIN32_FIND_DATAA { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; CHAR cFileName[ MAX_PATH ]; CHAR cAlternateFileName[ 14 ]; } WIN32_FIND_DATAA; typedef WIN32_FIND_DATAA WIN32_FIND_DATA; |
To determine whether the item represented by the WIN32_FIND_DATA structure is a file or a directory, test the dwFileAttributes field for a FILE_ATTRIBUTE_DIRECTORY flag:
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // It's a directory. } else { // It's a file. } |
The cFileName and cAlternateFileName fields hold the file or directory name. cFileName contains the long name; cAlternateFileName contains the short (8.3 format) name. When the enumeration is complete, you should close any handles returned by ::FindFirstFile with ::FindClose.
To demonstrate, the following routine enumerates all the files in the current directory and writes their names to the debug output window:
WIN32_FIND_DATA fd; HANDLE hFind = ::FindFirstFile (_T ("*.*"), &fd); if (hFind != INVALID_HANDLE_VALUE) { do { if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) TRACE (_T ("%s\n"), fd.cFileName); } while (::FindNextFile (hFind, &fd)); ::FindClose (hFind); } |
Enumerating all the subdirectories in the current directory requires just one simple change:
WIN32_FIND_DATA fd; HANDLE hFind = ::FindFirstFile (_T ("*.*"), &fd); if (hFind != INVALID_HANDLE_VALUE) { do { if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) TRACE (_T ("%s\n"), fd.cFileName); } while (::FindNextFile (hFind, &fd)); ::FindClose (hFind); } |
The more interesting case is how you can enumerate all the directories in a given directory and its subdirectories. The following function enumerates all the directories in the current directory and its descendants, writing the name of each directory to the debug output window. The secret? Whenever it encounters a directory, EnumerateFolders descends into that directory and calls itself recursively.
void EnumerateFolders () { WIN32_FIND_DATA fd; HANDLE hFind = ::FindFirstFile (_T ("*.*"), &fd); if (hFind != INVALID_HANDLE_VALUE) { do { if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { CString name = fd.cFileName; if (name != _T (".") && name != _T ("..")) { TRACE (_T ("%s\n"), fd.cFileName); ::SetCurrentDirectory (fd.cFileName); EnumerateFolders (); ::SetCurrentDirectory (_T ("..")); } } } while (::FindNextFile (hFind, &fd)); ::FindClose (hFind); } } |
To use this function, navigate to the directory in which you want the enumeration to begin and call EnumerateFolders. The following statements enumerate all the directories on drive C:
::SetCurrentDirectory (_T ("C:\\")); EnumerateFolders (); |
We'll use a similar technique in Chapter 10 to populate a tree view with items representing all the folders on a drive.