PDA

View Full Version : How to keep the GUI responsive?



drjones
14th July 2008, 20:24
I'm trying to implement a program, where a lot of computations are done. Of course the GUI is blocked until the computation is done.
I tried using (boost) threads, but you're not allowed to update widgets (like a progress bar) or replace a QLabel by an image (using QPixmap) from within a seperate (non-GUI) thread.
Has anyone any idea, how to update a progressbar (which is owned by the main window) and show an image in the mainwindow, which gets available during the computation?

I already tried to use a QTimer (owned by the main window), but its timeout() event handler was never called.
Even the following test didn't work:


int g_progress = 0;
long int g_count = 1000000000;

TestMainWindow::TestMainWindow(QWidget* parent, Qt::WindowFlags flags)
: QMainWindow(parent, flags)
{
// create gui elements defined in the Ui_MainWindow class
setupUi(this);
progressBar->setRange(0, 100);
m_timer = new QTimer();
connect(m_timer, SIGNAL(timeout()), this, SLOT(updateProgress()));
}

//is executed on a button click
void TestMainWindow::compute()
{
m_timer->start();
for (int i=0; i < g_count; i++)
{
//progress in percent
g_progress = (int) (i * 100/(float) g_count);
}
m_timer->stop();
}

//this method is never called. Why?
void TestMainWindow::updateProgress()
{
progressBar->setValue(g_progress);
}


TestMainWindow is derived from QMainWindow.

Is there a better way (using threads) to do heavy computations while keeping the GUI responsive (keep in mind that I want to update the GUI during computation)?

Wow, that's a long post... :)

Thanks in advance!

wysota
14th July 2008, 21:13
You basically have two choices:

1. using threads
In this situation you need to divide your computation process into two parts - the computation itself and all related to updating the display. As you noticed the latter can only be done from within the main (aka GUI) thread, so you need to pass the results of the computation from within worker threads to the main thread. In Qt4 there are two safe ways of doing that - one is to use signals and slots and the other is to use custom events. So basically you are doing some computations and when you want to update the display, post a custom event to an object living in the main thread and reimplement its custom event handler ith routines needed to update the display.

2. not using threads
The same can be achieved without threads. There are two variants of this solution - a dirty one and a proper one. The dirty one relies on calling QCoreApplication::processEvents() periodically during the computation to allow the application to process its gui events and thus remain responsive. The solution is not clean because you are triggering the event queue in an artificial way and it theory you can still starve the event loop or your computation. The proper way of doing what you want is to divide your computation into small chunks that can be processed separately in a sequence. Then you should connect a timer with a 0 timeout to a slot that will be executing subsequent steps of the computation (or alternatively each chunk will start a single shot timer connected to the next chunk of the computation). When you start the timer and return the flow to the event queue, Qt will take care of the rest by calling the first chunk of your computation. The result will be similar to the processEvents() method but will allow you to pause, cancel or slow down the computation process at each step by manipulating the timers.

caduel
14th July 2008, 21:13
QTimer::timeout() is called in the event loop.
When a button is clicked, a slot is called from the event loop.
Execution of the eventloop is suspended until that slot returns
(unless you start another event loop in the slot or call QEventLoop::processEvents, or QCoreApplication::processEvents() regularly in the slot).

The better way to do it is threads. It is a bit more complicated, but worth getting one's head around, though.

HTH

markcole
14th July 2008, 21:55
I have done exactly what you are asking using QThread.

mchara
15th July 2008, 07:41
Even thread without executed eventLoop(which is responsible for receiving events) can emit signals.
Gui can have slot that updates progressBar when receives signal from thread.
I think it's most common solution,
at least works fine in mine application.

drjones
16th July 2008, 12:19
Thanks a lot for your help. I have now an application with a responsive GUI :)

First of all here's what I did:
I've got a GUI class, named PhotoMosaicMainWindow (derived from QMainWindow), which owns a data object of type/class PhotoMosaicManager (responsible for heavy computations like building a mosaic image, given a database of images and a single motive).
When the "Compute" button of the main window is pressed, its event handler calls the PhotoMosaicManager's computeMosaic method.
Now I create a (boost) thread and pass the compute Method as follows:

if (m_computationThread != 0)
delete m_computationThread;
m_computationThread = new boost::thread(boost::bind(&(PhotoMosaicManager::computeMosaic), m_mosaicManager));

In order to access the progress bar (or other parts of the GUI), I emit signals from the PhotoMosaicManager's computeMosaic method. The signals are connected to the corresponding slots in the main window's constructor:


PhotoMosaicMainWindow::PhotoMosaicMainWindow(QWidg et* parent, Qt::WindowFlags flags)
: QMainWindow(parent, flags)
{
// create gui elements defined in the Ui_MainWindow class
setupUi(this);
progressBar->setRange(0, 100);
m_mosaicManager = new PhotoMosaicManager();
m_mosaicManager->setManagerListener(this);
m_computationThread = 0;
connect(m_mosaicManager, SIGNAL(updateProgress(int)), this, SLOT(updateProgress(int)));
connect(m_mosaicManager, SIGNAL(computationFinished()), this, SLOT(finishedComputation()));
}

