.NET GDI+ Draw Tree / Hierarchy

In this article, you'll learn to use GDI+ and the Bitmap, Graphic, SolidBrush, and FillRectangle classes to draw a hierarchy as a graphic that has user interface interactivity.

Download C# Source Code

I went looking for a chart control or code sample that would render an image of my hierarchical data tree the other day and couldn't find one that suited my needs. We want to be able to display characteristics of our data in a rich graphical way for use in auditing and reporting tools associated with our research applications. Of course, it needed to work on both the desktop and via our web based applications. While the sample code is a windows form application, the classes are suited for use in an ASP.NET application.

The code sample contained in this article was initially inspired by the Microsoft Research TreeView control and associated Tree Generation control. Their control does some interesting things but wasn't really a good fit for my needs (not to mention it appears to improperly render some tree structures).

I set out to write my own drawing mechanism that would be based on a data structure similar to the TreeView control's TreeNode class. It ended up being your run of the mill class with a strongly typed collection of child nodes. The idea being that I could set the specific drawing properties including where the data point would be drawn, where it would be drawn in relationship to its parent, and even the make the overall dimensions of the image just big enough to accurately display all the data. Then, and only then, would it be appropriate to draw the data to the image. This simple concept has given me the ground work to draw the same set of data in a variety of different ways other than a tree.

One of the other aspects of my design was that I could easily integrate the image generated into my windows forms application or my web application and make it interactive. In the windows app, I could draw things on the graphic based on mouse over, mouse up, or any other event raised in the control that renders my image. I've included a few extremely basic methods to demonstrate this in the sample form class. One of the samples highlights the connector lines for the selected thread. In a web environment, I could easily construct an image map to perform the same types of tasks with client side JavaScript.

Our final implementation of this will likely incorporate icons and / or images along with some text affects. All I've done here is provide the basic framework for rendering the tree and give you a good start on rendering the data in various others ways as well. If you want to incorporate icons and images, just make sure you adjust the InitializeXAndYCoordinates method to account for the size of your icons and images.

Here are a few snapshots of what the tree renders. I've also updated this article on October 3, 2005 to include an option to horizontally render a node and its immediate children:


Let's take a look at the code below:
  
Sample Form Excerpt

  private void Form1_Load(object sender, System.EventArgs e)
  {
     // Create some sample data of around 500 nodes.
    root = TreeSample.SampleData.Create(2,6);
    DrawTree();
  }

  private void DrawTree()
  {

    Bitmap bm = null;
    int width = 0;
    int height = 0;

    try
    {

       // Set our original starting position for the
       // root of the tree.

       root.X = 10;
       root.Y = 10;
              
       // Without actually drawing the nodes, set
       // the x and y property values of where they'll
       // be drawn.

       using(TreeGenerator tg = new TreeGenerator(8,8,24,24))
       {

         tg.InitializeXYCoordinates(root);

         // Using the internal MaxCurrentX and MaxCurrentY
         // values which take into account node placement
         // and any text shown, get the dimensions that we
         // should use to size the image to match our hierarchy
         // size.

         height = tg.GetImageHeight();  
         width = tg.GetImageWidth();
            
         // Create the image from scratch.  We do
         // this outside the generator class to offer
         // maximum flexibility concerning the image for
         // this such as transparency and image background.

         // This also gives us the flexibility to draw
         // numerous trees in different locations on the
         // same image.

         bm = new Bitmap(width,height,PixelFormat.Format32bppArgb);

         Graphics g = Graphics.FromImage(bm);

         g.SmoothingMode  = SmoothingMode.AntiAlias;

         // Draw the background color.  Comment these
         // four lines out and insert code to populate
         // the .BackGroundImage if desired.

         Rectangle bgImg = new Rectangle(0,0,width,height);
            
         SolidBrush bgBrush = new SolidBrush(Color.White);

         g.FillRectangle(bgBrush,bgImg);

         bgBrush.Dispose();

         // Draw the node hierarchy use the root node
         // as a starting point.

         tg.DrawNodes(g,root);
                  
         g.Dispose();

         // Save our image to a file if we want
         // to see the entire thing in a browser.

         bm.Save("test.gif",ImageFormat.Gif);

         // Get a quick peek on the windows forms.

         this.pictureBox1.Width = bm.Width;
         this.pictureBox1.Height = bm.Height;
         this.pictureBox1.Image = bm;
            
      }
            
    }
    catch (Exception err) { Debug.WriteLine(err.Message); }          
}


