Is there a faster method of deleting shapes in Excel - vba

I've successfully added shapes into cells (msoShapeOval) in a pivot table. I need to clear and recreate these shapes if the pivot / slicer selection changes. My current method works, but it is slow. Is there any better method to clear shapes in bulk? Note: I do know the exact cell range where all these shapes exist. I've also appied :
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Current code:
Dim Shp as Shape
For Each Shp In rng.Parent.Shapes
If InStrB(Shp.Name, "$") > 0 Then Shp.Delete
Next

It is possible to delete the shapes at once without selecting, with some fine tuning. Let's imagine you want to delete the rectangulars from this:
What you need to do is the following:
loop through all the objects
make an array with all the rectangular's names
delete the objects in the array
Tricky part is the looping through the objects, because you need to increment your array every time, which is not a built-in functionality (like in collection). incrementArray is the function for this.
Furthermore, the first time you increment to the unassigned array, you need to check whether it is allocated (achieved with the IsArrayAllocated function below).
Option Explicit
Sub TestMe()
Dim shp As Shape
Dim arrOfShapes() As Variant 'the () are important!
With ActiveSheet
For Each shp In .Shapes
If InStrB(shp.Name, "Rec") > 0 Then
arrOfShapes = incrementArray(arrOfShapes, shp.Name)
End If
Next
If IsArrayAllocated(arrOfShapes) Then
Debug.Print .Shapes.Range(arrOfShapes(0)).Name
.Shapes.Range(arrOfShapes).Delete
End If
End With
End Sub
The additional functions:
Public Function incrementArray(arrOfShapes As Variant, nameOfShape As String) As Variant
Dim cnt As Long
Dim arrNew As Variant
If IsArrayAllocated(arrOfShapes) Then
ReDim arrNew(UBound(arrOfShapes) + 1)
For cnt = LBound(arrOfShapes) To UBound(arrOfShapes)
arrNew(cnt) = CStr(arrOfShapes(cnt))
Next cnt
arrNew(UBound(arrOfShapes) + 1) = CStr(nameOfShape)
Else
arrNew = Array(nameOfShape)
End If
incrementArray = arrNew
End Function
Function IsArrayAllocated(Arr As Variant) As Boolean
On Error Resume Next
IsArrayAllocated = IsArray(Arr) And _
Not IsError(LBound(Arr, 1)) And _
LBound(Arr, 1) <= UBound(Arr, 1)
End Function
Credits to this guy for the finding that the arrOfShapes should be declared with parenthesis (I have spent about 30 minutes researching why I could not pass it correctly) and to CPearson for the IsArrayAllocated().

To delete all shapes except slicers:
Sub RemoveAllExceptSlicers()
Dim sh As Shape
For Each sh In ActiveSheet.Shapes
If Not sh.Type = MsoShapeType.msoSlicer Then
sh.Delete
End If
Next
End Sub

Related

How to group all objects on a slide, which can be grouped, and resize the group?

