Wt: Treelist example

Wt 3.3.0

- Main Page
- Related Pages
- Modules
- Namespaces
- Classes

Treelist example

In this example we will step through the code of the Tree List example. The source code of the entire example is available as leafs of the tree. Note that Wt offers a Tree List widget as part of the library (see WTreeNode), of which this example is a down-stripped version.

The example in particular demonstrates the use of stateless slot learning to simultaneously implement client-side and server-side event handling in C++.

The tree constructed as hierarchy of tree nodes. A single tree node is implemented in the class TreeNode. TreeNode uses the helper class IconPair for rendering icons that have a state (such as the expand/collapse icons). We start with a walk-over of this class.

## IconPair: a pair of icons that reflects state.

For the implementation of the tree list expand/collapse icons, as well as the label icons (such as the folder icon), we use class IconPair. It takes a pair of icons and shows only one at a time. Passing clickIsSwitch = true to the constructor will make the icon react to click events to switch the current icon.

This is the class definition of IconPair:

    class IconPair : public Wt::WCompositeWidget
    {
    public:
      IconPair(const std::string icon1URI, const std::string icon2URI,
               bool clickIsSwitch = true, Wt::WContainerWidget *parent = 0);

      void setState(int num);

      int state() const;

      Wt::WImage *icon1() const { return icon1_; }

      Wt::WImage *icon2() const { return icon2_; }

      void showIcon1();

      void showIcon2();

    private:
      Wt::WContainerWidget *impl_;

      Wt::WImage *icon1_;

      Wt::WImage *icon2_;

    public:
      Wt::EventSignal<Wt::WMouseEvent>& icon1Clicked;

      Wt::EventSignal<Wt::WMouseEvent>& icon2Clicked;

    private:
      int previousState_;

      void undoShowIcon1();

      void undoShowIcon2();
    };

IconPair is a composite widget, implemented as a WContainerWidget which contains two WImage objects. The class defines two slots: IconPair::showIcon1() and IconPair::showIcon2(), which show the respective icon, while hiding the other icon.

Although Wt is a C++ (server-side) library, it can also generate client-side JavaScript code for instant visual response. This example will use this capability to implement all of the tree navigation at the client-side for those clients that support JavaScript -- as if it were implemented as a JavaScript library. But since everything is still plain C++ code, it works whatever technology is available or lacking at the client side. Think of a stateless slot implementation as creating a forked implementation, with JavaScript in the client for visual response -- when JavaScript is available, and C++ at the server. When no JavaScript is available, everything happens at the server.

The key concept behind Wt's capability to implement things at the client-side is stateless slot implementations. A stateless slot is, besides a normal C++ function that may be connected to a signal, a C++ function that promises to always have the same behaviour (until it is reset, as we will see later).

This applies to the two functions showIcon1() and showIcon2(), as they simply set the corresponding icon, irrespective of any application state. The library offers two methods for stateless slot implementations: AutoLearned and PreLearned. An AutoLearned stateless slot will only "become client-side" after the first invocation. Applied to our tree widget, this would mean that the first click on any icon would require a round-trip to the server the first time only. An AutoLearned stateless slot simply requires an indication that the particular slot confirms to the contract of being stateless. A PreLearned stateless slot, on the other hand, is "client-side" from the first invocation. To implement a PreLearned stateless however, we need to do some extra work by providing methods that exactly undo the effect of the slot. We provide here two such undo methods: undoShowIcon1() and undoShowIcon2().

