Home » Code, Development, Tools

Deleting Visio Shapes Programmatically

delete-shapes-prog-thumb-2A user recently asked a question in the forum; “How can I (programmatically) delete all but one shape on a Visio page?” While I answered the question directly in the forum, I thought I’d expand on the topic in a full-blown article.

I’ll use VBA (Visual Basic for Applications) examples in this article, as it is available to all users of Visio, whether or not they are professional programmers. As with many Office applications, Visio has VBA built right in (just hit Alt + F11 and you’ll see it!)

I’m also going to take the trouble to keep the code as clean as possible. So there are generally three bits of code that make each example work. I won’t show all three each time, but keep in mind that we have

  1. A “Test It” routine for “calling the code from the outside”. These will start with “TestIt_”.
  2. The main loop that processes all shapes on a page. These will start with “m_deleteShapes_”.
  3. Shape-test or filter procedures, which start with “m_filter_”. These keep the guts of examining a shape out of the shape-processing loops described in item #2.

This organization lets our Subs and Functions remain relatively small, and encourages code re-use, as opposed to following the copy -> paste -> spaghetti, that so many of use when we are just starting to play around with a new concept.

First, Isolate a Test, or a “Shape Filter”

Let’s define a simple filter function that we can easily reuse throughout the article. This sort of “refactoring” makes procedures shorter, code easier to read, and allows us to easily re-use the filter over and over in different bits of example code.

Later on in the article, when we are iterating through all the shapes on a page, the decision to delete or not delete will be a single line of code, instead of a bunch of lines within a loop. This is WAY clearer to read, and to write about. But that single line of shape-testing code will refer to a procedure that can be infinitely complicated.

Our first tester will not be infinitely complicated, though. It will be the lowly m_filter_isWiderThan function. It takes two arguments, a Visio.Shape object, and a decimal number of millimeters. If a the shape is wider than this, it returns True, otherwise, it returns False.

1
2
3
4
5
6
7
8
9
10
11
12
Private Function m_filter_isWiderThan( _
         ByRef visShp As Visio.Shape, _
         ByVal widthMillimeters As Double)
 
   '// Get the width of the shape in millimeters:
   Dim w As Double
   w = visShp.CellsU("Width").Result(Visio.VisUnitCodes.visMillimeters)
 
   '// Return a Boolean comparing w to widthMillimeters:
   m_filter_isWiderThan = (w > widthMillimeters)
 
End Function
Private Function m_filter_isWiderThan( _
         ByRef visShp As Visio.Shape, _
         ByVal widthMillimeters As Double)

   '// Get the width of the shape in millimeters:
   Dim w As Double
   w = visShp.CellsU("Width").Result(Visio.VisUnitCodes.visMillimeters)

   '// Return a Boolean comparing w to widthMillimeters:
   m_filter_isWiderThan = (w > widthMillimeters)

End Function

We can test to see if a shape is, say wider than 100mm, like this:

1
2
3
4
5
6
   '// Get the first selected shape in a drawing window:
   Dim shp As Visio.Shape
   Set shp = Visio.ActiveWindow.Selection(1)
 
   '// Dump some info to the Immediate panel in VBA:
   Debug.Print m_filter_isWiderThan(shp, 100)
   '// Get the first selected shape in a drawing window:
   Dim shp As Visio.Shape
   Set shp = Visio.ActiveWindow.Selection(1)

   '// Dump some info to the Immediate panel in VBA:
   Debug.Print m_filter_isWiderThan(shp, 100)

Now you can write all sorts of filters or tests, and call them with just one line of code inside of a shape-processing loop. As your tests get more nitpicky and detailed, you’ll be happy you separated out the code and won’t have to edit a test inside of some long, hard-to-read procedure.

So let’s look at these shape-processing loops now.

Deleting Shapes One by One – The Wrong Way

To get the job of deleting certain shapes, the most natural thing in the world is to visit each shape on a page, test it, then delete it if it passes (or fails) the test.

