Context Awareness and Recursion Part 4 of 4

I finished up Part 3 in this series with some methods to display a hierarchical TreeView of Controls.  In the last post, I mentioned that we would continue with a demonstration of populating a menu of controls.  Hopefully at this point we’ve done this enough that you can adapt the methods to your own needs.  The code, although quite similar to the TreeView, gave me a bit of a headache but I believe to have come up with a usable solution.  The snag I ran into was wanting the top level menu item to reflect the form itself, but not have a submenu item that also pointed to the form.  Once I ironed that out, it was all downhill from there.

Once again, we’ll be using the ToolboxBitmapAttribute to spruce up our routine, or we would have some plain looking menus. The Hierarchical menu code below isn’t as polished as I was hoping it would be, but it gets the job done.  To use this example, create a new Windows Forms project and replace Form1 code with the code below:

Public Class Form1
    Dim ms As New MenuStrip
    Dim imglst As New ImageList

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

        With ms
            .Items.Clear()
            .SuspendLayout()
            .ImageList = imglst
            .Parent = Me
            .Items.Add(GetControlMenu(Me))
            .ResumeLayout()
        End With

    End Sub

    Private Function GetControlMenu(ByVal root As Object, Optional ByVal pmnuitem As ToolStripMenuItem = Nothing)
        'pmnuitem ~ ParentMenuItem
        'cmnuitem ~ ChildMenuItem
        'For objects with no name
        Dim mnuName As String = IIf(root.name = "", "UnNamed " & root.name.GetType.Name, root.name)
        Dim cMnuItem As New ToolStripMenuItem

        If imglst.Images.ContainsKey(root.GetType.Name) = False Then
            'Add a new picture
            Dim bm As New ToolboxBitmapAttribute(root.GetType)
            imglst.Images.Add(root.GetType.Name, bm.GetImage(root.GetType, False))
        End If

        If pmnuitem Is Nothing Then
            pmnuitem = New ToolStripMenuItem
            With pmnuitem
                .Name = Me.Name & " Controls"
                .Text = .Name
                .Image = imglst.Images(root.GetType.Name)
            End With
            For Each item As Control In root.Controls
                GetControlMenu(item, pmnuitem)
            Next
        Else
            With cMnuItem
                .Name = mnuName
                .Text = mnuName & "(" & root.GetType.Name & ")"
                .Image = imglst.Images(root.GetType.Name)
            End With
            AddHandler cMnuItem.Click, AddressOf MenuItemClicked

            pmnuitem.DropDownItems.Add(cMnuItem)

            For Each item As Control In root.Controls
                GetControlMenu(item, cMnuItem)
            Next
        End If
        Return pmnuitem

    End Function

    Private Function MenuItemClicked(ByVal sender As System.Object, ByVal e As System.EventArgs)
        If TypeOf sender Is ToolStripMenuItem Then
            MsgBox("You clicked the " & sender.text & " menu item", MsgBoxStyle.Information, "Control Menu Demo")
            Return True
        Else
            Return False
        End If
    End Function

End Class

Before you start it up, you may want to drag a few random controls to the Form, possibly a container or two with some child controls.  Once you actually have some controls on the form, Press F5 to try it out.  I’ve included only a simple handler that provides a message box when you Click a menu item.  Like the TreeView example in Part 3 of this series, the menu items use the ToolBoxBitMapAttribute method of GetImage to use with the items.  This example isn’t as short as the TreeView example, but it has the same basic logic flow; it populates and object with items returned from a Function.  In this case, the menu item can’t be populated directly from the MenuStrip, so we have to create the top level MenuItem the first time through the routine.  I’d like to have been able to have the routine only call itself in one logic tree, but I couldn’t quite get that result.  Basically, the IF…THEN block is used to determine whether or not Object passed is Nothing, and if so create the root menu item.  Once that’s done, the pMnuItem is no longer nothing, so the next time around that logic tree is skipped.  I added a few controls to my test and yours should appear similar:

Screen Shot Menu

If you have read the first three entries of this series and still have questions, please feel free to post a comment on this blog.  I’d like to jump to the next and what I believe to be the final answer to the original taskings; the creation of a bread crumb display based on the CurrentControl.  Before we do that, get a new project ready because we are going to abandon the code from Part 1 and 2.   At the end of this series, I’m going to post a complete block of code that includes the menus, navigation, treeview, propertygrid, and all the other goodies all in one file.  Until then, lets build our “breadcrumb” style indicator.  Again, I’m coming up short with a practical reason why you would do this, but it may be useful in troubleshooting, auditing, or navigation of some sort.  Open your mind a bit; what if we were building an application with many types of custom controls; such as a drawing program or even a game?  A menu of objects may certainly come in handy!  You could use a popup menu (context) instead and set the object to the item that received the Right Click and just show it’s sub items.

Who’s Your Daddy?

And for that matter, who’s his daddy?  What about his?  Well, there’s a built in function that can tell us what object another object is owned by, and it’s no surprise that it’s the Parent property.  Each object, all the way up to the Form, has a parent.  It would be simple enough to retrieve an Object’s Parent, but we can’t know definitively how many Objects are in it’s ancestry unless we use, wait for it…recursion.  We can also get the Form that the Object is owned by using the [Object].FindForm.FindForm method.  What we are missing are the Objects between the Form and the Object itself.  Additionally, some controls can be orphaned.  We could instantiate a control, set the properties, and even add code for the control but it can’t be referenced as a member of a controls collection until it’s given a parent…how sad.  Fortunately for our purposes, we won’t need anything as complex as an Ahnentafel, but simply a direct line from the Form to the Control.

Back on track, we are going to build a breadcrumb type display, something that would look like this:

Form1 >> GroupBox1 >> GroupBox2 >> TextBox1

This would indicate that we have the Cursor hovering over TextBox1, that is a child of GroupBox2, that is a child of GroupBox1, that is a child of Form1.  In order to make this all work, we’ll need our GetChildAtPoint code back with the timer.  I cleaned it up a bit since Part 1 and Part 2 of this series, and simplified the layout a bit.  We’ll get to that in a bit, for now we need to discuss our BreadCrumb type display.  I’ve created the function, GetAncestry, that takes an Object and returns a string like discussed above.  Again, we use recursion.

