Go back to Richel Bilderbeek's homepage.

Go back to Richel Bilderbeek's C++ page.

 

 

 

 

 

(C++) WtSimpleChat

 

WtSimpleChat is an official Wt example developed by Emweb.

 

 

 

If you cannot get it too work, the problem and its solution is shown below.

 

 

 

 

 

Technical facts

 

Application type(s)

Operating system(s) or programming environment(s)

IDE(s):

Project type:

C++ standard:

Compiler(s):

Libraries used:

 

 

 

 

 

Qt project file: CppWtSimpleChat.pro

 

#-------------------------------------------------
#
# Project created by QtCreator 2011-04-10T11:16:42
#
#-------------------------------------------------
QT       += core
QT       -= gui
LIBS += -lwt -lwthttp -lboost_program_options
QMAKE_CXXFLAGS += -DNDEBUG
TARGET = CppWtSimpleChat
CONFIG   += console
CONFIG   -= app_bundle
TEMPLATE = app
SOURCES += simpleChat.cpp \
  SimpleChatWidget.cpp \
  SimpleChatServer.cpp
HEADERS += \
    SimpleChatWidget.h \
    SimpleChatServer.h

 

 

 

 

 

SimpleChatServer.cpp

 

/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/

#include "SimpleChatServer.h"

#include <iostream>
#include <boost/lexical_cast.hpp>

using namespace Wt;

const WString ChatEvent::formattedHTML(const WString& user) const
{
  switch (type_) {
  case Login:
    return "<span class='chat-info'>"
      + user_ + " joined the conversation.</span>";
  case Logout:
    return "<span class='chat-info'>"
      + ((user == user_) ? "You" : user_)
      + " logged out.</span>";
  case Message:{
    WString result;

    result = WString("<span class='")
      + ((user == user_) ? "chat-self" : "chat-user")
      + "'>" + user_ + ":</span>";

    if (message_.toUTF8().find(user.toUTF8()) != std::string::npos)
      return result + "<span class='chat-highlight'>" + message_ + "</span>";
    else
      return result + message_;
  }
  default:
    return "";
  }
}


SimpleChatServer::SimpleChatServer()
  : chatEvent_(this)
{ }

bool SimpleChatServer::login(const WString& user)
{
  boost::mutex::scoped_lock lock(mutex_);
  
  if (users_.find(user) == users_.end()) {
    users_.insert(user);

    chatEvent_.emit(ChatEvent(ChatEvent::Login, user));

    return true;
  } else
    return false;
}

void SimpleChatServer::logout(const WString& user)
{
  boost::mutex::scoped_lock lock(mutex_);
  
  UserSet::iterator i = users_.find(user);

  if (i != users_.end()) {
    users_.erase(i);

    chatEvent_.emit(ChatEvent(ChatEvent::Logout, user));
  }
}

WString SimpleChatServer::suggestGuest()
{
  boost::mutex::scoped_lock lock(mutex_);

  for (int i = 1;; ++i) {
    std::string s = "guest " + boost::lexical_cast<std::string>(i);
    WString ss = s;

    if (users_.find(ss) == users_.end())
      return ss;
  }
}

void SimpleChatServer::sendMessage(const WString& user, const WString& message)
{
  boost::mutex::scoped_lock lock(mutex_);

  chatEvent_.emit(ChatEvent(user, message));
}

SimpleChatServer::UserSet SimpleChatServer::users()
{
  return users_;
}

 

 

 

 

 

SimpleChatServer.h

 

// This may look like C code, but it's really -*-$ C++ -*-
/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/
#ifndef SIMPLECHATSERVER_H_
#define SIMPLECHATSERVER_H_

#include <Wt/WObject>
#include <Wt/WSignal>
#include <Wt/WString>

#include <set>
#include <boost/thread.hpp>

/**
* @addtogroup chatexample
*/
/*@{*/

/*! \brief Encapsulate a chat event.
*/
class ChatEvent
{
public:
  /*! \brief Enumeration for the event type.
   */
  enum Type { Login, Logout, Message };

  /*! \brief Get the event type.
   */
  Type type() const { return type_; }

  /*! \brief Get the user who caused the event.
   */
  const Wt::WString& user() const { return user_; }

  /*! \brief Get the message of the event.
   */
  const Wt::WString& message() const { return message_; }

  /*! \brief Get the message formatted as HTML, rendered for the given user.
   */
  const Wt::WString formattedHTML(const Wt::WString& user) const;

private:
  Type type_;
  Wt::WString user_;
  Wt::WString message_;