private void pictureBox1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{

    try
    {

       DataNode node = root.GetDataNodeByPosition(root,e.X,e.Y);

       if (node == null) { return; }

       if (node.LineColor == Color.Orange)
       {
          SetUnSelectedThreadDisplay(node,node.UniqueID);
       }
       else
       {
          SetSelectedThreadDisplay(node,node.UniqueID);   
       }
            
       DrawTree();

    }
    catch (Exception err) { Debug.WriteLine(err.Message); }
}

private void SetSelectedThreadDisplay(DataNode node,string uniqueID)
{

       if (node.UniqueID == uniqueID)
       {
         SetSelectedNodeDisplay(node);
         SetParentNodeDisplay(node);
         return;
       }

       for(int i=0;i<node.Nodes.Count;i++)
       {
         SetSelectedThreadDisplay(node.Nodes[i],uniqueID);
       }

    return;
}

private void SetUnSelectedThreadDisplay(DataNode node,string uniqueID)
{

       if (node.UniqueID == uniqueID)
       {
         SetUnSelectedNodeDisplay(node);
         SetUnSelectedParentNodeDisplay(node);
         return;
       }

       for(int i=0;i<node.Nodes.Count;i++)
       {
         SetUnSelectedThreadDisplay(node.Nodes[i],uniqueID);
       }

    return;
}

private void SetParentNodeDisplay(DataNode node)
{

      if (node.Parent == null) { return; }
      SetSelectedNodeDisplay(node.Parent);
      SetParentNodeDisplay(node.Parent);

    return;
}

private void SetUnSelectedParentNodeDisplay(DataNode node)
{

     if (node.Parent == null) { return; }
     SetUnSelectedNodeDisplay(node.Parent);
     SetUnSelectedParentNodeDisplay(node.Parent);

   return;
}

private void SetSelectedNodeDisplay(DataNode node)
{

      node.LineColor = Color.Red;
      node.LineDashStyle = DashStyle.Solid;  
      node.LineThickness = 2;

   return;
}


private void SetUnSelectedNodeDisplay(DataNode node)
{

      node.LineColor = Color.Gray;
      node.LineDashStyle = DashStyle.Dash;  
      node.LineThickness = 1;

    return;
}


TreeGenerator Class
using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;  
using System.Drawing.Text;
using System.Diagnostics;
using EggHeadCafe.Drawing;

namespace EggHeadCafe.Drawing
{

   public class TreeGenerator : EggHeadCafe.Drawing.Shared,IDisposable
   {


       public TreeGenerator(int markerHeight,
                           int markerWidth,
                           int connectorWidth,
                           int connectorHeight)
      {

               MarkerHeight = markerHeight;
               MarkerWidth = markerWidth;
               ConnectorHeight = connectorHeight;
               ConnectorWidth = connectorWidth;
               dummy = new Bitmap(5,5);
               graphicFunctions = Graphics.FromImage(dummy);
       }
      
       public void Dispose()
      {
         if (Disposed) { return; }
        graphicFunctions.Dispose();
        dummy.Dispose();
      }
      
      public int GetImageWidth()
      {
         return MaxCurrentX + ConnectorWidth;
      }
      
      public int GetImageHeight()
      {
         return MaxCurrentY + ConnectorHeight;
      }


