Click here to Skip to main content
15,894,410 members
Articles / Multimedia / GDI+

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.6K   1.8K   38  
An owner draw implementation of a VB.NET treeview to show some bold text in nodes.
Public Class TreeViewEx
	Inherits TreeView

	Private Structure RECT
		Friend left As Integer
		Friend top As Integer
		Friend right As Integer
		Friend bottom As Integer
	End Structure

	Private Structure NMHDR
		Friend hwndFrom As IntPtr
		Friend idFrom As IntPtr
		Friend code As Integer
	End Structure

	Private Structure NMCUSTOMDRAW
		Friend hdr As NMHDR
		Friend dwDrawStage As Integer
		Friend hdc As IntPtr
		Friend rc As RECT
		Friend dwItemSpec As IntPtr
		Friend uItemState As Integer
		Friend lItemlParam As IntPtr
	End Structure

	Private Structure NMTVCUSTOMDRAW
		Friend nmcd As NMCUSTOMDRAW
		Friend clrText As Integer
		Friend clrTextBk As Integer
		Friend iLevel As Integer
	End Structure

	Private Function HandleNotify(ByVal m As System.Windows.Forms.Message) As Integer

		' Reference:
		' http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/commctls/custdraw/custdraw.asp

		Const NM_FIRST As Integer = 0
		Const NM_CUSTOMDRAW As Integer = (NM_FIRST - 12)

		' Drawstage flags
		Const CDDS_PREPAINT As Integer = &H1
		Const CDDS_POSTPAINT As Integer = &H2
		Const CDDS_PREERASE As Integer = &H3
		Const CDDS_POSTERASE As Integer = &H4

		Const CDDS_ITEM As Integer = &H10000
		Const CDDS_ITEMPREPAINT As Integer = (CDDS_ITEM Or CDDS_PREPAINT)
		Const CDDS_ITEMPOSTPAINT As Integer = (CDDS_ITEM Or CDDS_POSTPAINT)
		Const CDDS_ITEMPREERASE As Integer = (CDDS_ITEM Or CDDS_PREERASE)
		Const CDDS_ITEMPOSTERASE As Integer = (CDDS_ITEM Or CDDS_POSTERASE)
		Const CDDS_SUBITEM As Integer = &H20000

		' Custom draw return flags
		Const CDRF_DODEFAULT As Integer = &H0
		Const CDRF_NEWFONT As Integer = &H2
		Const CDRF_SKIPDEFAULT As Integer = &H4
		Const CDRF_NOTIFYPOSTPAINT As Integer = &H10
		Const CDRF_NOTIFYITEMDRAW As Integer = &H20
		Const CDRF_NOTIFYSUBITEMDRAW As Integer = &H20		 ' Flags are the same, we can distinguish by context
		Const CDRF_NOTIFYPOSTERASE As Integer = &H40

		Dim tNMHDR As NMHDR
		Dim tNMTVCUSTOMDRAW As NMTVCUSTOMDRAW
		Dim iResult As Integer = 0
		Dim objObject As Object
		Dim objGraphics As Graphics
		Dim objTreeNode As TreeNode
		Dim objTreeNodeEx As TreeNodeEx


		Try

			If Not m.LParam.Equals(IntPtr.Zero) Then

				objObject = m.GetLParam(GetType(NMHDR))

				If TypeOf objObject Is NMHDR Then

					tNMHDR = DirectCast(objObject, NMHDR)

					If tNMHDR.code = NM_CUSTOMDRAW Then

						objObject = m.GetLParam(GetType(NMTVCUSTOMDRAW))

						If TypeOf objObject Is NMTVCUSTOMDRAW Then

							tNMTVCUSTOMDRAW = DirectCast(objObject, NMTVCUSTOMDRAW)

							Select Case tNMTVCUSTOMDRAW.nmcd.dwDrawStage

								Case CDDS_PREPAINT
									iResult = CDRF_NOTIFYITEMDRAW

								Case CDDS_ITEMPREPAINT
									iResult = CDRF_NOTIFYPOSTPAINT

								Case CDDS_ITEMPOSTPAINT

									objTreeNode = TreeNode.FromHandle(Me, tNMTVCUSTOMDRAW.nmcd.dwItemSpec)
									objTreeNodeEx = DirectCast(objTreeNode, TreeNodeEx)

									objGraphics = Graphics.FromHdc(tNMTVCUSTOMDRAW.nmcd.hdc)

									PaintTreeNode(objTreeNodeEx, objGraphics)

									objGraphics.Dispose()

									iResult = CDRF_DODEFAULT

							End Select

						End If

					End If

				End If

			End If

		Catch ex As Exception
			System.Windows.Forms.MessageBox.Show(ex.ToString)
		End Try

		Return iResult

	End Function

	Private Sub PaintTreeNode(ByVal objTreeNodeEx As TreeNodeEx, ByVal objGraphics As Graphics)

		Dim objBoldFont As Font
		Dim objNormalFont As Font
		Dim objBackgroundBrush As Brush
		Dim objForegroundBrush As Brush
		Dim objPen As Pen
		Dim sngPosX As Single
		Dim sngPosY As Single
		Dim sngWidth As Single
		Dim sngHeight As Single
		Dim sTextPortion1 As String
		Dim sTextPortion2 As String
		Dim sTextPortion3 As String
		Dim iTreeNodeWidth As Integer
		Dim sngTreeNodeWidthPortion1 As Single
		Dim sngTreeNodeWidthPortion2 As Single
		Dim sngTreeNodeWidthPortion3 As Single

		' Get the fonts
		objNormalFont = Me.Font
		objBoldFont = New Font(objNormalFont, FontStyle.Bold)

		' Get the position and dimensions of the treenode
		sngWidth = objTreeNodeEx.Bounds.Width - 2
		sngHeight = objTreeNodeEx.Bounds.Height - 2
		sngPosX = objTreeNodeEx.Bounds.X + 1
		sngPosY = objTreeNodeEx.Bounds.Y + 1

		' If there is text in bold, we need to get the width of each portion
		If objTreeNodeEx.BoldTextInitialPosition >= 0 And objTreeNodeEx.BoldTextLength > 0 Then

			' Get the text before the bold portion
			sTextPortion1 = objTreeNodeEx.Text.Substring(0, objTreeNodeEx.BoldTextInitialPosition)

			' Get the text of the bold portion
			sTextPortion2 = objTreeNodeEx.Text.Substring(objTreeNodeEx.BoldTextInitialPosition, objTreeNodeEx.BoldTextLength)

			' Get the text after the bold portion
			If objTreeNodeEx.BoldTextInitialPosition + objTreeNodeEx.BoldTextLength < objTreeNodeEx.Text.Length Then
				sTextPortion3 = objTreeNodeEx.Text.Substring(objTreeNodeEx.BoldTextInitialPosition + objTreeNodeEx.BoldTextLength)
			End If

			' Get the width of each portion, taking into account the font
			sngTreeNodeWidthPortion1 = MeasureCorrectedTextWidth(objGraphics, objNormalFont, sngWidth, sngHeight, sTextPortion1)
			sngTreeNodeWidthPortion2 = MeasureCorrectedTextWidth(objGraphics, objBoldFont, sngWidth, sngHeight, sTextPortion2)
			sngTreeNodeWidthPortion3 = MeasureCorrectedTextWidth(objGraphics, objNormalFont, sngWidth, sngHeight, sTextPortion3)

			' Get the total width
			iTreeNodeWidth = CType(sngTreeNodeWidthPortion1 + sngTreeNodeWidthPortion2 + sngTreeNodeWidthPortion3, Integer)

		Else
			iTreeNodeWidth = objTreeNodeEx.Bounds.Width
		End If

		' Make a correction to ensure always a correct width
		iTreeNodeWidth += 6

		' Get the brushes. Note: we should take into account the BackColor and ForeColor of the treenode (left as exercise)
		If Me.SelectedNode Is objTreeNodeEx Then
			objBackgroundBrush = SystemBrushes.Highlight
			objForegroundBrush = SystemBrushes.HighlightText
		Else
			objBackgroundBrush = SystemBrushes.Window
			objForegroundBrush = SystemBrushes.WindowText
		End If

		' Fill the background rectangle
		objGraphics.FillRectangle(objBackgroundBrush, objTreeNodeEx.Bounds.X, objTreeNodeEx.Bounds.Y, iTreeNodeWidth, objTreeNodeEx.Bounds.Height)

		' Draw focus rectangle if it is the selected treenode
		If Me.SelectedNode Is objTreeNodeEx Then
			objPen = New Pen(Color.Gray, 1)
			objPen.DashStyle = Drawing.Drawing2D.DashStyle.Dot
			objGraphics.DrawRectangle(objPen, objTreeNodeEx.Bounds.X, objTreeNodeEx.Bounds.Y, iTreeNodeWidth - 1, objTreeNodeEx.Bounds.Height - 1)
			objPen.Dispose()
		End If

		' Draw the text
		If objTreeNodeEx.BoldTextInitialPosition >= 0 Then

			' Part 1
			If sTextPortion1 <> "" Then
				sngPosX += PaintText(objGraphics, sTextPortion1, objNormalFont, objForegroundBrush, sngPosX, sngPosY, sngWidth, sngHeight)
			End If

			' Part 2
			If sTextPortion2 <> "" Then
				sngPosX += PaintText(objGraphics, sTextPortion2, objBoldFont, objForegroundBrush, sngPosX, sngPosY, sngWidth, sngHeight)
			End If

			' Part 3
			If sTextPortion3 <> "" Then

				' If the first character after the bold portion is a space character, add an extra pixel
				If sTextPortion3.StartsWith(" ") Then
					sngPosX += 1
				End If

				sngPosX += PaintText(objGraphics, sTextPortion3, objNormalFont, objForegroundBrush, sngPosX, sngPosY, sngWidth, sngHeight)

			End If

		Else
			sngPosX += PaintText(objGraphics, objTreeNodeEx.Text, objNormalFont, objForegroundBrush, sngPosX, sngPosY, sngWidth, sngHeight)
		End If

		objBoldFont.Dispose()

	End Sub

	Private Function PaintText(ByVal objGraphics As Graphics, ByVal sText As String, ByVal objFont As Font, ByVal objBrush As Brush, ByVal sngPosX As Single, ByVal sngPosY As Single, ByVal sngWidth As Single, ByVal sngHeight As Single) As Single

		Dim sngNewWidth As Single

		objGraphics.DrawString(sText, objFont, objBrush, sngPosX, sngPosY)

		sngNewWidth = MeasureCorrectedTextWidth(objGraphics, objFont, sngWidth, sngHeight, sText)

		Return sngNewWidth

	End Function

	Private Function MeasureCorrectedTextWidth(ByVal objGraphics As Graphics, ByVal objFont As Font, ByVal sngWidth As Single, ByVal sngHeight As Single, ByVal sText As String) As Single

		Dim sngSingleTextWidth As Single
		Dim sngDoubleTextWidth As Single
		Dim sngResult As Single = 0.0

		If sText <> "" Then

			' 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
			sngSingleTextWidth = MeasureTextWidth(objGraphics, objFont, sngWidth, sngHeight, sText)
			sngDoubleTextWidth = MeasureTextWidth(objGraphics, objFont, sngWidth * 2, sngHeight, sText & sText)

			sngResult = sngDoubleTextWidth - sngSingleTextWidth

		End If

		Return sngResult

	End Function

	Private Function MeasureTextWidth(ByVal objGraphics As Graphics, ByVal objFont As Font, ByVal sngWidth As Single, ByVal sngHeight As Single, ByVal sText As String) As Single

		Dim sngResult As Single
		Dim colCharacterRanges(0) As CharacterRange
		Dim colRegions(1) As Region
		Dim objStringFormat As StringFormat
		Dim objLayoutRectangleF As RectangleF
		Dim objMeasureRectangleF As RectangleF

		objStringFormat = New StringFormat()

		' Allow enough width for the bold case
		If objFont.Bold Then
			sngWidth = sngWidth * 2
		End If

		objLayoutRectangleF = New RectangleF(0, 0, sngWidth, sngHeight)

		colCharacterRanges(0) = New CharacterRange(0, sText.Length)
		objStringFormat.SetMeasurableCharacterRanges(colCharacterRanges)
		colRegions = objGraphics.MeasureCharacterRanges(sText, objFont, objLayoutRectangleF, objStringFormat)
		objMeasureRectangleF = colRegions(0).GetBounds(objGraphics)
		sngResult = objMeasureRectangleF.Width

		Return sngResult

	End Function

	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

End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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