  /*
   * Both user and html will be formatted as html
   */
  ChatEvent(const Wt::WString& user, const Wt::WString& message)
    : type_(Message), user_(user), message_(message)
  { }

  ChatEvent(Type type, const Wt::WString& user)
    : type_(type), user_(user)
  { }

  friend class SimpleChatServer;
};

/*! \brief A simple chat server
*/
class SimpleChatServer : public Wt::WObject
{
public:
  /*! \brief Create a new chat server.
   */
  SimpleChatServer();

  /*! \brief Try to login with given user name.
   *
   * Returns false if the login was not successfull.
   */
  bool login(const Wt::WString& user);

  /*! \brief Logout from the server.
   */
  void logout(const Wt::WString& user);

  /*! \brief Get a suggestion for a guest user name.
   */
  Wt::WString suggestGuest();

  /*! \brief Send a message on behalve of a user.
   */
  void sendMessage(const Wt::WString& user, const Wt::WString& message);

  /*! \brief %Signal that will convey chat events.
   *
   * Every client should connect to this signal, and process events.
   */
  Wt::Signal<ChatEvent>& chatEvent() { return chatEvent_; }

  /*! \brief Typedef for a collection of user names.
   */
  typedef std::set<Wt::WString> UserSet;

  /*! \brief Get the users currently logged in.
   */
  UserSet users();

private:
  Wt::Signal<ChatEvent>         chatEvent_;
  boost::mutex                  mutex_;

  UserSet                       users_;
};

/*@}*/

#endif // SIMPLECHATSERVER_H_

 

 

 

 

 

SimpleChatWidget.cpp

 

/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/

#include "SimpleChatWidget.h"
#include "SimpleChatServer.h"

#include <Wt/WApplication>
#include <Wt/WContainerWidget>
#include <Wt/WEnvironment>
#include <Wt/WHBoxLayout>
#include <Wt/WVBoxLayout>
#include <Wt/WLabel>
#include <Wt/WLineEdit>
#include <Wt/WText>
#include <Wt/WTextArea>
#include <Wt/WPushButton>
#include <Wt/WCheckBox>

#include <iostream>

using namespace Wt;

SimpleChatWidget::SimpleChatWidget(SimpleChatServer& server,
   Wt::WContainerWidget *parent)
  : WContainerWidget(parent),
    server_(server),
    app_(WApplication::instance()),
    messageReceived_("sounds/message_received.mp3")
{
  user_ = server_.suggestGuest();
  letLogin();

  // this widget supports server-side updates its processChatEvent()
  // method is connected to a slot that is triggered from outside this
  // session's event loop (usually because another user enters text).
  app_->enableUpdates();
}

SimpleChatWidget::~SimpleChatWidget()
{
  logout();
}

void SimpleChatWidget::letLogin()
{
  clear();

  WVBoxLayout *vLayout = new WVBoxLayout();
  setLayout(vLayout, AlignLeft | AlignTop);

  WHBoxLayout *hLayout = new WHBoxLayout();
  vLayout->addLayout(hLayout);

  hLayout->addWidget(new WLabel("User name:"), 0, AlignMiddle);
  hLayout->addWidget(userNameEdit_ = new WLineEdit(user_), 0, AlignMiddle);
  userNameEdit_->setFocus();

  WPushButton *b = new WPushButton("Login");
  hLayout->addWidget(b, 0, AlignMiddle);
  hLayout->addStretch(1);

  b->clicked().connect(SLOT(this, SimpleChatWidget::login));
  userNameEdit_->enterPressed().connect(SLOT(this, SimpleChatWidget::login));

  vLayout->addWidget(statusMsg_ = new WText());
  statusMsg_->setTextFormat(PlainText);
}

void SimpleChatWidget::login()
{
  WString name = WWebWidget::escapeText(userNameEdit_->text());

  if (!startChat(name))
    statusMsg_->setText("Sorry, name '" + name + "' is already taken.");
}

void SimpleChatWidget::logout()
{
  if (eventConnection_.connected()) {
    eventConnection_.disconnect(); // do not listen for more events
    server_.logout(user_);

    letLogin();
  }
}

