Context Awareness and Recursion Part 1 of 4

I’ve been spending a lot of time with Visual Basic Express 2008 lately, mostly because I have yet to upgrade my Visual Studio .Net 2002 and wanting to learn more about Microsoft .NET Framework 3.5.  I was working on a totally obscure application for my own use and realized while looking at help, searching online, and using other documentation that there are so many different help methodologies.  Classic, HTML, Document Readers, Document Explorers, online Help, etc..  Why are there so many?  Why does technology advance faster than developers can keep up with?

I’m getting off the subject here, to get back on track, I’m going to be showing methods, my own, to answer the following questions:

1.  How do I know what control is under the mouse cursor?  What if the control is a child of another control?  What if the control is located on a child form?

2.  How do I create a hierarchical menu of controls on a form?

3.  How do I populate a treeview of controls on a form?

4.  What child controls are located within a specific control?

5.  What is the parent of the current control?  What about it’s parent?  How many ancestors does a control have?  How many, and what are the descendents of a given control?

Typical with anything I deem important enough to post online, there’s no air code here, and this isn’t copied from somewhere else and branded as my own.  That being said there’s only so many ways to do something, sometimes only one, but what follows is what I’ve done to accomplish the tasks at hand.

Setting up the Project

At first it would seem simple enough, checking into the Framework I find a function named GetChildAtPoint.  The GetChildAtPoint function returns the control that is located at a specific Point.   The form’s MouseMove EventArgs certainly pass a Point type to the event, however, the kicker here is that the MouseMove event fires when the cursor is on the form, and if the cursor is on the form, then there is no control under the cursor.

I’m going to walk you through things that don’t work, so you can better understand why the final solution (one of many possible) does work, so be patient.   If you have the home game and want to play along, create a new Windows Forms project, replace the Form1.vb code with the following code:

Public Class Form1

    Private Class mydisplbl
        Inherits Label
        Sub New()

            Dock = DockStyle.Fill
            AutoSize = False
            Anchor = AnchorStyles.Left + AnchorStyles.Top

        End Sub
    End Class
    Private Class myvallbl
        Inherits Label
        Sub New()

            Dock = DockStyle.Fill
            AutoSize = False
            Anchor = AnchorStyles.Left + AnchorStyles.Top

        End Sub
    End Class

    Dim lbl1, lbl2, lbl3, lbl4 As New mydisplbl
    Dim gb As New GroupBox
    Dim lblMouseMove, lblCursorPos, lblCurrentControlName, lblCurrentControlType As New myvallbl
    Dim CurrentControl As Object
    Dim tbl As New TableLayoutPanel

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        gb.Text = "Navigation"
        gb.Dock = DockStyle.Left
        gb.Width = 240 'Adjust accordingly
        Me.Text = "GetChildAtPoint Example"
        lbl1.Text = "Form Mouse Move:"
        lbl2.Text = "Cursor Position:"
        lbl3.Text = "Current Control Name:"
        lbl4.Text = "Current Control Type:"
        With tbl
            With .Controls
                .Add(lbl1, 0, 0)
                .Add(lbl2, 0, 1)
                .Add(lbl3, 0, 2)
                .Add(lbl4, 0, 3)
                .Add(lblMouseMove, 1, 0)
                .Add(lblCursorPos, 1, 1)
                .Add(lblCurrentControlName, 1, 2)
                .Add(lblCurrentControlType, 1, 3)
            End With
            .Dock = DockStyle.Fill
        End With

        tbl.ColumnStyles.Clear()
        Dim cs As New ColumnStyle
        cs.SizeType = SizeType.Percent
        cs.Width = 50
        tbl.ColumnStyles.Add(cs)
        cs = New ColumnStyle
        cs.SizeType = SizeType.Percent
        cs.Width = 50
        tbl.ColumnStyles.Add(cs)
        gb.Controls.Add(tbl)
        Me.Controls.Add(gb)

    End Sub
    
End Class

GetChildAtPoint

Before we proceed, press F5 to ensure you don’t have any errors.  At this point, our code simply sets up the grid we will use to display the values for our project.  So good so far I hope, but the project does nothing at this point.  We are starting with the MouseMove event, and you will quickly see why this doesn’t work.  Paste the following code into your Form1 code window:

Private Sub Form1_MouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
        lblMouseMove.Text = e.Location.ToString
        lblCursorPos.Text = Cursor.Position.ToString

        'This will not work, the cursor.position is relative to the screen
        CurrentControl = Me.GetChildAtPoint(Me.PointToClient(Cursor.Position))
        If CurrentControl Is Nothing Then CurrentControl = Me

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

End Sub

Press F5 and move the mouse around on the form.  You can see that while the form has focus, both of the values (lblMouseMove and lblCursorPos) are updated.  When you position the mouse over the groupbox, or even the title bar of the form, the values stop updating.  Also, for future reference, note that the values are certainly different.  The e.Location returns where the cursor is relative to the form.  The Cursor.Position returns a point relative to your screen.  The next option I considered was to use a keydown event on the form, check for a specific Keycode.  This certainly returns the proper Control that is under the Cursor.  We don’t yet have any controls that accept keystrokes, but later we will so add the following line to the Form1_Load event:

Me.KeyPreview = True

Now, to continue our example, paste the following code into Form1:

