Lock / Unlock drawing views in VBA


Locking and unlocking of drawing views is a common task in CATIA V5. To save a time you can automate this operation with VBA macro. Following procedure checks the lock status of the first view in a CATIA V5 drawing and then sets all views to the opposite.


Option Explicit

Sub LockUnlockViews()
    Dim drwViews As DrawingViews
    Set drwViews = CATIA.ActiveDocument.Sheets.ActiveSheet.Views
    
    ' have to be defined as Variant, Boolean is not working
    ' because of CATIA vs VBA boolean incompatibility
    Dim myLockStatus As Variant
    myLockStatus = drwViews.Item(3).LockStatus
    
    Dim i As Integer
    Dim myPrefix As String, myIdent As String, mySuffix As String

    ' loop starts from 3 because the first two views are CATIA
    ' automatically generated views Main View, Background View
    For i = 3 To drwViews.Count
        drwViews.Item(i).LockStatus = Not myLockStatus

'        Debug.Print drwViews.Item(i).Name
    Next
End Sub
    

The code is simple and self-explanatory, there are however two things to notice:

1. Variable myLockStatus is declared as Variant. Had it been a Boolean, it wouldn't have worked properly. A reason is incompatibility of the Boolean type between VBA and V5 Automation methods. This is described in V5Automation.chm help file in a chapter About VB, VBA, Debug, and Portability.

There is a known limitation concerning the usage of the Boolean type in the V5 Automation methods invoked from VBA. In V5 applications, the Boolean type is defined as an 'unsigned char' where the VBA definition is a short. When a V5 method returns True, the returned integer value is 1, though VBA is expecting -1.
...
Note that this limitation is specific to VBA and is not concerning VBScript. V5Automation.chm

2. A loop starts at 3 because the first two views are automatically generated views named Main View and Background View. Custom views therefore start at index 3.

To test it, put the code shown above into a standard module.


Export a product structure to Excel in VBA


To loop through the content of an assembly you can use programming technique called recursion. Recursion is a powerful concept where a procedure calls itself. Although it is not the most effective way how to write a code, there are cases when advantages of this technique overweight its drawbacks like higher memory usage or lower efficiency. Also, it is very important to design your recursive procedure to terminate the recursion properly otherwise you end up stuck in an infinite loop and Out of stack space error.

You can almost always substitute recursion for a loop!



Recursion - the right way

In the Main procedure, we define an empty variant array which is our container for an assembly content. As a container, we could have used also other data types like Collection or Dictionary, but unlike collections, arrays can be directly exported to an Excel worksheet.

For an initial call to a recursive subroutine, we need also a top assembly product stored in prod variable. Both variables are then passed as parameters to our recursive procedure ParseAssyTree. Procedure SimpleArrayToExcel is just a helper function responsible for an Excel export.


Option Explicit

Sub Main()
    Dim prod As Product
    Dim bom() As Variant
    
    bom = Array()
    Set prod = CATIA.ActiveDocument.Product
    
    ' call to a recursive procedure
    ParseAssyTree prod, bom
    
    ' export an array to excel
    SimpleArrayToExcel bom
End Sub

' recursive procedure
Sub ParseAssyTree(currProduct As Variant, content(), Optional level As Long = 0)
    ReDim Preserve content(UBound(content) + 1)
    
    content(UBound(content)) = String(level, "!") & level & " " & currProduct.PartNumber & " : (" & currProduct.Name & ")"
    
    Dim curr As Variant
    For Each curr In currProduct.Products
        ParseAssyTree curr, content, level + 1
    Next
End Sub
    

Procedure ParseAssyTree has also an additional Optional argument called level. We use it to determine how deep we are immersed in a structure. On every recursive call, we resize an array by one with preserving its existing items. We then assign a string value which describes a current product to the last item of an array. In the following loop, we call our recursive procedure for every child of a current product with a level variable increased by one. A "terminal condition" is assured by currProduct.Products collection which is empty at the ends of a structure (usually CATIA Parts with no children) and therefore a procedure inside is not executed.

To get a nice indented Excel output we add at the beginning of every item in the content array a number of exclamation marks ("!") equal to the level variable. We use it later in SimpleArrayToExcel procedure.

Changes in a recursive procedure