Enough talk! Let's look at the implementation, starting with the constructor.

    IconPair::IconPair(const std::string icon1URI, const std::string icon2URI,
                       bool clickIsSwitch, Wt::WContainerWidget *parent)
      : Wt::WCompositeWidget(parent),
        impl_(new Wt::WContainerWidget()),
        icon1_(new Wt::WImage(icon1URI, impl_)),
        icon2_(new Wt::WImage(icon2URI, impl_)),
        icon1Clicked(icon1_->clicked()),
        icon2Clicked(icon2_->clicked())
    {

IconPair inherits from WCompositeWidget. A composite widget is a widget which is composed from other widgets, in a way not exposed in its API. In this way, you may later change the implementation without any problem.

Notice how we constructed three widgets that are used in the implementation: two images (icon1_ and icon2_), and a container (impl_) to hold them. The images are added to the container by passing the container as the last argument in their constructor.

WCompositeWidget requires to set the implementation widget, which is in our case a WContainerWidget:

      setImplementation(impl_);

We declare the slots showIcon1() and showIcon2() as stateless slots, allowing for client-side optimisation, and offer an undo function which facilitates a PreLearned client-side implementation.

The calls to WObject::implementStateless() state that the slots showIcon1() and showIcon2() are stateless slots, and their visual effect may be learned in advance. The effect of these statements is merely an optimization. Any non-visual effects of these slots are still propagated and executed, as expected.

      implementStateless(&IconPair::showIcon1, &IconPair::undoShowIcon1);
      implementStateless(&IconPair::showIcon2, &IconPair::undoShowIcon2);

Next, we declare the widget to be an inline widget. An inline widget will be layed out following the natural flow of text (left to right). This does not really matter for our example, since TreeNode will do the layout with a WTable, but we do so to provide consistency with a WImage which is also inline by default.

      setInline(true);

The initial state is to show the first icon:

      icon2_->hide();

To react to click events, we connect signals with slots:

      if (clickIsSwitch) {
        icon1_->clicked().connect(icon1_, &Wt::WImage::hide);
        icon1_->clicked().connect(icon2_, &Wt::WImage::show);

        icon2_->clicked().connect(icon2_, &Wt::WImage::hide);
        icon2_->clicked().connect(icon1_, &Wt::WImage::show); //

We change the cursor to a pointer to hint that clicking these icons may do something useful.

        decorationStyle().setCursor(Wt::PointingHandCursor);
      }
    } //

We also change the cursor to a pointer to hint that clicking these icons will in fact perform an action.

The rest of the class definition is:

    void IconPair::setState(int num)
    {
      if (num == 0) {
        icon1_->show();
        icon2_->hide();
      } else {
        icon1_->hide();
        icon2_->show();
      }
    }

    int IconPair::state() const
    {
      return (icon1_->isHidden() ? 1 : 0);
    }

    void IconPair::showIcon1()
    {
      previousState_ = (icon1_->isHidden() ? 1 : 0);
      setState(0);
    }

    void IconPair::showIcon2()
    {
      previousState_ = (icon1_->isHidden() ? 1 : 0);
      setState(1);
    }

    void IconPair::undoShowIcon1()
    {
      setState(previousState_);
    }

    void IconPair::undoShowIcon2()
    {
      setState(previousState_);
    } //

Note the implementations of undoShowIcon1() and undoShowIcon2(): they simply, but accurately, reset the state to what it was before the respective showIcon1() and showIcon2() calls.

## TreeNode: an expandable tree node.

TreeNode contains the implementation of the tree, as a hierarchy of tree nodes. The layout of a single node is done using a 2x2 WTable:

       |-----------------------|
       | +/- | label           |
       |------------------------
       |     | child1          |
       |     | child2          |
       |     | child3          |
       |     |       ...       |
       |-----------------------|

The TreeNode manages a list of child nodes in a WContainerWidget which will be hidden and shown when the node is expanded or collapsed, and children are collapsed when the node is expanded.

This is the TreeNode class definition:

    class TreeNode : public Wt::WCompositeWidget
    {
    public:
      TreeNode(const std::string labelText,
               Wt::TextFormat labelFormat,
               IconPair *labelIcon, Wt::WContainerWidget *parent = 0);

      void addChildNode(TreeNode *node);

      void removeChildNode(TreeNode *node);

      const std::vector<TreeNode *>& childNodes() const { return childNodes_; }

      void collapse();

      void expand();

    private:
      std::vector<TreeNode *> childNodes_;

      TreeNode                 *parentNode_;

      Wt::WTable               *layout_;

      IconPair                 *expandIcon_;

      Wt::WImage               *noExpandIcon_;

      IconPair                 *labelIcon_;

      Wt::WText                *labelText_;

      Wt::WText                *childCountLabel_;

      Wt::WContainerWidget     *expandedContent_;

      void adjustExpandIcon();

      bool isLastChildNode() const;

      void childNodesChanged();

      bool wasCollapsed_;

      void undoCollapse();

      void undoExpand();

      enum ImageIndex { Middle = 0, Last = 1 };

      static std::string imageLine_[];
      static std::string imagePlus_[];
      static std::string imageMin_[];
    }; //

