Wt  3.3.6
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:

1 /*! \brief An icon pair (identical to WIconPair)
2  *
3  * This widget manages two images, only one of which is shown at a single
4  * time.
5  *
6  * The widget may also react to click events, by changing state.
7  *
8  * This widget is part of the %Wt treelist example, where it is used
9  * to represent the expand/collapse icons, and the corresponding
10  * map open/close icon.
11  *
12  * \sa TreeNode
13  */
14 class IconPair : public Wt::WCompositeWidget
15 {
16 public:
17  /*! \brief Construct a two-state icon widget.
18  *
19  * The constructor takes the URI of the two icons. When clickIsSwitch
20  * is set true, clicking on the icon will switch state.
21  */
22  IconPair(const std::string icon1URI, const std::string icon2URI,
23  bool clickIsSwitch = true, Wt::WContainerWidget *parent = 0);
24 
25  /*! \brief Set which icon should be visible.
26  *
27  * The first icon has number 0, and the second icon has number 1.
28  *
29  * \sa state()
30  */
31  void setState(int num);
32 
33  /*! \brief Get the current state.
34  *
35  * \sa setState()
36  */
37  int state() const;
38 
39  /*! \brief Get the first icon image
40  */
41  Wt::WImage *icon1() const { return icon1_; }
42 
43  /*! \brief Get the second icon image
44  */
45  Wt::WImage *icon2() const { return icon2_; }
46 
47  /*! \brief Set state to 0 (show icon 1).
48  */
49  void showIcon1();
50 
51  /*! \brief Set state to 1 (show icon 2).
52  */
53  void showIcon2();
54 
55 private:
56  Wt::WContainerWidget *impl_;
57 
58  //! First icon.
59  Wt::WImage *icon1_;
60 
61  //! Second icon.
62  Wt::WImage *icon2_;
63 
64 public:
65  /*! \brief Signal emitted when clicked while in state 0 (icon 1 is
66  * shown).
67  */
68  Wt::EventSignal<Wt::WMouseEvent>& icon1Clicked;
69 
70  /*! \brief Signal emitted when clicked while in state 1 (icon 2 is
71  * shown).
72  */
73  Wt::EventSignal<Wt::WMouseEvent>& icon2Clicked;
74 
75 private:
76  //! Undo state for prelearning stateless showIcon1() and showIcon2() slots
77  int previousState_;
78 
79  //! Undo function for prelearning showIcon1()
80  void undoShowIcon1();
81 
82  //! Undo function for prelearning showIcon2()
83  void undoShowIcon2();
84 };

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.

1 IconPair::IconPair(const std::string icon1URI, const std::string icon2URI,
2  bool clickIsSwitch, Wt::WContainerWidget *parent)
3  : Wt::WCompositeWidget(parent),
4  impl_(new Wt::WContainerWidget()),
5  icon1_(new Wt::WImage(icon1URI, impl_)),
6  icon2_(new Wt::WImage(icon2URI, impl_)),
7  icon1Clicked(icon1_->clicked()),
8  icon2Clicked(icon2_->clicked())
9 {

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:

1  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.

1 
2  implementStateless(&IconPair::showIcon1, &IconPair::undoShowIcon1);
3  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.

1  setInline(true);

The initial state is to show the first icon:

1  icon2_->hide();

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

1 
2  if (clickIsSwitch) {
3  icon1_->clicked().connect(icon1_, &Wt::WImage::hide);
4  icon1_->clicked().connect(icon2_, &Wt::WImage::show);
5 
6  icon2_->clicked().connect(icon2_, &Wt::WImage::hide);
7  icon2_->clicked().connect(icon1_, &Wt::WImage::show); //

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

1 
2  decorationStyle().setCursor(Wt::PointingHandCursor);
3  }
4 } //

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:

1 
2 void IconPair::setState(int num)
3 {
4  if (num == 0) {
5  icon1_->show();
6  icon2_->hide();
7  } else {
8  icon1_->hide();
9  icon2_->show();
10  }
11 }
12 
13 int IconPair::state() const
14 {
15  return (icon1_->isHidden() ? 1 : 0);
16 }
17 
18 void IconPair::showIcon1()
19 {
20  previousState_ = (icon1_->isHidden() ? 1 : 0);
21  setState(0);
22 }
23 
24 void IconPair::showIcon2()
25 {
26  previousState_ = (icon1_->isHidden() ? 1 : 0);
27  setState(1);
28 }
29 
30 void IconPair::undoShowIcon1()
31 {
32  setState(previousState_);
33 }
34 
35 void IconPair::undoShowIcon2()
36 {
37  setState(previousState_);
38 } //

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:

