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.
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.
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:
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.
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.