Many folks trying this for the first time will do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'// Call a delete procedure using the active drawing page:
Public Sub TestIt_Bad()
   Call m_deleteShapes_Bad(Visio.ActivePage)
End Sub
 
'// Delete shapes one by one, but with a problem!
Private Sub m_deleteShapes_Bad(ByRef visPg As Visio.Page)
 
   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes
 
      '// Delete shapes that are wider than an inch (25.4mm):
      If ( m_filter_isWiderThan(shp, 25.4) ) Then
         Call shp.Delete
      End If
 
   Next
 
End Sub
'// Call a delete procedure using the active drawing page:
Public Sub TestIt_Bad()
   Call m_deleteShapes_Bad(Visio.ActivePage)
End Sub

'// Delete shapes one by one, but with a problem!
Private Sub m_deleteShapes_Bad(ByRef visPg As Visio.Page)

   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes

      '// Delete shapes that are wider than an inch (25.4mm):
      If ( m_filter_isWiderThan(shp, 25.4) ) Then
         Call shp.Delete
      End If

   Next

End Sub

While it seems reasonable, this code won’t run reliably. That is because the For Each loop is running through the shapes on the page, and you are (potentially) deleting shapes from that collection. Some shapes will end up being skipped because the collection will shrink; they will never be tested because the rug was pulled out from under them!

Say the first shape gets deleted. It’s kind of like you deleted shape #1, then all the other shapes shift down one index. Now you go to index #2 (hidden by For…Each so you don’t have to think about it), but the old shape #2 is now shape #1, so you’ve jumped over it!

Note also, the first Sub, called TestIt_Bad simply calls m_deleteShapes_Bad. And m_deleteShapes_Bad calls m_filterIsWiderThan in turn.

I will generally put a “Test It” procedure atop the examples, and it will call one of the meaty “delete” procedures by passing in the active Visio drawing page, and perhaps some other arguments. This allows you to put your cursor in the “Test It” procedure, and press F5 to run the code. It will work, because the “Test It” procedures have no arguments, and they are Public.

If you click the mouse inside of a sub or function that takes arguments, then try to run it with F5, you’ll get a pop-up asking you which of the public, parameter-less procedures in the project you would like to run. Since VBA can’t assume what to use for the arguments of a procedure, it has to start with one that has no arguments, so it presents you with a list of possibilities.

Count Backwards!

The solution to the last problem is to just run a loop in reverse, which requires a For…Next loop. Have a look here!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Public Sub TestIt_ReverseForLoop()
   Call m_deleteShapes_Selection(Visio.ActivePage)
End Sub
 
Private Sub m_deleteShapes_ReverseForLoop(ByRef visPg As Visio.Page)
 
   '// Count backwards in a loop--from visPg.Shapes.Count to 1...
   Dim i As Integer
   Dim shp As Visio.Shape
   For i = visPg.Shapes.Count To 1 Step -1 '//...BACKWARDS!
 
      '// We must set the shape object, since this isn't
      '// a For...Each:
      Set shp = visPg.Shapes(i)
 
      '// Delete shapes that are wider than an inch (25.4mm):
      If ( m_filter_isWiderThan(shp, 25.4) ) Then
         Call shp.Delete
      End If
 
   Next
 
   '// Cleanup:
   Set shp = Nothing
 
End Sub
Public Sub TestIt_ReverseForLoop()
   Call m_deleteShapes_Selection(Visio.ActivePage)
End Sub

Private Sub m_deleteShapes_ReverseForLoop(ByRef visPg As Visio.Page)

   '// Count backwards in a loop--from visPg.Shapes.Count to 1...
   Dim i As Integer
   Dim shp As Visio.Shape
   For i = visPg.Shapes.Count To 1 Step -1 '//...BACKWARDS!

      '// We must set the shape object, since this isn't
      '// a For...Each:
      Set shp = visPg.Shapes(i)

      '// Delete shapes that are wider than an inch (25.4mm):
      If ( m_filter_isWiderThan(shp, 25.4) ) Then
         Call shp.Delete
      End If

   Next

   '// Cleanup:
   Set shp = Nothing

