Click here to Skip to main content
15,885,278 members
Articles / Multimedia / GDI+
Article

Custom Draw TreeView in VB.NET

Rate me:
Please Sign up or sign in to vote.
4.37/5 (14 votes)
25 May 20052 min read 163.3K   1.8K   38   13
An owner draw implementation of a VB.NET treeview to show some bold text in nodes.

Introduction

This article shows the owner draw technique for a TreeView using Visual Basic .NET to draw some portions of the text of the nodes in bold font, as shown in the image.

Background

The owner draw technique for Windows Common Controls is well documented in the following MSDN article: Customizing a Control's Appearance Using Custom Draw&, which I recommend to read. The article explains the notification messages, the paint cycles and drawing stages, and provides a C++ example, so I won't repeat it here.

Using the code

A TreeNodeEx class (derived from TreeNode) is provided in the source code, which allows you to specify in the constructor the node text, the initial text position that will use the bold font, and the length of the bold text.

A helper function like this is provided to add nodes to a TreeView:

VB
Private Function AddNodeToTreeView(ByVal colNodes As TreeNodeCollection, _
     ByVal sText As String, ByVal iBoldTextInitialPosition As Integer, _
     ByVal iBoldTextLength As Integer) As TreeNodeEx

   Dim objTreeNodeEx As TreeNodeEx

   objTreeNodeEx = New TreeNodeEx(sText, _
                   iBoldTextInitialPosition, iBoldTextLength)
   colNodes.Add(objTreeNodeEx)

   Return objTreeNodeEx

End Function

A TreeViewEx class (derived from TreeView) is provided too. This class performs the owner draw with the tree nodes. The class is used as follows:

VB
Private m_ctlTreeViewEx As TreeViewEx

Private Sub Form1_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load

   Dim objRootTreeNodeEx As TreeNodeEx

   m_ctlTreeViewEx = New TreeViewEx()
   Me.Controls.Add(m_ctlTreeViewEx)
   m_ctlTreeViewEx.Left = 0
   m_ctlTreeViewEx.Top = 0
   m_ctlTreeViewEx.Dock = DockStyle.Fill

   objRootTreeNodeEx = AddNodeToTreeView(m_ctlTreeViewEx.Nodes, _
                       "This is the first node", 12, 5)

   AddNodeToTreeView(objRootTreeNodeEx.Nodes, "The second node", 4, 6)
   AddNodeToTreeView(objRootTreeNodeEx.Nodes, "Third node", 0, 5)
   AddNodeToTreeView(objRootTreeNodeEx.Nodes, "Node 4", 5, 1)
   AddNodeToTreeView(objRootTreeNodeEx.Nodes, "Last node", -1, 0)

   objRootTreeNodeEx.Expand()

End Sub

Points of Interest

There are some points of interest in the source code:

  • Windows Common Controls send NM_CUSTOMDRAW notifications through WM_NOTIFY messages to the parent window. So, we would need to intercept that message in the parent window, outside of our treeview control, which breaks the encapsulation rules. Fortunately, the .NET Framework allows controls to receive that message "reflected". To do this, the .NET Framework adds the value 0x2000 to the value of the WM_NOTIFY message and sends it to the control. Therefore, the TreeView control can receive the WM_NOTIFY message sent to its parent window using the following code in its own WndProc procedure:
    VB
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    
       Const WM_NOTIFY As Integer = &H4E
    
       Dim iResult As Integer
       Dim bHandled As Boolean = False
    
       If m.Msg = (&H2000 Or WM_NOTIFY) Then
       ' It is the reflected WM_NOTIFY message sent to the parent
    
          If m.WParam.Equals(Me.Handle) Then
             iResult = HandleNotify(m)
             m.Result = New IntPtr(iResult)
             bHandled = True
          End If
    
       End If
    
       If Not bHandled Then
          MyBase.WndProc(m)
       End If
    
    End Sub
  • To draw the text of a node, which mixes bold and non-bold portions, we need to draw the initial non-bold portion, the bold portion, and the final non-bold portion. To do this, we need to know the length of each portion in pixels, to set the coordinate X of the next portion, and we need a very accurate measure to avoid "holes" between two portions. It happens that when using the function Graphics.MeasureCharacterRanges to measure drawn strings, some pixels are added to the exact result. Since we need the exact result (in order to draw the next text just after the previous one), we can use the following trick: we measure the length of the text and the length of the text duplicated: since in both cases the extra pixels are added, the difference will be the exact length.

