Sunday 26 August 2012

Updating a Qt GUI from a boost::thread

OK, first things first.

After seeing how horrible the previous post looked, I've decided to look into the subject, to improve the readability here. After checking several alternatives, I've decided to use an online formatter to format the code and copy/paste the HTML into this blog. Some problems remain unsolved, but I'll leave that to another day.

Now, then. Lately, most of my work has been on PoCs (Proof-of-Concept), and I'll discuss one of those here, now: Updating a Qt GUI from a boost::thread.

For this, I used the following classes:
  • WorkWindow. The GUI; inherits from QDialog.
  • Task. The class that does the actual work and updates the GUI.
  • TaskController. I use this to glue both classes together. The PoC doesn't require it, but a more real-world (i.e., complex) example would require something to fulfill this role.
WorkWindow contains a button that starts the task:
boost::function<void(std::string)> f_res =
        boost::bind(&WorkWindow::AddSearchResult, this, ui->treeWidget_1, _1);

    m_tc->StartTask(f_res);

We create a boost::function to call WorkWindow::AddSearchResult(), binding a particular widget to the call. This is part of what I called a few posts back a callback-with-a-sideline-in-information-hiding - whoever invokes this function object has no knowledge that one of the arguments of its invocation is actually a GUI widget. m_tc is a pointer to a TaskController, and this is the invoked method:

void TaskController::StartTask(boost::function<void(std::string)> f_res)
{
    Task t(f_res);
    m_thread = new boost::thread(t);
}

We simply create a new task and fire its thread. m_thread is a pointer to a boost::thread, held by TaskController. This is the task's operator(), invoked by boost::thread:

void Task::operator()()
{
    for(auto i : str_vec)
    {
        m_UpdateUI(i);
    }
}

str_vec is an std::vector<std::string>, and simulates the results of Task's processing. m_UpdateUI is a boost::function<void(std::string)>, and is initialized with f_res.

So, Task::operator() invokes WorkWindow::AddSearchResult(), passing a string:

void WorkWindow::AddSearchResult(QTreeWidget* w, std::string s)
{
    emit SigAddSearchResult(w, s);
}

As we can see, AddSearchResult() emits a signal, forwarding both arguments. And, on WorkWindow's constructor, we connected this signal with its slot:

connect(this, SIGNAL(SigAddSearchResult(QTreeWidget*, std::string)),
        SLOT(SlotAddSearchResult(QTreeWidget*, std::string)),
        Qt::QueuedConnection);

We use Qt::QueuedConnection to ensure the slot is executed in the GUI's thread, not on Task's thread. We could use the default value, Qt::AutoConnection, and Qt would choose the correct mode, by comparing native thread IDs.

Finally, the slot:

void WorkWindow::SlotAddSearchResult(QTreeWidget* w, std::string s)
{
    QTreeWidgetItem* elem = new QTreeWidgetItem();
    elem->setText(0, s.c_str());
    w->invisibleRootItem()->addChild(elem);
}

It's actually pretty simple. To recap:
- The GUI requests a task execution, handing the task a callback to update the GUI.
- The task is started in its own thread. When it has data, it invokes the callback.
- The callback emits a signal.
- The slot processes the signal, updating the UI. The slot must be processed in the GUI's thread, and this is enforced when both signal and slot are connected.

And that's it. As a final note, using QThread provides better integration, and simplifies all this somewhat, but the steps above remain unchanged.

No comments:

Post a Comment