Wednesday, November 14, 2012

Using Actors with Qt

Actor programming is nice and makes our lives easier, but at some point, we have to display the output of our program to the user. Not everything is a command line tool. Hence, we have to use some sort of GUI library, which raises the question how we pass messages from actors - safely! - to the GUI at runtime. Well, what if we could send an ordinary message to the GUI as if it's an actor? What if the GUI can send us ordinary messages whenever the user pushes some buttons? Briefly speaking, can we treat GUI elements as actors? Luckily, the answer is yes! We have to write some gluecode, but in fact, you can treat everything as an actor with libcppa.

Getting Started

In this article, we will focus on the Qt library, as it is the open source GUI library of choice for C++ developers. In the next article, we will have a look at the general concept of actor companions. An actor companion is an actor that co-exists with another object. In our case, this object is a QWidget-base class. The companion is used to receive and send messages, but it does not have any behavior itself. All it takes to enable a class to have such a companion is to use the actor_widget_mixin.

The following class implements a simple group-based chat widget with a QTextEdit for text output and a QLineEdit for user input (chat messages). The full source code can be found in the examples folder in the Git repository.
#include <QWidget>
#include "cppa/qtsupport/actor_widget_mixin.hpp"

class ChatWidget : public cppa::actor_widget_mixin<QWidget> {

  Q_OBJECT

  typedef cppa::actor_widget_mixin<QWidget> super;

  // ...

 public:

  ChatWidget(QWidget* parent = nullptr, Qt::WindowFlags f = 0);

  // ...

 private:

  std::string m_name;
  cppa::group_ptr m_chatroom;

};
The mixin adds the following member functions to ChatWidget:
  • actor_ptr as_actor()
    returns the companion of this widget
  • set_message_handler
    sets the partial function for message processing

Handle Messages to the Widget

In our example, the widget should handle 'join', 'setName', and 'quit' messages as well as display chat messages (received as std::string). We encode our message handling directly into the constructor of ChatWidget:
ChatWidget::ChatWidget(QWidget* parent, Qt::WindowFlags f) : super(parent, f) {
  set_message_handler (
    on(atom("join"), arg_match) >> [=](const group_ptr& what) {
      if (m_chatroom) {
        send(m_chatroom, m_name + " has left the chatroom");
        self->leave(m_chatroom);
      }
      self->join(what);
      print(("*** joined " + to_string(what)).c_str());
      m_chatroom = what;
      send(what, m_name + " has entered the chatroom");
    },
    on(atom("setName"), arg_match) >> [=](string& name) {
      send(m_chatroom, m_name + " is now known as " + name);
      m_name = std::move(name);
      print("*** changed name to "
            + QString::fromUtf8(m_name.c_str()));
    },
    on(atom("quit")) >> [=] {
      close(); // close widget
    },
    on<string>() >> [=](const string& txt) {
      // don't print own messages
      if (self != self->last_sender()) {
        print(QString::fromUtf8(txt.c_str()));
      }
    }
  );
}

Pitfalls & Things to Know

The code is pretty straight forward if you are already familiar with libcppa, but there is one important thing to note: Unhandled messages are lost. Although the widget is able to handle libcppa messages now, it does not have a mailbox. Messages are delivered to the widget as QEvent objects that are disposed afterwards.

The second thing to note is that you should never use self outside the message handler. This includes using functions such as send, because self is internally used to determine the sender of a message. The reason to this limitation is that libcppa's self "pointer" is thread-local. Using self would therefore convert the Qt thread your widget runs in to an actor, but it wouldn't address your widget's companion.

To send a message from outside of your handler, you have to tell libcppa who is the sender of the message by hand:
send_as(as_actor(), m_chatroom, "hello world!");


Have fun!

No comments:

Post a Comment