Stingray® Foundation : Chapter 7 Layout Manager : Layout Manager Architecture
Layout Manager Architecture
The Layout Manager architecture is partly based in part on the Composite design pattern (see “The Composite Pattern.”) The Layout Manager consists of a collection of nodes arranged in a tree-like hierarchy of responsibility.
Layout Nodes
All the nodes in the layout tree are expected to implement the interface stingray::foundation::ILayoutNode, which defines the minimum set of functionality expected from the members of the tree. A layout node is defined as any object that implements this interface, as well as the composite pattern interface.
Every node is assigned a rectangle it is responsible for. Rectangles associated to child nodes should not go outside the boundaries of the parent’s rectangle. The root node of the composite owns the rectangle affected by all layout operations. Typically, this rectangle corresponds to some window’s client area.
Conceptually, layout nodes are either proactive or reactive in nature. Proactive nodes, also known as composites, hold the layout algorithms. Each proactive node encapsulates one layout algorithm. Examples are CRelativeLayout and CScaleLayout. Proactive nodes are designed to have and administrate child nodes.
Reactive nodes, also known as primitives, are home to the leaf objects controlled by the proactive nodes. When you implement the appropriate functions in the ILayoutNode interface, a reactive node can respond to events driven by its parent node and position, resize, and render itself as appropriate. CWindowLayoutNode is an example of a reactive node, designed to link to a window. Nodes derived from CDCLayoutBase are also reactive nodes.
The proactive versus reactive node distinction is only conceptual in nature. Syntactically, both types of nodes realize ILayoutNode and both possess the same type-interface. Only the intended use of the object defines its designation. Some objects can be both proactive and reactive; for example, CSplitterLayout can be considered of both types.
In general, proactive nodes are not visible entities. For example, an algorithmic layout node is a “black box rectangle” that is responsible for administrating all its children within that rectangle. It is entirely possible, however, that one of its children is also a proactive node that administers its child nodes. This is the strength of a polymorphic layout node in a composite, tree-like hierarchy.
SFL provides a default base class for all layout nodes, CLayoutNode. CLayoutNode mixes a default implementation of the ILayoutNode interface with the implementation of the composite pattern. It also declares the creation and destruction methods required by the class factory, as discussed later in this section. In addition to that, CLayoutNode derives from the IEventRouter and IEventListener interfaces, so it can receive and process window messages and route them through the layout tree. Deriving your custom layout node classes from CLayoutNode is not required, but it is recommended, to make these services available to every layout node.
Layout Recalculation Process
The Layout Manager framework is responsible for rearranging the contents of a window when required by some external or internal condition, such as a resize operation. The actual procedure involves two steps: recalculation and realization.
Recalculation
During the first step, the recalculation stage, proactive nodes, or composites, have the responsibility to act. The nodes follow their particular layout algorithm to logically rearrange the rectangles of their child nodes.
The RecalcLayout() method is called on the root node of the Layout tree, giving it a new rectangle on which to rearrange the contents. In the RecalcLayout() implementation, a node calculates the rectangles that will be assigned to its child nodes, based on its own assigned rectangle. RecalcLayout() is then called on each of those nodes, so they, in turn, can manage the positioning of their child nodes, and so on. A child node can contest the rectangle being assigned to it, if its parent specifies that it is willing to negotiate. However, the parent node always has the last word. The parent can deny the region requested by a child and assign another totally different one, if the request doesn’t fit in the layout algorithm the parent implements.
The recalculation stage does not affect the visible objects in the screen. Recalculation deals with the rectangles assigned to the nodes in a completely logical fashion.
Realization
The second step in the layout recalculation process is the realization stage. It is during this stage that the new rectangles assigned to each node during the recalculation process are reflected by the screen objects.
Reactive nodes, or primitives, are responsible for the execution of RealizeNode(). These nodes are associated with visible objects on the screen, like child windows, images or decorations.
For example, on RealizeNode(), a CWindowLayoutNode instance should call some Win32 API like SetWindowPos() to adjust its associated window to the new area.
A recalculation is usually triggered by resizing window messages (WM_SIZE). Sometimes, you want to specify a maximum or minimum size for a node. This can be achieved by the SetMinMaxSize() method in the ILayoutNode interface, as shown in Example 48.
Example 48 – Setting maximum and minimum window sizes
// the dialog will never get smaller than 475x450
// or larger than 900x600
pRootNode->SetMinMaxSize(CSize(475,450), CSize(900,600));
Node Creation
The creation of layout nodes is performed by an specialized class, the layout factory. The design of this class is based on the Object Factory design pattern (see “The Object Factory Pattern.”)
Using the CLayoutFactory class requires the definition of a layout map, which specifies the layout node classes that will be used in the application. The layout map gives the layout factory the information needed for it to be able to create new instances of those classes. The concept is very similar to the object map mechanism used in ATL to define COM class factories for the COM objects exported by a server.
For example, if your application uses the layout node classes CBorderClientLayout, CWindowLayoutNode, CSplitterLayout and CBorderEdge, you should include somewhere in your code the following lines:
Example 49 – Defining a layout map
BEGIN_LAYOUT_MAP()
LAYOUT_MAP_ENTRY(foundation::CBorderClientLayout)
LAYOUT_MAP_ENTRY(foundation::CSplitterLayout)
LAYOUT_MAP_ENTRY(foundation::CGripperWrapper)
LAYOUT_MAP_ENTRY(foundation::CBorderEdge)
LAYOUT_MAP_ENTRY(foundation::CWindowLayoutNode)
END_LAYOUT_MAP()
The advantage of using a layout map is that your application only pays the price of the layout node classes it will actually use. The layout factory does not need to hard-code all the known layout node classes, which would include them in your final executable file, even if they are not used in the application.
A layout map also adds flexibility to the design. If you come up with additional layout node classes that you wish to use in your applications, it is not necessary to modify the CLayoutFactory class or derive a new class from it. Including additional entries in your layout map makes your new layout node classes available to the layout factory.
Each node class is identified by a GUID, just like in COM. The class factory uses this information to identify the class creator function in the layout map.
To create a new node instance, the layout class factory publishes a method called CreateLayoutNode(). This method receives the GUID of the class you want to instantiate, and returns a newly created instance of that class.
The layout factory also provides a DestroyLayoutNode() method, which destroys and deallocates the node passed as a parameter, as well as its descendants.
Node Initialization
After it is created but before it is used, a layout node needs to be properly initialized. The ILayoutNode interface publishes an Init() method, which takes two parameters. The first one is the handle of the window associated with the root of the layout operation. All nodes need this information in case they need to interact with the window. A second optional parameter identifies a handle for a child window; this parameter is used only in the CWindowLayoutNode, which is associated with that child window.
The final part of the initialization process is integrating the layout node into the layout tree, as the child of another layout node. This is generally performed using the AddLayoutNode() method of the ILayoutNode interface. Some specialized layout node classes may require you to use some other mechanism for this, like the AddPane() procedure for splitters.