Private Function GetAncestry(ByVal obj As Control, Optional ByVal crumbtrail As String = "") As String

        'AKA WhosYerDaddy, and WhosHisDaddy, etc...
        'takes a control name, calls itself recursively until parent=me (thisform)
        'the string will have to be built right to left until we get me (thisform)

        'Nothing to do
        If obj Is Me Then Return MyClass.Name

        'check for NoName
        Dim crumb As String = obj.Name
        If crumb = "" Then crumb = "Unnamed " & obj.GetType.Name

        If obj.Parent Is Me Then 'all done
            Return Me.Name & " >> " & crumb & crumbtrail
        Else 'still looking up the tree
            Return GetAncestry(obj.Parent, " >> " & crumb & crumbtrail)
        End If

End Function

The GetAncestry Function returns a string, and can be called during the Timer Tick Event or just about any other time you want to pass it a Control name and get back a your breadcrumb or Ancestry string.   Since we start with an empty string, the the Function builds it from scratch.  I made the crumbtrail optional so you don’t have to call it with an empty string.  To use the Function, use the syntax String=GetAncestry(Object).  In the Timer1_Tick event, I’m calling it with:

lblBreadCrumb.Text = "You are here: " & GetAncestry(CurrentControl)

You could easily change the “ >> ” to another delimiter and display it top to bottom with a Split if you desire, or simply use it in a label at the top of the form for a view of “Where Am I?” on this form, like I’ve shown here.

Providing Some Usefulness

None of the code in this series of posts would be much use if we didn’t actually do something with the information returned by the functions.  What you do with the information depends on your original need for actually wanting to know what control is currently under the mouse cursor.  It is ultimately up to you.  Maybe you’d like to offer a custom help solution, audit control utilization, temporarily draw to a control, allow a user to identify a control for troubleshooting, etc…  You are only limited by your imagination.

I’ve wrapped up the routines provided in this series into a single file; the hierarchical treeview, hierarchical menu, context labels, property grid, and a few other goodies.  Our example code now includes it’s own context awareness copy to clipboard (F3) functionality as an example.  I put all of the code into a single form class you can use right away.  The screen shot below was taken while the mouse was hovering over the form’s title bar and pressing F3, then pasting directly into this blog.

Screen Shot, Full App

The next images were obtained by positioning the cursor over a single control and pressing F3, then pasting into this Blog. 

image  image image

These are just a few examples on the practical use of our CurrentControl Function, I can certainly see a use for application documentation.  Another use for the CurrentControl Function could be launching some custom help tool, as an example only I’ve handled the F1 KeyDown with the following code:

Case Keys.F1 'Launch first google hit on MSDN based on the control type
                System.Diagnostics.Process.Start _
                ("http://www.google.com/search?btnI=I%27m+Feeling+Lucky&q=" & _
                 obj.GetType.ToString & "%20site:msdn.microsoft.com")

That’s about it for this series.  I’ve covered the basic tasks identified in the first post, and unless I stop adding features we’ll wind up with a useful application that actually does something.  The cleaned up and final code can be found in the code block below:

'Demonstration of using recursion, control collections, GetChildAtPoint
'Jason L Stenklyft original code only as usual
'01 Jan 2009
'For more information visit www.stenklyft.com or email jason@stenklyft.com

Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms

Public Class frmMain
    Inherits Form

#Region "Declarations"
    Enum enumRetImageTypes
        retImage
        retKeyName
    End Enum

    Public Class CoordLbl
        Inherits System.Windows.Forms.Label
        Sub New()
            Dock = DockStyle.Top
            AutoSize = True
            Padding = New Padding(3)
            Text = "Uninitiated"
        End Sub
    End Class
    Public Class GrpHdrLbl
        Inherits System.Windows.Forms.Label
        Sub New()
            Dock = DockStyle.Top
            ForeColor = Color.FromKnownColor(KnownColor.ActiveCaptionText)
            BackColor = Color.FromKnownColor(KnownColor.ActiveCaption)
            Padding = New Padding(0, 3, 0, 3)
            Text = "Uninitiated"
        End Sub
    End Class

    Private pnlLeft, pnlright As New Panel
    Private lblMouseMove, lblCursorPos, lblCurrentControlName, _
    lblCurrentControlType, lblCurrentControlLoc, lblCursorOnCurrent, _
    lblCurrentControlImage, lblCurrentControlHandle As New CoordLbl
    Public CurrentControl As Object
    Public LastControl As Object
    Private pg As New PropertyGrid
    Private btnCreateMDIChild As New Button
    Private btnRebuildMenu As New Button
    Private btnRebuildTVW As New Button
    Private chkPI As New CheckBox
    Public tmr As New Timer
    Private lblBreadCrumb As New Label
    Private lbltvwHeader, lblControlHeader, lblPropHeader As New GrpHdrLbl
    Private ms As New MenuStrip
    Private imglst As New ImageList
    Private tvwDocOutline As New TreeView
    Private ts As New ToolStrip
    Private mnuHelp, mnuAbout As New ToolStripMenuItem
    Private tslbl As New System.Windows.Forms.ToolStripLabel
    Private iMDIClientPTR As New IntPtr 'for use painting mdi client

#End Region