      public void InitializeXYCoordinates(DataNode parentNode)
      {
           DataNode node;
           int index = 0;
           int x = 0;
           SizeF textSize;

               if (parentNode.Parent == null)
               {
                   parentNode.RelativeIndex = "1";
                   MaxCurrentY = 0;
                   MaxCurrentX = 0;
               }

               x = parentNode.X + ConnectorWidth;

               if (MaxCurrentY == 0)
               {
                  MaxCurrentY = parentNode.Y;
               }

               if (MaxCurrentX == 0)
               {
                  MaxCurrentX = x;
               }

               if (x > MaxCurrentX)
               {
                 MaxCurrentX = x;
               }

               if (parentNode.DrawText)
               {
                   if (parentNode.Text.Trim().Length > 0)
                   {
                     textSize = graphicFunctions.MeasureString(parentNode.Text,
                                                               parentNode.TextFont);
                     parentNode.TextWidth = (int)textSize.Width;
                     parentNode.TextHeight = (int)textSize.Height;
                     MaxCurrentX = x + (int)textSize.Width;
                   }
               }
              
               for(int i=0;i<parentNode.Nodes.Count;i++)
               {

                  MaxCurrentY += ConnectorHeight;

                  node = parentNode.Nodes[i];
                
                  index = i + 1;

                  node.RelativeIndex = node.Parent.RelativeIndex + "." + index.ToString();

                  node.X = x;
                  
                  node.Y = MaxCurrentY;

                  InitializeXYCoordinates(node);

               }

        }
        
        
     public void DrawNodes(Graphics g,DataNode parentNode)
    {

          // DrawLineConnector(g,parentNode);
             DrawAngleConnector(g,parentNode);
             DrawMarker(g,parentNode);
             DrawText(g,parentNode);

             for(int i=0;i<parentNode.Nodes.Count;i++)
             {                  
               DrawNodes(g,parentNode.Nodes[i]);
             }


    }
  

  }
}


// Shared Class
using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;  
using System.Drawing.Text;
using System.Diagnostics;
using EggHeadCafe.Drawing;

namespace EggHeadCafe.Drawing
{

   public class Shared
   {

         protected int MarkerHeight = 10;
        protected int MarkerWidth = 10;
        protected int ConnectorWidth = 24;
        protected int ConnectorHeight = 24;

         // Keep track of the maximum point to the
        // right and the maximum point towards
        // the bottom.  Makes sure that each
        // element can be drawn below or to
        // the right of the previous one.

        protected int MaxCurrentX = 0;
        protected int MaxCurrentY = 0;
        
        // Use a dummy bitmap and graphics
        // object enabling us to use methods
        // prior to actually creating the real
        // graphic.  ie measure text

        protected Bitmap dummy;
        protected Graphics graphicFunctions;
        protected bool Disposed = false;

        public int GetMarkerCenterX(int position)
         {
             return position + (int)(MarkerWidth * .50);
         }

        public int GetMarkerCenterY(int position)
         {
             return position + (int)(MarkerHeight * .5);
         }

        public void DrawLineConnector(Graphics g,DataNode node)
        {
      
          float parentX = 0;
          float parentY = 0;
          float childX = 0;
          float childY = 0;

             if (node.Parent == null) { return; }

             // I stored the positions in local variables to make
             // it easier for you to provide fancier line drawing
             // capabilities.

             parentX = (float)GetMarkerCenterX(node.Parent.X);
             parentY = (float)GetMarkerCenterY(node.Parent.Y);
             childX = (float)GetMarkerCenterX(node.X);
             childY = (float)GetMarkerCenterY(node.Y);

             parentY = parentY + (float)(MarkerHeight * .5);

             PointF pt1 = new PointF(parentX,parentY);

             PointF pt2 = new PointF(childX,childY);

             PointF[] points = { pt1, pt2 };

             Pen fillPen = new Pen(node.LineColor,node.LineThickness);

             fillPen.DashStyle = node.LineDashStyle;  
              
             g.DrawLines(fillPen,points);

             fillPen.Dispose();

             points = null;

        return;
   }