End Sub

It’s a bit more work, because we have to declare the Integer i, then bother to set the shp object according to that index, whereas For…Each took care of that for us. But because we only delete shapes from the end of the page.Shapes collection, we won’t miss any along the way!

Delete a Bunch of Shapes At Once!

If you are deleting lots of shapes, and doing it frequently, and, say the shapes have lots of things that depend on them (connectors, callouts, containers, ShapeSheet relationships, etc.) then it theoretically should be faster to delete a Selection of shapes with one call, rather than deleting each shape individually. Instead of giving Visio instructions for each shape, we can tell Visio to delete a bunch of shapes once. So Visio gets to handle most of the work internally, rather than communicating with our code–with all the scary COM overhead that involves–for each item.

To do this, we still loop through all the shapes and test each one, but we add any qualifying shapes to a Visio.Selection object, then delete that selection object after the looping. We also don’t need to resort to tricks like looping backwards.

Here’s an overview of the flow:

  1. Create an empty selection object
  2. Loop through the shapes and add qualifiers to the selection
  3. Delete the selection if it isn’t empty

Simple. Let’s see it in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Public Sub TestIt_BatchDelete()
   Call m_deleteShapes_Selection(Visio.ActivePage)
End Sub
 
Private Sub m_deleteShapes_Selection(ByRef visPg As Visio.Page)
 
   '// Create an EMPTY selection of shapes (we'll add shapes
   '// to it later):   
   Dim selToDel As Visio.Selection
   Set selToDel = visPg.CreateSelection( _
   Visio.VisSelectionTypes.visSelTypeEmpty)
 
   '// We can use For...Each again, because we're not deleting
   '// shapes from *within* the loop.
   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes
 
      '// Add shape to selection if it is 'wide', but don't
      '// whack the shape here:
      If ( m_filter_isWiderThan(shp, 25.4) ) Then
 
         '// Annoying Visio-syntax for (simply) selecting
         '// a shape:
         Call selToDel.Select(shp, Visio.VisSelectArgs.visSelect)
 
      End If
 
   Next
 
   '// Delete the selection, if it is not empty:
   If (selToDel.Count > 0) Then
      '// This is where shapes get whacked:
      Call selToDel.Delete
   End If
 
   '// Cleanup:
   Set selToDel = Nothing
   Set shp = Nothing
 
End Sub
Public Sub TestIt_BatchDelete()
   Call m_deleteShapes_Selection(Visio.ActivePage)
End Sub

Private Sub m_deleteShapes_Selection(ByRef visPg As Visio.Page)

   '// Create an EMPTY selection of shapes (we'll add shapes
   '// to it later):   
   Dim selToDel As Visio.Selection
   Set selToDel = visPg.CreateSelection( _
   Visio.VisSelectionTypes.visSelTypeEmpty)

   '// We can use For...Each again, because we're not deleting
   '// shapes from *within* the loop.
   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes

      '// Add shape to selection if it is 'wide', but don't
      '// whack the shape here:
      If ( m_filter_isWiderThan(shp, 25.4) ) Then

         '// Annoying Visio-syntax for (simply) selecting
         '// a shape:
         Call selToDel.Select(shp, Visio.VisSelectArgs.visSelect)

      End If

   Next

   '// Delete the selection, if it is not empty:
   If (selToDel.Count > 0) Then
      '// This is where shapes get whacked:
      Call selToDel.Delete
   End If

   '// Cleanup:
   Set selToDel = Nothing
   Set shp = Nothing

End Sub

If you’re wondering about the “Cleanup” bits, well, long ago, I learned that COM code needs to clean up references to objects. I was told that it is a good practice in VBA. This is especially true if your application gets large and is doing lots and lots of calls to Visio. If you are just doing small batches of things once in awhile, then you will probably close Visio often enough that things will magically be cleaned up.