1 class TreeNode : public Wt::WCompositeWidget
2 {
3 public:
4  /*! \brief Construct a tree node with the given label.
5  *
6  * The label is formatted in a WText with the given formatting.
7  * The labelIcon (if not 0) will appear next to the label and its state
8  * will reflect the expand/collapse state of the node.
9  *
10  * Optionally, a userContent widget may be associated with the node.
11  * When expanded, this widget will be shown below the widget, but above
12  * any of the children nodes.
13  */
14  TreeNode(const std::string labelText,
15  Wt::TextFormat labelFormat,
16  IconPair *labelIcon, Wt::WContainerWidget *parent = 0);
17 
18  /*! \brief Adds a child node.
19  */
20  void addChildNode(TreeNode *node);
21 
22  /*! \brief Removes a child node.
23  */
24  void removeChildNode(TreeNode *node);
25 
26  /*! \brief Returns the list of children.
27  */
28  const std::vector<TreeNode *>& childNodes() const { return childNodes_; }
29 
30  /*! \brief Collapses this node.
31  */
32  void collapse();
33 
34  /*! \brief Expands this node.
35  */
36  void expand();
37 
38 private:
39  //! List of child nodes.
40  std::vector<TreeNode *> childNodes_;
41 
42  //! The parent node.
43  TreeNode *parentNode_;
44 
45  //! Layout (2x2 table).
46  Wt::WTable *layout_;
47 
48  //! The icon for expanding or collapsing.
49  IconPair *expandIcon_;
50 
51  //! The single image shown instead of the expand/collapse icon when no children.
52  Wt::WImage *noExpandIcon_;
53 
54  //! The icon next to the label.
55  IconPair *labelIcon_;
56 
57  //! The label.
58  Wt::WText *labelText_;
59 
60  //! The children count '(x)' for x children.
61  Wt::WText *childCountLabel_;
62 
63  //! The container in which the children are managed.
64  Wt::WContainerWidget *expandedContent_;
65 
66  //! Adjust the expand icon
67  void adjustExpandIcon();
68 
69  //! Returns if is the last child within its parent (is rendered differently)
70  bool isLastChildNode() const;
71 
72  //! Rerender when children have changed.
73  void childNodesChanged();
74 
75  //! Was collapsed (for undo of prelearned collapse() and expand() slots.
76  bool wasCollapsed_;
77 
78  //! Undo function for prelearning collapse()
79  void undoCollapse();
80 
81  //! Undo function for prelearning expand()
82  void undoExpand();
83 
84  //! Two sets of images, for a normal node, and for the last node.
85  enum ImageIndex { Middle = 0, Last = 1 };
86 
87  static std::string imageLine_[];
88  static std::string imagePlus_[];
89  static std::string imageMin_[];
90 }; //

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:

1 TreeNode::TreeNode(const std::string labelText,
2  Wt::TextFormat labelFormat,
3  IconPair *labelIcon,
4  Wt::WContainerWidget *parent)
5  : Wt::WCompositeWidget(parent),
6  parentNode_(0),
7  labelIcon_(labelIcon)
8 {

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.

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

We will implement the treenode as 2 by 2 table.

1  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).

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

The expanded content is a WContainerWidget.

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

We create the label and child count text widgets:

1 
2  labelText_ = new Wt::WText(labelText);
3  labelText_->setTextFormat(labelFormat);
4  labelText_->setStyleClass("treenodelabel");
5  childCountLabel_ = new Wt::WText();
6  childCountLabel_->setMargin(7, Wt::Left);
7  childCountLabel_->setStyleClass("treenodechildcount");

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

1 
2  layout_->elementAt(0, 0)->addWidget(expandIcon_);
3  layout_->elementAt(0, 0)->addWidget(noExpandIcon_);
4 
5  if (labelIcon_) {
6  layout_->elementAt(0, 1)->addWidget(labelIcon_);
7  labelIcon_->setVerticalAlignment(Wt::AlignMiddle);
8  }
9  layout_->elementAt(0, 1)->addWidget(labelText_);
10  layout_->elementAt(0, 1)->addWidget(childCountLabel_);
11 
12  layout_->elementAt(1, 1)->addWidget(expandedContent_);
13 
14  layout_->elementAt(0, 0)->setContentAlignment(Wt::AlignTop);
15  layout_->elementAt(0, 1)->setContentAlignment(Wt::AlignMiddle);

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

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

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().

