A Progress-indicating Status Line in C++ (Part 2)

Written by Stefan Ruck

Introduction
In the first part of this small series, published in EDM/2 Volume 4, Issue 5, I introduced the progress-indicating status line and the needed message handler to you. This time I will show you a simple base file class which is wrapped around the API-calls and a derived class which sends status messages to a given status line.

The Base File Class
The primary purpose of this class named AFile is to hide the main file-handling API functions. It just covers opening, reading, writing and of course closing a file. Let's have a look at its member.

The class AFile is defined in afile.hpp and afile.cpp.

The Constructor Member
There is only one constructor which takes two arguments, pBuffer (a PVOID) and ulBufferSize (a ULONG). They are initialized to zero by default.

What are these arguments good for? If you know the buffer you want to read the file's contents into, and its size, you can pass a pointer to the buffer and the buffer's size to the file object as soon as you create it. When you do any operation (reading, writing) later on this file you don't have to pass the buffer and its size any more because they are already known. This may be useful, e.g. for configuration files.

The members which store this information are declared as const so they can't be changed after construction.

The open Members
The difference between the two AFile::open members is the way you pass the filename. It can be a pointer to a string or a reference to an IString object. The other arguments are similar to the DosOpen function. The only difference is that the file handle is not needed because it's a private member of the file object.

The AFile::reOpen member can only be used when a file object was opened before and is actually closed. Because the filename of an AFile object is kept in a private member you don't have to pass it again when you re-open the same file using the same object.

The Close Member
This member is implemented for closing the file during the lifetime of the file object. Destroying a file object always closes the file.

The read and write Members
The default value of both parameters (PVOID pBuffer and ULONG ulBufferSize) is 0L. If you call AFile::read or AFile::write without passing a pointer to the buffer and/or the buffer's size the values passed to the constructor are given to the DOS-API-functions.

In addition to the OS/2 API return-values there are three more return-values:
 * AFILE_FILE_NOT_OPEN is returned in case the file connected to the object is not open.
 * AFILE_NO_BUFFER_POINTER indicates that no pointer to a buffer is passed to the constructor or to the member. And AFILE_NO_BUFFER_SIZE means that the size of the buffer is missing.

The Refresh-Flag on AFile::getFileSize

This flag is used internally when the AFile::queryFileInfo member is called. If it is set to true DosQueryFileInfo is called even if nothing about the file has changed. If it is set to false DosQueryFileInfo is only called when it was not called before (AFile::m_pFilestatus3, which keeps the file info, is not set in this case). You have to handle it yourself when the refresh-flag should be set.

The Message-Sending File Class
The main purpose of this article is to present a class which uses the progress-indicating status line to show the state of a process. This class is AEditFile. It is defined in editfile.hpp and editfile.cpp.

We can't use AFile directly because the methods AFile::read and AFile::write do not return until they have finished and the whole file is read/written. So what we need now are methods which read/write the data not in one big step but in a few small ones. They are AFileEdit::read and AFileEdit::write.

The class AFile is the base class of AEditFile, which posts MYM_STATUS_PROCEED messages (I hope you remember this message) to the status line while reading/writing a file. The differences between this class and AFile are the members for reading/writing, and the fact that this class keeps the file's contents itself. So you can't pass a pointer to the buffer or the buffer's size to the constructor.

The size of the file can't be changed.

This class doesn't work on files of no size.

I think there is no explanation needed for the constructor and destructor.

The read Member
Figure 1: AEditFile::read.

AEditFile holds its own buffer (see "The allocateDataBuffer Member" later in this article). So there's no pointer to a buffer or the buffer size needed as a parameter. But this member needs the HWND of the status line which indicates the progress.

Because we read the data in (maybe) several steps, we must compute the number of bytes left to read (stored in ulBytesLeftToRead). And we have to keep the position to append the new data block in the buffer. This value is kept by ulReadAtBufferPos.

If the buffer was not allocated by AEditFile::allocateDataBuffer, an exception of type char * is thrown.