If you’re programming Visio using C# or VB.NET, you don’t have to do this any more. .NET has “garbage collection” which should take care of things for you. If you are a COM expert (COM is a technology that enables Microsoft Office automation), leave a better-informed comment below if I’ve said something wrong.

Other Considerations

Often times, VBA is used to create “utility” or “tool” code. Convenience macros that help you to get your job done more quickly, and eliminate boring, tedious, error-prone handwork.

But if you are developing a more robust solution, where the code could be delivered to the far reaches of your your organization or The Globe, there a few nitpicky things to consider.

DeleteEx

In addition to the Delete method, Visio.Shape and Visio.Selection objects have the DeleteEx method, which offers you some finer control over what happens when you delete a shape or a set of shapes. DeleteEx takes one argument, that as of Visio 2013 can be a combination of five enumeration values stored in the Visio.VisDeleteFlags enumeration, which you can also read about on MSDN, or just have a peek here:

  • Visio.VisDeleteFlags.visDeleteNormal (0)
    Match the deletion behavior that is in the user interface.
  • Visio.VisDeleteFlags.visDeleteHealConnectors(1)
    Delete connectors that are attached to deleted shapes.
  • Visio.VisDeleteFlags.visDeleteNoHealConnectors(2)
    Do not delete connectors that are attached to deleted shapes.
  • Visio.VisDeleteFlags.visDeleteNoContainerMembers (4)
    Do not delete unselected members of containers or lists.
  • Visio.VisDeleteFlags.visDeleteNoAssociatedCallouts (8)
    Do not delete unselected callouts that are associated with shapes.

As Visio’s user-interface has gotten more sophisticated over the years, more and more things can potentially happen auto-magically. For instance, deleting a flowchart shape in the middle of a procedure can now result in the inbound and outbound connectors “healing”. Instead of having two dangling connectors where the shape used to be, Visio is smart enough to delete one connector, then route the other to the previous and next shapes.

With DeleteEx, you can choose whether or not your delete code allows or disallows this type of behavior, along with other similarities involving callouts and containers.

Notice that the flag values are powers of two, which suggests a BIT MASK. So you can combine these values into a single flag that has multiple ramifications:

1
2
3
4
5
6
7
8
9
'// Make a single flags value that combines some of the
'// enumeration values:
Dim flags As Integer
 
flags = Visio.VisDeleteFlags.visDeleteNoAssociatedCallouts +
Visio.VisDeleteFlags.visDeleteNoContainerMembers +
Visio.VisDeleteFlags.visDeleteNoHealConnectors
 
Call shp.DeleteEx(flags)
'// Make a single flags value that combines some of the
'// enumeration values:
Dim flags As Integer

flags = Visio.VisDeleteFlags.visDeleteNoAssociatedCallouts +
Visio.VisDeleteFlags.visDeleteNoContainerMembers +
Visio.VisDeleteFlags.visDeleteNoHealConnectors

Call shp.DeleteEx(flags)

Check if Shape is Locked Against Deletion

A shape that you want to delete could be locked. Via the user-interface, you can lock a shape from deletion using the Shape > Shape Design > Protection dialog, and checking From Deletion.

Inside the ShapeSheet, it is the cell LockDelete located in the Protection section. A value that is not-zero means the shape is locked against (protected from) deletion.

In code, you can check a shape–and unlock it–like this:

1
2
3
4
5
6
7
8
9
10
11
12
Public Sub ShapeUnlockDelete(ByRef visShp As Visio.Shape)
 
   If ( visShp.CellsU("LockDelete").ResultIU <> 0 ) Then
 
      '// Force the value to zero, so the shape is safe
      '// to delete. This will blow away even GUARDed
      '// formulas:
      visShp.CellsU("LockDelete").ResultIUForce = 0
 
   End If
 
