Design of Portable and Reusable GUI Controls

Author: Egor Kazachkov
Published On: Thursday, August 09, 2007 | Last Modified On: Tuesday, September 02, 2008

Introduction

This paper is intended for GUI developers who want to write portable, reusable, and fast controls for visualizing big and complex data. There are some common problems such as bad performance and usability issues such as the inability to present big data sets in clear way, so the user can easily navigate and analyze it. Also, program data structures and visual data representation often becomes very dependent. Thus, control becomes very specialized and cannot be used in other applications without making significant changes. This paper presents an approach to design complex controls that allow solving the problems discussed above. In this paper, examples of the graph viewer control are used to illustrate the basic ideas. These ideas can also be applied to a variety of other controls.

Definitions

A Graph is a set of objects and relations between them. Objects are called nodes. The relation between the nodes is called edges. So, a visible graph is a set of nodes (squares, rectangles, circles with or without labels, and so on), and edges (lines or curves) connecting the nodes. The algorithm that defines the position of the nodes and edges is called a layout.

Note that the nodes can contain other nodes and edges (subgraphs). All nodes that have edges going from them to any given node are called parents of this node. All nodes that have edges going to them from any given node are called children of this node.


Common problems in graph control
There are many applications where graph control can be used for visualizing data. If you want to use the same graph control for different applications you must provide a way to customize it. Any such customization must not affect the performance of the application. Following is a list of possible problems that you may encounter when you apply control to different applications:

  • Appearance of nodes and edges. Graph elements may either have a different color and shape, or may or may not have a text label, and so on. It is impossible to predict the appearance of the graph elements and implement them in advance. Also, there are a lot of possible graph layouts used in different cases. You may need a way to change this according to the application.

  • User interaction. Sometimes you may display a static graph, which cannot be changed and sometimes you may allow the user to change the graph in some way (add or delete nodes and edges, move them around, change captions, and so on).

  • Processing external data. When you deal with big data sets you often receive data from an external source, such as a local file or a remote database.
The Graph control must provide the scrolling and zooming facilities to navigate through big graphs. The following section discusses how this problem can be solved with high flexibility and without any performance degradation.


Customized Appearance and User Interaction
Usually you can differentiate between the two parts of control, namely data elements, which represent individual pieces of data, and the core part, which organize data elements as whole. Graph data elements consist of nodes and edges. The core part provides features such as scrolling, zooming, drawing, and event processing using customized versions from data elements, when needed. For example, when you click the mouse button, the core part defines where this event occurs. If it occurs on some data element then the event information is passed to this data element handler else core the part processes event itself.

There are two things in a graph which vary from application to application that you should customize. First is the appearance and behavior of data elements and second is the way you organize the elements. If you want customize your control to be easy, you need to define interfaces for those things and use them to control only through interfaces. So, if you want to change something you would just need to implement these interfaces in the new way, without changing the code in the control. This is called the “strategy” pattern.

Graph control has the following strategies:

  • INodeHandler

  • IEdgeHandler

  • ILayout
class INodeHandler
{
    public:
       // Draws given Node
      	virtual void Draw(Node) = 0;

       // Returns size of describing rectangle. This function used by layouts to determine
       // nodes positions without intersections.
      	virtual idvc::dsize GetSize(Node)= 0;

       // Sets zoom factor which will be used for all subsequent Draw calls. This function
       // is used by control core part in zooming implementation.
	virtual void SetZoomFactor(double f) {};

       // Processes mouse click event
       virtual ChangesType HandleClick(Node n, double inX, double inY,
                                     int kstate, idvc::MouseButton Button);

	// Processes tooltip event
       virtual ChangesType HandleOnTooltip(Node n, CGraphTooltipEvent* pEvent);

}; // end INodeHandler

class ILayout
{
    public:

        /// This function should make new layout for all nodes inside
        /// given node and calculate it size. Positions for nodes inside
        /// must be defined regarding upper left corner of given node,
        /// assuming it has coordinates (0,0).
        virtual void Make( Node ) = 0;

        /// The same as Make, but it should use information from
        /// previous layouts and try keep mutual positions of already
        /// placed nodes.
        virtual void Update( Node ) = 0;

        /// This function recalculates coordinates of nodes and edges
        /// when changed sizes of owned nodes or skip parameters of 
  	 /// ILayout.
        /// It assumed that Make or Update was called before and sizes 
        /// of all owned nodes is known. Unlike Make and Update, it 
  	 /// is not recursive.
        virtual void Resize( Node ) = 0;
};

The above class defines the layout strategy functions for three different cases, namely:

  • when a graph needs a full rearrangement

  • when a graph structure has partially changed, and you need to rearrange only the changed part

  • when only the sizes of a node have changed, and you need recalculate the coordinates (no need to define mutual placement of nodes and edges)
The main goal of such a differentiation is to reduce the layout calculation time. If you add one node to a big graph you need not recalculate the layout of the whole graph.