bool SimpleChatWidget::startChat(const WString& user)
{
  if (server_.login(user)) {
    eventConnection_
      = server_.chatEvent().connect(SLOT(this,
       SimpleChatWidget::processChatEvent));
    user_ = user;    

    clear();

    /*
     * Create a vertical layout, which will hold 3 rows,
     * organized like this:
     *
     * WVBoxLayout
     * --------------------------------------------
     * | nested WHBoxLayout (vertical stretch=1)  |
     * |                              |           |
     * |  messages                    | userslist |
     * |   (horizontal stretch=1)     |           |
     * |                              |           |
     * --------------------------------------------
     * | message edit area                        |
     * --------------------------------------------
     * | WHBoxLayout                              |
     * | send | logout |       stretch = 1        |
     * --------------------------------------------
     */
    WVBoxLayout *vLayout = new WVBoxLayout();

    // Create a horizontal layout for the messages | userslist.
    WHBoxLayout *hLayout = new WHBoxLayout();

    // Add widget to horizontal layout with stretch = 1
    hLayout->addWidget(messages_ = new WContainerWidget(), 1);
    messages_->setStyleClass("chat-msgs");
    // Display scroll bars if contents overflows
    messages_->setOverflow(WContainerWidget::OverflowAuto);

    // Add another widget to hirozontal layout with stretch = 0
    hLayout->addWidget(userList_ = new WContainerWidget());
    userList_->setStyleClass("chat-users");
    userList_->setOverflow(WContainerWidget::OverflowAuto);

    hLayout->setResizable(0, true);

    // Add nested layout to vertical layout with stretch = 1
    vLayout->addLayout(hLayout, 1);

    // Add widget to vertical layout with stretch = 0
    vLayout->addWidget(messageEdit_ = new WTextArea());
    messageEdit_->setStyleClass("chat-noedit");
    messageEdit_->setRows(2);
    messageEdit_->setFocus();

    // Create a horizontal layout for the buttons.
    hLayout = new WHBoxLayout();

    // Add button to horizontal layout with stretch = 0
    hLayout->addWidget(sendButton_ = new WPushButton("Send"));
    WPushButton *b;

    // Add button to horizontal layout with stretch = 0
    hLayout->addWidget(b = new WPushButton("Logout"));

    // Add stretching spacer to horizontal layout
    hLayout->addStretch(1);

    // Add nested layout to vertical layout with stretch = 0
    vLayout->addLayout(hLayout);

    setLayout(vLayout);

    /*
     * Connect event handlers:
     *  - click on button
     *  - enter in text area
     *
     * We will clear the input field using a small custom client-side
     * JavaScript invocation.
     */

    // Create a JavaScript 'slot' (JSlot). The JavaScript slot always takes
    // 2 arguments: the originator of the event (in our case the
    // button or text area), and the JavaScript event object.
    clearInput_.setJavaScript
      ("function(o, e) {"
       "" + messageEdit_->jsRef() + ".value='';"
       "}");

    // Bind the C++ and JavaScript event handlers.
    sendButton_->clicked().connect(SLOT(this, SimpleChatWidget::send));
    messageEdit_->enterPressed().connect(SLOT(this, SimpleChatWidget::send));
    sendButton_->clicked().connect(clearInput_);
    messageEdit_->enterPressed().connect(clearInput_);

    // Prevent the enter from generating a new line, which is its
    // default function
    messageEdit_->enterPressed().setPreventDefault(true);

    b->clicked().connect(SLOT(this, SimpleChatWidget::logout));

    WText *msg = new WText
      ("<div><span class='chat-info'>You are joining the conversation as "
       + user_ + "</span></div>", messages_);
    msg->setStyleClass("chat-msg");

    updateUsers();
    
    return true;
  } else
    return false;
}

void SimpleChatWidget::send()
{
  if (!messageEdit_->text().empty()) {
    server_.sendMessage(user_, messageEdit_->text());
    if (!WApplication::instance()->environment().ajax())
      messageEdit_->setText(WString::Empty);
  }

  messageEdit_->setFocus();
}

void SimpleChatWidget::updateUsers()
{
  userList_->clear();

  SimpleChatServer::UserSet users = server_.users();

  UserMap oldUsers = users_;
  users_.clear();

  for (SimpleChatServer::UserSet::iterator i = users.begin();
       i != users.end(); ++i) {
    WCheckBox *w = new WCheckBox(*i, userList_);
    w->setInline(false);

    UserMap::const_iterator j = oldUsers.find(*i);
    if (j != oldUsers.end())
      w->setChecked(j->second);
    else
      w->setChecked(true);

    users_[*i] = w->isChecked();
    w->changed().connect(SLOT(this, SimpleChatWidget::updateUser));

    if (*i == user_)
      w->setStyleClass("chat-self");
  }
}

void SimpleChatWidget::updateUser()
{
  WCheckBox *b = dynamic_cast<WCheckBox *>(sender());
  users_[b->text()] = b->isChecked();
}