How do I group all objects (or "shapes"?) on a slide and resize that group?
Subsequently the "big" group should be ungrouped.
My attempt fails with "function expected":
Sub Group_And_Resize()
Dim Sld As Slide
With Sld.Shapes
With .SelectAll.Group //Error here
.Width = 907
End With
End With
End Sub
By manual experimentation, I learned that some objects (or "shapes"?) cannot be added to a group, such as slide numbers which are automatically generated. Is there any possibility to exclude those from the selection?
If you want to group the shapes together first and then set the overall width to 907, you can use this code:
Sub Group_And_Resize()
Dim Sld As Slide, a As Variant, i As Integer
Set Sld = ActivePresentation.Slides(1) ' your slide
ReDim a(1 To Sld.Shapes.Count)
For i = LBound(a) To UBound(a)
a(i) = Sld.Shapes(i).Name
Next
Sld.Shapes.Range(a).Group.Width = 907
End Sub
If you want to make the width of each of the shapes on the slide = 907, you can use the following code:
Sub Group_And_Resize()
Dim Sld As Slide
Set Sld = ActivePresentation.Slides(1) ' your slide
Sld.Shapes.Range.Width = 907
End Sub
This is based on Алексей Р's answer but solves a few problems and also is more generic. It allows calling the routine on any slide in the active presentation and setting the width to any desired value. See comments for more details.
Sub Test()
With ActivePresentation
Call Group_And_Resize(.Slides(1), 200)
End With
End Sub
Sub Group_And_Resize(Sld As Slide, sngWidth As Single)
Dim a As Variant, i As Long ' Array indices are longs, not integers
Dim oGroup As Shape
' Call NonPlaceholderShapeCount to get number of
' shapes that are not placeholders, since placeholders
' cannot be grouped. Use that to ReDim the array:
ReDim a(1 To NonPlaceholderShapeCount(Sld))
For i = LBound(a) To UBound(a)
' Again, make sure we don't try to group placeholders
If Not Sld.Shapes(i).Type = msoPlaceholder Then
a(i) = Sld.Shapes(i).Name
End If
Next
' Get a reference to the new group
' since we need to set several properties on it
Set oGroup = Sld.Shapes.Range(a).Group
' This ensures that the group (and its shapes)
' aren't distorted:
oGroup.LockAspectRatio = True
' and finally, set the width
oGroup.Width = sngWidth
End Sub
Function NonPlaceholderShapeCount(Sld As Slide) As Long
' Returns the number of non-placeholder shapes on Sld
Dim x As Long
Dim lCount As Long
With Sld
For x = 1 To .Shapes.Count
If Not .Shapes(x).Type = msoPlaceholder Then
lCount = lCount + 1
End If
Next
End With
NonPlaceholderShapeCount = lCount
End Function

VBA Autorange function for Array to Range in excel