End Sub
Public Sub ShapeUnlockDelete(ByRef visShp As Visio.Shape)

   If ( visShp.CellsU("LockDelete").ResultIU <> 0 ) Then

      '// Force the value to zero, so the shape is safe
      '// to delete. This will blow away even GUARDed
      '// formulas:
      visShp.CellsU("LockDelete").ResultIUForce = 0

   End If

End Sub

If you don’t do this, and try to delete a locked shape, or try to delete a selection that contains locked shapes, you’ll get this error:

Shape protection, container, and/or layer properties prevent complete execution of this command.

Which looks like this in Visio 2013:

delete-locked-shape-error

Which…umm…could generate a support call for you. Greeeeat.

Check if Shape is on a Locked Layer

Shapes can belong to a layer, or even multiple layers. If one or more of those layers is locked, then trying to delete them will give you the same error as a protected-against-deletion shape, mentioned in the last section.

Note: shapes on invisible layers WILL be processed when you walk the Shapes collection of a page, unless you take steps to ignore them. If you don’t want to delete invisible shapes, then that is (yet another) thing you should check. Fortunately/unfortunately, there are lots of ways that a shape could be invisible, so I will skip the details in this article.

Turn Off Undo

If you are deleting large batches of shapes, Visio will place all of that information into an undo scope, which can slow down your code execution quite a bit, once you get into the 100s and 1000s of objects. If you are sure that the action does not need to be undone, then you can temporarily turn off undo using the UndoEnabled property of the Visio.Application, Visio.InvisibleApplication and Visio.Document objects. This will save Visio the effort of noting everything that got deleted, and how to restore it all.

It is important to turn UndoEnabled back on when you’re done. For this reason, make sure your not-undo-able code is wrapped with some kind of error handling, so that even if something goes wrong, undo will be re-enabled. In VBA, it is so very 1990s. In something modern, like C# or VB.NET, you would use a try…catch block or a try…catch…finally block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Public Sub TestIt_NoUndo()
 
   On Error GoTo ErrorHandler
 
   '// Overly verbose variable declarations that might
   '// make it easier for you to parameterize your own
   '// code...
 
   Dim visApp As Visio.Application
   Dim visPg As Visio.Page
 
   Set visApp = Visio.Application
   Set visPg = visApp.ActivePage
 
   '// Turn off undo:
   visApp.UndoEnabled = False
 
   '// Call the batch undo:
   Call m_deleteShapes_Selection(visPg)
 
   GoTo Cleanup '//...ie skip over ErrorHandler
 
ErrorHandler:
 
   Debug.Print "An error occurred in Sub TestIt_NoUndo! " & vbCrLf & VBA.Error$
 
Cleanup:
 
   visApp.UndoEnabled = True '//...reactivate undo!
 
   Set visPg = Nothing
   Set visApp = Nothing
 
End Sub
Public Sub TestIt_NoUndo()

   On Error GoTo ErrorHandler

   '// Overly verbose variable declarations that might
   '// make it easier for you to parameterize your own
   '// code...

   Dim visApp As Visio.Application
   Dim visPg As Visio.Page

   Set visApp = Visio.Application
   Set visPg = visApp.ActivePage

   '// Turn off undo:
   visApp.UndoEnabled = False

   '// Call the batch undo:
   Call m_deleteShapes_Selection(visPg)

   GoTo Cleanup '//...ie skip over ErrorHandler

ErrorHandler:

   Debug.Print "An error occurred in Sub TestIt_NoUndo! " & vbCrLf & VBA.Error$

Cleanup:

   visApp.UndoEnabled = True '//...reactivate undo!

   Set visPg = Nothing
   Set visApp = Nothing

End Sub

Don’t you just love the GoTo statements in VBA!?

Create an Undo Scope With a Useful Description

Alternatively, you can create your own custom undo scope, and give it a meaningful name. Such code wraps our delete-the-wide-shapes code, and names it “Delete Shapes More Than 25.4mm Wide”.