We could change our recursive procedure to perform other actions than just getting a structure description. We could use it to get other properties of a product like parameters, position, weight or we could just rename nodes. We could use a Dictionary as a container for a structure to get a list of all unique items in our assembly together with their counts.



' export simple (one dimensional) array to Excel
Sub SimpleArrayToExcel(simpleArr() As Variant)
    Const xlDelimited = 1
    
    ' create excel object
    Dim xlApp As Object
    Set xlApp = CreateObject("Excel.Application")
    
    ' add workbook
    Dim wbook As Object
    Set wbook = xlApp.Workbooks.Add
    
    ' set copyTo range to the same size as array
    Dim copyTo As Object
    Set copyTo = wbook.Sheets(1).Range("B2").Resize(UBound(simpleArr) + 1, 1)
    
    ' copy content of array into excel range
    copyTo.Value = xlApp.Transpose(simpleArr)
    
    ' split text to columns by "!" character
    copyTo.TextToColumns Destination:=copyTo, DataType:=xlDelimited, Other:=True, OtherChar:="!"
    
    ' show excel
    xlApp.Visible = True
End Sub
    

In the last part of a code, we export a structure to an Excel file. We define a procedure which takes a one-dimensional array as a parameter and export its content to a worksheet. In order to have a different indentation for product nodes based on a level, we split the text into separate columns with Excel function TextToColumns by "!" (exclamation mark) character as a delimiter. As a result, we get a worksheet with level based indentation as shown below.


...

Excel output sample



To test it copy all procedures into a standard module and launch Export sub.

Recursion - a wrong way

For those who are eager to see what happens when things go wrong look at the example below.


Option Explicit
    
Sub RecursionTest()
    InfiniteLoop (0)
End Sub

Function InfiniteLoop(counter As Long) As Long
    counter = counter + 1
    
    If counter Mod 1000 = 0 Then Debug.Print counter
    
    InfiniteLoop counter
End Function
    

In this example, a terminal condition is not defined and our procedure gets trapped into an infinite loop. After a number of repetitions (in my case it was around six thousand), you get the following error message:

...

Out of stack space error in VBA


Export a CATIA table to Excel with VBA


This simple script copies the content of CATIA table into an Excel worksheet. Before launching it, please ensure that:

  • Active document is a CATIA drawing.
  • Active view contains a CATIA table.
  • If there are more tables in the active view only first one is going to be exported.

How it works

In a main sub Export we declare a 2D variant array with the same size as our CATIA table. To get table dimensions we use properties of DrawingTable object NumberOfRows and NumberOfColumns. In a following loop, we get the content of each cell using GetCellString and assign a cell text into an array item. Finally, we call ArrayToExcel procedure which is responsible for data export.


Option Explicit

Sub Export()
    ' set drawing document
    Dim drwDoc As DrawingDocument
    Set drwDoc = CATIA.ActiveDocument
    
    ' set drawing drwSheet
    Dim drwSheet As DrawingSheet
    Set drwSheet = drwDoc.Sheets.ActiveSheet
    
    ' set drawing drwView
    Dim drwView As DrawingView
    Set drwView = drwSheet.Views.ActiveView
    
    ' set drawing drwTable
    Dim drwTable As DrawingTable
    Set drwTable = drwView.Tables.Item(1)
    
    ' get number of drwTable rows
    Dim rowsNo As Long
    rowsNo = drwTable.NumberOfRows
    
    ' get number of drwTable columns
    Dim colsNo As Long
    colsNo = drwTable.NumberOfColumns
    
    
    Dim i As Long, j As Long
    ' set array size acoording to drwTable size
    ReDim arr(rowsNo - 1, colsNo - 1) As Variant
    
    ' loop through drwTable cells
    For i = 1 To rowsNo
        For j = 1 To colsNo
            ' write cell content to an array item
            arr(i - 1, j - 1) = drwTable.GetCellString(i, j)
        Next
    Next
    
    ArrayToExcel arr
End Sub
    

Data transfer between CATIA (or any other application) and Excel is an expensive operation that should be kept to a minimum. You can increase the speed of operation by passing arrays of data to the worksheet in a single operation rather than using loops and copy one cell at a time. This way you can copy even a large amount of data with the best performance. I find export to Excel much easier than writing to a text file.