The public interface of the TreeNode provides methods to manage its children, and two public slots to expand or collapse the node. Remember, a slot is nothing more than a method (and the public slots: does not actually mean anything, except providing a hint to the user of this class that these methods are made to be connected to signals).

We start with the implementation of the constructor:

    TreeNode::TreeNode(const std::string labelText,
                       Wt::TextFormat labelFormat,
                       IconPair *labelIcon,
                       Wt::WContainerWidget *parent)
      : Wt::WCompositeWidget(parent),
        parentNode_(0),
        labelIcon_(labelIcon)
    {

We start with declaring stateless implementations for the slots. It is good practice to do this first, since it must be done before any connections are made to the slots.

      // pre-learned stateless implementations ...
      implementStateless(&TreeNode::expand, &TreeNode::undoExpand);
      implementStateless(&TreeNode::collapse, &TreeNode::undoCollapse);

We will implement the treenode as 2 by 2 table.

      setImplementation(layout_ = new Wt::WTable());

We create all icons. Since currently the node is empty, we only show the no-expand version (which is simply a horizontal line).

      expandIcon_ = new IconPair(imagePlus_[Last], imageMin_[Last]);
      expandIcon_->hide();
      noExpandIcon_ = new Wt::WImage(imageLine_[Last]);

The expanded content is a WContainerWidget.

      expandedContent_ = new Wt::WContainerWidget();
      expandedContent_->hide();

We create the label and child count text widgets:

      labelText_ = new Wt::WText(labelText);
      labelText_->setTextFormat(labelFormat);
      labelText_->setStyleClass("treenodelabel");
      childCountLabel_ = new Wt::WText();
      childCountLabel_->setMargin(7, Wt::Left);
      childCountLabel_->setStyleClass("treenodechildcount");

Now we add all widgets in the proper table cell, and set the correct alignment.

      layout_->elementAt(0, 0)->addWidget(expandIcon_);
      layout_->elementAt(0, 0)->addWidget(noExpandIcon_);

      if (labelIcon_) {
        layout_->elementAt(0, 1)->addWidget(labelIcon_);
        labelIcon_->setVerticalAlignment(Wt::AlignMiddle);
      }
      layout_->elementAt(0, 1)->addWidget(labelText_);
      layout_->elementAt(0, 1)->addWidget(childCountLabel_);

      layout_->elementAt(1, 1)->addWidget(expandedContent_);

      layout_->elementAt(0, 0)->setContentAlignment(Wt::AlignTop);
      layout_->elementAt(0, 1)->setContentAlignment(Wt::AlignMiddle);

Finally, we connect the click events of the expandIcon to the expand and collapse slots.

      expandIcon_->icon1Clicked.connect(this, &TreeNode::expand);
      expandIcon_->icon2Clicked.connect(this, &TreeNode::collapse);
    } //

WTable::elementAt(int row, int column) is used repeatedly to add or modify contents of the table cells, expanding the table geometry as needed. Finally, we make connections from the expand and collapse icons to the slots we define in the TreeNode class.

Again, we optimize the visual effect of expand() and collaps() in client-side JavaScript, which is possible since they both have an effect independent of application state. Typically, one will start with a default dynamic slot implementation, and indicate stateless implementations where desired and possible, using one of the two mechanisms of stateless slot learning.

The "business logic" of the TreeNode is simply to manage its children. Whenever a child is added or removed, adjustments to its look are updated by calling childNodesChanged().

    bool TreeNode::isLastChildNode() const
    {
      if (parentNode_) {
        return parentNode_->childNodes_.back() == this;
      } else
        return true;
    }

    void TreeNode::addChildNode(TreeNode *node)
    {
      childNodes_.push_back(node);
      node->parentNode_ = this;

      expandedContent_->addWidget(node);

      childNodesChanged();
    }

    void TreeNode::removeChildNode(TreeNode *node)
    {
      childNodes_.erase(std::find(childNodes_.begin(), childNodes_.end(), node));

      node->parentNode_ = 0;

      expandedContent_->removeWidget(node);

      childNodesChanged();
    } //