#Region "Load"
    Public Sub New()

        With mnuAbout
            .Text = "&About"
        End With

        With mnuHelp
            .Text = "&Help"
            .DropDown.Items.Add(mnuAbout)
        End With

        With ts
            .Dock = DockStyle.Bottom
            .Name = "ts"
            .GripStyle = ToolStripGripStyle.Hidden
            .Items.Add(tslbl)
            .SendToBack()
            .Tag = "Toolstrip"
        End With

        With tslbl
            .Name = "ts"
            .Text = "Tag Info Displays Here"
            .Alignment = ToolStripItemAlignment.Right

        End With

        With Me
            .KeyPreview = True
            .Text = "Control Recursion and GetChildAtPoint Demo"
            .Controls.Add(pnlLeft)
            .Controls.Add(pnlright)
            .Controls.Add(ts)
            .Size = New Size(800, 600)
            .Name = "frmMain"
            .Tag = "This is the main form's Tag"
        End With

        With lbltvwHeader
            .Text = "Document Outline"
        End With

        With lblControlHeader
            .Text = "Control && Coordinates"
        End With

        With lblPropHeader
            .Text = "Properties"
        End With

        With lblCurrentControlImage
            .ImageAlign = ContentAlignment.MiddleRight
            .TextAlign = ContentAlignment.MiddleLeft
            .Text = "Current Control Image:      "
        End With

        With tvwDocOutline
            .Dock = DockStyle.Fill
            .ImageList = imglst
            .Margin = New Padding(3)
            .Name = "tvwDocOutline"
        End With

        With tmr
            .Interval = 100
            .Enabled = False
        End With

        With chkPI
            .Dock = DockStyle.Top
            .Text = "Enable &Property Inspector"
        End With

        With btnCreateMDIChild
            .Dock = DockStyle.Top
            .Name = "btnCreateMDIChild"
            .Text = "Create MDI Child"
            .Margin = New Padding(6)
        End With

        With btnRebuildMenu
            .Dock = DockStyle.Top
            .Name = "btnRebuildMenu"
            .Text = "Rebuild Menu"
            .Margin = New Padding(6)
        End With

        With btnRebuildTVW
            .Dock = DockStyle.Top
            .Name = "btnRebuildTVW"
            .Text = "Rebuild TreeView"
            .Margin = New Padding(6)
        End With

        With pg
            .Dock = DockStyle.Fill
            .Name = "pg"
        End With

        With pnlLeft
            .Dock = DockStyle.Left
            .Name = "pnlgbLeft"
            .Padding = New Padding(3)
            .Width = Me.Width * 0.3
            With .Controls
                .Add(lbltvwHeader)
                .Add(lblCurrentControlHandle)
                .Add(lblCurrentControlImage)
                .Add(lblCurrentControlType)
                .Add(lblCurrentControlName)
                .Add(lblCurrentControlLoc)
                .Add(lblCursorOnCurrent)
                .Add(lblMouseMove)
                .Add(lblCursorPos)
                .Add(btnRebuildTVW)
                .Add(btnRebuildMenu)
                .Add(btnCreateMDIChild)
                .Add(chkPI)
                .Add(lblControlHeader)
                .Add(tvwDocOutline)
            End With
        End With

        With pnlright
            .Text = "Properties"
            .Name = "pnlRight"
            .Dock = DockStyle.Right
            .Width = Me.Width * 0.3
            .Padding = New Padding(3)
            With .Controls
                .Add(pg)
                .Add(lblPropHeader)
            End With
        End With

        With lblBreadCrumb
            .Dock = DockStyle.Top
            .Name = "lblBreadCrumb"
            .Text = "You Are Here: " & Me.Name
            .TextAlign = ContentAlignment.TopLeft
            .Height = 32 'Tall enough for two lines on my screen, adjust as req.
            .AutoEllipsis = True
            .AutoSize = False
            .Parent = Me
        End With

        With ms
            .Items.Clear()
            .SuspendLayout()
            .ImageList = imglst
            .Name = "ms"
            .Parent = Me
            .Items.Add(GetControlMenu(Me))
            .Items.Add(mnuHelp)
            .ResumeLayout()
        End With

        tvwDocOutline.BringToFront()
        FillTreeView(tvwDocOutline, Me)

        AddHandler btnCreateMDIChild.Click, AddressOf NewMDIChild
        AddHandler chkPI.CheckedChanged, AddressOf CheckChanged
        AddHandler tmr.Tick, AddressOf timertick
        AddHandler btnRebuildMenu.Click, AddressOf RebuildMenu
        AddHandler btnRebuildTVW.Click, AddressOf RebuildTVW
        AddHandler mnuAbout.Click, AddressOf mnuabout_Click
        AddHandler MyBase.KeyDown, AddressOf frmKeyDown
        AddHandler MyBase.MouseMove, AddressOf FormMouseMove
    End Sub

    <STAThread()> _
    Shared Sub main()
        Application.EnableVisualStyles()
        Application.Run(New frmMain)
    End Sub

    Private Sub InitializeComponent()
        Me.SuspendLayout()
        '
        'frmMain
        '
        Me.ClientSize = New System.Drawing.Size(578, 273)
        Me.Name = "frmMain"
        Me.ResumeLayout(False)

    End Sub
#End Region
    
#Region "TreeView Code"
    Private Sub RebuildTVW(ByVal sender As System.Object, ByVal e As System.EventArgs)

        FillTreeView(tvwDocOutline, Me)

    End Sub

    Function GetControlsForTreeNode(ByVal root As System.Object, Optional ByRef pnode As TreeNode = Nothing)

        Dim newpnode As New TreeNode
        If pnode Is Nothing Then pnode = New TreeNode

        Dim nodName As String
        If root.name = "" Then
            nodName = "UnNamed "
        Else
            nodName = root.name
        End If

        newpnode = pnode.Nodes.Add(nodName, nodName & " " & root.GetType.Name, GetObjectImage(root, enumRetImageTypes.retKeyName), GetObjectImage(root, enumRetImageTypes.retKeyName))

        For Each item As Control In root.Controls
            GetControlsForTreeNode(item, newpnode)
        Next
        Return newpnode

    End Function

    Sub FillTreeView(ByVal tvw As TreeView, ByVal root As System.Object)
        With tvw
            .Nodes.Clear()
            .ImageList = imglst
            .Nodes.Add(GetControlsForTreeNode(root))
            .Nodes(0).ExpandAll()
            .SelectedNode = .Nodes(0)
        End With
    End Sub

#End Region

#Region "MDI Functions"
    Private Function NewMDIChild(ByVal sender As System.Object, ByVal e As System.EventArgs)

        Try
            If Me.IsMdiContainer = False Then lblMouseMove.Text = "Form MouseMove Unavailable"
            Me.IsMdiContainer = True
            Dim frm As New Form
            Dim btn As New Button
            Dim lbl As New Label

            With frm
                .BringToFront()
                .MdiParent = Me
                .Name = "MDIChild" & Me.MdiChildren.Count.ToString
                .Text = "New MDI Child Form #" & Me.MdiChildren.Count
            End With

            With btn
                .Text = "Child of " & frm.Name
                .Name = "btn" & frm.Name
                .Location = (New Point(12, 12))
                .AutoSize = True
                .AutoSizeMode = Windows.Forms.AutoSizeMode.GrowAndShrink
                .Parent = frm
            End With

            With lbl
                .Text = "Child of " & frm.Name
                .Name = "lbl" & frm.Name
                .Location = New Point(12, 40)
                .Parent = frm
            End With
            AddHandler frm.Paint, AddressOf drawgrid
            frm.Show()

        Catch ex As Exception
            MsgBox(ex.Message)
            Return False
        End Try

        Return True

    End Function
    Public Sub drawgrid(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs)
        ControlPaint.DrawGrid(e.Graphics, Me.ClientRectangle, New Size(8, 8), Color.White)
    End Sub

