Reminder
Last time we implemented a tab navigation function for our custom control. We saw how to use the object-spy to locate "interesting" RO properties and methods, and how to switch to debug view to drill even deeper into the inner control structure. Today we’ll master these techniques to implement our second goal – pressing a specific button within a tab.
As we’ll see, this turns out to be a much more complicated task. From the looks of things (though looks can be deceiving), the buttons aren’t just flattened within the tab, but rather reside within different sublevels. This means that unlike in the tab navigation function, we won’t be able to implement a simple search loop, and things are probably going to get real ugly, real fast.
Exploring the UI structure
It seems that our first order of business is accessing the different "chunks" of controls within our tab. Like the tabs themselves, the chunks are probably stored within a collection. So, we turn to our friend the object-spy, and try to locate a promising blank RO property. This time we select to look at the active tab (and not the whole ribbon), and go through its properties. One property that immediately pops up is "Chunks" (good thing the demo called these pieces "Chunk 1″ and "Chunk 2″), so we decide to explore it further.
Validating the properties
Before we go any further, it’s a good idea to validate the property we chose. It would be a shame to work through 9 sublevels of Chunks.Items.Item(0).Items.Item… just to find out, hours later, that we’ve been barking up the wrong tree. So, let’s make sure Chucks is really what it seems to be. We’ll use the debug view to drill further into the chucks collection, and see if the items names/text/tooltip is what it’s supposed to be. We activate the following script, with a breakpoint on the "Wait" statement.
Set oTab = swfwindow("swfname:=AdvancedQTPRibbon").SWFObject("swfname:=RibbonTab1″)
Wait 1
Now we can use the debug view to explore the oTab.Object.Chunks collection. Pretty soon we find the right way to access the chuck’s UI name : oTab.Object.Chunks.Item(0).Text, and we see it’s indeed the object we’ve been looking for (the .Text value is "Chunk 1″). Luckily, another quick experiment in the debug view reveals the much needed .Count property of the chunks collection. And now we can begin to write our primary search loop:
For i = 0 to oTab.Object.Chunks.Count-1
'Inner search goes here
Next
Drilling down the structure
The immediate next step is to drill down further, looking for a buttons collection of some sort. A quick debug-view exploration will reveal that each chunk has a .Items collection, which seems promising. However, when we try to validate the property by exploring the objects it contains, we find a weird mismatch: Chuck 1 has 1 item in that collection, while Chunk 2 has 3 items.
Sometimes this might be enough to drop the property, and look for another. However, we suspected something like this might happen, and this actually confirms our UI diagnosis that there could be a mediating level between the parent chunk, and the actual buttons. We can confirm this diagnosis by switching to Tab 2, and finding out that the chunk there has 6 sub-items. The perfect match between the items collection and the number of "inner-chucks" can’t just be a coincidence.
Dealing with multiple hierarchies
OK, so we’ve beached the chunks collection, but what now? As we saw, a chuck could contain the buttons directly, or through an "inner-chuck". How can we distinguish between the two cases? And perhaps there could be a third case of an inner-inner-chunk (which just happens not to be in the example)? How can we write a script that deals with different levels of drill-downs?
Well, let’s build us a game-plan: Let’s pretend we can know if an inner-chunk has inner-inner-chunks. In this case we can imagine an algorithm taking each chunk, and drilling down again and again until it can no longer do so. This would mean the algorithm had reached the bottom level of the chunks tree – a button. We can then check if that button is the one we’ve searched for, and if not, climb back up, and starting the algorithm again on the next chuck, one level up. This method is a classic recursive tree search:
This set of instructions can call itself over and over until it reaches the last sub-level on each and every chuck. This means that no matter in what chuck or sub-level the button resides, if it’s there, we’ll find it. Luckily, the drilldown in every level is done with the .Items collection, so our code can be clean and generic.
Turning design into code
We’ll see how that pseudo-code flow-chart gets turned into a QTP script in a second, but first, we have to find a way to know if we’ve reached to bottom level, or if we can drill further down. It would have been great if the bottom level objects would have had a .Items collection with .Count = 0. Unfortunately, a quick test with the debug-view will show that sometimes, the bottom objects don’t have a .Items property at all! A few more guesses produces the .HasItems property, which looks promising at first, but soon turns out to be valid only some for objects with the .Items property.
There is a work-around: we could try to access the .Items property, and check to see if we get an error or not. In order to do so, we will have to wrap our code with an error protection belt (so the script won’t fail), like this (say oLevelItem is our object):
On error resume next
If oLevelItem.HasItems = True Then bHasChildren = True
If err.number <> 0 Then bHasChildren = False
On Error goto 0
This code will produce a true/false flag - bHasChildren, which can tell us whether we should drill down another level or not. Now we also need to know if we’ve reached the relevant chunk. I’ve decided to write this CheckItem function, which checks both the chunk’s name and tooltip for a match:
Function CheckItem(oItem, sTargetName)
Dim bResult
bResult = False
'We might query a property which doesn’t exist
On error resume next
If oItem.Text = sTargetName Then bResult = True
If bResult = False Then 'Keep searching?
If oItem.ToolTip.FeatureName = sTargetName Then bResult = True
End If
If err.number <> 0 Then bResult = False 'Protects from corrupted code
On error goto 0
CheckItem = bResult
End Function
With these in hand, we can proceed to write our main recursive function:
Function SearchLevel (oLevelItem, sButtonName)
Dim oResult
Dim oChildren
Dim i
Dim bHasChildren
Set oResult = Nothing ‘Set default answer
bHasChildren = False
If CheckItem(oLevelItem, sButtonName) = True then 'Jackpot! We have our chunk.
Set oResult = oLevelItem
Set SearchLevel = oResult
Exit function
End If
'We might query a property which doesn’t exist
On error resume next
If oLevelItem.HasItems = True Then bHasChildren = True
If err.number <> 0 Then bHasChildren = False
On Error goto 0
If bHasChildren = True Then
For i = 0 to oLevelItem.Items.Count -1
Set oResult = SearchLevel (oLevelItem.Items.Item(i), sButtonName) ‘Recursion
If not oResult is nothing Then exit for 'We’ve found it!
Next
End If
Set SearchLevel = oResult
End Function
OK, so we got this great function that returns the relevant button’s object. What now? We want to press the button, which is a QTP operation, but our object is a run-time application object, which QTP knows nothing about. This is the time to take a big QTP no-no, and turn it into a yes.
Not to fear, XY is here
Usually, the "Click X, Y" commands are considered to be extremely problematic. They’re prone to instability maintenance issues due to changing resolutions and fonts between computers and users. However, sometimes we can extract the XY coordinates dynamically; assuring that the command will be successful.
Almost all .net objects have a magical .Bounds property. The property can give us, in real time; the XY coordinates of the object, relative to its hosting frame. In our case, we’ll get the button’s XY coordinates, relative to the hosting tab. This means that we could do something like oTab.Click X, Y – and click the button.
Below is a possible code snippet for breaking the .Bounds property into iX iY variables:
sBounds = oButton.Bounds
arrBounds = Split(sBounds, ",")
arrBound = Split(arrBounds(0), "=")
iX = arrBound(1)
arrBound = Split(arrBounds(1), "=")
iY = arrBound(1)
Bringing it all together
So, let’s sum up: We can take a tab, and loop through its UI chunks. We can drill down through unlimited hierarchies, until we find the sub-chunk with the button we need. Once we got the button, we can use its .Bounds property to calculate the XY coordinates needed to click it.
Of course buttons are not the only low-level controls (there are checkboxes, comboboxes etc.), but once we get their object, we can find ways to work with them. So we could click a checkbox, much like a button, and we can use a combobox .Items collection to simulate a user selection of a certain item.
Now all that’s left is to write our main function, that will use SearchLevel and CheckItem:
'The main function:
Function ClickButton(oRibbon, sButtonName)
Dim i
Dim oTab
Dim oChunk
Dim oButton
Dim sBounds
Dim arrBounds, arrBound
Dim iX, iY
'a trick to select the current tab
Set oTab = oRibbon.SWFObject("swfname:=" & oRibbon.Object.SelectedTab.Name)
'loop through the tab’s chuncks
For i = 0 to oTab.Object.Chunks.Count-1
'Search each chunk recursively
Set oButton = SearchLevel(oTab.Object.Chunks.Item(i), sButtonName)
If Not oButton is Nothing Then Exit For 'if we’ve found - get out of here
Next
If Not oButton is Nothing Then 'Did we find a button?
'Split the button’s location text into X, Y
sBounds = oButton.Bounds
arrBounds = Split(sBounds, ",")
arrBound = Split(arrBounds(0), "=")
iX = arrBound(1)
arrBound = Split(arrBounds(1), "=")
iY = arrBound(1)
oTab.Click iX, iY 'Do the actual click
ClickButton = True
Else
ClickButton = False
End If
End Function
If you want to check the code, copy the three functions to QTP, and activate them with these commands (for example):
Set oRibbon = swfwindow("swfname:=AdvancedQTPRibbon").swfobject("swfname:=ribbon1″)
Call ClickButton(oRibbon, "Cool Image")
Posted in VBScript Techniques


Yaron Assa






August 20th, 2008 at 11:45 pm
Hi I want to capture the Checkbox value in the GetROProperty and I dont see a way for it, any help in this.