History

  • 24-May-2005. Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here



Comments and Discussions

 
GeneralException thrown when debugging through VS2005 Pin
Enriad7-Mar-07 14:08
Enriad7-Mar-07 14:08 
QuestionTreeview display width bug Pin
Vaibhav Deshpande23-Oct-06 3:57
Vaibhav Deshpande23-Oct-06 3:57 
GeneralAny possibility for C# version Pin
zhaozh30-Jun-06 10:42
zhaozh30-Jun-06 10:42 
GeneralRe: Any possibility for C# version Pin
PerfectlyNormalBeast18-May-09 6:29
PerfectlyNormalBeast18-May-09 6:29 
<pre>using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Data;
using System.Text;
using System.Windows.Forms;

namespace TreeViewTester
{
     /// &lt;summary&gt;
     /// This treeview can mix bold and normal text
     /// &lt;/summary&gt;
     public partial class TreeViewBold : TreeView
     {
          #region Structs for HandleNotify

          private struct RECT
          {
               internal int left;
               internal int top;
               internal int right;
               internal int bottom;
          }

          private struct NMHDR
          {
               internal IntPtr hwndFrom;
               internal IntPtr idFrom;
               internal int code;
          }

          private struct NMCUSTOMDRAW
          {
               internal NMHDR hdr;
               internal int dwDrawStage;
               internal IntPtr hdc;
               internal RECT rc;
               internal IntPtr dwItemSpec;
               internal int uItemState;
               internal IntPtr lItemlParam;
          }

          private struct NMTVCUSTOMDRAW
          {
               internal NMCUSTOMDRAW nmcd;
               internal int clrText;
               internal int clrTextBk;
               internal int iLevel;
          }

          #endregion

          #region Constructor

          public TreeViewBold()
          {
               InitializeComponent();
          }

          #endregion

          #region Overrides

          protected override void WndProc(ref Message m)
          {
               const int WM_NOTIFY = 0x4E;

               bool isHandled = false;

               if (m.Msg == (0x2000 | WM_NOTIFY))          //     It is the reflected WM_NOTIFY message sent to the parent
               {
                    if (m.WParam.Equals(this.Handle))
                    {
                         int result = HandleNotify(m);
                         m.Result = new IntPtr(result);
                         isHandled = true;
                    }
               }

               if (!isHandled)
               {
                    base.WndProc(ref m);
               }
          }

          #endregion

          #region Private Methods

          private int HandleNotify(Message m)
          {
               //     Reference:
               //     http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/commctls/custdraw/custdraw.asp

               #region Constants

               const int NM_FIRST = 0;
               const int NM_CUSTOMDRAW = NM_FIRST - 12;

               //     Drawstage flags
               const int CDDS_PREPAINT = 0x1;
               const int CDDS_POSTPAINT = 0x2;
               const int CDDS_PREERASE = 0x3;
               const int CDDS_POSTERASE = 0x4;

               const int CDDS_ITEM = 0x10000;
               const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT);
               const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT);
               const int CDDS_ITEMPREERASE = (CDDS_ITEM | CDDS_PREERASE);
               const int CDDS_ITEMPOSTERASE = (CDDS_ITEM | CDDS_POSTERASE);
               const int CDDS_SUBITEM = 0x20000;