#End Region

#Region "Meat and Potatoes"
    Private Sub FormMouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs)
        lblMouseMove.Text = "Form MouseMove: " & e.Location.ToString
    End Sub

    Private Sub timertick(ByVal sender As System.Object, ByVal e As System.EventArgs)

        CurrentControl = Me.GetChildAtPoint(Me.PointToClient(Cursor.Position))

        If CurrentControl Is Nothing Then CurrentControl = Me

        'This is how we launch the recursion function, only if the CurrentControl has children
        If CurrentControl.Controls.Count > 0 Then
            CurrentControl = GetCurrentControl(CurrentControl)
        End If

        'Bailout and don't keep updating the status/images if the currentcontrol
        'is the same as the lastcontrol.  Keep the CPU Cooler!
        If CurrentControl Is LastControl Then
            lblCursorPos.Text = "Cursor Position: " & Cursor.Position.ToString
            Exit Sub
        Else
            LastControl = CurrentControl
        End If

        'Stupid Stuff you can do
        ActuallyDoSomething(CurrentControl)

        lblCurrentControlName.Text = "Current Control Name: " & CurrentControl.Name.ToString
        lblCurrentControlType.Text = "Current Control Type: " & CurrentControl.GetType.Name
        lblCurrentControlHandle.Text = "Current Control Handle: " & CurrentControl.handle.ToString
        lblCurrentControlLoc.Text = "Current Control Location: " & CurrentControl.location.ToString
        lblCursorPos.Text = "Cursor Position: " & Cursor.Position.ToString

        lblCurrentControlImage.Image = GetObjectImage(CurrentControl, enumRetImageTypes.retImage)

        pg.SelectedObject = CurrentControl
        lblBreadCrumb.Text = "You Are Here: " & GetAncestry(CurrentControl)
        'Display the tag of the control, if no tag display the name & type
        If CurrentControl.tag = "" Then
            tslbl.Text = CurrentControl.name & _
                         " (" & CurrentControl.GetType.Name & ")"
        Else
            tslbl.Text = CurrentControl.tag
        End If

    End Sub

    Private Function CheckChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Try
            tmr.Enabled = sender.checked
            If sender.checked Then
                sender.text = "Disable &Property Inspector"
            Else
                sender.text = "Enable &Property Inspector"
            End If
        Catch ex As Exception

        End Try
        Return Nothing
    End Function

    Private Function ActuallyDoSomething(ByVal c As Control)

        'You can use this area, or just user the Timer Tick event, to actually
        'do something now you have a handle to the control under the cursor
        'based on the timer tick.  Just one of many options.

        'Just in case we need a reference to the MDI Client area later.
        'this is here, and not during the intial load, because the form 
        'isn't an mdi parent initially.

        If TypeOf c Is MdiClient Then
            c.Name = Me.Name & "_MDIClientArea"
            c.Tag = "I'm the MDI Client, last entered at " & Now.ToUniversalTime
            iMDIClientPTR = c.Handle
            Return Nothing
        End If


        Return Nothing

    End Function

    Private Function GetCurrentControl(ByVal PassedCtrl As Control) As Control

        Dim childControl As Control
        childControl = PassedCtrl.GetChildAtPoint(PassedCtrl.PointToClient(Cursor.Position))

        If childControl Is Nothing Then
            lblCursorOnCurrent.Text = "Position in Control: " & _
            PassedCtrl.PointToClient(Cursor.Position).ToString

            Return PassedCtrl
            'Kick it back you are in a container, but you are not hovering
            'over a control
        Else
            If childControl.Controls.Count > 0 Then
                'You are in a container, and hovering over a child control
                'that is also a container
                Return GetCurrentControl(childControl)
            Else
                lblCursorOnCurrent.Text = "Position in Control: " & _
                childControl.PointToClient(Cursor.Position).ToString
                Return childControl
                'You are in a container, and hovering over a control
            End If
        End If

    End Function

    Private Function GetAncestry(ByVal obj As Control, Optional ByVal crumbtrail As String = "") As String
        'AKA WhosYerDaddy, and WhosHisDaddy, etc...
        'takes a control name, calls itself recursively until parent=me (thisform)
        'the string will have to be built right to left until we get me (thisform)

        'Nothing to do
        If obj Is Me Then Return MyClass.Name

        'check for NoName
        Dim crumb As String = obj.Name
        If crumb = "" Then crumb = "Unnamed " & obj.GetType.Name

        If obj.Parent Is Me Then 'all done
            Return Me.Name & " >> " & crumb & crumbtrail
        Else 'still looking up the tree
            Return GetAncestry(obj.Parent, " >> " & crumb & crumbtrail)
        End If

    End Function

    Private Function GetObjectImage(ByVal obj As System.Object, ByVal opt As enumRetImageTypes)
        'Returns or Adds then Returns an image or imagekey name
        'from the imagelist for the obj type.  Sometimes you need an image,
        'sometimes you need just a keyname
        '
        If imglst.Images.ContainsKey(obj.GetType.Name) = False Then
            'Add a new picture
            Dim bm As ToolboxBitmapAttribute
            If TypeOf (obj) Is System.Windows.Forms.Form Then
                'This is a hack In My Opinion, I can't get a FORM image
                'unless I pull one from a new instance of a form...
                'if I use the current class, I always get a gear.
                'At least it works, any form type uses the bitmapattribute from a 
                'NEW form.  The same thing needs to be done in the menu.
                bm = New ToolboxBitmapAttribute(New Form().GetType)
            Else
                bm = New ToolboxBitmapAttribute(obj.GetType)
            End If
            imglst.Images.Add(obj.GetType.Name, bm.GetImage(obj.GetType, True))
        End If
        Select Case opt 'Uses custom enum
            Case enumRetImageTypes.retKeyName
                Return obj.GetType.Name
            Case Else 'enumRetImageTypes.retImage
                Return imglst.Images(obj.GetType.Name)
        End Select
    End Function

    Private Sub frmKeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs)
        'dont call the handler unless the inspector is on and currentcontrol isnot nothing
        If chkPI.Checked And CurrentControl IsNot Nothing Then
            ContextHandler(CurrentControl, e)
        End If
    End Sub

    Private Function ContextHandler(ByVal obj As System.Object, ByVal e As KeyEventArgs)

        'This routine handles all keydown events on the form, which has the keypreview set to true
        Select Case e.KeyCode
            Case Keys.F1 'Launch first google hit on MSDN based on the control type
                System.Diagnostics.Process.Start("http://www.google.com/search?btnI=I%27m+Feeling+Lucky&q=" & obj.GetType.ToString & "%20site:msdn.microsoft.com")
            Case Keys.F2 'Copy the text of the control to the clipboard
                Clipboard.SetText(obj.Text)
            Case Keys.F3, Keys.F4 'Why don't you take a picture, it will last longer!
                'Maybe this could be used to capture images for your application 
                'documentation?
                Dim g As Graphics = Graphics.FromHwnd(obj.handle)
                Dim bm As Bitmap = New Bitmap(obj.width, obj.height, g)
                obj.DrawToBitmap(bm, New Rectangle(0, 0, bm.Width, bm.Height))

                If e.KeyCode = Keys.F4 Then
                    Dim dlg As New SaveFileDialog
                    With dlg
                        .AddExtension = True
                        .DefaultExt = "png"
                        .FileName = obj.name.ToString
                        .Filter = "Portable Network Graphics (*.png)|*.png|All Files (*.*)|*.*"
                        .Title = "Save captured image as"
                        .CheckPathExists = True
                        .OverwritePrompt = True
                    End With
                    If dlg.ShowDialog = Windows.Forms.DialogResult.OK Then
                        bm.Save(dlg.FileName, System.Drawing.Imaging.ImageFormat.Png)
                        'This could be extended to multiple formats
                    End If
                Else ' F3
                    Clipboard.SetImage(bm)
                End If
                g = Nothing
                bm = Nothing

            Case Keys.F5
            Case Keys.F6
            Case Keys.F7
            Case Keys.F8
            Case Keys.F9

        End Select
        Return True
    End Function