   public void DrawAngleConnector(Graphics g,DataNode node)
   {
      
     float parentX = 0;
     float parentY = 0;
     float midX = 0;
     float midY = 0;
     float childX = 0;
     float childY = 0;

       if (node.Parent == null) { return; }

       // I stored the positions in local variables to make
       // it easier for you to provide fancier line drawing
       // capabilities.

       // Set the connector points to the middle of
       // the marker nodes.  The existing .X and .Y
       // coordinates are the very top and left of the marker.

       parentX = (float)GetMarkerCenterX(node.Parent.X);
       parentY = (float)GetMarkerCenterY(node.Parent.Y);
       midX = (float)GetMarkerCenterX(node.Parent.X);
       midY = (float)GetMarkerCenterY(node.Y);
       childX = (float)GetMarkerCenterX(node.X);
       childY = (float)GetMarkerCenterY(node.Y);

       parentY = parentY + (float)(MarkerHeight * .5);

       PointF pt1 = new PointF(parentX,parentY);

       PointF pt2 = new PointF(midX,midY);

       PointF pt3 = new PointF(childX,childY);

       PointF[] points = { pt1, pt2,pt3 };

       Pen fillPen = new Pen(node.LineColor,node.LineThickness);

       fillPen.DashStyle = node.LineDashStyle;  
              
       g.DrawLines(fillPen,points);

       fillPen.Dispose();

       points = null;

   return;
}


public void DrawMarker(Graphics g,DataNode node)
{

     Rectangle rect = new Rectangle(node.X,node.Y,MarkerWidth,MarkerHeight);

     SolidBrush fillBrush = new SolidBrush(node.MarkerColor);

     g.FillEllipse(fillBrush,rect);

     fillBrush.Dispose();

   return;
}


public void DrawText(Graphics g,DataNode node)
{
      
   float x = 0F;
   float y = 0F;

      if (!node.DrawText) { return; }
      if (node.Text.Trim().Length < 1) { return; }

      SolidBrush TextBrush = new SolidBrush(node.TextColor);

      StringFormat sf = new StringFormat();

      sf.Alignment = StringAlignment.Near;

      sf.Trimming = StringTrimming.EllipsisCharacter;

      sf.LineAlignment = StringAlignment.Center;

      g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

       // Set the text just to the left of the marker
      // and add a little bit of spacing between
      // to the right of the marker before drawing
      // the text.

      x = (float)GetMarkerCenterX(node.X);
      y = (float)GetMarkerCenterY(node.Y);

      x += MarkerWidth;

      g.DrawString(node.Text,node.TextFont,TextBrush,x,y,sf);

      TextBrush.Dispose();

      sf.Dispose();

   return;
  }


}
}


DataNode Class
using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;  
using System.Diagnostics;

namespace EggHeadCafe.Drawing
{

    public class DataNode  
    {

       private bool StopRecursion = false;
       private DataNode ReturnNode = null;

       public DataNode()
       {
            mNodes = new EggHeadCafe.Drawing.DataNodeCollection(this);
       }

       private string mRelativeIndex = "";

       public string RelativeIndex
       {
          get { return mRelativeIndex; }
          set { mRelativeIndex = value; }
       }
        
       private EggHeadCafe.Drawing.DataNodeCollection mNodes = null;

       public EggHeadCafe.Drawing.DataNodeCollection Nodes
       {
          get { return mNodes; }
          set { mNodes = value; }
       }
        
       private EggHeadCafe.Drawing.DataNode mParent = null;

       public EggHeadCafe.Drawing.DataNode Parent
       {
         get { return mParent; }
         set { mParent = value; }
       }

       private System.Drawing.Color mBackColor = System.Drawing.Color.Black;

       public System.Drawing.Color BackColor
       {
          get { return mBackColor; }
          set { mBackColor = value; }
       }

       private System.Drawing.Color mTextColor = System.Drawing.Color.Black;

       public System.Drawing.Color TextColor
       {
          get { return mTextColor; }
          set { mTextColor = value; }
       }
        
