A Progress-indicating Status Line in C++ (Part 1)
Written by Stefan Ruck
Contents
Introduction
While developing a hex editor for OS/2, I needed something to show the user the state of progress of reading/writing a file. First I decided to bring up continuous dots in the status line. But that didn't satisfy me because the user can't see how much work is done. The standard progress indicator of the ICLUI had two main disadvantages. First I have to put the tick text below it (which expands the size of my status line), second it's a separate window. I was thinking of a slider inside of my status line which shows the current level of progress written in percent and graphically by the length of the slider arm. The progress indicator used by the installation routines of some windows applications is a good example for the slider itself.
In the following screen shot you can see how the status line should look like when all work is done.
This contribution consists of three articles.
- In this first part I will introduce the status line class and the event handler needed in multi-threaded environments.
- In the second part I will present a file class which uses the status line to show the state of progress.
- The third part I will show how to use these components in a simple multi-threaded program which just reads and writes files.
The Status Event Handler
>Reading/writing a file may take more than 1/10 second which is the longest time a program should use to handle a message. So this task should be done in a separate thread. But just performing it on a thread of its own doesn't give any information about its state to the user. So there's a status line needed. And the proceeding thread somehow has to communicate with this status line to tell it about its state.
This communication can be done using well defined methods of the status line class. Doing so you have to pass a pointer or a reference of this class to the acting object (e.g. a file) so it can use these methods. And the operating object always has to know what kind of class this pointer/reference is.
I prefer to send messages in a message driven system. Because we know the window which shall receive the messages we just need its handle to send the messages to it. We don't have to care about the receiving window itself when we send the message.
For the status line we need three additional messages and a handler to dispatch our messages.
The messages are:
#define MYM_STATUS_START_PROCESS (WM_USER+2000) #define MYM_STATUS_PROCEED (WM_USER+2001) #define MYM_STATUS_END_PROCESS (WM_USER+2002)
Figure 2: Process messages.
MYM_STATUS_START_PROCESS is to send when the process starts. Its first message parameter is a pointer to PROCESS_START_PARAMS.
typedef struct _PROCESS_START_PARAMS { ULONG ulProcessSize, ulTickSize; LONG lResourceId; }PROCESS_START_PARAMS;
Figure 3: PROCESS_START_PARAMS structure.
We need a pointer to a structure because there are more than two variables we have to set at the beginning of the process.
ulProcessSize is the total size of the process, e.g. a file's size in bytes.
ulTickSize is the progress size per tick, e.g. the bytes read per reading.
lResourceId is the resource id of the text which names the process. You may ask why I don't pass a pointer to a string instead of the resource id. Well the answer is simple. I prefer language independent source code. So I put all the text strings into the string table of my resource file to convert a program into any user language I like without changing a single line of code.
MYM_STATUS_PROCEED is send any time the process proceeded a given part, e.g. when a fixed number of bytes were read. This number of bytes must be equal to ulTickSize sent by MYM_STATUS_START_PROCESS.
MYM_STATUS_END_PROCESS indicates the end of the process.
The handler AStatusHandler itself is easy to create. We just need to derive it from IHandler and override the IHandler::dispatchHandlerEvent member function.
//********************************************* // Dispatch command functions for this handler //********************************************* Boolean AStatusHandler::dispatchHandlerEvent(IEvent& evt) { switch(evt.eventId()) { case MYM_STATUS_START_PROCESS: return (startProcess (evt)); case MYM_STATUS_PROCEED: return (proceed (evt)); case MYM_STATUS_END_PROCESS: return (endProcess (evt)); default: return (false); } }
Figure 4: AStatusHandler::dispatchHandlerEvent.
As you can see in Figure 4 there is a member function for each message. These new members are AStatusHandler::startProcess, AStatusHandler::proceed and AStatusHandler::endProcess. I declared all of them as pure virtual functions to force any class derived by AStatusHandler to handle every status message.
This class is defined in stamsgh.hpp and stamsgh.cpp, the messages in mymsg.h.
The Progress-indicating Status Line
Now that we've done some basics, let's turn to the main thing.
What I want my status line to do is the following. First it should show text which tells the user what's going on, e.g. saving or reading a file, on the left. Second it should show the progress in percent by number, centred in the middle of the place left between the text and right border. Third there should be a slider which shows graphically how much work is done. And as a special task this slider should move over the numeric display and change the colour of the overlaid part.
The name of our status line class is AProcessStatus. As the main base class I choose IStaticText. Additionally we need to derive it from IPaintHandler because we have to do a lot of painting ourselves. And, of course, we have to derive it from our status event handler AStatusHandler.
This class is defined in procssta.hpp and procssta.cpp.
We will have a closer look at some of the member functions. I haven't printed the whole source code in this article. If there is something omitted it is marked by three dots. It was not easy for me to decide the sequence of the explanation. There are some things you might only understand when you have a look at the AProcessStatus::paintWindow member. But I think you can't understand what's going on there without looking at AProcessStatus::startProcess, AProcessStatus::proceed and AProcessStatus::endProcess. So I put the explanation of these three members in front of AProcessStatus::paintWindow. I hope you will get the meaning.
The Constructor Member
//******************************************** // AProcessStatus::AProcessStatus Constructor // for statusline //******************************************** AProcessStatus::AProcessStatus (IWindow * parentWindow) : IStaticText (WND_STATUS_MAIN, parentWindow, parentWindow) { // reset all member variables fist resetAllVariables (); . . . }
Figure 5: AProcessStatus::AProcessStatus.
The first member called in the constructor is AProcessStatus::resetAllVariables. The reason is quite simple. If you create a class by allocating it on the heap, you can't tell what the memory block just allocated looks like. Resetting all the member variables at first makes sure that they have a valid and desired value.
The startProcess Member
//******************************************* // AProcessStatus :: startProcess // MYM_STATUS_START_PROCESS messages received //******************************************* Boolean AProcessStatus::startProcess (IEvent& evt) { . . . // get the 1. event parameter PROCESS_START_PARAMS * pStartProcess = ((PROCESS_START_PARAMS *)((char *) evt.parameter1 ())); . . . // get the total number of ticks. m_ulTotalTicks = pStartProcess->ulProcessSize / pStartProcess->ulTickSize; . . // keep the resource id of the process description m_lResourceId = pStartProcess->lResourceId; // reset the number of ticks actually proceeded m_ulActualTick = 0L; // we want to paint the process's name m_nPaintWhat = ePaintStartProcess; // paint event is "self-made" m_bPaintSentByMe = true; . . . // send a paint event WinInvalidateRect (handle(), &m_Rectl, FALSE); // reset the flag m_bPaintSentByMe = false; return (true); }
Figure 6: AProcessStatus::startProcess.
As you remember, startProcess is one of the AStatusHandler pure virtual member functions. It is called when a MYM_STATUS_START_PROCESS event occurs.
To use the passed information we have to declare a PROCESS_START_PARAMS pointer which is set to the first event parameter. Using this pointer we are able to calculate the number of ticks (MYM_STATUS_PROCEED events) we will receive during this process. This is done by dividing the process size by the tick size. We need this to know by what amount we have to increase the percentage done when an MYM_STATUS_PROCEED event occurs.
We set AProcessStatus::m_lResourceId to the pStartParams -> lResourceId because we need this information later on in one of our painting member functions.
One really important thing is to set AProcessStatus::m_bPaintSendByMe to true.
This is needed in conjunction with AProcessStatus::m_nPaintWhat in the
AProcessStatus::paintWindow member function to decide if a paint event was sent
by the system or was "self-made" and what to do when it is "self-made".
At the very least, we invalidate the window's rectangle because we want to do some painting. Returning true is recommended because we've proceeded the event.
The proceed Member
//************************************ // AProcessStatus::proceed // MYM_STATUS_PROCEED message received //************************************ Boolean AProcessStatus :: proceed (IEvent& evt) { m_ulActualTick++; // one more tick proceeded // calculate the slider size per tick m_dSizePerTick = (double) (size (). width () - m_lStartSliderX) / (double) m_ulTotalTicks; // get the new slider size m_ulNewSliderSize = (ULONG)((double) m_ulActualTick * m_dSizePerTick); // did the size change? if(m_ulNewSliderSize == m_ulOldSliderSize) // no ==> nothing to do ==> return return (true); // calculate the percentage proceeded m_ulPercent = (m_ulActualTick * (ULONG) 100) / m_ulTotalTicks; // did the percentage change? if(m_ulPercent != m_ulPercentReady) // yes { // keep the new percentage proceeded m_ulPercentReady = m_ulPercent; // set the left end of the rect to invalidate m_Rectl.xLeft = m_lStartSliderX + m_ulNewSliderSize; // did the slider overlays the percent number total if(m_Rectl.xLeft < m_lEndPercentNumberX) // no { // set what we want to paint m_nPaintWhat = ePaintPercentNumber; . . . // send a paint event WinInvalidateRect (handle(), &m_Rectl, FALSE); // reset the flag m_bPaintSentByMe = false; } } // set what we want to paint m_nPaintWhat = ePaintSlider; // set the rect to invalidate m_Rectl.xLeft = m_lStartSliderX; m_Rectl.xRight = m_lStartSliderX + m_ulNewSliderSize; . . . // send a paint event WinInvalidateRect (handle(), &m_Rectl, FALSE); // reset the flag m_bPaintSentByMe = false; return (true); }
Figure 7: AProcessStatus::proceed.
The proceed member is called when a MYM_STATUS_PROCEED event occurs. There are no parameters passed.
Before doing anything else we check out if it is necessary to paint something. We can do this by comparing the actual slider size (AProcessStatus::m_ulOldSliderSize) with the new size. The size per tick is calculated every time this member is called because the window's width might have changed since the last calculation.
Now some explanation about the way AProcessStatus::m_ulNewSliderSize is calculated. It is done by this code:
m_ulNewSliderSize = (ULONG) ((double) m_ulActualTick * m_dSizePerTick);
You might ask why there is so much casting. I'm doing so to be sure about the result type of my arithmetic operation. Because the size per tick almost always has decimal places, it must be kept in a float or double. Multiplying it with the actual number of ticks (AProcessStatus::m_ulActualTick) it can't be casted into an ULONG because it might be less than 1. In this case the result would always be 0 (the decimal places will be truncated). To receive a double I cast AProcessStatus::m_ulActualTick into a double (I had an additional look at Herbert Schildt's "C: The complete Reference" [2. Edition, Berkeley, 1990, Osborne McGraw-Hill, p. 61f]. He wrote that "The C compiler converts all operands up to the type of the largest operand...". I can't tell why but somehow I don't trust the compiler. So I convert AProcessStatus::m_ulActualTick into a double myself to be absolutely sure about it's the right data type.). The result is casted back to ULONG. Why? Ever tried to move your window to (4.1232, 100.43234)? Ok, now that we know the new slider size we compare it to the old one. If both are the same, the function returns because there's nothing left to do.
The main reason for checking out if something changed is to speed up the program. Calculating and comparing two or three values is surely faster than invalidating a rectangle and doing a lot of painting just because of nothing.
The new percentage proceeded is calculated by m_ulPercent = (m_ulActualTick * (ULONG) 100) / m_ulTotalTicks;. Normally, you will calculate a percent value by first dividing the part by the whole and then multiply the result by 100. If you do so using ULONG variables, you'll always receive 0 as the result. That's because the part divided by the whole almost is less 1 and the decimal places would be truncated.
Because we need to display parts of the percent number in two different colours, there are two member functions where it is painted into the status line. One of them is AProcessStatus::paintPercentNumber, which paints it in black. The second is AProcessStatus::paintSlider, where it is painted white into the slider. AProcessStatus::paintPercentNumber is only called when the percentage has changed and the slider does not overlay the whole number. AProcessStatus::paintSlider is called when the slider's size expands. I would like to designate the call of AProcessStatus::paintPercentNumber and AProcessStatus::paintSlider as a "indirect" call inside of the proceed member. I do only invalidate a rectangle which causes a paint event. The decision of which of them will be called inside of the AProcessStatus::paintWindow member is made in AProcessStatus::m_nPaintWhat, which will be set to the appropriate value before I invalidate the rectangle.
When AProcessStatus::paintPercentNumber is called, the left side of the invalidated rectangle matches with the right end of the slider. This makes sure we don't paint the new value into the slider using the wrong colour. The operating system ensures that only the part of painting inside of the invalidated rectangle will be displayed. You can start left of the invalidated area and end right of it, only the part inside the rectangle will be refreshed on the display. If you keep this in mind you know how to display a character in multiple colours.
If the slider overlays the percent number totally, AProcessStatus::paintPercentNumber will not be called. This is checked by the line if(m_Rectl.xLeft < m_lEndPercentNumberX).
To make sure that only the overlaid part of the percent number will be painted using the overlay colour, the right side of the invalidated rectangle equals to the right end of the slider when AProcessStatus::paintSlider is called.
The endProcess Member
//**************************************** // AProcessStatus :: endProcess // MYM_STATUS_END_PROCESS message received //**************************************** Boolean AProcessStatus::endProcess (IEvent& evt) { // set what we want to paint // (clean up the status line) m_nPaintWhat = ePaintStd; // the process stopped m_bProcessIsRunning = false; . . . // send a paint event WinInvalidateRect (handle(), &m_Rectl, FALSE); return (true); }
Figure 8: AProcessStatus::endProcess.
The AProcessStatus::endProcess member is called when a MYM_STATUS_END_PROCESS event occurs. There are no parameters passed.
This member is used to reset some members and to cause a paint event to wipe away the slider from the status line.
The paintWindow Member
//******************************** // AProcessStatus :: paintWindow // control member for paint events //******************************** Boolean AProcessStatus::paintWindow (IPaintEvent& evt) { // did I send the paint event? if(m_bPaintSentByMe) // yes { // reset the boolean m_bPaintSentByMe = false; // is the process to indicate still running if(!m_bProcessIsRunning) // no: return return (true); // the process is running, what is to do? switch(m_nPaintWhat) { // do the painting for process's start case ePaintStartProcess: return (paintStartProcess (evt)); // paint the percent number case ePaintPercentNumber: return (paintPercentNumber (evt)); // paint the slider case ePaintSlider: return (paintSlider (evt)); // no special painting default: return (paintStd (evt)); } } // the paint event was sent by the system paintStd (evt); // is a process to indicate running? if(m_bProcessIsRunning) // yes { // do the painting as if the process starts paintStartProcess (evt); // reset the slider size m_ulOldSliderSize = 0L; // because of m_ulActualTick++ in paintStartProcess m_ulActualTick--; // will call proceed to do the rest of the painting WinPostMsg (handle(), MYM_STATUS_PROCEED, 0, 0); } return (true); }
Figure 9: AProcessStatus::paintWindow.
Above we've seen that paint events are not just sent by the operating system but also by the status line itself. To make the AProcessStatus::paintWindow member able to figure out who sent the paint event and what to do, there are three members set. AProcessStatus::m_bPaintSentByMe tells the AProcessStatus::paintWindow member if the object sent the event itself.
AProcessStatus::m_bProcessIsRunning is only set as long as the indicated process is running (I had the experience that sometimes in a multi-threaded program a paint message posted by the AProcessStatus::proceed member was proceeded while the process itself was already finished). AProcessStatus::m_nPaintWhat tells which part of the status line needs to be refreshed.
If AProcessStatus::m_bPaintSentByMe is set, the first thing to do is to reset it to make sure we don't forget it. If AProcessStatus::m_bProcessIsRunning is false (equals to zero, no process is running), AProcessStatus::paintWindow doesn't have to do anything else. I'm resetting AProcessStatus::m_bPaintSentByMe past the WinInvalidateRect calls in AProcessStatus::startProcess and AProcessStatus::proceed too. It's because the window doesn't receive a paint message when its owner gets minimized (I've used PMSpy32 to check it out). If AProcessStatus::m_bPaintSentByMe just becomes reset in AProcessStatus::paintWindow, it'll stay set even if the system sends a paint event to the status line. Therefore the status line won't become refreshed correctly, e.g it was covered by another window.
If the system causes the paint event (the window was resized, minimized, restored...), the whole status line gets repainted. First the basic painting is done by AProcessStatus::paintStd. If a process is still running, all the status information needs to be refreshed. So we first paint the process description. AProcessStatus::m_ulOldSliderSize must be set to 0L to make sure that the slider will be repainted. AProcessStatus::m_nActualTick (number of ticks proceeded) must be decreased because it is increased by AProcessStatus::proceed which will be called soon. Then we just need to post a MYM_STATUS_PROCEED message to let AProcessStatus::proceed do the rest.
Remarks
Maybe you've noticed that I use GpiQueryCurrentPosition to get AProcessStatus::m_lEndPercentNumberX instead of adding AProcessStatus::m_pFont->textWidth(m_szBuffer) to AProcessStatus::m_lStartPercentNumberX. I've chosen this way because IFont::textWidth doesn't always return the right size. This depends on the actual font. I approved it by loading the width table (it is used by IFont::textWidth to compute a text width), displayed each character and compared the displayed size to the width table. On several fonts I got differences. If you like, I will send you the source code by email to check it on your machine.
I do not use local variables in those member functions which are called very often during a process. I thought it would save some time because there's no allocating/deallocating needed. Well of course, it saves this time. But I did some EXTRA tracing using both methods, local and global variables. And I have to say that in my program it doesn't makes any differences.
Conclusion
What we've done in this part is to create two classes which give any program the ability to tell the user about the state of a long term process. This does not have to be only reading/writing a file. It can also be used for printing, searching or whatever you like.
In the next part I will show a simple base file class, which is wrapped around the API file functions, and a derived file class which uses the status line to show the user the progress of reading/writing a file.