#End Region

#Region "Menu Code"

    Private Function GetControlMenu(ByVal root As Object, Optional ByVal pmnuitem As ToolStripMenuItem = Nothing)
        'pmnuitem ~ ParentMenuItem
        'cmnuitem ~ ChildMenuItem
        'For objects with no name

        Dim mnuName As String
        If root.name = "" Then
            mnuName = "UnNamed"
        Else
            mnuName = root.name
        End If

        Dim cMnuItem As New ToolStripMenuItem

        If pmnuitem Is Nothing Then
            pmnuitem = New ToolStripMenuItem
            With pmnuitem
                .Name = Me.Name & " Controls"
                .Text = .Name
                .Image = GetObjectImage(root, enumRetImageTypes.retImage)
            End With
            For Each item As Control In root.Controls

                GetControlMenu(item, pmnuitem)
            Next
        Else
            With cMnuItem
                .Name = mnuName
                .Text = mnuName & " " & root.GetType.Name
                .Image = GetObjectImage(root, enumRetImageTypes.retImage)
            End With
            AddHandler cMnuItem.Click, AddressOf MenuItemClicked

            pmnuitem.DropDownItems.Add(cMnuItem)

            For Each item As Control In root.Controls
                GetControlMenu(item, cMnuItem)
            Next
        End If
        Return pmnuitem

    End Function

    Private Function MenuItemClicked(ByVal sender As System.Object, ByVal e As System.EventArgs)
        If TypeOf sender Is ToolStripMenuItem Then
            MsgBox("You clicked the " & sender.text & " menu item", MsgBoxStyle.Information, "Control Menu Demo")
            Return True
        Else
            Return False
        End If
    End Function

    Private Sub RebuildMenu(ByVal sender As System.Object, ByVal e As System.EventArgs)
        With ms
            .Items.Clear()
            .SuspendLayout()
            .Items.Add(GetControlMenu(Me))
            .ResumeLayout()
        End With
    End Sub

#End Region

#Region "AboutBox Code"
    Public Sub OKButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Me.DialogResult = Windows.Forms.DialogResult.OK
        sender.findform.close()
    End Sub
    Private Sub mnuabout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)

        Dim frmabout As New Form
        Dim lbl As New Label
        Dim btnok As New Button
        Dim frmSize As New Size(400, 320)

        Dim strAbout As String = _
        "Demonstration of using recursion, control collections, and GetChildAtPoint.  Copyright © Stenklyft Enterprises 2009.  Original code (as usual).  Created 01 Jan 2009.  For more information visit blog.stenklyft.com or email jason@stenklyft.com."
        strAbout += vbCrLf + "The purpose of this code is to accompany the posts in my blog titled ""Context Awareness and Recursion"".  It demonstrates showing the control, however nested, that is currently under the mouse cursor.  Using a timer tick is critical in this example."
        strAbout += vbCrLf + "Possible uses: Custom help, theme/skins, accessibility, Auditing, Troubleshooting, etc...Imagination is the only limit!"

        With btnok
            .AutoSize = True
            .Text = "&OK"
            .Anchor = AnchorStyles.Right + AnchorStyles.Bottom
            .Name = "btnOK"
            .NotifyDefault(True)
            .Location = New Point(frmSize.Width - 16 - .Width, _
                                        frmSize.Height - 8 - .Height - _
                                        (New Form().Height - _
                                        New Form().ClientSize.Height))
        End With

        AddHandler btnok.Click, AddressOf OKButton_Click

        With lbl
            .AutoSize = False
            .Anchor = AnchorStyles.Top
            .Text = strAbout
            .Dock = DockStyle.Top
            .Height = 260
        End With

        With frmabout
            .Size = frmSize
            .AcceptButton = btnok
            .Padding = New Padding(12)
            .MinimizeBox = False
            .MaximizeBox = False
            .Text = "About " & My.Application.Info.ProductName
            .FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog
            .Controls.Add(btnok)
            .Controls.Add(lbl)
            .ShowDialog(Me)
        End With

    End Sub

#End Region

End Class

If you have any questions please feel free to leave a comment.  Thanks for reading!

Context Awareness and Recursion Part 3 of 4