       private System.Drawing.Color mMarkerColor = System.Drawing.Color.Black;

       public System.Drawing.Color MarkerColor
       {
          get { return mMarkerColor; }
          set { mMarkerColor = value; }
       }

       private System.Drawing.Color mLineColor = System.Drawing.Color.Black;

       public System.Drawing.Color LineColor
       {
         get { return mLineColor; }
         set { mLineColor = value; }
       }

       private int mLineThickness = 1;

       public int LineThickness
       {
         get { return mLineThickness; }
         set { mLineThickness = value; }
       }

       private DashStyle mLineDashStyle = DashStyle.DashDot;

       public System.Drawing.Drawing2D.DashStyle LineDashStyle
       {
         get { return mLineDashStyle; }
         set { mLineDashStyle = value; }
       }

       private System.Drawing.Font mTextFont = null;

       public System.Drawing.Font TextFont
       {
         get { return mTextFont; }
         set { mTextFont = value; }
       }
        
       private bool mDrawText = false;

       public bool DrawText
       {
         get { return mDrawText; }
         set { mDrawText = value; }
       }

       private string mUniqueID = "";

       public string UniqueID
       {
         get { return mUniqueID; }
         set { mUniqueID = value; }
       }

       private string mText = "";

       public string Text
       {
         get { return mText; }
         set { mText = value; }
       }

       private int mX = 0;

       public int X
       {
         get { return mX; }
         set { mX = value; }
       }

       private int mY = 0;

       public int Y
       {
         get { return mY; }
         set { mY = value; }
       }

       private int mTextWidth = 0;

       public int TextWidth
       {
          get { return mTextWidth; }
          set { mTextWidth = value; }
       }

       private int mTextHeight = 0;

       public int TextHeight
       {
         get { return mTextHeight; }
         set { mTextHeight = value; }
       }

       private int mConnectorAngle = 90;

       public int ConnectorAngle
       {
         get { return mConnectorAngle; }
         set { mConnectorAngle = value; }
       }

       public DataNode GetDataNodeByUniqueID(DataNode node,string uniqueID)
       {
            
          try
          {

              StopRecursion = false;
              ReturnNode = null;

               GetDataNodeByKey(node,uniqueID);

               if (ReturnNode != null)
              {
                 return ReturnNode;
               }

           }
          catch { throw; }
          finally { StopRecursion = false; }
          return null;
        }

         public DataNode GetDataNodeByPosition(DataNode node,int x,int y)
        {
            
            try
            {

              StopRecursion = false;
              ReturnNode = null;

               GetDataNodeByXAndY(node,x,y);

               if (ReturnNode != null)
              {
                 return ReturnNode;
               }

             }
             catch { throw; }
            finally { StopRecursion = false; }
            return null;
        }

         private void GetDataNodeByKey(DataNode node,string uniqueID)
         {

               if (StopRecursion) { return; }

               if (node.UniqueID == uniqueID)
               {
                    ReturnNode = node;
                    StopRecursion = true;
                     return;
               }

               for(int i=0;i<node.Nodes.Count;i++)
               {
                   GetDataNodeByKey(node.Nodes[i],uniqueID);
               }

            return;
        }


         private void GetDataNodeByXAndY(DataNode node,int x,int y)
         {

               if (StopRecursion) { return; }

               int textRight = node.X + node.TextWidth;
               int textBottom = node.Y + node.TextHeight;

               if ((x >= node.X) && (x <= textRight))
               {
                 if ((y >= node.Y) && (y <= textBottom))
                 {
                    ReturnNode = node;
                    StopRecursion = true;
                     return;
                 }
               }

               for(int i=0;i<node.Nodes.Count;i++)
               {
                   GetDataNodeByXAndY(node.Nodes[i],x,y);
               }

            return;
        }


    }
}

By Robbe Morris   Popularity  (3433 Views)
Picture
Biography - Robbe Morris
Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.  Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. Microsoft MVP