void SimpleChatWidget::processChatEvent(const ChatEvent& event)
{
  /*
   * This is where the "server-push" happens. This method is called
   * when a new event or message needs to be notified to the user. In
   * general, it is called from another session.
   */

  /*
   * First, take the lock to safely manipulate the UI outside of the
   * normal event loop, by having exclusive access to the session.
   */
  WApplication::UpdateLock lock = app_->getUpdateLock();

  /*
   * Format and append the line to the conversation.
   *
   * This is also the step where the automatic XSS filtering will kick in:
   * - if another user tried to pass on some JavaScript, it is filtered away.
   * - if another user did not provide valid XHTML, the text is automatically
   *   interpreted as PlainText
   */
  bool needPush = false;

  /*
   * If it is not a normal message, also update the user list.
   */
  if (event.type() != ChatEvent::Message) {
    needPush = true;
    updateUsers();
  }

  bool display = event.type() != ChatEvent::Message
    || (users_.find(event.user()) != users_.end() && users_[event.user()]);

  if (display) {
    needPush = true;

    WText *w = new WText(event.formattedHTML(user_), messages_);
    w->setInline(false);
    w->setStyleClass("chat-msg");

    /*
     * Leave not more than 100 messages in the back-log
     */
    if (messages_->count() > 100)
      delete messages_->children()[0];

    /*
     * Little javascript trick to make sure we scroll along with new content
     */
    app_->doJavaScript(messages_->jsRef() + ".scrollTop += "
       + messages_->jsRef() + ".scrollHeight;");

    /* If this message belongs to another user, play a received sound */
    if (event.user() != user_)
      messageReceived_.play();
  }

  if (needPush)
    app_->triggerUpdate();
}

 

 

 

 

 

SimpleChatWidget.h

 

// This may look like C code, but it's really -*-$ C++ -*-
/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/

#ifndef SIMPLECHATWIDGET_H_
#define SIMPLECHATWIDGET_H_

#include <Wt/WContainerWidget>
#include <Wt/WJavaScript>
#include <Wt/WSound>

namespace Wt {
  class WApplication;
  class WPushButton;
  class WText;
  class WLineEdit;
  class WTextArea;
}

class SimpleChatServer;
class ChatEvent;

/**
* \defgroup chatexample Chat example
*/
/*@{*/

/*! \brief A self-contained chat widget.
*/
class SimpleChatWidget : public Wt::WContainerWidget
{
public:
  /*! \brief Create a chat widget that will connect to the given server.
   */
  SimpleChatWidget(SimpleChatServer& server, Wt::WContainerWidget *parent = 0);

  /*! \brief Delete a chat widget.
   */
  ~SimpleChatWidget();

  /*! \brief Show a simple login screen.
   */
  void letLogin();

  /*! \brief Start a chat for the given user.
   *
   * Returns false if the user could not login.
   */
  bool startChat(const Wt::WString& user);

private:
  typedef std::map<Wt::WString, bool> UserMap;
  UserMap users_;

  SimpleChatServer&     server_;
  Wt::WApplication     *app_;

  Wt::JSlot             clearInput_;

  Wt::WString           user_;

  Wt::WLineEdit        *userNameEdit_;
  Wt::WText            *statusMsg_;

  Wt::WContainerWidget *messages_;
  Wt::WContainerWidget *messageEditArea_;
  Wt::WTextArea        *messageEdit_;
  Wt::WPushButton      *sendButton_;
  Wt::WContainerWidget *userList_;

  boost::signals::connection eventConnection_;

  Wt::WSound messageReceived_;

  void login();
  void logout();
  void send();
  void updateUsers();
  void updateUser();

  /* called from another session */
  void processChatEvent(const ChatEvent& event);
};

/*@}*/

#endif // SIMPLECHATWIDGET

 

 

 

 

 

simpleChat.cpp

 

/*
* Copyright (C) 2008 Emweb bvba, Heverlee, Belgium.
*
* See the LICENSE file for terms of use.
*/

#include <Wt/WApplication>
#include <Wt/WContainerWidget>
#include <Wt/WPushButton>
#include <Wt/WText>

#include "SimpleChatServer.h"
#include "SimpleChatWidget.h"

using namespace Wt;

/**
* @addtogroup chatexample
*/
/*@{*/

/*! \brief The single chat server instance.
*/
SimpleChatServer theServer;

/*! \brief A chat demo application.
*/
class ChatApplication : public WApplication
{
public:
  /*! \brief Create a new instance.
   */
  ChatApplication(const WEnvironment& env);

private:
  /*! \brief Add another chat client.
   */
  void addChatWidget();
};