I finished up the last post with a promise of a treeview, which we will get to.  First, I wanted to take a short side trip on why we are using a timer and what may seem to be some over engineering.  We could accomplish basically the same results that we’ve achieved so far by using a MouseEnter Event handler for each control on the form.  A recursive routine could add an event handler for each control. 

Alternative Method using MouseEnter

If your application would never create any controls at runtime, and you wouldn’t mind a few bugs, then a much simpler method could be employed.  You’d miss out on coordinates also, but that may not be important either.  So, if you simply just want to know the name of the control under the mouse, then I’ll provide a routine.  Note, the behavior of certain controls will “lock” the MouseEnter Event until they are released, such as the dropdown button on a combo box.  If you can deal with that, here we go:

Function Init(ByVal root As Control)
    AddHandler root.MouseEnter, AddressOf UpdateCurrentControl
    Try
        For Each item As Control In root.Controls
            AddHandler item.MouseEnter, AddressOf UpdateCurrentControl
            If item.HasChildren Then
                Init(item)
            End If
        Next
    Catch ex As Exception
        Return ex.Message
    End Try
    Return True
End Function

Function UpdateCurrentControl(ByVal sender As System.Object, ByVal e As System.EventArgs)
    Try
        CurrentControl = sender
        'Your code goes here, for example:
        Me.Text = CurrentControl.name & " " & CurrentControl.GetType.Name
    Catch ex As Exception
        Return False
    End Try
    Return True
End Function

Add these two functions for your form and call it at runtime with:

Init(Me)

And don’t forget to declare CurrentControl in the form constructor:

Dim CurrentControl As Object

The Init Function is a recursive routine that will add an event handler to the MouseEnter event for each control, and all child controls, on your form.  The UpdateCurrentControl function essentially handles the the MouseEnter event.  This would be where you place your code, whatever your needs are.  For the purpose of demonstration, the code above simply updates the Text of the form (Title Bar) with the name and type of CurrentControl.  If you were just wanting to use a status bar panel to show the Tag property, and you didn’t need a handle to the CurrentControl, you could omit the declaration of CurrentControl, not set the property, and just use the Sender properties to show your text. 

For basic purposes, this is really all you need and is much simpler than the code in the two previous posts.  A drawback would be that you’d have to add an event handler pointing to the UpdateCurrentControl each time you added a control, and the other issues previously mentioned.  I like this version, which is very simple, but for my purposes it simply won’t work; I need something that is more dynamic and can deal with controls being added and removed, and child forms being created and removed.  Back to the original subject.

The Recursive Hierarchical TreeView of Controls

The IDE includes a Document Outline, in which you can move, promote, and demote controls at design time.  There may be occasions when you would like similar functionality at runtime.  Troubleshooting, debugging, auditing, and navigation all come to mind.  Once again, we will use recursion to obtain our list of controls but this time we will populate a TreeNode Control.  The Function I’m providing here returns a TreeNode object that has a root node starting at the object you pass to it.  We will pass the Function the form (Me), and it will return a TreeNode that we can add to a TreeView Control.  Start a new Windows Forms project, open up the code editor for Form1, and replace it with the following code:

Public Class Form1
    Dim imglst As New ImageList
    Dim tvw As New TreeView

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

        With tvw
            .ImageList = imglst
            .Nodes.Add(GetControlsForTreeNode(Me))
            .Dock = DockStyle.Fill
        End With

        Dim frmtvw As New Form
        With frmtvw
            .Controls.Add(tvw)
            .Text = Me.Name & " Controls"
            .TopMost = True
            .Show()
        End With

    End Sub

    Function GetControlsForTreeNode(ByVal root As System.Object, _
                                    Optional ByRef pnode As TreeNode = Nothing)

        Dim newpnode As New TreeNode
        If pnode Is Nothing Then pnode = New TreeNode

        If imglst.Images.ContainsKey(root.GetType.Name) = False Then
            'Add a new picture
            Dim bm As New ToolboxBitmapAttribute(root.GetType)
            imglst.Images.Add(root.GetType.Name, bm.GetImage(root.GetType, False))
        End If

        newpnode = pnode.Nodes.Add(root.name, root.name & " (" & _
            root.GetType.Name & ")", root.GetType.Name, root.GetType.Name)

        For Each item As Control In root.Controls
            GetControlsForTreeNode(item, newpnode)
        Next
        Return newpnode

    End Function

End Class

Before you start debugging, add a few random controls to the form, such as a GroupBox other container and a few child controls, such as I’ve done like the following image:

image

Press F5 to start debugging.  A second Windows Form, TopMost, will be instantiated at runtime with a TreeView fully populated with Form1 objects. 

image

How it Works, Step by Step

A Treeview Control, tvw, and ImageList, imglst, are declared in the constructor.  During the Form1_Load Event, the following things happen:

The TreeView tvw Nodes collection is populated with the Results of the Function GetControlsForTreeNode.  Since the GetControlsForTreeNode returns a single node, it’s simply called with the Nodes.Add method.  The Form (Me) is passed to the Function to be used as the root node.  You could pass another control to the Function, and it will return itself and it’s children back as a single node.  If we had passed GroupBox1, the TreeView would contain GroupBox1 as the root node with Button1 and CheckBox2 as child nodes. 

The GetControlsForTreeNode Function takes an Object and optional TreeNode as input, and returns a single Treenode with our without child nodes.  It uses recursion and will add nodes to the root node, and add images extracted from the class toolbox bitmap to the Imagelist.  If your Form1 has no controls, you will get back only a single entry in your Node collection. 

An new TreeNode newpnode is declared for use within the Function.  If the passed Node is nothing, then it is the first time through the Function, i.e. when you initially called it.  In this case, pnode is nothing, so pnode is instantiated as a new TreeNode.  This will be the root TreeNode. 

An nice addition to the Function is the use of the ToolBoxBitmapAttribute for the class.  The next part of the Function checks the existing ImageList imglst to see if it already contains an image with a key named root.GetType.name.  To construct the Image Key names, I’m taking the object passed, getting the type of object, and using the short name of the class (root.GetType.Name).  This will result in Image Key names like Button, Label, and TextBox instead of System.Windows.Forms.Button, and System.Windows.Forms.Label, and System.Windows.Forms.Textbox.  The short version will be much easier to reference later if we need to type it out by hand.  If we’ve determined that the imglst ImageList doesn’t contain a matching image, then we load a new image into the imglst ImageList using the ToolBoxBitMapAttribute for the root’s Class.   The GetImage method of the ToolBoxBitMapAttribute returns an image, if one exists.  If one doesn’t exist we’ll get back a generic image (cog, gear).   The False parameter you see on the GetImage line specifies we want the 16*16 version, and not the 32*32 version.

