Staying
Responsive
During Intensive Processing
When we call
QApplication::
exec
()
, we start Qt's event loop. Qt issues a few events on startup to show and paint the widgets. After that, the event loop is running, constantly checking to see if any events have occurred and dispatching these events to
QObject
s in the application.
While one event is being
processed
, additional events may be generated and appended to Qt's event queue. If we
spend
too much time processing a particular event, the
user
interface will become
unresponsive
. For example, any events generated by the window system while the application is saving a file to disk will not be processed until the file is saved. During the save, the application will not respond to
requests
from the window system to repaint itself.
One solution is to use multiple threads: one thread for the application's user interface and another thread to perform file saving (or any other
time-consuming
operation). This way, the application's user interface will stay responsive while the file is being saved. We will see how to achieve this in Chapter 17.
A simpler solution is to make frequent calls to
QApplication::processEvents()
in the file saving code. This function
tells
Qt to process any pending events, and then returns control to the caller. In fact,
QApplication::exec()
is little more than a
while
loop around a
processEvents()
function call.
Here's an example of how we can keep the user interface responsive using
processEvents()
, based on the file saving code for
Spreadsheet
(p. 77):
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
...
for (int row = 0; row < NumRows; ++row) {
for (int col = 0; col < NumCols; ++col) {
QString str = formula(row, col);
if (!str.isEmpty())
out << (Q_UINT16)row << (Q_UINT16)col << str;
}
qApp->processEvents();
}
return true;
}
One danger with this approach is that the user might close the main window while the application is still saving, or even click
FileSave
a second time, resulting in undefined behavior. The
easiest
solution to this problem is to replace the
qApp->processEvents();
call with a
qApp->eventLoop()->processEvents(QEventLoop::ExcludeUserInput);
call, which tells Qt to ignore mouse and key events.
Often, we want to show a
QProgressDialog
while a long running operation is taking place.
QProgressDialog
has a progress bar that keeps the user informed about the progress being made by the application.
QProgressDialog
also provides a
Cancel
button that allows the user to abort the operation. Here's the code for saving a Spreadsheet file using this approach:
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
...
QProgressDialog progress(tr("Saving file..."), tr("Cancel"),
NumRows);
progress.setModal(true);
for (int row = 0; row < NumRows; ++row) {
progress.setProgress(row);
qApp->processEvents();
if (progress.wasCanceled()) {
file.remove();
return false;
}
for (int col = 0; col < NumCols; ++col) {
QString str = formula(row, col);
if (!str.isEmpty())
out << (Q_UINT16)row << (Q_UINT16)col << str;
}
}
return true;
}
We create a
QProgressDialog
with
NumRows
as the total number of steps. Then, for each row, we call
setProgress()
to update the progress bar.
QProgressDialog
automatically computes a percentage by dividing the current progress value by the total number of steps. We call
QApplication::processEvents()
to process any repaint events or any user clicks or key presses (for example, to allow the user to click
Cancel
). If the user clicks
Cancel
, we abort the save and remove the file.
We don't call
show()
on the
QProgressDialog
because progress dialogs do that for
themselves
. If the operation turns out to be short, presumably because the file to save is small or because the machine is fast,
QProgressDialog
will detect this and will not show itself at all.
There is a completely different way of dealing with long running operations. Instead of performing the processing when the user requests, we can
defer
the processing until the application is idle. This can work if the processing can be safely
interrupted
and resumed, since we cannot predict how long the application will be idle.
In Qt, this approach can be implemented by using a special kind of timer: a 0-millisecond timer. These timers time out whenever there are no pending events. Here's an example
timerEvent()
implementation that shows the idle processing approach:
void Spreadsheet::timerEvent(QTimerEvent *event)
{
if (event->timerId() == myTimerId) {
while (step < MaxStep && !qApp->hasPendingEvents()) {
performStep(step);
++step;
}
} else {
QTable::timerEvent(event);
}
}
If
hasPendingEvents()
returns
true
, we stop processing and give control back to Qt. The processing will resume when Qt has handled all its pending events.
|