The expand icon of the last child is rendered differently, as it needs to terminate the vertical guide line. To keep the implementation simple, we simply let every child reset its proper look by calling adjustExpandIcon().

    void TreeNode::childNodesChanged()
    {
      for (unsigned i = 0; i < childNodes_.size(); ++i)
        childNodes_[i]->adjustExpandIcon();

When getting a first child, or losing the last child, the expand icon changes too.

      adjustExpandIcon();

We also update the childCount label.

      if (childNodes_.size())
        childCountLabel_
          ->setText("(" + boost::lexical_cast<std::string>(childNodes_.size())
                    + ")");
      else
        childCountLabel_->setText("");

Finally, we call WObject::resetLearnedSlots(). Because the expand() slot depends on the number of children, because it needs to collapse all children -- this slot is not entirely stateless, breaking the contract for a stateless slot. However, we can get away with still implementing as a stateless slot, by indicating when the state has changed.

      resetLearnedSlots();
    } //

The implementation of the collapse slot is as follows:

    void TreeNode::collapse()
    {

First we record the current state, so the undo method can exactly undo what happened.

      wasCollapsed_ = expandedContent_->isHidden();

Next, we implement the actual collapse logic:

      expandIcon_->setState(0);
      expandedContent_->hide();
      if (labelIcon_)
        labelIcon_->setState(0);
    } //

Similarly, the implementation of the expand slot. However, in this case we need to collapse all children as well.

    void TreeNode::expand()
    {
      wasCollapsed_ = expandedContent_->isHidden();

      expandIcon_->setState(1);
      expandedContent_->show();
      if (labelIcon_)
        labelIcon_->setState(1);

      /*
       * collapse all children
       */
      for (unsigned i = 0; i < childNodes_.size(); ++i)
        childNodes_[i]->collapse();
    } //

Since we implement these slots as prelearned stateless slots, we also need to define the undo functions. Note that Because expand() also collapses all child nodes, the undo function of expand() is not simply collapse() and vice-versa.

    void TreeNode::undoCollapse()
    {
      if (!wasCollapsed_) {
        // re-expand
        expandIcon_->setState(1);
        expandedContent_->show();
        if (labelIcon_)
          labelIcon_->setState(1);
      }
    }

    void TreeNode::undoExpand()
    {
      if (wasCollapsed_) {
        // re-collapse
        expandIcon_->setState(0);
        expandedContent_->hide();
        if (labelIcon_)
          labelIcon_->setState(0);
      }

      /*
       * undo collapse of children
       */
      for (unsigned i = 0; i < childNodes_.size(); ++i)
        childNodes_[i]->undoCollapse();
    } //

Finally, the adjustExpandIcon() function sets the correct images, which depends on how the node relates to its siblings. The last node looks a bit different.

    void TreeNode::adjustExpandIcon()
    {

We set the expand icon images:

      ImageIndex index = isLastChildNode() ? Last : Middle;

      if (expandIcon_->icon1()->imageLink().url() != imagePlus_[index])
        expandIcon_->icon1()->setImageLink(imagePlus_[index]);
      if (expandIcon_->icon2()->imageLink().url() != imageMin_[index])
        expandIcon_->icon2()->setImageLink(imageMin_[index]);
      if (noExpandIcon_->imageLink().url() != imageLine_[index])
        noExpandIcon_->setImageLink(imageLine_[index]);

Then, we set the vertical guide line if not the last child, and nothing if the last child:

      if (index == Last) {
        layout_->elementAt(0, 0)
          ->decorationStyle().setBackgroundImage("");
        layout_->elementAt(1, 0)
          ->decorationStyle().setBackgroundImage("");
      } else {
        layout_->elementAt(0, 0)
          ->decorationStyle().setBackgroundImage("icons/line-trunk.gif",
                                                 Wt::WCssDecorationStyle::RepeatY);
        layout_->elementAt(1, 0)
          ->decorationStyle().setBackgroundImage("icons/line-trunk.gif",
                                                 Wt::WCssDecorationStyle::RepeatY);
      } //

Finally, we select the correct icon, depending on whether the node has children:

      if (childNodes_.empty()) {
        if (noExpandIcon_->isHidden()) {
          noExpandIcon_->show();
          expandIcon_->hide();
        }
      } else {
        if (expandIcon_->isHidden()) {
          noExpandIcon_->hide();
          expandIcon_->show();
        }
      }
    } //

And that's it. By using the TreeNode class in a hierarchy, we can create a tree widget. The tree widget will be implemented entirely in JavaScript, if available, and otherwise as plain HTML. In any case, client-side and server-side state are completely synchronized, and identical by definition since they are derived from the same C++ code.

All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator