Excel VBA - Check Report Slicer Selections (Skip if ALL are selected) - vba

I need help with some VBA code. I have an AgeRange slicer and I have a working script that inserts a row, adds a timestamp, and then reports the slicer selections.
I'd like to add something to this that will SKIP the process if ALL the items in the slicer are selected (True).
Is there something that I can insert that says "If the slicer hasn't been touched (all items are true), then end sub".
Here's what I have for code so far:
Dim cache As Excel.SlicerCache
Set cache = ActiveWorkbook.SlicerCaches("Slicer_AgeRange")
Dim sItem As Excel.SlicerItem
For Each sItem In cache.SlicerItems
If sItem.Selected = True Then xAge = xAge & sItem.Name & ", "
Next sItem
Rows("1:1").Select
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Range("A1").Select
ActiveCell.FormulaR1C1 = Format(Now(), "MM-DD-YYYY HH:MM AM/PM")
Range("B1").Select
ActiveCell.FormulaR1C1 = xAge
Range("C1").Select
End Sub
Any help is greatly appreciated!

This is a bit more than you asked for, but I figured I would share since I just wrote this for my own use. It clears all slicers physically located on a worksheet only if they are filtered (not all selected). For your question, the good bit is the for each item loop. and the line right after it.
Sub clearWorksheetSlicers(ws As Worksheet)
'clears all slicers that have their shape on a specific worksheet
Dim slSlicer As Slicer
Dim slCache As SlicerCache
Dim item As SlicerItem
Dim hasUnSel As Boolean
For Each slCache In ThisWorkbook.SlicerCaches
For Each slSlicer In slCache.Slicers
If slSlicer.Shape.Parent Is ws Then
For Each item In slCache.SlicerItems
If item.Selected = False Then
hasUnSel = True
Exit For
End If
Next item
If hasUnSel = True Then slCache.ClearManualFilter
hasUnSel = False
End If
Next slSlicer
Next slCache
End Sub