If there is an error, the scope’s changes are rejected, and the user sees no change (the attempt is undone). If the action is successful, then wide shapes are deleted, and the user sees this custom entry in the undo drop-down:

custom-undo-scopeThis is a nice touch that really lets users know what is going on.

You call BeginUndoScope once, but should call EndUndoScope twice, once with accept changes and once with reject changes in the case of an error. Pay attention to the GoTo statements below which handle the jumping to the correct EndUndoScope treatment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Public Sub TestIt_CustomUndoScope()
 
   On Error GoTo ErrorHandler
 
   '// Overly verbose variable declarations that might
   '// make it easier for you to parameterize your own
   '// code...
 
   Dim visApp As Visio.Application
   Dim visPg As Visio.Page
 
   Set visApp = Visio.Application
   Set visPg = visApp.ActivePage
 
   '// Start the undo scope:
   Dim lUndoScopeID As Long
   lUndoScopeID = visApp.BeginUndoScope("Delete Shapes More Than 25.4mm Wide")
 
   '// Call the batch undo:
   Call m_deleteShapes_Selection(visPg)
 
   GoTo AcceptUndo '//...ie skip over ErrorHandler
 
ErrorHandler:
 
   Debug.Print "An error occurred in Sub TestIt_CustomUndoScope! " & vbCrLf & VBA.Error$
 
   '// Finish the undo scope, and REJECT the changes:
   Call visApp.EndUndoScope(lUndoScopeID, False) '//...FALSE here!
 
   GoTo Cleanup
 
AcceptUndo:
 
   '// Finish the undo scope, and ACCEPT the changes:
   Call visApp.EndUndoScope(lUndoScopeID, True) '//...TRUE here!
 
Cleanup:
 
   Set visPg = Nothing
   Set visApp = Nothing
 
End Sub
Public Sub TestIt_CustomUndoScope()

   On Error GoTo ErrorHandler

   '// Overly verbose variable declarations that might
   '// make it easier for you to parameterize your own
   '// code...

   Dim visApp As Visio.Application
   Dim visPg As Visio.Page

   Set visApp = Visio.Application
   Set visPg = visApp.ActivePage

   '// Start the undo scope:
   Dim lUndoScopeID As Long
   lUndoScopeID = visApp.BeginUndoScope("Delete Shapes More Than 25.4mm Wide")

   '// Call the batch undo:
   Call m_deleteShapes_Selection(visPg)

   GoTo AcceptUndo '//...ie skip over ErrorHandler

ErrorHandler:

   Debug.Print "An error occurred in Sub TestIt_CustomUndoScope! " & vbCrLf & VBA.Error$

   '// Finish the undo scope, and REJECT the changes:
   Call visApp.EndUndoScope(lUndoScopeID, False) '//...FALSE here!

   GoTo Cleanup

AcceptUndo:

   '// Finish the undo scope, and ACCEPT the changes:
   Call visApp.EndUndoScope(lUndoScopeID, True) '//...TRUE here!

Cleanup:

   Set visPg = Nothing
   Set visApp = Nothing

End Sub

Create a DeleteSafe Method

If you think you will face any of the “unable to delete” concerns above, consider creating a DeleteSafe method that will check if a shape is locked against deletion, or on a locked layer. It’s up to you to either unlock the shape (or layer), or to not delete the shape. Whatever you do, you shouldn’t try to delete it without unlocking it, because error messages scare users.

If you are deleting a batch of shapes in a selection, then a better idea would be to pass each qualifying shape through a PrepareForSafeDelete method, which unlocks them and makes them safe to delete.

This can get complicated, because to do things properly you should really note any layers that you switch from locked to unlocked, delete the shapes, then re-lock the layers. I won’t detail that code in this article, because it will add too much length. We’ll settle for an overview instead.