               //     Custom draw return flags
               const int CDRF_DODEFAULT = 0x0;
               const int CDRF_NEWFONT = 0x2;
               const int CDRF_SKIPDEFAULT = 0x4;
               const int CDRF_NOTIFYPOSTPAINT = 0x10;
               const int CDRF_NOTIFYITEMDRAW = 0x20;
               const int CDRF_NOTIFYSUBITEMDRAW = 0x20;          //     Flags are the same, we can distinguish by context
               const int CDRF_NOTIFYPOSTERASE = 0x40;

               #endregion

               int retVal = 0;

               try
               {
                    if (!m.LParam.Equals(IntPtr.Zero))
                    {
                         object objObject = m.GetLParam(typeof(NMHDR));

                         if (objObject is NMHDR)
                         {
                              NMHDR tNMHDR = (NMHDR)objObject;

                              if (tNMHDR.code == NM_CUSTOMDRAW)
                              {
                                   objObject = m.GetLParam(typeof(NMTVCUSTOMDRAW));

                                   if (objObject is NMTVCUSTOMDRAW)
                                   {
                                        NMTVCUSTOMDRAW tNMTVCUSTOMDRAW = (NMTVCUSTOMDRAW)objObject;

                                        switch (tNMTVCUSTOMDRAW.nmcd.dwDrawStage)
                                        {
                                             case CDDS_PREPAINT:
                                                  retVal = CDRF_NOTIFYITEMDRAW;
                                                  break;

                                             case CDDS_ITEMPREPAINT:
                                                  retVal = CDRF_NOTIFYPOSTPAINT;
                                                  break;

                                             case CDDS_ITEMPOSTPAINT:
                                                  TreeNodeBold treeNode = (TreeNodeBold)TreeNode.FromHandle(this, tNMTVCUSTOMDRAW.nmcd.dwItemSpec);

                                                  using (Graphics graphics = Graphics.FromHdc(tNMTVCUSTOMDRAW.nmcd.hdc))
                                                  {
                                                       //     Paint this tree node
                                                       PaintTreeNode(treeNode, graphics);
                                                  }

                                                  retVal = CDRF_DODEFAULT;
                                                  break;
                                        }
                                   }
                              }
                         }
                    }
               }
               catch (Exception ex)
               {
                    MessageBox.Show(ex.ToString(), "TreeView.HandleNotify", MessageBoxButtons.OK, MessageBoxIcon.Error);
               }