ChatApplication::ChatApplication(const WEnvironment& env)
  : WApplication(env)
{
  setTitle("Wt Chat");
  useStyleSheet("simplechat.css");
  messageResourceBundle().use("simplechat");

  root()->addWidget(new WText(WString::tr("introduction")));

  SimpleChatWidget *chatWidget = new SimpleChatWidget(theServer, root());
  chatWidget->setStyleClass("chat");

  root()->addWidget(new WText(WString::tr("details")));

  WPushButton *b = new WPushButton("I'm schizophrenic ...", root());
  b->clicked().connect(SLOT(b, WPushButton::hide));
  b->clicked().connect(SLOT(this, ChatApplication::addChatWidget));
}

void ChatApplication::addChatWidget()
{
  SimpleChatWidget *chatWidget2 = new SimpleChatWidget(theServer, root());
  chatWidget2->setStyleClass("chat");
}

WApplication *createApplication(const WEnvironment& env)
{
  return new ChatApplication(env);
}

int main(int argc, char **argv)
{
  return WRun(argc, argv, &createApplication);
}

/*@}*/

 

 

 

 

 

Additional preparations

 

Added the following arguments to the Run Settings:

 

--docroot . --http-address 0.0.0.0 --http-port 8080

 

 

 

 

 

Output from the wthttpd server when it is not working

 

Starting /home/richel/qtsdk-2010.04/bin/Projects/Website/CppWtSimpleChat-build-desktop/CppWtSimpleChat...
[2011-Apr-12 10:36:51.581110] 8824 - [notice] "Wt: initializing built-in httpd"
[2011-Apr-12 10:36:51.581231] 8824 - [notice] "Reading Wt config file: /etc/wt/wt_config.xml (location = '/home/richel/qtsdk-2010.04/bin/Projects/Website/CppWtSimpleChat-build-desktop/CppWtSimpleChat')"
[2011-Apr-12 10:36:51.581789] 8824 - [notice] "Started server: http://0.0.0.0:8080"
[2011-Apr-12 10:36:55.029443] 8824 [/ gQop6M7Wgwi35eUo] [notice] "Session created (#sessions = 1)"
Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24
127.0.0.1 - - [2011-Apr-12 10:36:55.030755] "GET / HTTP/1.1" 200 1836
127.0.0.1 - - [2011-Apr-12 10:36:55.103744] "GET /?wtd=gQop6M7Wgwi35eUo&request=script&rand=431931263 HTTP/1.1" 200 30085
127.0.0.1 - - [2011-Apr-12 10:36:55.138307] "GET /resources//themes/default/wt.css HTTP/1.1" 404 85
127.0.0.1 - - [2011-Apr-12 10:36:55.138488] "GET /resources/swfobject.js HTTP/1.1" 404 85
127.0.0.1 - - [2011-Apr-12 10:36:55.311107] "GET /favicon.ico HTTP/1.1" 404 85

 

Pressing F5 (Refresh) once, to check for runtime errors, adds the following

 

[2011-Apr-12 10:36:58.735773] 8824 [/ fmn6vnZeV8Ih3vIh] [notice] "Session created (#sessions = 2)"
127.0.0.1 - - [2011-Apr-12 10:36:58.736617] "GET / HTTP/1.1" 200 1836
Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24
127.0.0.1 - - [2011-Apr-12 10:36:58.790913] "GET /?wtd=fmn6vnZeV8Ih3vIh&request=script&rand=854652532 HTTP/1.1" 200 30092
127.0.0.1 - - [2011-Apr-12 10:36:58.930977] "GET /resources//themes/default/wt.css HTTP/1.1" 404 85
127.0.0.1 - - [2011-Apr-12 10:36:58.936669] "GET /simplechat.css HTTP/1.1" 304 0
127.0.0.1 - - [2011-Apr-12 10:36:58.984089] "GET /favicon.ico HTTP/1.1" 404 85
127.0.0.1 - - [2011-Apr-12 10:36:58.984100] "GET /resources/swfobject.js HTTP/1.1" 404 85

 

 

 

 

 

Solution when it is not working

 

Thanks to Koen Deforche and Wim Dumon for pointing me out the problem: the wthttpd server output with '404' denotes 'not found'. Put these files in place and it works! Or do not choose to do a 'Shadow Build'.

 

 

 

 

 

Go back to Richel Bilderbeek's C++ page.

Go back to Richel Bilderbeek's homepage.

 

Valid XHTML 1.0 Strict