As I am working with large csv files, I decided to load them into VBA memory instead of loading in my spreadsheet to make it quicker and lighter.
So I have a function CSVtoArray that read through my CSV and gives me an array.
Then if I still want to see my data in excel I just write {=(CSVtoArray(my_csv_path)} in my s/s.
But since the size of my csv changes over time, I wanted to write a function called AutoRange that would automatically fit the display area in my spreadsheet according to the size of my range.
So this is what I wrote but it's not working, it does nothing, only the cell in which I am writing the formula is filled.
Function AutoRange(my_array As Variant)
Dim nb_rows, nb_cols As Integer
Dim current_cell, target_range As Range
nb_rows = UBound(my_array, 1)
nb_cols = UBound(my_array, 2)
Set current_cell = Selection
current_cell.Resize(nb_rows, nb_cols).FormulaArray = current_cell.Formula
AutoRange = Selection
End Function
Thanks in advance guys.
Functions are for returning things. And if used in the cell are for returning things to that cell, not to manipulate other cells. Do you want actually want a sub like?
Code:
Option Explicit
Public Sub TEST()
Dim my_Array()
my_Array = [A1].CurrentRegion.Value
AutoRange my_Array
End Sub
Public Sub AutoRange(ByVal my_Array As Variant)
Dim nb_rows As Long, nb_cols As Long
Dim current_cell As Range
nb_rows = UBound(my_Array, 1)
nb_cols = UBound(my_Array, 2)
Set current_cell = Selection
current_cell.Resize(nb_rows, nb_cols).FormulaArray = current_cell.Formula
End Sub
Result:
From your comments: If you want to use as a function (Not a UDF, which cannot alter other cells) then you can use the following way, though I advise against it as it bad practice:
Option Explicit
Public Sub TEST()
Dim my_Array()
my_Array = [A1].CurrentRegion.Value
Dim target_Range As Range
Set target_Range = AutoRange(my_Array)
End Sub
Public Function AutoRange(ByVal my_Array As Variant) As Range
Dim nb_rows, nb_cols As Long
Dim current_cell, target_Range As Range
nb_rows = UBound(my_Array, 1)
nb_cols = UBound(my_Array, 2)
Set current_cell = Selection
Set target_Range = current_cell.Resize(nb_rows, nb_cols)
Set AutoRange = target_Range
target_Range.FormulaArray = current_cell.Formula
End Function
Ok so I did it another way,
I have my AutoRange sub :
Sub AutoRange(my_Array As Variant, top_left_corner As Range)
' Here we take an array in input, the one we want to display, and the top left corner of the range where we want to put it
Dim nb_rows, nb_cols As Integer
nb_rows = UBound(my_Array, 1)
nb_cols = UBound(my_Array, 2)
Set current_cell = top_left_corner
top_left_corner.Resize(nb_rows, nb_cols).FormulaArray = top_left_corner.Formula
End Sub
and then I added a Worksheet_change sub to my s/s:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Address = "$A$1" Then
if Target.value="load data" then
Call Autorange(my_array, my_range)
else
Range(my_range, my_range.End(xlDown).End(xlToRight)).clearcontents
End If
End Sub
so I just need to say if I want to load my data or not and it will adjust.
I assume that at my former company they were using an addin rather than VBA itself.
thanks anyways guys.
cheers

Do not Delete worksheets if it is in array

Dim L As Double
Dim Workings() As Variant
Workings = Array("Due SO not Billed", "Working Paper", "Ageing Over 14 Days")
On Error Resume Next
Application.DisplayAlerts = False
For L = 1 To Worksheets.Count
If Worksheets(L).Name <> Workings Then
Worksheets(L).Delete
Exit For
End If
Next L
Application.DisplayAlerts = True
On Error GoTo 0
I tried writing above code. The purpose is I have 10 worksheets in a workbook but final output requires only 3 sheets and I want to delete the rest of the worksheets. I tried above code with array and whatever name I gave in the array should be saved and all other remaining sheets should be be deleted. I'm getting type mismatch error. Can someone help please?
Use Match to test whether the worksheet's Name is in the array:
Dim Workings() As Variant
Dim ws As Worksheet
Workings = Array("Due SO not Billed", "Working Paper", "Ageing Over 14 Days")
Application.DisplayAlerts = False
For Each ws In Worksheets
If IsError(Application.Match(ws.Name, Workings, False)) Then
ws.Delete
End If
Next
Application.DisplayAlerts = True
Smells like homework. It's not working because the Workings variable is an array while the other is a string, they are different types. To test if a string is inside an array do
If Not IsInArray(Worksheets(L).Name, Workings)
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
Using VBA Filter function gives you the added benefit of comparing Text. In this way you do not have to be concerned with capitalization.
Sub DeleteWorksheets()
Dim Workings() As Variant
Dim ws As Worksheet
Workings = Array("Due SO not Billed", "Working Paper", "Ageing Over 14 Days")
Application.DisplayAlerts = False
For Each ws In Worksheets
If UBound(Filter(Workings, ws.Name, True, vbTextCompare)) = -1 Then ws.Delete
Next
Application.DisplayAlerts = True
End Sub
This is my valueInArray function:
Public Function valueInArray(myValue As Variant, myArray As Variant) As Boolean
Dim cnt As Long
For cnt = LBound(myArray) To UBound(myArray)
If CStr(myValue) = CStr(myArray(cnt)) Then
valueInArray = True
Exit Function
End If
Next cnt
End Function
It is quite a lot of code and it loops, but it works. Plus, it works ok for both integers and strings.

Why does autofit row VBA code in Excel keep causing #value error in my formula?

I have a concatenate based on offset array code that I'm using to combine data.
Public Function concatPlusIfs(rng As Range, sep As String, lgCritOffset1 As Long, lgCritOffset2 As Long, varCrit1 As Variant, lgCritOffset3 As Long, lgCritOffset4 As Long, varCrit2 As Variant, Optional noDup As Boolean = False, Optional skipEmpty As Boolean = False) As String
Dim CL As Range, strTemp As String
If noDup Then 'remove duplicates, use collection to avoid them
Dim newRow As New Collection
On Error Resume Next
For Each CL In rng.Cells
If skipEmpty = False Or Len(Trim(CL.Text)) > 0 Then
If CL.Offset(lgCritOffset1, lgCritOffset2) = varCrit1 And CL.Offset(lgCritOffset3, lgCritOffset4) = varCrit2 Then newRow.Add CL.Text, CL.Text
End If
Next
For i = 0 To newRow.Count
strTemp = strTemp & newRow(i) & sep
Next
Else
For Each CL In rng.Cells
If skipEmpty = False Or Len(Trim(CL.Text)) > 0 Then
If CL.Offset(lgCritOffset1, lgCritOffset2) = varCrit1 And CL.Offset(lgCritOffset3, lgCritOffset4) = varCrit2 Then strTemp = strTemp & CL.Text & sep
End If
Next
End If
concatPlusIfs = Left(strTemp, Len(strTemp) - Len(sep))
End Function
The code works great. It's not mine, but I tweaked someone else's code. The problem is that it will sometimes return a small amount of text and other times a large amount of text. I need the rows to autofit height. Before I started using the new concatPlusIfs formula, I used a code on the worksheet to autofit row height, but it cause a weird problem with the above code and only the above code and I can't find any mention of this type of problem. It works fine with all other arrays or non array formulas that I'm using. Basically what happens is that for a fraction of a second I can see the correct output in the cell and then I get #value!. I have no idea what's going on. I've tried autofit rows as a macro instead and it had the same effect. If I manually autofit the row everything is fine, but that's not a viable option.
Does anyone understand what would cause a problem like this? Or How can I fix it?
I'm not using any merged rows anywhere on the sheet.
Here are a few of the autofit strategies I've tried. One as a macro:
Sub AutoFit()
Worksheets("Sheet1").Range("A2:A" & Rows.Count).Rows.AutoFit
End Sub
Also as a code on the sheet,
Private Sub Worksheet_Change(ByVal Target As Range)
Target.EntireRow.AutoFit
End Sub
And,
Private Sub Worksheet_Calculate()
Application.EnableEvents = False
Me.Rows.AutoFit
'or be specific
Me.Rows("1:33").AutoFit
Application.EnableEvents = True
End Sub
Thank you for any help with this.
You most likely get #VALUE! error when your formula tries to convert the ### from the .Text property to value. That is why you should use .Value2 or .Value instead.

VBA Find and replace Issue

I have a workbook that comes from someone else so the file path is referring to that person's local drive. So I need to replace the file path with the one in my local drive. I tried 3 methods and they all failed. Please give me some guidelines. Basically, I am trying to find a replace 2 file paths within a formula for an entire sheet (almost all the cells) (see below):
='U:\Futochan\2012\[Futochan2012.xlsm]Counts'!E6+'U:\Futochan\2013\[Futochan2013.xlsm]Counts'!E6
1st Method:
Did this manually. DAta -> Edit Links -> Change sources (Failed, keep prompted me for the links)
2nd Method:
VBA: Did the range.replace. It only replaced the first cell and halted.
3rd Method:
VBA: Did a cell by cell loop :"For each cell in range". I turned off everything. It worked but took 2 hours. :/
Please help!! Thanks!
Firstly, any reason why you can't do a manual find and replace all for "U:\Futochan\2012[Futochan2012.xlsm]"? If it is just two links, and this is a one-off, this is by far the quickest approach.
For Range.replace, what was your range? If you call it on Worksheet.Cells.replace(...) it should replace all instances.
Finally, a quick approach that does not involve Range.Replace is below, but again, reinventing the wheel is a less than preferable approach :)
Private stringsToReplace As New Collection
Sub blah()
Dim ws As Worksheet
Dim arr
Dim formulaCells As Range, area As Range
Dim i As Long, j As Long
stringsToReplace.Add Array("old1", "new1") 'add as many pairs as you like in the format of Array(oldString,newString)
Set ws = ActiveSheet
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
On Error Resume Next
Set formulaCells = ws.Cells.SpecialCells(xlCellTypeFormulas) 'only look at formula cells for speed
On Error GoTo 0
If Not formulaCells Is Nothing Then
For Each area In formulaCells 'we will load into an array in memory, to prevent the horrendously slow enumeration through cells
If area.Count = 1 Then
area.Formula = newFormulaText(area.Formula)
Else
arr = area.Formula
For i = LBound(arr, 1) To UBound(arr, 1)
For j = LBound(arr, 2) To UBound(arr, 2)
arr(i, j) = newFormulaText(arr(i, j))
Next j
Next i
area.Formula = arr
End If
Next area
End If
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
Function newFormulaText(ByVal oldText As String) As String
Dim oldNewPair
Dim newText As String
newText = oldText
For Each oldNewPair In stringsToReplace
newText = Replace(newText, oldNewPair(0), oldNewPair(1))
Next oldNewPair
newFormulaText = newText
End Function