Nvm. I got it on my own. :)
Dim cache As Excel.SlicerCache
Dim sName As Slicers
Dim sItem As Excel.SlicerItem
Dim xSlice As String
Dim xName As String
For Each cache In ActiveWorkbook.SlicerCaches
xName = StrConv(Replace(cache.Name, "AgeRange", "Ages")
xCheck = 0
For Each sItem In cache.SlicerItems
If sItem.Selected = False Then
xCheck = xCheck + 1
Else
xCheck = xCheck
End If
Next sItem
If xCheck > 0 Then
For Each sItem In cache.SlicerItems
If sItem.Selected = True Then
xSlice = xSlice & sItem.Caption & ", "
End If
Next sItem
Rows("1:1").Select
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Range("B1").Select
ActiveCell.FormulaR1C1 = xName & ": " & xSlice
xSlice = ""
End If
Next cache
Range("A1").Select
ActiveCell.FormulaR1C1 = Format(Now(), "MM-DD-YYYY HH:MM AM/PM")
End Sub

The code in this thread by Dallas Frank looks like it should work, and all the properties explicitly called do exist but for some reason in my pivot table the SlicerItems collection is empty. I have to check each SlicerItem via
SlicerCache.SlicerCacheLevels.Item(1).SlicerItems
This replacement is not precisely what the original requestor asked but it is illustrative of the use of SlicerCacheLevel that got me where I needed to be when SlicerCache.SlicerItems turned out not to exist
Sub AllSlicersSelected(WorksheetWithPT As Worksheet)
Dim SlicerItems As SlicerItems
Dim SlicerItem As SlicerItem
Dim SlicerCaches As SlicerCaches
Dim SlicerCache As SlicerCache
Dim SlicerCacheLevel As SlicerCacheLevel
Dim Slicer As Slicer
Dim strSlicerItemsNotSelected As String
Dim bHaveWhatWeNeed As Boolean
Dim vSlicerItemsToSelect As Variant
Set SlicerCaches = ThisWorkbook.SlicerCaches
For Each SlicerCache In SlicerCaches
For Each Slicer In SlicerCache.Slicers
If Slicer.Shape.Parent Is WorksheetWithPT Then
bHaveWhatWeNeed = True
Exit For
End If
Next
If bHaveWhatWeNeed Then
Exit For
End If
Next
For Each SlicerCacheLevel In SlicerCache.SlicerCacheLevels
For Each SlicerItem In SlicerCacheLevel.SlicerItems
If Not SlicerItem.Selected Then
strSlicerItemsNotSelected = strSlicerItemsNotSelected & Chr(0)
End If
Next
Next
If Len(strSlicerItemsNotSelected) > 0 Then
vSlicerItemsToSelect = Split(Mid(strSlicerItemsNotSelected, 2), Chr(0))
For Each SlicerItem In vSlicerItemsToSelect
SlicerItem.Selected = True
Next
End If
End Sub

Using Dallas Franks solution, I ran into a 1004 issue where it was showing a method/object error. Could be because I am using PowerQuery to generate Power Pivots and immediately found that sometimes you must use slicer cache levels.
Dallas Franks solution was too good to start from the beginning so I found a way to slightly change it to use SlicerChacheLevel(s) and it works very well!
Sub clearWorksheetSlicers(ws As Worksheet)
'clears all slicers that have their shape on a specific worksheet
Dim slSlicer As Slicer
Dim slCache As SlicerCache
Dim item As SlicerItem
Dim hasUnSel As Boolean
Dim sllvl As SlicerChacheLevel
For Each slCache In ThisWorkbook.SlicerCaches
For Each slSlicer In slCache.Slicers
If slSlicer.Shape.Parent Is ws Then
For Each sllvl In slCache.SlicerCacheLevels
For Each item In sllvl.SlicerItems
If item.Selected = False Then
hasUnSel = True
Exit For
End If
Next item
Next sllvl
If hasUnSel = True Then slCache.ClearManualFilter
hasUnSel = False
End If
Next slSlicer
Next slCache
End Sub

Related

Macro fires 50% of the time when changing slicer item

I have a particular problem and couldn't find any solution anywhere on the internet.
So I have a pivot table which is connected to 6 slicers and also a chart which data range is dependent on pivot table values.
I've made a macro which updates chart scales everytime a value in any of the worksheet cells is changed. Here is the macro:
Public Sub worksheet_Change(ByVal Target2 As Range)
If ActiveSheet.Name = "Dashboard" Then
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.DataEntryMode = xlOff
'Chart_axis Macro
Sheets("Dashboard").ChartObjects("Chart 9").Activate
If ActiveSheet.Range("B19") = "excluding CE" Then
ActiveChart.Axes(xlValue).MinimumScale = Range("E3").Value
ActiveChart.Axes(xlValue).MaximumScale = Range("E4").Value
Else
ActiveChart.Axes(xlValue).MinimumScale = Range("A3").Value
ActiveChart.Axes(xlValue).MaximumScale = Range("A4").Value
End If
ActiveChart.Refresh
ActiveSheet.Range("B18").Select
Application.EnableEvents = True
Application.ScreenUpdating = True
End If
End Sub
In order to work as intended i also had to made a function which reads the active elements of a slicer:
Public Function GetSelectedSlicerItems(SlicerName As String) As String
Application.Volatile
Set coll = New Collection
Dim cache As Excel.SlicerCache
Dim i As Integer
Set cache = ActiveWorkbook.SlicerCaches(SlicerName)
Dim sItem As Excel.SlicerItem
Dim result As String
For Each sItem In cache.SlicerItems
If sItem.Selected And sItem.HasData Then
'Debug.Print sItem.Name
'Debug.Print sItem.HasData
'GetSelectedSlicerItems = (sItem.Name)
coll.Add sItem.Name
End If
Next sItem
For i = 1 To coll.Count
'Debug.Print coll(i)
result = result & coll(i) & ", "
Next i
result = Left(result, Len(result) - 2)
GetSelectedSlicerItems = result
End Function
My problem is that while the value of the function always updates when the slicer item is changed, the macro only does it randomly about 50% of the time.
Screenshot of my report:
The formulas containing the selected slicer items function are on the top right.
So do you have any idea how to make it work 100% of the time?
Thanks in advance,
Alan
Edit: i forgot to add that it's only the issue if only one slicer is highlited. When i select multiple slicers (with ctrl+click) it always works.

VBA search for value on next sheet

is there I way for searching a value on the next sheet (ActiveSheet.Next.Activate) without jumping on to it?
Here the whole Code:
the problem is, it jumps to the next sheet even if there is no searched value.
Dim ws As Worksheet
Dim Loc As Range
Dim StrVal As String
Dim StrRep As String
Dim i As Integer
Private Sub CommandButton1_Click()
i = 1
Call Replacing
End Sub
Private Sub CommandButton2_Click()
i = 2
Call Replacing
End Sub
Public Sub Replacing()
StrVal = Userform1.Textbox1.Text
StrRep = Me.Textbox1.Text
if Trim(StrVal) = "" Then Exit Sub
Dim fstAddress As String
Dim nxtAddress As String
For Each ws In ThisWorkbook.Worksheets
With ws
Set Loc = .Cells.Find(what:=StrVal)
fstAddress = Loc.Address
If Not Loc Is Nothing Then
If Not StrRep = "" And i = 1 Then
Loc.Value = StrRep
Set Loc = .Cells.FindNext(Loc)
ElseIf i = 2 Then Set Loc = Range(ActiveCell.Address)
Set Loc = .Cells.FindNext(Loc)
nxtAddress = Loc.Address
If Loc.Address = fstAddress Then
ActiveSheet.Next.Activate '****Here it should jump only if found something on the next sheet****
GoTo repeat
nxtAddress = Loc.Address
End If
If Not Loc Is Nothing Then Application.Goto ws.Range(nxtAddress), False
End If
i = 0
End If
End With
Set Loc = Nothing
repeat:
Next ws
End Sub
the variable "i" which switches between the values 0, 1 and 2 is bound to two buttons. these buttons are "Replace" and "Skip (to next found value)".
This code asks on each occurrence of StrVal whether you want to replace the value or skip it.
I found a problem checking if Found_Address = First_Found_Address:
If you've replaced the value in in First_Found_Address it won't find that address again and miss the starting point in the loop.
Also the original source of the code stops at the last value using Loop While Not c Is Nothing And c.Address <> firstAddress. The problem here is that if the value in c is being changed eventually c will be Nothing but it will still try and check the address of c - causing an error (Range Find Method).
My solution to this is to build up a string of visited addresses on the sheet and checking if the current address has already been visited using INSTR.
I've included the code for calling from a button click or from within another procedure.
Private Sub CommandButton1_Click()
FindReplace Userform1.Textbox1.Text, 1
End Sub
Private Sub CommandButton2_Click()
FindReplace Userform1.Textbox1.Text, 1, Me.Textbox1.Text
End Sub
Sub Test()
FindReplace "cd", 1, "ab"
End Sub
Sub FindReplace(StrVal As String, i As Long, Optional StrRep As String = "")
Dim ws As Worksheet
Dim Loc As Range
Dim fstAddress As String
Dim bDecision As Variant
For Each ws In ThisWorkbook.Worksheets
'Reset the visited address list on each sheet.
fstAddress = ""
With ws
Set Loc = .Cells.Find(what:=StrVal, LookIn:=xlValues, LookAt:=xlWhole, SearchDirection:=xlNext)
If Not Loc Is Nothing Then
Do
fstAddress = fstAddress & "|" & Loc.Address
Loc.Parent.Activate 'Activate the correct sheet.
Loc.Activate 'and then the cell on the sheet.
bDecision = MsgBox("Replace value?", vbYesNo + vbQuestion, "Replace or Select value?")
If bDecision = vbYes Then
Loc = StrRep 'Raise the blade, make the change.
'Re-arrange it 'til it's sane.
End If
Set Loc = .Cells.FindNext(Loc)
If Loc Is Nothing Then Exit Do
Loop While InStr(fstAddress, Loc.Address) = 0
End If
End With
Next ws
End Sub

Add separate columns into a dictionary

I am trying to add data from a split range (X5:X?,AX5:AX?) into a VBA dictionary. ? Is determined as the last row of data within the sheet. I am new to VBA and trying to force my way through this.
Public Sub Test
'Creates a dictionary object
Dim orderstatus As Object, path As String
Set orderstatus = CreateObject("Scripting.Dictionary")
Dim order, status 'key and object names
order = "Order #": status = "Order Status"
path = ThisWorkbook.path
'Central District--A Head Water Order Summary
Dim app As New Excel.Application, book As Excel.Workbook
app.Visible = False
Set book = app.Workbooks.Add(path & "\CENTRAL DIST\A HEAD - WATER ORDER SUMMARY.xls")
'A Head #1
Dim A1Head As Integer, last As Integer, l as Integer
l = 4
book.Worksheets("A HEAD #1").Activate
last = Range("X" & Rows.Count).End(xlUp).Row
Set lastCol = Range("X5:X" & last, "AX5:AX" & last)
For Each l In lastCol.Cells
orderstatus.Add lastCol.Value
Next
End Sub
Any help is greatly appreciated!
I think something like this is what you're looking for:
Sub tgr()
Dim OrderStatus As Object
Dim i As Long
Dim Key As Variant
Set OrderStatus = CreateObject("Scripting.Dictionary")
With Application
.ScreenUpdating = False
.EnableEvents = False
End With
With Workbooks.Open(ThisWorkbook.Path & "\CENTRAL DIST\A HEAD - WATER ORDER SUMMARY.xls").Sheets("A HEAD #1")
For i = 5 To .Cells(.Rows.Count, "X").End(xlUp).Row
If Not OrderStatus.Exists(.Cells(i, "X").Value) Then OrderStatus(.Cells(i, "X").Value) = .Cells(i, "AX").Value
Next i
.Parent.Close False
End With
'Print dictionary to text file
Close #1
Open ThisWorkbook.Path & "\OrderStatus Output.txt" For Output As #1
Print #1, "Key" & vbTab & "Value"
For Each Key In OrderStatus.Keys
Print #1, Key & vbTab & OrderStatus(Key)
Next Key
Close #1
With Application
.ScreenUpdating = True
.EnableEvents = True
End With
End Sub
Change this
orderstatus.Add lastCol.Value
to this
orderstatus.Add l.Value, 1
This assumes you will have no duplicates because you aren't checking for that and will get an error if you do have duplicates.
you're messing up with Range object and Row index
and you'd better abandon the Activate/ActiveXXX pattern and use fully qualified range references
give this code a try
Option Explicit
Public Sub Test()
'Creates a dictionary object
Dim orderstatus As Object
Set orderstatus = CreateObject("Scripting.Dictionary")
'Central District--A Head Water Order Summary
Dim app As New Excel.Application, book As Excel.Workbook
app.Visible = False
Set book = app.Workbooks.Add(ThisWorkbook.path & "\CENTRAL DIST\A HEAD - WATER ORDER SUMMARY.xls")
'A Head #1
Dim dataRng As Range, r As Range
Dim last As Integer
With book.Worksheets("A HEAD #1")
For Each r In .Range("X5", .Cells(.Rows.Count, "X").End(xlUp))
orderstatus(r.value) = r.Offset(, 26).value
Next
End With
End Sub
Moreover if you're running this macro from within an Excel session already, you don't need to get another instance of it nor explicitly reference it:
Option Explicit
Public Sub Test()
'Creates a dictionary object
Dim orderstatus As Object
Set orderstatus = CreateObject("Scripting.Dictionary")
'Central District--A Head Water Order Summary
Dim book As Workbook
Set book = Workbooks.Add(ThisWorkbook.path & "\CENTRAL DIST\A HEAD - WATER ORDER SUMMARY.xls")
'A Head #1
Dim dataRng As Range, r As Range
Dim last As Integer
With book.Worksheets("A HEAD #1")
For Each r In .Range("X5", .Cells(.Rows.Count, "X").End(xlUp))
orderstatus(r.value) = r.Offset(, 26).value
Next
End With
End Sub

VBA Code to Autofill

Have a column H with alphanumeric characters. Some cells in this column have the content (RAM) followed by 5 digits starting from 00000 to 99999.
If cell H219 has the content (RAM) 23596 then i have to fill cell A219 with a comment "completed".
This has to be done for all cells with the content "(RAM) followed by 5 digits"
Sub Macro16_B()
' ' Macro16_B Macro ' '
intRowCount = Worksheets("Reconciliation").UsedRange.Rows.Count
For i = 11 To intRowCount
If InStr(Range("H" & i).Value, "(RAM 00000-99999") Then
Range("A" & i).Value = "Completed"
End If
Next i
End Sub
A non-VBA answer could be (if the cell doesn't have extra text other than (RAM) & 5 numbers):
=IFERROR(IF(LEN(VALUE(TRIM(SUBSTITUTE(H1,"(RAM)",""))))=5,"completed",""),"")
My VBA answer would be:
Sub Test()
Dim rLastCell As Range
Dim rCell As Range
With Worksheets("Reconciliation")
Set rLastCell = .Columns(8).Find("*", , , , xlByColumns, xlPrevious)
If Not rLastCell Is Nothing Then
For Each rCell In .Range(.Cells(1, 8), rLastCell)
If rCell Like "*(RAM) #####*" Then
rCell.Offset(, -7) = "complete"
End If
Next rCell
End If
End With
End Sub
Cheers #Excelosaurus for heads up on the * would've forgotten it as well. :)
One way is to use the Like operator. The precise format of your string is not clear so you may have to amend (and assuming case insensitive). # represents a single number; the * represents zero or more characters.
Sub Macro16_B()
Dim intRowCount As Long, i As Long
' ' Macro16_B Macro ' '
intRowCount = Worksheets("Reconciliation").UsedRange.Rows.Count
For i = 11 To intRowCount
If Range("H" & i).Value Like "(RAM) #####*" Then
Range("A" & i).Value = "Completed"
End If
Next i
End Sub
Well, there are already 2 good answers, but allow me to paste my code here for good measure, the goal being to submerge #user2574 with code that can be re-used in his/her next endeavors:
Sub Macro16_B()
'In the search spec below, * stands for anything, and # for a digit.
'Remove the * characters if you expect the content to be limited to "(RAM #####)" only.
Const SEARCH_SPEC As String = "*(RAM #####)*"
Dim bScreenUpdating As Boolean
Dim bEnableEvents As Boolean
'Keep track of some settings.
bScreenUpdating = Application.ScreenUpdating
bEnableEvents = Application.EnableEvents
On Error GoTo errHandler
'Prevent Excel from updating the screen in real-time,
'and disable events to prevent unwanted side effects.
Application.ScreenUpdating = False
Application.EnableEvents = False
'Down with business...
Dim scanRange As Excel.Range
Dim cell As Excel.Range
Dim content As String
Dim ramOffset As Long
With ThisWorkbook.Worksheets("Reconciliation").Columns("H")
Set scanRange = .Worksheet.Range(.Cells(11), .Cells(.Cells.Count).End(xlUp))
End With
For Each cell In scanRange
content = CStr(cell.Value2)
If content Like SEARCH_SPEC Then
cell.EntireRow.Columns("A").Value = "Completed"
End If
Next
Recover:
On Error Resume Next
'Restore the settings as they were upon entering this sub.
Application.ScreenUpdating = bScreenUpdating
Application.EnableEvents = bEnableEvents
Exit Sub
errHandler:
MsgBox Err.Description, vbExclamation + vbOKOnly, "Error"
Resume Recover
End Sub

Excel Macro works slow, how to make it faster?

Stackovwerflow community.
I do believe that this question was asked x1000 times here, but i just wasn not able to find a solution for my slow macro.
This macro serves for unhiding certain areas on worksheets if correct password was entered. What area to unhide depends on cell value. On Sheet1 i have a table that relates certain cell values to passwords.
Here's the code that i use.
1st. Part (starts on userform named "Pass" OK button click)
Private Sub CommandButton1_Click()
Dim ws As Worksheet
DoNotInclude = "PassDB"
For Each ws In ActiveWorkbook.Worksheets
If InStr(DoNotInclude, ws.Name) = 0 Then
Application.ScreenUpdating = False
Call Module1.Hide(ws)
Application.ScreenUpdating = True
End If
Next ws
End Sub
2nd Part.
Sub Hide(ws As Worksheet)
Application.Cursor = xlWait
Dim EntPass As String: EntPass = Pass.TextBox1.Value
If EntPass = Sheet1.Range("G1").Value Then ' Master-Pass, opens all
Sheet1.Visible = xlSheetVisible
ws.Unprotect Password:="Test"
ws.Cells.EntireRow.Hidden = False
Pass.Hide
Else
Dim Last As Integer: Last = Sheet1.Range("A1000").End(xlUp).Row
Dim i As Integer
For i = 2 To Last
Dim region As String: region = Sheet1.Range("A" & i).Value
Dim pswd As String: pswd = Sheet1.Range("B" & i).Value
If EntPass = pswd Then
ws.Unprotect Password:="Test"
ws.Cells.EntireRow.Hidden = False
Dim b As Integer
Dim Last2 As Integer: Last2 = ws.Range("A1000").End(xlUp).Row
For b = 2 To Last2
ws.Unprotect Password:="Test"
If ws.Range("A" & b).Value <> region Then
ws.Range("A" & b).EntireRow.Hidden = True
End If
If ws.Range("A" & b).Value = "HEADER" Then
ws.Range("A" & b).EntireRow.Hidden = False
End If
ws.Protect Password:="Test"
Next b
End If
Next i
End If
Application.Cursor = xlDefault
Sheet2.Activate
Sheet2.Select
Pass.Hide
End Sub
It works fast enough if I enter master-pass to get access to every hidden area, but if i enter cell.value related password, it takes about 5-6 minutes before macro will unhide required areas on every worksheet.
I'd be really grateful if someone could point out the reasons of slow performance and advise changes to be made in code. Just in case, i've uploaded my excel file here for your convenience.
http://www.datafilehost.com/d/d46e2817
Master-Pass is OPENALL, other passwords are "1" to "15".
Thank you in advance and best regards.
Try batching up your changes:
Dim rngShow as Range, c as range
ws.Unprotect Password:="Test" 'move this outside your loop !
For b = 2 To Last2
Set c = ws.Range("A" & b)
If c.Value = "HEADER" Then
c.EntireRow.Hidden = False
Else
If c.Value <> region Then
If rngShow is nothing then
Set rngShow = c
Else
Set rngShow=application.union(c, rngShow)
End If
End If
End If
Next b
If Not rngShow is Nothing Then rngShow.EntireRow.Hidden = False
ws.Protect Password:="Test" 'reprotect...
You might also want to toggle Application.Calculation = xlCalculationManual and Application.Calculation = xlCalculationAutomatic
You can also try moving your Application.Screenupdating code out of the loop, it's going to update for every sheet as written.
Private Sub CommandButton1_Click()
Dim ws As Worksheet
Application.ScreenUpdating = False ''<- Here
DoNotInclude = "PassDB"
For Each ws In ActiveWorkbook.Worksheets
If InStr(DoNotInclude, ws.Name) = 0 Then
Call Module1.Hide(ws)
End If
Next ws
Application.ScreenUpdating = True ''<- Here
End Sub