Procedure ArrayToExcel takes a 2D variant array as an argument, creates a new Excel document and assign the content of an array into an Excel range. Up to this point the Excel instance is running hidden in a background, in a last step we show it by setting an Application.Visible property to True.


' export 2D array to Excel
Sub ArrayToExcel(arr2D() As Variant)
    ' create Excel object
    Dim xlApp As Object
    Set xlApp = CreateObject("Excel.Application")
    
    ' add workbook
    Dim wbook As Object
    Set wbook = xlApp.Workbooks.Add
    
    ' set destination range
    Dim destination As Object
    Set destination = wbook.Sheets(1).Range("B2")
    
    ' copy content of 2D array into excel range
    destination.Resize(UBound(arr2D, 1) + 1, UBound(arr2D, 2) + 1).Value = arr2D
    
    ' show excel
    xlApp.Visible = True
End Sub
    

To test it please copy both procedures into a standard module and launch Export sub.

SelectionSets object in CATIA VBA


Selection sets are nice CATIA feature to group and store selected elements of any type. They are pretty well described in V5Automation reference. The purpose of the methods of a SelectionSets object like CreateSelectionSet, DeleteSelectionSet or RenameSelectionSet is quite obvious, other methods like AddCSOIntoSelectionSet or PutSelectionSetIntoCSO are a bit less obvious, but at the first glance we have enough to start with some testing. However, there is no real-world example or use case which could help us at the beginning.

What's worse, there is no information about how to get or create this object. Or at least I could not find it in official VBA reference. But after a bit of googling I found an answer. The missing piece of the puzzle. A hint how to get a SelectionSets object of a product. And although it looks quite weird and a bit scary, it works.


Set selSets = doc.Product.GetItem("CATIAVBSelectionSetsImpl")
    

With this information there is no problem to write a simple script to test a functionality of selection sets in VBA.

How things work

  • Open product
  • Select some parts or assemblies and launch macro

Sub Main()
    Dim doc, sel, setName
    
    setName = "Example"
    
    Set doc = CATIA.ActiveDocument
    Set sel = doc.Selection
    
    Dim selSets
    ' get SelectionSets object (a tricky part)
    Set selSets = doc.Product.GetItem("CATIAVBSelectionSetsImpl")
    
    ' create new selection set
    selSets.CreateSelectionSet setName
    ' add selected elements to selection set
    selSets.AddCSOIntoSelectionSet setName

    sel.Clear
    
    ' activate the content of selection set
    selSets.PutSelectionSetIntoCSO setName

    'Dim a()
    'selSets.GetListOfSelectionSet a
End Sub
    

A new selection set named "Example" is created and highlighted and it contains selection content. Press Ctrl+G to open Selection Sets Selection dialog window.

However, I have a problem to get a list of selection sets. Last 2 lines of code should work, but I experience a CATIA crash on my machine when I uncomment them. If you find a way how to get such a list, please let me know :).

Graph Tree Reordering in VBA


One of the most annoying CATIA features, especially when you work with large assemblies is reordering of specification tree in Products. Even as a programmer there is almost nothing you can do about it, because there is no direct way of reordering components in specification tree. Such a function has not been exposed to CATIA API yet. There are a few tools available on the internet and maybe you already use some of them. You may have even thought how the hell they work. So stop wondering, today I am going to show you how they (might) works :).

Most (probably all of them) make use of the Windows Automation API in particular Microsoft UI Automation. What is it? Here is a description from the official Microsoft website:

Microsoft UI Automation is an accessibility framework that enables Windows applications to provide and consume programmatic information about user interfaces (UIs). It provides programmatic access to most UI elements on the desktop.

And this is exactly what we need to reorder our specification tree. To get a programmatic access to CATIA and its Graph tree reordering window and to simulate user interaction.



Before we start

Unfortunately we can not write our automation module directly in CATIA VBA editor. Reason is that during code execution User Interface become blocked and unresponsive and because we need an interactivity, we have to access the CATIA application from outside. The best way is to use your favorite application from the MS Office package. My favorite application is Excel.

Before we can use UI Automation library we have to establish a reference to the UIAutomationCore.dll. To do this go to Tools -> References in your VBA editor and choose UIAutomationClient library.



