A 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
- A “Test It” routine for “calling the code from the outside”. These will start with “TestIt_”.
- The main loop that processes all shapes on a page. These will start with “m_deleteShapes_”.
- 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.
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:
'// 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:
'// 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!
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:
- Create an empty selection object
- Loop through the shapes and add qualifiers to the selection
- Delete the selection if it isn’t empty
Simple. Let’s see it in action:
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:
'// 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:
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:
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.
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:
This 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.
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:
- Create a collection of layers that were locked, but are unlocked.
- Loop through a collection of shapes, adding qualifiers to a “to-delete” selection.
- Prepare each shape for deletion.
- Possibly disable undo for Visio or the document.
Preparing each shape for deletion would involve:
- Unlocking the LockDelete ShapeSheet cell.
- 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.
- 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:
- Re-lock any layers that got unlocked.
- Optionally deal with invisible layers.
- 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.
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!
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!
shashidhar says
Hello Visio Guy,
I need code for deleting the picture (enhanced metafile). i tried with few of above code but they are not working.
i am a mechanical engineer, so i am not much familiar with the codings.
please help me.
Visio Guy says
Hi shashidhar ,
A metafile is just another shape.Are you trying to identify all the shapes on a page that are metafiles?
If that’s the case, then look for shp.Type = Visio.VisShapeTypes.visTypeMetafile. Remember, if you’re in a loop, go backwards so that you delete shapes off the end of a collection. Don’t delete shapes in a “For Each shp In pg.Shapes” or you’ll get into trouble!
Ki Won Kim says
How can I delete Callouts from a selection?
Visio Guy says
Hi Ki Won,
If you are talking about the Callouts that you create using the Insert > Diagram Parts. > Callout dropdown, then you can check the IsCallout property.
For example, select a shape in the active drawing window, then run this VBA in the Immediate window:
?Visio.ActiveWindow.Selection(1).IsCallout ‘– True or False
Note: there isn’t a shape type for callout. If you select a callout and run this:
?Visio.ActiveWindow.Selection(1).Type
You’ll get “3” wich is Visio.VisShapeTypes.visTypeShape