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.