Coding

We start with a procedure and step by step I'll be adding new lines with some explanation to help you to understand at least some basic background about UI Automation programming. If you do not have a time to follow, just go to the end of this post and there you can find a complete code of the (standard) module.


Option Explicit

Sub CATMain()
    Dim CATIA, doc, prod, sel
    
    Set CATIA = GetObject(, "CATIA.Application")    ' get CATIA Application
    Set doc = CATIA.ActiveDocument
    Set prod = doc.Product
    Set sel = doc.Selection
    
    ' select top product in a CATIA tree
    sel.Clear
    sel.Add prod
    
    ' launch "Graph tree reordering" command
    CATIA.StartCommand "Graph tree reordering"

    ...
End Sub
    

In this initial part we get our CATIA.Application object and set also other related variables for document, product and selection. In order to successfully pass this part of the code CATIA have to be running and active document has to be an assembly (product). In a next step we have added the top product node into CATIA selection object and with CATIA.StartCommand we launch "Graph tree reordering" command. At this moment a Graph tree reordering window should appear.


...

Dim winAutomation As CUIAutomation
Set winAutomation = New CUIAutomation

Dim desktop As IUIAutomationElement
' get reference to the root element (desktop)
Set desktop = winAutomation.GetRootElement

Dim allWindowsCond As IUIAutomationCondition
' retrieves a predefined condition that selects all elements
Set allWindowsCond = winAutomation.CreateTrueCondition

Dim childs As IUIAutomationElementArray
' find all elements & put them into element array
Set childs = desktop.FindAll(TreeScope_Children, allWindowsCond)

Dim i As Long, currChild As IUIAutomationElement
Dim catiaWindow As IUIAutomationElement

' loop through all element and find CATIA by window name which contains "CATIA V5" string
For i = 0 To childs.Length - 1
    Set currChild = childs.GetElement(i)
    
    If InStr(currChild.CurrentName, "CATIA V5") Then
        Set catiaWindow = currChild ' set main catia window
    End If
    
    'Debug.Print currChild.CurrentName, currChild.CurrentClassName
Next

...
    

As a next step we initialize UI Automation library. To start a search for our CATIA window we need to obtain a root element. Root element is the desktop. We can obtain this element by using the GetRootElement method. We can search for descendant elements by using methods, such as FindFirst and FindAll. To retrieve UI Automation elements, you must specify a condition. A condition is a set of criteria that defines the elements that you want to retrieve. The simplest condition is the true condition, which is a predefined object that specifies that all elements in the search scope are to be returned. You can obtain an interface to the true condition by using CreateTrueCondition.

In the rest of the code we find all children of desktop root element using allWindowsCond (a True condition to return all children of desktop element) and we put them into array. In a following loop, we perform a search for main CATIA window by window caption. If any of the desktop children contains string "CATIA V5" (it is likely our CATIA window) we store it in catiaWindow variable. If you have opened more windows with this caption only the last one is stored. Therefore, avoid having more windows with such a caption, because instead of CATIA you can easily catch other windows (like notepad with CATIA V5.txt opened)


...    

Dim graphWinCond As IUIAutomationCondition
Set graphWinCond = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "Graph tree reordering")

Dim graphWin As IUIAutomationElement

'wait for Graph window to open and get it
Do
    Set graphWin = catiaWindow.FindFirst(TreeScope_Children, graphWinCond)
    
    ' do not freeze application in case of infinite loop
    DoEvents
Loop While graphWin Is Nothing

' get OK button
Dim btnOKCondition As IUIAutomationCondition, btnOk As IUIAutomationElement

Set btnOKCondition = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "OK")
Set btnOk = graphWin.FindFirst(TreeScope_Children, btnOKCondition)

' get Move Down button
Dim btnMoveDownCondition As IUIAutomationCondition, btnMoveDown As IUIAutomationElement

Set btnMoveDownCondition = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "Move Down")
Set btnMoveDown = graphWin.FindFirst(TreeScope_Descendants, btnMoveDownCondition)

...
    