Fast Painting and Event Processing
The last but not least problem, which you should resolve is fast reaction on events (at least the Repaint event). As you deal with big data sets your control should allow you to quickly scroll and zoom the content. The main problem here is that the event processing and drawing function are defined by user (through the interfaces described above), and each element in control can have its own implementation of drawing and event processing. Hence, you cannot guarantee fast processing. Nevertheless, you can reduce the quantity of element functions calls.

void CContent::DrawContent(idvc::IPainter* p)
{
    // determine invalid rectangles which should be repainted
    idvcfrw::CInvalidRegion InvalidRegions(draw_rect, valid_rect);

    for(int i = 0; i < InvalidRegions.size(); ++i)
    {   
	 // get rectangle corresponding to next invalid region
        idvc::drect rect = InvalidRegions[i];

        // find and repaint nodes which intersect with invalid rectangle
        NodeSet nodes = graph->HitNodeTest(rect.left, rect.top, rect.right, rect.bottom);
        for_each(nodes.begin(), nodes.end(), DrawNode(p,scale));

	 // find and repaint edges which intersect with invalid rectangle
        EdgeSet es = graph->HitEdgeTest(rect.left, rect.top, rect.right, rect.bottom);
        for_each(es->Begin(), es->End(), DrawEdge(p,scale));
    };
};

Event processing (at least for window events) can be organized similar to drawing, exploiting the fact that window events also have a point or a rectangle where they occur. So control can determine nodes and edges, which are affected by any given event and calls event processing functions only for these elements, greatly reducing processing time.


Data Loading
When you deal with big data sets they are usually stored in some external data source. It may vary in different applications (file, database, etc.). So you need a mechanism to allow fast data to load independently, from an external data source. Fast often means partial because if you get really big data set you cannot load it quickly no matter what. But control usually needs only a small part of the data for processing and you should load only this part.

There are two possible ways to implement partial loading. First is similar to the fast drawing and event processing described above. You need to define an interface which is independent from the data source and use it to load data. You should try to implement a method that allows you to define the data you need to be loaded without full loading. Then you can define the data elements that should be loaded and call the load functions through the interface only for these elements.

It is not always possible to define the elements to load automatically. Another way to implement partial loading is getting user input. In this case, the user defines when and what data should be loaded.

ChangesType PortNodeHandler::HandleClick(Node n, double inX, double inY, 
                                         int kstate, idvc::MouseButton Button)
{
    ChangesType processed = ctNone;
    idvc::dpoint pos = n->GetPosition();
    idvc::dsize size = n->GetSize();

    // if node doesn’t have nested nodes and was clicked by left mouse button
    if ( (n->GetOwned()->GetCount() == 0) && (Button == idvc::mbLeft) )
    {
        if (node_drawer.IsLeftPortClicked(n, inX, inY))
	 {
	     bool hide = ( CountAllParents(n) == CountVisibleParents(n) );
 	     // if all parents visible
	     if( hide )   Fold(n, fdParents);
	     else       Unfold(n, fdParents);
	 }
        else if (node_drawer.IsRightPortClicked(n, inX, inY))
	 {
	     bool hide = ( CountAllChildren(n) == CountVisibleChildren(n) );
	     // if all children visible
	     if( hide ) Fold(n, fdChildren);
	     else     Unfold(n, fdChildren);
	 }
        else
        {
            // if user clicks on node itself mark it as selected
            SetFlag(n, Node::fSelected, !IsFlagSet(n, Node::fSelected));
        };
        processed = ctAll;
    };
    OnClick.fire(n, inX, inY, kstate, Button);
    return processed;
};


Conclusion
Following are the main ideas behind constructing portable and fast controls:

  • Divide your control in two parts. First are classes that can be customized and which represent data elements and methods for their organization (strategies). Second is the core (permanent) part that provides common features such as scrolling zooming, drawing, and event processing. The core part uses strategies when needed only through rigorously defined interfaces.

  • The core part should minimize calls of customized parts. When you deal with big data sets only small subsets are drawn or processed every time. So if you can implement fast selection of required subsets and call the customized parts for this subset only then will the performance will be increased.

  • Control should minimize data loading when you use an external data source. It can be done in two ways. First is the same as minimizing calls of the customized parts. Note that you should minimize the calls of the external data source. This method is possible when you can selectively load and can determine in advance what you should load. The second method involves user interaction. As the user deals with small subsets of data every time you can implement the customized parts in such way that visible elements allow the user to manually select the elements to be loaded for further work.
The following picture depicts how these principles are applied to the graph control:

Picture 1. Graph Design.
Use these principles to get a highly customizable, portable, and fast control, which can deal with huge data sets and can be tuned for using in many applications.


Related Links

About the Author
Egor Kazachkov is a software engineer, working in the Data Visualization Controls team at Intel Corporation. Areas of interest include graph algorithms and visualization, algorithms analysis and design, C++ programming, and functional programming.

Post a comment If you have any questions, please contact our support team.