A workflow to safely delete shapes might go like this:

  1. Create a collection of layers that were locked, but are unlocked.
  2. Loop through a collection of shapes, adding qualifiers to a “to-delete” selection.
  3. Prepare each shape for deletion.
  4. Possibly disable undo for Visio or the document.

Preparing each shape for deletion would involve:

  1. Unlocking the LockDelete ShapeSheet cell.
  2. Examining layers that the shape belongs to for locked status. If any are locked, unlock them, then add them to the collection of “got unlocked” layers.
  3. Optionally turn on invisible layers, or make a note of shapes that were invisible (for one reason or another) and inform the user either before or after that invisible shapes might be affected.

And finally:

  1. Re-lock any layers that got unlocked.
  2. Optionally deal with invisible layers.
  3. Re-enable undo.

A Final Example: Delete Shapes NOT on a Layer

In the original forum question, the user wanted to delete “all but one shape.” What he really wanted to do was delete all shapes not on the “Buttons” layer. So there was probably an ActiveX button on the page used to clear shapes, or something similarly useful, and of course, the code shouldn’t delete the mechanism for calling itself. So he put the button control on a layer.

Notice how the code has been parameterized: m_deleteShapes_BatchNotOnLayer takes both a Visio.Page object, and a layer name. You specify the “layer to exclude” right at the top in TestIt_DeleteShapesNotOnLayer. This code is pretty darn re-usable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Public Sub TestIt_DeleteShapesNotOnLayer()
 Call m_deleteShapes_BatchNotOnLayer(Visio.ActivePage, "Buttons") '//...the layer name to exclude!
End Sub
 
Private Sub m_deleteShapes_BatchNotOnLayer( _
         ByRef visPg As Visio.Page, _
         ByVal sExcludeLayerName)
 
   Dim selToDel As Visio.Selection
   Set selToDel = visPg.CreateSelection( _
   Visio.VisSelectionTypes.visSelTypeEmpty)
 
   '// We can use For...Each again, because we're not deleting
   '// shapes from *within* the loop.
   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes
 
      '// Add shape to selection if it is NOT on the sExcludeLayerName layer:
      If (m_filter_isOnLayer(shp, sExcludeLayerName) = False) Then
 
         Call selToDel.Select(shp, Visio.VisSelectArgs.visSelect)
 
      End If
 
   Next
 
   '// TODO: Process the shapes in selToDel to make sure they
   '//       can really be deleted. An exercise for the 
   '//       student...
   '//
   '// It might go like this:
   '//
   '// 1. Unlock locked shapes in selToDel
   '// 2. Unlock locked layers that contain shapes in selToDel
   '// 3. Delete selToDel
   '// 4. Re-lock layers that were unlocked
   '// 5. Build custom undo scope, or turn off undo
 
   '// Delete the selection, if it is not empty:
   If (selToDel.Count > 0) Then
      Call selToDel.Delete
   End If
 
   '// Cleanup:
   Set selToDel = Nothing
   Set shp = Nothing
 
End Sub
 
Private Function m_filter_isOnLayer( _
         ByRef visShp As Visio.Shape, _
         ByVal sLayerName As String) As Boolean
 
   '// Start pessimistically:
   m_filter_isOnLayer = False
 
   '// Shapes can belong to more than one layer.
   '// Check all layers that the shape might belong to:
   Dim lyr As Visio.Layer
   Dim i As Integer
   For i = 1 To visShp.LayerCount
 
      '// Get the ith layer for the shape:
      Set lyr = visShp.Layer(i)
 
      '// Compare the layer name to sLayerName, not case-sensitive:
      If (StrComp(lyr.Name, sLayerName, vbTextCompare) = 0) Then
         m_filter_isOnLayer = True
         Exit For
      End If
 
   Next i
 
   '// Cleanup:
   Set lyr = Nothing
 
End Function
Public Sub TestIt_DeleteShapesNotOnLayer()
 Call m_deleteShapes_BatchNotOnLayer(Visio.ActivePage, "Buttons") '//...the layer name to exclude!