               //     Exit Function
               return retVal;
          }

          private void PaintTreeNode(TreeNodeBold treeNode, Graphics graphics)
          {
               if (treeNode == null)
               {
                    return;
               }

               //     Get the fonts
               Font fontNormal = this.Font;
               using (Font fontBold = new Font(fontNormal, FontStyle.Bold))
               {
                    //     Get the position and dimensions of the treenode (these get modified later, so I can't store them in a rectangle)
                    float x = treeNode.Bounds.X + 1f;
                    float y = treeNode.Bounds.Y + 1f;
                    float width = treeNode.Bounds.Width - 2f;
                    float height = treeNode.Bounds.Height - 2f;

                    #region Calculate Locations

                    int treeNodeWidth;
                    string textPortion1 = "";
                    string textPortion2 = "";
                    string textPortion3 = "";

                    //     If there is text in bold, we need to get the width of each portion
                    if (treeNode.BoldTextInitialPosition &gt;= 0 &amp;&amp; treeNode.BoldTextLength &gt; 0)
                    {
                         #region Contains Bold Text

                         //     Get the text before the bold portion
                         textPortion1 = treeNode.Text.Substring(0, treeNode.BoldTextInitialPosition);

                         //     Get the text of the bold portion
                         textPortion2 = treeNode.Text.Substring(treeNode.BoldTextInitialPosition, treeNode.BoldTextLength);

                         //     Get the text after the bold portion
                         if (treeNode.BoldTextInitialPosition + treeNode.BoldTextLength &lt; treeNode.Text.Length)
                         {
                              textPortion3 = treeNode.Text.Substring(treeNode.BoldTextInitialPosition + treeNode.BoldTextLength);
                         }

                         //     Get the width of each portion, taking into account the font
                         float treeNodeWidthPortion1 = MeasureCorrectedTextWidth(graphics, fontNormal, width, height, textPortion1);
                         float treeNodeWidthPortion2 = MeasureCorrectedTextWidth(graphics, fontBold, width, height, textPortion2);
                         float treeNodeWidthPortion3 = MeasureCorrectedTextWidth(graphics, fontNormal, width, height, textPortion3);

                         //     Get the total width
                         treeNodeWidth = Convert.ToInt32(treeNodeWidthPortion1 + treeNodeWidthPortion2 + treeNodeWidthPortion3);

                         #endregion
                    }
                    else
                    {
                         //     Standard Text
                         treeNodeWidth = treeNode.Bounds.Width;
                    }

                    //     Make a correction to ensure always a correct width
                    treeNodeWidth += 6;

                    #endregion

                    #region Get Brushes

                    //     Get the brushes. Note: we should take into account the BackColor and ForeColor of the treenode (left as exercise)
                    Brush objBackgroundBrush = null;
                    Brush objForegroundBrush = null;
                    if (this.SelectedNode == treeNode)
                    {
                         objBackgroundBrush = SystemBrushes.Highlight;
                         objForegroundBrush = SystemBrushes.HighlightText;
                    }
                    else
                    {
                         objBackgroundBrush = SystemBrushes.Window;
                         objForegroundBrush = SystemBrushes.WindowText;
                    }

                    #endregion

                    //     Fill the background rectangle
                    graphics.FillRectangle(objBackgroundBrush, treeNode.Bounds.X, treeNode.Bounds.Y, treeNodeWidth, treeNode.Bounds.Height);

                    //     Draw focus rectangle if it is the selected treenode
                    if (this.SelectedNode == treeNode)
                    {
                         using (Pen objPen = new Pen(Color.Gray, 1))
                         {
                              objPen.DashStyle = DashStyle.Dot;
                              graphics.DrawRectangle(objPen, treeNode.Bounds.X, treeNode.Bounds.Y, treeNodeWidth - 1, treeNode.Bounds.Height - 1);
                         }
                    }

                    #region Draw Text

                    //     Draw the text
                    if (treeNode.BoldTextInitialPosition &gt;= 0)
                    {
                         //     Part 1
                         if (textPortion1 != "")
                         {
                              x += PaintText(graphics, textPortion1, fontNormal, objForegroundBrush, x, y, width, height);
                         }

                         //     Part 2
                         if (textPortion2 != "")
                         {
                              x += PaintText(graphics, textPortion2, fontBold, objForegroundBrush, x, y, width, height);
                         }

                         //     Part 3
                         if (textPortion3 != "")
                         {
                              //     If the first character after the bold portion is a space character, add an extra pixel
                              if (textPortion3.StartsWith(" "))
                              {
                                   x += 1;
                              }

                              x += PaintText(graphics, textPortion3, fontNormal, objForegroundBrush, x, y, width, height);
                         }
                    }
                    else
                    {
                         x += PaintText(graphics, treeNode.Text, fontNormal, objForegroundBrush, x, y, width, height);
                    }

                    #endregion
               }
          }

          private float PaintText(Graphics graphics, string text, Font font, Brush brush, float x, float y, float width, float height)
          {
               graphics.DrawString(text, font, brush, x, y);

               float retVal = MeasureCorrectedTextWidth(graphics, font, width, height, text);
               return retVal;
          }

          private float MeasureCorrectedTextWidth(Graphics graphics, Font font, float width, float height, string text)
          {
               float retVal = 0f;

               if (text != "")
               {
                    //     The measurement routine (MeasureCharacterRanges) adds some extra pixels to the result, that we want to discard.
                    //     To do this, we meausure the string and the string duplicated, and the difference is the measure that we want.
                    //     That is:
                    //     A = X + C
                    //     B = 2X + C
                    //     Where A and B are known (the measures) and C is unknown. We are interested in X, which is X = B - A
                    float singleTextWidth = MeasureTextWidth(graphics, font, width, height, text);
                    float doubleTextWidth = MeasureTextWidth(graphics, font, width * 2, height, text + text);

                    retVal = doubleTextWidth - singleTextWidth;
               }

               return retVal;
          }

          private float MeasureTextWidth(Graphics graphics, Font font, float width, float height, string text)
          {
               //     Allow enough width for the bold case
               float actualWidth = width;
               if (font.Bold)
               {
                    actualWidth *= 2;
               }

               RectangleF layoutRect = new RectangleF(0, 0, actualWidth, height);

               CharacterRange[] charRanges = new CharacterRange[1];
               charRanges[0] = new CharacterRange(0, text.Length);

               StringFormat stringFormat = new StringFormat();
               stringFormat.SetMeasurableCharacterRanges(charRanges);

               Region[] regions = graphics.MeasureCharacterRanges(text, font, layoutRect, stringFormat);
               RectangleF measureRect = regions[0].GetBounds(graphics);

               //     Exit Function
               return measureRect.Width;
          }

          #endregion
     }

     #region Class: TreeNodeBold

     public partial class TreeNodeBold : TreeNode
     {
          #region Declaration Section

          //TODO:   Support a list of bold selections
          private int _boldTextInitialPosition;
          private int _boldTextLength;

          #endregion

          #region Constructor

          public TreeNodeBold(string text, int boldTextInitialPosition, int boldTextLength)
          {
               base.Text = text;
               _boldTextInitialPosition = boldTextInitialPosition;
               _boldTextLength = boldTextLength;
          }

          #endregion

          #region Public Properties

          public int BoldTextInitialPosition
          {
               get
               {
                    return _boldTextInitialPosition;
               }
          }

          public int BoldTextLength
          {
               get
               {
                    return _boldTextLength;
               }
          }

          #endregion
     }

     #endregion
}
</pre>