Three questions:
1) In http://lists.trolltech.com/qt-interest/2004-05/thread00803-0.html I've read the following warning:
"Warning: Slots that generate window system events or use window system
functions must not be connected to a signal that is emitted from a thread
that is not the GUI thread."
But this is exactly what I'm doing, isn't it? I've got a slot which updates the progressbar, where the corresponding signal is emitted from a non-GUI thread. Is this a problem?
2) I would like to design the software in such a way, that all Qt stuff is encapsulated and completely separated from the data and computation classes like PhotoMosaicManager. But in order to emit signals from PhotoMosaicManager's computeMosaic method, I had to derive this class from QObject and use Qt's "emit" syntax. Any suggestions, how I can separate Qt/GUI from data classes? Add some kind of "third layer" between GUI and computation, which is responsible for communication between these two (the third layer could emit the signals)?
3) corresponding the timer problem, I've had earlier (let me know, if I've got the point):
my timer-event-handler wasn't called, because I started the timer from within an event handler of a button click (that's why the event loop of the GUI was disrupted) and because I started the heavy computation within this button-event handler immediately after calling timer->start(). Until timer->stop was called at the end of the button-event-handler, the heavy computation was done, so control was never returned back to the GUI event loop during computation and the timer-event could never be triggered...?

caduel
16th July 2008, 12:59
1) cross thread signals are safe: they are not executed 'right now' like normal signals, but rather queued and executed later (in the context of the thread the slots's object is in)
2) you can use any callback mechanism in your PhotoMosaicManager; e.g. a Boost.Function, or a simple function pointer. Just pass it a function that internally connects to your Qt stuff. That way PhotoMosaicManager does not interact with Qt (directly).
3) yes, I think you did get that

HTH

wysota
16th July 2008, 13:38
1) In http://lists.trolltech.com/qt-interest/2004-05/thread00803-0.html I've read the following warning:
"Warning: Slots that generate window system events or use window system
functions must not be connected to a signal that is emitted from a thread
that is not the GUI thread."
But this is exactly what I'm doing, isn't it? I've got a slot which updates the progressbar, where the corresponding signal is emitted from a non-GUI thread. Is this a problem?

The message concerns Qt3 and not Qt4. It was not safe to use signals across threads in Qt3. In Qt4 you may do that but if you are using boost threads, I would suggest forcing queued connections instead of automatic onces in the connect() statements.

Currently your object lives in the main thread, so signals are emitted as regular function calls and that may cause you some trouble.


2) I would like to design the software in such a way, that all Qt stuff is encapsulated and completely separated from the data and computation classes like PhotoMosaicManager. But in order to emit signals from PhotoMosaicManager's computeMosaic method, I had to derive this class from QObject and use Qt's "emit" syntax. Any suggestions, how I can separate Qt/GUI from data classes? Add some kind of "third layer" between GUI and computation, which is responsible for communication between these two (the third layer could emit the signals)?
Provide an abstract interface your "plugins" would implement so that it is possible to call them from an external source and also an abstract interface your "plugin" can call to contact the main application. Implementation of those interfaces can but don't have to contain Qt code.


3) corresponding the timer problem, I've had earlier (let me know, if I've got the point):
my timer-event-handler wasn't called, because I started the timer from within an event handler of a button click (that's why the event loop of the GUI was disrupted) and because I started the heavy computation within this button-event handler immediately after calling timer->start(). Until timer->stop was called at the end of the button-event-handler, the heavy computation was done, so control was never returned back to the GUI event loop during computation and the timer-event could never be triggered...?

Yes, that's correct. Timers are checked at the end of event processing routine, so when that's never called, your timers can't timeout.

drjones
16th July 2008, 15:33
Thanks again.
I added notifyComputationFinished() and notifyProgressChanged(int progress) to the abstract ManagerListener class, which is implemented by PhotoMosaicMainWindow.


class ManagerListener
{
public:
virtual bool wasComputationCancelled() = 0;
virtual void notifyComputationFinished() = 0;
virtual void notifyProgressChanged(int progress) = 0;
};

In both methods I emit signals which are connected to slots in the same class:


connect(this, SIGNAL(computationFinishedSignal()), this, SLOT(finishedComputation()) /*, Qt::QueuedConnection*/);
connect(this, SIGNAL(progressChangedSignal(int)), this, SLOT(updateProgress(int)) /*, Qt::QueuedConnection*/);


The PhotoMosaicManager object calls the notify* methods from its computeMosaic method (via an object of type ManagerListener).

It seems a bit weird, that the PhotoMosaicMainWindow object sends signals to itself, which it will handle then, too. But this is necessary to assure thread safety (GUI update during heavy computation).
Nevertheless it works, thanks for your help!
Is this solution the one wysota has proposed?