1 
2 bool TreeNode::isLastChildNode() const
3 {
4  if (parentNode_) {
5  return parentNode_->childNodes_.back() == this;
6  } else
7  return true;
8 }
9 
10 void TreeNode::addChildNode(TreeNode *node)
11 {
12  childNodes_.push_back(node);
13  node->parentNode_ = this;
14 
15  expandedContent_->addWidget(node);
16 
17  childNodesChanged();
18 }
19 
20 void TreeNode::removeChildNode(TreeNode *node)
21 {
22  childNodes_.erase(std::find(childNodes_.begin(), childNodes_.end(), node));
23 
24  node->parentNode_ = 0;
25 
26  expandedContent_->removeWidget(node);
27 
28  childNodesChanged();
29 } //

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().

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

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

1 
2  adjustExpandIcon();

We also update the childCount label.

1 
2  if (childNodes_.size())
3  childCountLabel_
4  ->setText("(" + boost::lexical_cast<std::string>(childNodes_.size())
5  + ")");
6  else
7  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.

1 
2  resetLearnedSlots();
3 } //

The implementation of the collapse slot is as follows:

1 
2 void TreeNode::collapse()
3 {

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

1  wasCollapsed_ = expandedContent_->isHidden();

Next, we implement the actual collapse logic:

1 
2  expandIcon_->setState(0);
3  expandedContent_->hide();
4  if (labelIcon_)
5  labelIcon_->setState(0);
6 } //

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

1 
2 void TreeNode::expand()
3 {
4  wasCollapsed_ = expandedContent_->isHidden();
5 
6  expandIcon_->setState(1);
7  expandedContent_->show();
8  if (labelIcon_)
9  labelIcon_->setState(1);
10 
11  /*
12  * collapse all children
13  */
14  for (unsigned i = 0; i < childNodes_.size(); ++i)
15  childNodes_[i]->collapse();
16 } //

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.

1 
2 void TreeNode::undoCollapse()
3 {
4  if (!wasCollapsed_) {
5  // re-expand
6  expandIcon_->setState(1);
7  expandedContent_->show();
8  if (labelIcon_)
9  labelIcon_->setState(1);
10  }
11 }
12 
13 void TreeNode::undoExpand()
14 {
15  if (wasCollapsed_) {
16  // re-collapse
17  expandIcon_->setState(0);
18  expandedContent_->hide();
19  if (labelIcon_)
20  labelIcon_->setState(0);
21  }
22 
23  /*
24  * undo collapse of children
25  */
26  for (unsigned i = 0; i < childNodes_.size(); ++i)
27  childNodes_[i]->undoCollapse();
28 } //

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.

1 
2 void TreeNode::adjustExpandIcon()
3 {

We set the expand icon images:

1  ImageIndex index = isLastChildNode() ? Last : Middle;
2 
3  if (expandIcon_->icon1()->imageLink().url() != imagePlus_[index])
4  expandIcon_->icon1()->setImageLink(imagePlus_[index]);
5  if (expandIcon_->icon2()->imageLink().url() != imageMin_[index])
6  expandIcon_->icon2()->setImageLink(imageMin_[index]);
7  if (noExpandIcon_->imageLink().url() != imageLine_[index])
8  noExpandIcon_->setImageLink(imageLine_[index]);

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

1 
2  if (index == Last) {
3  layout_->elementAt(0, 0)
4  ->decorationStyle().setBackgroundImage("");
5  layout_->elementAt(1, 0)
6  ->decorationStyle().setBackgroundImage("");
7  } else {
8  layout_->elementAt(0, 0)
9  ->decorationStyle().setBackgroundImage("icons/line-trunk.gif",
10  Wt::WCssDecorationStyle::RepeatY);
11  layout_->elementAt(1, 0)
12  ->decorationStyle().setBackgroundImage("icons/line-trunk.gif",
13  Wt::WCssDecorationStyle::RepeatY);
14  } //

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

1 
2  if (childNodes_.empty()) {
3  if (noExpandIcon_->isHidden()) {
4  noExpandIcon_->show();
5  expandIcon_->hide();
6  }
7  } else {
8  if (expandIcon_->isHidden()) {
9  noExpandIcon_->hide();
10  expandIcon_->show();
11  }
12  }
13 } //

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.


Generated on Mon Aug 8 2016 for the C++ Web Toolkit (Wt) by doxygen 1.8.11