End Sub

Private Sub m_deleteShapes_BatchNotOnLayer( _
         ByRef visPg As Visio.Page, _
         ByVal sExcludeLayerName)
 
   Dim selToDel As Visio.Selection
   Set selToDel = visPg.CreateSelection( _
   Visio.VisSelectionTypes.visSelTypeEmpty)
 
   '// We can use For...Each again, because we're not deleting
   '// shapes from *within* the loop.
   Dim shp As Visio.Shape
   For Each shp In visPg.Shapes
 
      '// Add shape to selection if it is NOT on the sExcludeLayerName layer:
      If (m_filter_isOnLayer(shp, sExcludeLayerName) = False) Then
 
         Call selToDel.Select(shp, Visio.VisSelectArgs.visSelect)
 
      End If
 
   Next
 
   '// TODO: Process the shapes in selToDel to make sure they
   '//       can really be deleted. An exercise for the 
   '//       student...
   '//
   '// It might go like this:
   '//
   '// 1. Unlock locked shapes in selToDel
   '// 2. Unlock locked layers that contain shapes in selToDel
   '// 3. Delete selToDel
   '// 4. Re-lock layers that were unlocked
   '// 5. Build custom undo scope, or turn off undo
 
   '// Delete the selection, if it is not empty:
   If (selToDel.Count > 0) Then
      Call selToDel.Delete
   End If
 
   '// Cleanup:
   Set selToDel = Nothing
   Set shp = Nothing
 
End Sub

Private Function m_filter_isOnLayer( _
         ByRef visShp As Visio.Shape, _
         ByVal sLayerName As String) As Boolean
 
   '// Start pessimistically:
   m_filter_isOnLayer = False
 
   '// Shapes can belong to more than one layer.
   '// Check all layers that the shape might belong to:
   Dim lyr As Visio.Layer
   Dim i As Integer
   For i = 1 To visShp.LayerCount
 
      '// Get the ith layer for the shape:
      Set lyr = visShp.Layer(i)
 
      '// Compare the layer name to sLayerName, not case-sensitive:
      If (StrComp(lyr.Name, sLayerName, vbTextCompare) = 0) Then
         m_filter_isOnLayer = True
         Exit For
      End If
 
   Next i
 
   '// Cleanup:
   Set lyr = Nothing

End Function

Note the TODO bit in m_deleteShapes_BatchNotOnLayer, which reiterates the tasks you might undertake to make a selection truly, truly, truly safe for deletion.

Well, that was a lot to swallow, a lot of details and a lot of examples. Thanks for reading, and I hope it gives a better insight on how to automate a lot of aspects of a Visio-based utility or solution!

4 Comments »

  • Miles Thomas says:

    Great Article.

    And a useful watch-out that VBA collections (at least in Visio) are not “read consistent”, which some database programmers would expect (at least those from an Oracle, DB/2 or Ingres/Postgres world because that’s the default behaviour of SQL queries in those databases…doesn’t matter what data manipulation that you or anyone else does subsequent to the start of the query, as long as the query is open for read, you get a consistent answer based on what the data looked like at the start).

    Personally, when faced with this lack of read-consistency, I would have built a work list of object identifiers in an array..copying the collection into local storage, and iterated on that rather than relying on working from the bottom up as this behaviour could change in a future version of Visio.

  • JK says:

    I think your VisDeleteFlags descriptions got a bit mixed up 😉

  • Visio Guy says:

    Thanks JK, fixed it!

  • Visio Guy says:

    Hmm, just noticed the code syntax formatting isn’t working either!

Leave a comment!

Add your comment below, or trackback from your own site. You can also subscribe to these comments via RSS.

Be nice. Keep it clean. Stay on topic. No spam.

You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code lang=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre lang="" extra="">

This is a Gravatar-enabled weblog. To get your own globally-recognized-avatar, please register at Gravatar.

*