Once we’ve determined that the image exists in our ImageList, or we have added a new one, then we can finally add the TreeNode to the collection.  I’m using the Name of the Object for the TreeNode Name, the Name and  Type of the Object for the Text property of the TreeNode, similar to the Document Outline in the IDE, and using the matching Image from our ImageList for the Image and Selected Image. 

At this point in the Function, we have still only added one Node to the Node Collection, we have yet to check to see if the passed Object, Root, has any children.  The next line handles that, and our recursion.  If the passed object, Root, has any children, then the Function calls itself passing the currently referenced Object’s children (item as control) and the currently added node as the root node.  It does this until the routine reaches the end of the initially passed object’s control collection.

Finally, a single TreeNode is returned from the GetControlsForTreeNode Function.  In our example, it is added to the TreeView control.  The example here doesn’t provide any actual functionality beyond a Hierarchical display of Controls, but you could easily extend it by adding Event handlers as the Nodes are populated.

That’s it for now, we will finish up with the next entry with a hierarchical menu of controls, hopefully at this point it isn’t too redundant, but someone, somewhere could probably use an example.  We will return back to our original example, with some cleaned up code and routines in the next entry.

Context Awareness and Recursion Part 2 of 4

This is a continuation of a previous entry, if you want to follow along in your own IDE, and you haven’t setup the application yet, please click here to catch up.

We finished up last time with a functioning, yet limited, property inspector using GetChildAtPoint and the cursor position.  What the program lacks at this point is identification of child controls.  Basically, we have a US map with state names, but no county or city names, not very useful, unless you’ve never seen a US map before.

I promised no air code, but I’m going to begin this post with some pseudo code using our US map analogy.  Here goes:

For Each state In country
    For Each county In state
        For Each city In county
            'Do something
        Next
    Next
Next

In a perfect world, this would work.  In general, if we were simply building a list of objects that were of type Country, State, County, and City, this would provide us the code we need.  But of course the world isn’t perfect, some states aren’t actually state at all, some states have other divisions besides counties, and some cities are not classified as cities.  Enough geography, back to our regularly scheduled program. 

Recursion

In order for us to know what control is under the cursor, we need to know the cursor location.  We nailed that last post.  The issue we ran into was that child controls weren’t registering, that was due to us using the Form as a reference point.  What we need to do is determine whether or not the CurrentControl object has any children, if so, use the CurrentControl object as a reference point for our GetChildAtPoint.  This is where the recursion comes in.  If each control on the main form only had non container type controls within it, basically Form/Container/Control, then we wouldn’t need to go any deeper.  We have no idea how many controls there will be, how deep they might be, or even what form they are on in an MDI child.  We need a function that calls itself, passing an object reference until we get a control that has no children.  What if you had a genealogy function to retrieve someone’s children?  You would pass the person to the function and it would return the children.  You certainly wouldn’t write an entirely new routine for each level if you wanted to know someone’s descendants.  You would use a method similar to the function below to find all descendants.  This is the recursion I am speaking of.  To setup this example, and continue building our project, we are going to add a new function called GetCurrentControl, and call that on the Timer1_Tick event.  Open your code editor for Form1 and paste in the following function:

Private Function GetCurrentControl(ByVal PassedCtrl As Control) As Control

       'This routine takes a control that has children as a parameter
       'If we get a valid control from the cursor position within the
       'container, and it is not a container itself, return the control
       'If we get a valid control from the cursor position within the 
       'container, and it IS a container, then use recursion
       'IF we dont get a child control, then we are in an empty spot
       'on the container, just kick back the name of what was passed

       Dim childControl As Control
       childControl = PassedCtrl.GetChildAtPoint(PassedCtrl.PointToClient(Cursor.Position))

       If childControl Is Nothing Then
           Return PassedCtrl
           'Kick it back you are in a container, but you are not hovering
           'over a control
       Else
           If childControl.Controls.Count > 0 Then
               'You are in a container, and hovering over a child control
               'that is also a container
               Return GetCurrentControl(childControl)
           Else
               Return childControl
               'You are in a container, and hovering over a control
           End If
       End If

   End Function

The key concept here is the line that reads “Return GetCurrentControl(childControl)”.  This is basically where the function returns a value using itself and the childcontrol local variable.  It will do this until there is no child control under the cursor.  To initiate this function, we are going to call it from the Timer1_Tick event IF the CurrentControl has children.  This is checked with…HasChildren property, or could also be checked using the controls.Count >0 method.  Either way seems to work.  Our code to call the the GetCurrentControl Function needs to happen before the label and propertygrid binding, so replace your Timer1_Tick Event handler with the following code:

Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick

       CurrentControl = Me.GetChildAtPoint(Me.PointToClient(Cursor.Position))
       If CurrentControl Is Nothing Then CurrentControl = Me

       'This is how we launch the recursion function, only if the CurrentControl has children
       If CurrentControl.Controls.Count > 0 Then
           CurrentControl = GetCurrentControl(CurrentControl)
       End If

       lblCurrentControlName.Text = CurrentControl.Name.ToString
       lblCurrentControlType.Text = CurrentControl.GetType.ToString
       pg.SelectedObject = CurrentControl

End Sub

It’s time.  Press F5.  With your Property Inspector enabled, mouse around the PropertyGrid or the GroupBox.  You now have the EXACT child control, however deeply nested, that is under the cursor.  We don’t have too many controls on the form to prove it goes deeper than one or two controls yet, but we are about to fix that.  Quit your debugging if you haven’t already, we are going to create a new form to prove our Functions are working.  First, we need to move that pesky CheckBox that’s floating out on the middle of the form, so add the following to the END of your Form1_Load Event:

chkPI.Dock = DockStyle.Bottom
gb.Controls.Add(chkPI)

Test your program again, the chkPI CheckBox should now be a child of the gb GroupBox.  If all is well, we will continue.  FYI if you are having any trouble following me at this point I’d love to hear about it, leave a comment.  I’m not trying to be inconsistent by creating some controls at design time and other controls at runtime, my goal here is to provide you with as much code as possible and minimize the amount of dragging and setting of properties.  That said, we will continue. 