and the designer file:
<pre>namespace TreeViewTester
{
     partial class TreeViewBold
     {
          /// &lt;summary&gt;
          /// Required designer variable.
          /// &lt;/summary&gt;
          private System.ComponentModel.IContainer components = null;

          /// &lt;summary&gt;
          /// Clean up any resources being used.
          /// &lt;/summary&gt;
          /// &lt;param name="disposing"&gt;true if managed resources should be disposed; otherwise, false.&lt;/param&gt;
          protected override void Dispose(bool disposing)
          {
               if (disposing &amp;&amp; (components != null))
               {
                    components.Dispose();
               }
               base.Dispose(disposing);
          }

          #region Component Designer generated code

          /// &lt;summary&gt;
          /// Required method for Designer support - do not modify
          /// the contents of this method with the code editor.
          /// &lt;/summary&gt;
          private void InitializeComponent()
          {
               components = new System.ComponentModel.Container();
          }

          #endregion
     }
}
</pre>
GeneralRe: Any possibility for C# version Pin
PerfectlyNormalBeast18-May-09 9:47
PerfectlyNormalBeast18-May-09 9:47 
QuestionEvents do not fire Pin
Thomas Abegglen9-Mar-06 20:26
Thomas Abegglen9-Mar-06 20:26 
AnswerRe: Events do not fire Pin
Thomas Abegglen9-Mar-06 20:36
Thomas Abegglen9-Mar-06 20:36 
QuestionAbout Checkbox Pin
whatda10120-Feb-06 12:29
whatda10120-Feb-06 12:29 
GeneralFore and back colors Pin
GWSyZyGy30-Sep-05 6:39
GWSyZyGy30-Sep-05 6:39 
Generalenable regular actions on custom draw treeview Pin
Member 43204713-Aug-05 0:58
Member 43204713-Aug-05 0:58 
QuestionHow about custom drawn icons? Pin
Amit G7-Aug-05 14:23
Amit G7-Aug-05 14:23 
GeneralGreat article - and very timely Pin
Mark Mischke7-Jul-05 0:40
Mark Mischke7-Jul-05 0:40 
Generalexcellent Pin
henok14-Jun-05 12:56
henok14-Jun-05 12:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.