After we have identified our main CATIA window, we try to find our already opened Graph tree reordering dialog window. This window is one the children of the main CATIA window and we could narrow our search only to catiaWindow element. We create a new property condition and with FindFirst method we perform a search of the element again by window name, which is now a string "Graph tree reordering". We put this method in a loop because at this moment Graph tree reordering dialog could be still in a process of opening and search could fail.

Finally, we identify buttons we need for reordering such as Move Down button and OK button to close reordering dialog.


... 
 
' control pattern definition (button click)
Dim patMoveDown As IUIAutomationInvokePattern, patOK As IUIAutomationInvokePattern

Set patMoveDown = btnMoveDown.GetCurrentPattern(UIA_InvokePatternId)
Set patOK = btnOk.GetCurrentPattern(UIA_InvokePatternId)

For i = 1 To prod.Products.Count - 1
    ' button click events invoked
    patMoveDown.Invoke
Next

patOK.Invoke
...
    

To simulate a button click we make use of IUIAutomationInvokePattern interface. We create invoke patterns for both buttons and in a following loop, we move first element in the tree to the last position by Invoke method of Move Down button. At the end we close the dialog window by calling Invoke method of the OK button.

And here is the complete source code of GraphTreeReordering module:


Option Explicit

Sub CATMain()
    Dim CATIA, doc, prod, sel
    
    Set CATIA = GetObject(, "CATIA.Application")    ' get CATIA Application
    Set doc = CATIA.ActiveDocument
    Set prod = doc.Product
    Set sel = doc.Selection
    
    ' select top product in a CATIA tree
    sel.Clear
    sel.Add prod
    
    ' launch "Graph tree reordering" command
    CATIA.StartCommand "Graph tree reordering"
    
    Dim winAutomation As CUIAutomation
    Set winAutomation = New CUIAutomation
    
    Dim desktop As IUIAutomationElement
    ' get reference to the root element (desktop)
    Set desktop = winAutomation.GetRootElement
    
    Dim allWindowsCond As IUIAutomationCondition
    ' retrieves a predefined condition that selects all elements
    Set allWindowsCond = winAutomation.CreateTrueCondition
    
    Dim childs As IUIAutomationElementArray
    ' find all elements & put them into element array
    Set childs = desktop.FindAll(TreeScope_Children, allWindowsCond)
    
    Dim i As Long, currChild As IUIAutomationElement
    Dim catiaWindow As IUIAutomationElement
    
    ' loop through all element and find CATIA by window name which contains "CATIA V5" string
    For i = 0 To childs.Length - 1
        Set currChild = childs.GetElement(i)
        
        If InStr(currChild.CurrentName, "CATIA V5") Then
            Set catiaWindow = currChild ' set main catia window
        End If
        
        'Debug.Print currChild.CurrentName, currChild.CurrentClassName
    Next
    
    Dim graphWinCond As IUIAutomationCondition
    Set graphWinCond = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "Graph tree reordering")
    
    Dim graphWin As IUIAutomationElement
    
    'wait for Graph window to open and get it
    Do
        Set graphWin = catiaWindow.FindFirst(TreeScope_Children, graphWinCond)
        
        ' do not freeze application in case of infinite loop
        DoEvents
    Loop While graphWin Is Nothing
    
    ' get OK button
    Dim btnOKCondition As IUIAutomationCondition, btnOk As IUIAutomationElement
    
    Set btnOKCondition = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "OK")
    Set btnOk = graphWin.FindFirst(TreeScope_Children, btnOKCondition)
    
    ' get Move Down button
    Dim btnMoveDownCondition As IUIAutomationCondition, btnMoveDown As IUIAutomationElement

    Set btnMoveDownCondition = winAutomation.CreatePropertyCondition(UIA_NamePropertyId, "Move Down")
    Set btnMoveDown = graphWin.FindFirst(TreeScope_Descendants, btnMoveDownCondition)
    
    ' control pattern definition (button click)
    Dim patMoveDown As IUIAutomationInvokePattern, patOK As IUIAutomationInvokePattern
    
    Set patMoveDown = btnMoveDown.GetCurrentPattern(UIA_InvokePatternId)
    Set patOK = btnOk.GetCurrentPattern(UIA_InvokePatternId)
    
    For i = 1 To prod.Products.Count - 1
        ' button click events invoked
        patMoveDown.Invoke
    Next

    patOK.Invoke
End Sub