As you might remember we want to use this member to read a file inside of a multi-threaded environment. Setting the m_bStopProcess to true using AEditFile::stopProcess, the main thread is able to terminate the operation without terminating the sub thread e.g. by calling IThread::stop (which should only be used in cases of "abnormal termination". For further information refer to Leong, Law, Love, Tsuji, Olson, "OS/2 C++ Class Library, Power GUI Programming with C Set++", New York, 1993, John Wiley & Sons, p. 533f). AEditFile::stopProcess is useful e.g. if the user closes the program or wants to abort the process by pressing ESC. If the process is stopped by setting AEditFile::m_bStopProcess to true, AEditFile::read returns ERROR_MORE_DATA.

AEditFile::read uses AFile::read to read the data. But it calls AFile::read several times, depending on the file's size. Every time AFile::read is called, a block of AEditFile::bytesToRead or ulBytesLeftToRead bytes, depending on the amount of bytes left to read, is read. As soon as a file is larger than AEditFile::bytesToRead, it will be read in several steps. Every time a block is read, a MYM_STATUS_PROCEED message is posted to the status line.

This line of code may seem a little bit cryptic to some of you: ret = AFile::read (m_pFileContents + ulReadAt BufferPos, (ulBytesLeftToRead > (ULONG) eBytesToRead ? eBytesToRead:ulBytesLeftToRead));. Well, I use the ? operator to check if the block of data left to read (ulBytesLeftToRead) is larger than the block I want to read by default (AEditFile::eBytesToRead). If so, AEditFile::eBytesToRead bytes is passed to AFile::read, otherwise ulBytesLeftToRead is passed.

Because we may not read the whole file in at once, AFile::read may return ERROR_MORE_DATA. This is correct, so I accept this return value. Any other non-zero return-value will break the read loop.

Only if a handle to a status line window is passed will the MYM_STATUS_PROCEED message be posted.

Every time a block of data is read, the position to place the data inside of the buffer (ulReadAtBufferPos) is increased by bytes read (returned by AFile::getBytesRead) while the ulBytesLeftToRead is decreased by the same value.

The write Member
Figure 2: AEditFile::write.

This member is similar to AEditFile::read, so I think there is just a little bit of explanation needed.

The proceed message is only posted if there are bytes left to write. Otherwise the status line will display a percentage greater than 100.

AEditFile::write writes the data, like DosWrite does, at the current position of the file pointer set by former read or write operations. To set the file-pointer, you have to use DosSetFilePtr or better create an AFile::setFilePtr member and use this one. I did not include such a member to AFile because I never keep a file open while editing it. And on every file open the file-pointer is set to the beginning of the file.

The allocateDataBuffer Member
This member is used to allocate the memory which keeps the file's contents. You should not call it before the file itself is open because the file size is needed.

If the memory is allocated successfully or a block of memory is already allocated, it returns true. If the file is not open, it returns false. It throws an exception of type ULONG if the size of the file is 0 or the size can not be retrieved. An exception of type char * is thrown if the memory allocation itself fails. So this member should be used always inside a try/catch block.

Concluding Remarks
Neither of the classes nor AStatusLine have a copy constructor. Because some data members are just pointers to a memory block allocated during runtime, you must not copy objects of these classes. The reason is that the default copy constructor can't handle those members in the correct way.

In AFile, there are some members set but never used. I included them for future methods, e.g like getOpenFlags, getOpenMode.

You might miss the two messages left defined in Part I, MYM_PROCESS_START and MYM_PROCESS_END.

In principle they can be posted by AEditFile::read and AEditFile::write, but I think they should be posted by the main object which starts the process because one of the values passed by the MYM_PROCESS_START message is a resource id. If you want to use the AEditFile class in several projects and use AEditFile::read and AEditFile::write for posting the start message, you must take care that there is always a resource defined which matches the resource id passed in this members.

The main object, which uses the classes shown in part I and part II in a (really) simple multi-threaded environment, will be the contents of the last part.