Private Sub Form1_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
       If e.KeyCode = Keys.F1 Then 'or choose your own
           CurrentControl = Me.GetChildAtPoint(Me.PointToClient(Cursor.Position))
           If CurrentControl Is Nothing Then CurrentControl = Me
           lblCurrentControlName.Text = CurrentControl.Name.ToString
           lblCurrentControlType.Text = CurrentControl.GetType.ToString
       End If
   End Sub

Press F5 and move the mouse around on the form.  You should still have the results from the mouse move event, we haven’t deleted that code yet, but we soon will.  Position the cursor over an empty area of the GroupBox control and press F1.  The labels now update and reflect the Name and Type of the GroupBox.  If you followed my example, we didn’t give the GroupBox a name so it will be blank.   If you move the mouse it will quickly overwrite the values unless you are holding down the F1 key.  Position the mouse over one of the labels in the groupbox and Press the F1 key.  The Current Control Type label still shows a GroupBox.  The reason behind this is that we are checking for the CurrentControl variable using GetChildAtPoint relative to the FORM.  The labels are not children (not directly anyway) of the Form.  They are owned by the TableLayoutPanel, which is owned by the GroupBox.  At this point we have functional code, but it doesn’t work on child controls.  That’s the next step.  Edit the Form1_MouseMove Event to remove the code we already proved doesn’t work.  Your Form1_MouseMove Event should look like the code below:

Private Sub Form1_MouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
    lblMouseMove.Text = e.Location.ToString
    lblCursorPos.Text = Cursor.Position.ToString
End Sub

Press F5 again to be safe and ensure you don’t have any errors before we make any more edits.  I can be a bit lazy at times so we are also going to move the applicable portion of the Form1_KeyDown code to a timer tick Event.  Delete the Form1_KeyDown code.  Add a Timer to the form, enable it, and use the following code for the Timer1_Tick Event:

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
    lblCurrentControlName.Text = CurrentControl.Name.ToString
    lblCurrentControlType.Text = CurrentControl.GetType.ToString
End Sub

Press F5 and move the mouse around.  You should have the results you had previously, but without the requirements of pressing a key.  If you don’t get the updates on the lblCurrentControlName or lblCurrentControlType labels, you may have forgotten to enable the timer.  At this point, anytime we move the mouse over a control that is a direct child of the form, our CurrentControl variable is set to that control.  If our project is planned never to use any nested controls (how fun would that be?) then we wouldn’t need to proceed.  At this point you have a handle to the control, you could open a webpage, launch a custom help tool, change the properties, or do anything else within your imagination to the control.  But since most Windows Forms projects include nested controls, we will continue our quest.   Size your form a bit wider and add a PropertyGrid, docked Right, to the Form.  If you want to continue creating the controls programmatically, paste the following code into the Form1 constructor:

Dim pg As New PropertyGrid

And the following code into the Form1_Load Event:

With pg
    .Dock = DockStyle.Right
    .Name = "pg"
    .Parent = Me
    .Width = Me.Width / 3 'Adjust accordingly
End With

Note that we set the .Parent property, basically this does the same thing as Me.Controls.Add(pg).  We also gave it a name, unlike our poor nameless GroupBox.  We are going to add another line of code to the Timer1_Tick Event that will bind the CurrentControl to the SelectedObject property of the PropertyGrid (pg).  Add the following line of code to the end of the Timer1_Tick event:

pg.SelectedObject = CurrentControl

Press F5 and move the mouse around.  The control under the cursor is now displayed in the labels, and also bound to the pg PropertyGrid.  You’ve just built a runtime property editor.  Relax, we are far from done.  At this point, your form should appear similar to mine, as a reference, here’s a screen shot of our project so far:

Screen Shot of Project In Work

For my purposes, I don’t always want the “Property Inspector” active, so we will add a CheckBox to the form, this time around we’ll add the control at design time.  Drag a CheckBox Control from the Toolbox to the form (somewhere in the middle), set the Name to “chkPI” and the Text to “Enable Property Inspector”.  Once that’s done, add the following code to Form1:

Private Sub chkPI_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles chkPI.CheckedChanged
      Timer1.Enabled = chkPI.Checked
End Sub

At this point, the Timer1 Control and the chkPI control are out of sync, so either check the chkPI at design time or set the Timer1 control property to disabled.   Before we go on, Press F5.  Hover over the chkPI CheckBox and toggle it on and off.  Move around to test your code, the Current Control labels shouldn’t update while the chkPI CheckBox is unchecked.  We can start to harness the power of what we have done now; either use the spacebar while you are over a control or just clear the CheckBox to turn off the Timer.  The PropertyGrid (pg) is still bound to the last control that was active while the Timer was Enabled.  You can now enter the PropertyGrid (pg) and change the properties at runtime.  And we haven’t even written that much code.  This is an amazing control in my opinion.  As a quick example, I changed the ForeColor of the CheckBox (chkPI) to Red.  I can’t stress enough how valuable this control is, especially since you can bind it to your own controls or Class.  It handles viewing and setting of properties in a way that would be difficult to reproduce on your own.  The purpose of this document isn’t to sell you on the value of the PropertyGrid, so we’ll move on. 

Save the project at this point, we have yet to drill down to those child controls using recursion.

We will get to that in my next post.