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 QObjects 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 File|Save 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.

Категории