Child Forms

We are going to be adding a Button to the Form1 form and having it change the Form1 form to be an MDI Parent, and it will also create an instance of a child form.  Declare a new Button named btnCreateMDIChild in the Form1 constructor as follows:

Dim btnCreateMDIChild As New Button

Add the following code to the end of the Form1_Load Event:

With btnCreateMDIChild
    .Text = "Create MDI Child"
    .Dock = DockStyle.Bottom
    .Name = "btnCreateMDIChild"
    .Parent = gb
End With
AddHandler btnCreateMDIChild.Click, AddressOf NewMDIChild

Now we need to create the NewMDIChild Function we referenced above.  The following code contains the NewMDIChild Function.  Edit as desired, but I tossed in the creation of a few child controls.  You could also reference an existing form, but I’m trying to keep this project as lean as possible.  The NewMDIChild Function follows:

Private Function NewMDIChild(ByVal sender As System.Object, ByVal e As System.EventArgs)

        Try
            Me.IsMdiContainer = True
            Dim frm As New Form
            Dim btn As New Button
            Dim lbl As New Label

            With frm
                .BringToFront()
                .MdiParent = Me
                .Name = "MDIChild" & Me.MdiChildren.Count.ToString
                .Text = "New MDI Child Form #" & Me.MdiChildren.Count
            End With

            With btn
                .Text = "Child of " & frm.Name
                .Name = "btn" & frm.Name
                .Dock = DockStyle.Top
                .Parent = frm
            End With

            With lbl
                .Text = "Child of " & frm.Name
                .Name = "lbl" & frm.Name
                .Dock = DockStyle.Top
                .Parent = frm
            End With

            frm.Show()

        Catch ex As Exception
            MsgBox(ex.Message)
            Return False
        End Try

        Return True

End Function
 

Before you get anxious and hit F5, you may want to add some code to make your form a little bigger, or manually adjust the width using the IDE, or your NewMDIChild function may result in forms that you can’t see.  Alternatively, just maximize the project once you start it up.    If I’ve been thorough enough so far, your application should appear similar to mine when you fire it up.  At this point, Pressing F5 on my computer, and clicking the btnCreateMDIChild Button three times results in the following:

Screen Shot 2

Mouse around on the child forms, child controls, and anything else you want.  Our Current Control Name and Current Control Type labels are updated accurately with information regardless where you have the cursor.  At this point we have multiple controls when the child forms are instantiated, so we need an accelerator key on the chkPI CheckBox, or we will not be able to effectively use it.  Since the only design time control we have is the chkPI CheckBox, set the text to “&Enable Property Inspector”.  The chkPI CheckBox will now accept ALT-E at runtime to toggle it on or off.  If it doesn’t ensure the UseMnenomic property is set to the default of True.

Current Control Position and Cursor on Current Control Position

There’s one more thing I want to do before we finish up this session, we are missing a display of where the cursor is relative to the current control, and the current control’s positionl.  We know where we are on the screen, and the form, but not where we are in context of the current container.  To get the CurrentControl Location at this point is straightforward, we can just reference the CurrentControl.Location property directly.   The next two labels we will add directly to the gb GroupBox, in hindsight I should have used a single label for the other coordinates/value pairs but we are beyond that.  Let’s declare two more labels in the Form constructor as follows:

Dim lblCurrentControlLoc, lblCursorOnCurrent As New Label

Add the following lines to the end of Form1_Load:

lblCurrentControlLoc.Dock = DockStyle.Top
lblCursorOnCurrent.Dock = DockStyle.Top
gb.Controls.Add(lblCurrentControlLoc)
gb.Controls.Add(lblCursorOnCurrent)

Add the following code to the end of the Timer1_Tick Event:

lblCurrentControlLoc.Text = "Current Control Location: " & _
        CurrentControl.location.ToString

Press F5 and move the mouse around, now we have a current control location that is relative to it’s parent.  The CurrentControl.Location is used to update the lblCurrentControlLoc Label.  One last thing before we finish up; the cursor position ON the current control.  We’ll have to grab that from the GetCurrentControl Function.  We could set a variable, modify the Return value of the Function, or just directly write the value to the label, I choose to write the value directly to the label within the logic of the Function.  Since there are three logic trees that Return a value from the Function, but one is recursive, we’ll add the same line to two areas of the Function.  We will return either the cursor position relative to the Passed Control, or the Child Control, the updated version of the GetCurrentControl Function follows, replace yours with this one:

Private Function GetCurrentControl(ByVal PassedCtrl As Control) As Control

        Dim childControl As Control
        childControl = PassedCtrl.GetChildAtPoint(PassedCtrl.PointToClient(Cursor.Position))

        If childControl Is Nothing Then
            lblCursorOnCurrent.Text = "Position in Control: " & _
            PassedCtrl.PointToClient(Cursor.Position).ToString
            Return PassedCtrl
            'Kick it back you are in a container, but you are not hovering
            'over a control
        Else
            If childControl.Controls.Count > 0 Then
                'You are in a container, and hovering over a child control
                'that is also a container
                Return GetCurrentControl(childControl)
            Else
                lblCursorOnCurrent.Text = "Position in Control: " & _
                childControl.PointToClient(Cursor.Position).ToString
                Return childControl
                'You are in a container, and hovering over a control
            End If
        End If

End Function

As you can see we’ve added two lines of code, only one of which will be executed based on the logic.  Both lines update the lblCursorOnCurrent, but use a different object reference.  Start your application and move the mouse around again, you should now have a display that shows you where the mouse is, within whatever the mouse is over.  So if you are centered vertically and horizontally on a control that has a size of 100,30, then the lblCursorOnCurrent should display 50,15. 

Finishing Up

Before I finish up this post, I realized our Cursor Position label update is still in the MouseMove event of the form.  Move the line in the Form1_MouseMove Event that updates lblCursorPos to the end of the Timer1_Tick Event.  The line in reference follows:

lblCursorPos.Text = Cursor.Position.ToString

Now we have a real time update of the cursor position, even when it is outside the form.  In the remaining posts of this series, we’ll add a few examples that demonstrate what we can do with this information; a hierarchical menu, a breadcrumb type display, and a treeview.

I hope you’ve found something you can use in the code we have covered up to this point.