Excel VBA: Click two buttons (shapes) that run two separate VBA scripts on spreadsheet open - vba

I have a spreadsheet completely locked down and control all sorting and filtering through VBA. I also have another script that hides the sheet on close and saves the file automatically to keep that sheet hidden.
I've been trying to figure out how I can use VBA to 'click' on one button (shape) which would clear anything that's been filtered and then 'click' on another button (shape) which would sort the spreadsheet alphabetically. The buttons (shapes) already work perfectly with user-interaction but I would also like these buttons (shapes) to automatically get triggered when the sheet is opened.
The first button is assigned to macro, SearchBox, associated with the following VBA -
Sub SearchBox()
Dim myButton As OptionButton
Dim SearchString As String
Dim ButtonName As String
Dim sht As Worksheet
Dim myField As Long
Dim DataRange As Range
Dim mySearch As Variant
Set sht = ActiveSheet
On Error Resume Next
sht.ShowAllData
On Error GoTo 0
Set DataRange = sht.ListObjects("DataTable").Range
mySearch = sht.Shapes("UserSearch").TextFrame.Characters.Text
If IsNumeric(mySearch) = True Then
SearchString = "=" & mySearch
Else
SearchString = "=*" & mySearch & "*"
End If
For Each myButton In sht.OptionButtons
If myButton.Value = 1 Then
ButtonName = myButton.Text
Exit For
End If
Next myButton
myField = Application.WorksheetFunction.Match(ButtonName, DataRange.Rows(1), 0)
DataRange.AutoFilter _
Field:=myField, _
Criteria1:=SearchString, _
Operator:=xlAnd
sht.Shapes("UserSearch").TextFrame.Characters.Text = ""
End Sub
The second one is much more simple which just sorts the data table by that specific column -
Sub Sort_Name()
Dim oneRange As Range
Dim aCell As Range
Set oneRange = Range("A4:H1162")
Set aCell = Range("A4")
oneRange.Sort Key1:=aCell, Order1:=xlAscending, Header:=xlYes
End Sub
Basically, I'm still learning and I feel it's possible to just trigger these buttons with a script but I've yet to figure it out. Any help would be appreciated.

In the code-behind for ThisWorkbook, you will be able to handle workbook events, including the Open event, which is fired by the workbook when it is opened.
Navigate to the module (double-click ThisWorkbook in the VBE's project explorer [Ctrl+R]), then locate the dropdowns at the top of the editor's code pane. From the left-hand dropdown, select Workbook; then, from the right-hand dropdown, select the Open event; the VBE automatically creates a method stub with the correct signature for you:
Private Sub Workbook_Open()
End Sub
Notice that the underscore has a very special meaning in VBA; when naming your procedures (especially implicitly or explicitly Public ones), consistently stick to PascalCase and avoid Snake_Case; this may not matter now, but as you progress as a developer you'll come to appreciate consistency in naming, and when you start working with interfaces, that underscore-in-public-members thing will start making the difference between code that works and code that doesn't even compile: taking the good habits early will save you headaches later.
Now, you need to invoke two procedures in that handler.
When you do this:
oneRange.Sort Key1:=aCell, Order1:=xlAscending, Header:=xlYes
You're invoking the Sort method of the oneRange object, which is an instance of the Range class.
When you do this:
MsgBox "Hi!"
You're invoking the MsgBox function that's in the VBA library, under the Interaction module (find it in the object browser [F2]). This would be equivalent:
VBA.Interaction.MsgBox "Hi!"
So, to invoke your SearchBox and then your SortName method, all you need to do is this:
Private Sub Workbook_Open()
SearchBox
SortName 'formerly known as Sort_Name
End Sub
Procedures do something - their names should always start with a verb, they're actions. "SearchBox" looks like a name, not an action. In programming, names are more like classes - a Range, a Workbook, etc.; consider renaming SearchBox to better convey what it does. If that's hard to do, it's likely because your procedure does too many things - like getting the name to use for filtering, and then applying a filter to a specific table, and then clearing the text of some shape.
You'll also want to watch out for implicit ActiveSheet references; currently SortName is assuming what the ActiveSheet is, and this is very likely to cause issues down the line.

So the best way to explain how I did what I was wanting to do is to look at what the button itself is calling to by right clicking it and clicking assign macro again. You'll see it look like this -
'YourSpreadSheetName.xlsm'!Sheet2.SearchBox
For me, what was throwing me off is I wasn't including the Sheet2 which is where the code is I'm trying to call.
By placing this code within the ThisWorkbook section with the Workbook_Open script, I was able to get it working the way I wanted it to -
Call Sheet2.SearchBox
Thanks again for the help all who commented.

Related

VBA with if case for checkbox in excel

I am having an userform where I have 8 Checkboxes in it.
Each checkbox is assigned to an call function called autofilter.
I would like to have an vba,in such a way that more than one Checkbox is used, then it should Display the result of selected Checkbox.
How can I achieve in VBA. I am struck how i should proceed with this Problem.
Expecting an help from Forum.
This is my autofilter program
Sub autofilter()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("Result")
wslr = ws.Cells(Rows.Count, 1).End(xlUp).Row
Set myfilt = ws.Range("A1:AFU" & wslr)
myfilt.autofilter Field:=12, Criteria1:= _
"USA"
End Sub
similarly, i have them for other Locations as well till autofilter7.
Right now, i have the code working in such a way that, if check box 1 is true it calls autofilter1.
I would like to have a VBA, in such a way that, when i select 1 or more checkboxes, it should call their autofilter function together. How can i achieve this ?
[![I have userform with Checkboxes designed like this.in the command button i have the following code,
If CheckBox1.Value = True Then
Call autofilter
End If
similarly, I have it same for other checkboxes.
]1]1
Difficult to answer without all the exact details, but I think you are looking for something like:
In the command button _Click sub code, you should have this:
Edited : notice Dim i as String at the top.
Dim formControl As Control
Dim i As String
'loop through every control in the userform
For Each formControl In Me.Controls
'Test if the control is a checkbox
If LCase(TypeName(formControl)) = "checkbox" Then
If formControl.Value = True Then
'The below is very crude and you should find a better way of getting parameter from checkbox
'The below also assumes you use ONE filterFunction that takes a parameter
'You need to get the number from the checkbox, so take the number from the name of the checkbox
i = Right(formControl.Name, 1) - 1
'myFilterFunction i (Use this only if you have parameterised your function)
'change i to empty string if it was 0.
i = IIf(i = 0, "", i)
'This calls a function represented by the string
Application.Run "myFilterFunction" & i
End If
End If
Next formControl
At the moment, the away you've describe it, the code should work. Replace the name of the function with the name of your autofilter function....

Use VLOOKUP to pass cell reference to a public variable?

I have a userform that opens on cell change in a column.
That userform contains checkboxes, which all trigger a second userform with a text box which looks up a cell on a hidden sheet for its contents. (The checkbox that's ticked determines which cell the textbox looks for). The user then edits the box, clicks a button, and the new text is written back to the same cell.
This is the VBA for when the checkbox is ticked. It works great. Hooray!
Dim vln As Variant
Dim reta As Worksheet
Set reta = ActiveWorkbook.Sheets("RetailerActivity")
Set vln = ActiveCell.Offset(-1, -3)
UserForm2.TextBox1.Text = Application.WorksheetFunction.VLookup(vln, reta.Range("A1:Z100"), 3, False)
UserForm2.TescoSave.Visible = True
UserForm2.Show
End Sub
When the textbox has been edited, I would like to write it back to the same cell it came from. I figure the easiest way to do that is to have a public variable (as range), and to pass the result of the vlookup into that variable so the second userform can have a line which reads
Private Sub ASave_Click()
publicvariable.Value = TextBox1.Value
userform1.hide
End Sub
Nice and easy, rather than doing a VLookup again. Right?
Either way, I can't seem to set the public variable as the lookup.
Outside of any sub I have
Public bums As Range
And in the code above, after the bit where I've set the text box, I've tried to add the line
Set bums = Application.WorksheetFunction.VLookup(vln, reta.Range("A1:Z100"), 3, False)
But the code errors with a "type mismatch".
If I try
Set bums = Range(Application.WorksheetFunction.VLookup(vln, reta.Range("A1:Z100"), 3, False))
I get method "Range" of object "_global" failed.
I code by cobbling bits off the internet, as you can probably tell, so this is I don't doubt a complete kludge.
Any advice would be super appreciated.
VLookup returns a value, not a Range. You could use Match to find the row and then Cells to get the actual reference - for example:
Dim vMatch
vMatch = Application.Match(vln, reta.Range("A1:A100"),0)
If Not IsError(vMatch) then
Set bums = reta.Cells(vMatch, "C")
else
msgbox "No match for " & vln
Exit Sub
End If
Personally I would also not use a public variable, but create a property for Userform2 to which you can assign the range.

Macro launching when a cell value changes due to a formula not by the user

I would like my Macro to launch whenever a value in a cell containing a formula changes.
i.e. the user is modifying another cell thus changing the value of the cell in question.
I have noticed that using the statement (found herein), only works if the user modifies the cell itself but not if the cell changes automatically - due to a formula as specified above.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A20")) Is Nothing Then ...
Any thoughts??
I tried to follow the answers from this question "automatically execute an Excel macro on a cell change" but it did not work...
Thanks in advance :)
A possible work-around comes from the fact that, to change a value, the user needs to change the selection first. So I would:
1) Declare a global variable called "oldValue" on top of the WS source code module:
Dim oldValue As Variant
2) Register the old value of your formula before the user types anything (let's say it's in Range("A4"), I let you adapt with the others):
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
oldValue = Range("A4")
End Sub
3) Check if the change has affected the formula in the Change event:
Private Sub Worksheet_Change(ByVal Target As Range)
If Range("A4") <> oldValue Then
MsgBox "User action has affected your formula"
End If
End Sub
I've tested with a simple sum, I'm able to write cells that are not involved without any prompt but if I touch one of the cells involved in the sum the MsgBox will show up. I let you adapt for multiple cases, for user adding/removing rows (in that case I suggest to name the ranges containing the formulas you want to track) and the worksheet references.
EDIT I'd like to do it at once, not by going through 2 processes, is it possible? The problem is my macro involves a range containing more than one cell so it will be hard to store old values for 10 cells.
If ranges are next to each other, then instead of using a variable you can use a collection:
Dim oldValues As New Collection
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
For j = 1 To 10
oldValues.Add Range("A" & j).Value
Next j
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
For j = 1 To 10
If Range("A" & j).Value <> oldValues(j) Then
MsgBox "The value of Range(A" & j & ") has changed"
End If
Next j
End Sub
Of course, if ranges are not close to each other, you can just store them anyway in the SelectionChange event like this:
oldValues.Add Range("A1").Value
oldValues.Add Range("B7").Value
'...
and if you done this ONCE, with 10 ranges only, it should be a reasonable solution to your problem.
You said, "I would like my Macro to launch whenever a value in a cell containing a formula changes..."
If having your code run whenever a cell containing a formula is recalculated (which is not exactly what you asked for), one solution might be to create a VBA function that simply returns that value passed to it, plus does whatever else you want to do when the formula is recalculated...
Public Function Hook(ByVal vValue As Variant) As Variant
Hook = vValue
' Add your code here...
End Function
...then "wrap" your formula in a call to this function. For example, if the formula you are interested in is =A1+1, you would change this to =Hook(A1+1), and the Hook function would be called whenever A1+1 is recalculated (for example, when the value in A1 changes). However, it is possible that recalculating A1+1 will yield the same result and still call the Hook function (for example, if the user re-enters the same value in A1).
You can have a go at this:
First, in a Module Code declare a Public Variable.
Public r As Range, myVal '<~~ Place it in Module
Second, initialize your variables in Workbook_Open event.
Private Sub Workbook_Open()
Set r = Sheet1.Range("C2:C3") '<~~ Change to your actual sheet and range
myVal = Application.Transpose(r)
End Sub
Finally, set up your Worksheet_Calculate event.
Private Sub Worksheet_Calculate()
On Error GoTo halt
With Application
.EnableEvents = False
If Join(myVal) <> Join(.Transpose(r)) Then
MsgBox "Something changed in your range"
'~~> You put your cool stuff here
End If
myVal = .Transpose(r)
forward:
.EnableEvents = True
End With
Exit Sub
halt:
MsgBox "Error " & Err.Number & ": " & Err.Description
Resume forward
End Sub
Above will trigger the event when values in C2:C3 changes.
Not really very neat but works in detecting changes in your target range. HTH.
Declaring a module -level variable like Matteo describes is definitely one good way to go.
Brian 's answer is on the right track with regards to keeping all is the code in the same place, but it's missing one critical part : Application.Caller
When used in function that is called by a single cell, Application.Caller will return the Range object of that cell. This way you can store the old value within the function itself when it is called, then once you're done with calculating the new value you can compare it with the old and run more code as required.
Edit: The advantage with Application.Caller is that the solution scales in and of itself, and does not change no matter how the target cells are arranged (I.e. Continuous or not).

Return different values from a vba user form depending on the button pressed

I have been creating an acronym finding macro that will sit on a custom toolbar in word. When run it searches the document for acronyms and places them in a table. I want to include some user forms so that as the macro finds an acronym the user can select the predefined definition (got from an excel document) or enter their own new one (I know multiple acronyms meanings is frowned upon but it happens).
Anyway I am stuck. I have created a user form with three buttons. A text input and a label. Now I have managed to set the label text with the acronym that was found however I can't seem to get the buttons to change a variable, userChoice, and if applicable save the newly entered definition.
below is the test macro i have been trying this out on
Sub userFormTest()
Dim objExcel As Object
Dim objWbk As Object
Dim rngSearch As Object
Dim rngFound As Object
Dim targetCellValue As String
Dim userChoice As Integer
Set objDoc = ActiveDocument
Set objExcel = CreateObject("Excel.Application")
Set objWbk = objExcel.Workbooks.Open("C:\Users\Dave\Documents\Test_Definitions.xlsx")
objExcel.Visible = True
objWbk.Activate
With objWbk.Sheets("Sheet1")
Set rngSearch = .Range(.Range("A1"), .Range("A" & .Rows.Count).End(-4162))
Set rngFound = rngSearch.Find(What:="AA", After:=.Range("A1"), LookAt:=1)
If rngFound Is Nothing Then
UserForm1.Label1.Caption = "Acronym: AA" & vbCr & _
"Definition: Not found, please enter a definition below" & vbCr & _
" or choose to ignore this acronym"
UserForm1.Show
'an if statement here so that if the add button was pressed it adds to doc etc
Else
targetCellValue = .Cells(rngFound.Row, 2).Value
UserForm2.Label1.Caption = "Acronym: AA" & vbCr & _
"Definition: " & targetCellValue
UserForm2.Show
'an if statement here so that if the add button was pressed it adds to doc etc
End If
End With
objWbk.Close Saved = True
Set rngFound = Nothing
Set rngSearch = Nothing
Set objWbk = Nothing
objExcel.Visible = True
Set objExcel = Nothing
Set objDoc = Nothing
End Sub
I do realise that this could be done in the button_click() subs however I already have all the documents open etc in the other macro. Or is it possible to link to those already open documents? To be honest either way I would prefer to return to the main macro and just use the form to the user input.
I do realise that this could be done in the button_click() subs. However I already have all the documents open etc in the other macro. Or is it possible to link to those already open documents?
You can definitely link between button_click() subs and your main macro to modify the value of userChoice.
Answer:
What you need is some userform element (like a textbox) that can hold your value so you can refer back to it in your main macro. It looks like you already have this element (based upon your caption "Not found, please enter a definition below"). Let's say that element is a TextBox called Definition. Then let's say you want to return to the main macro after people push an "Add" button, as it appears you do (based upon your comment "so that if the add button was pressed it adds to doc").
In each of both Userform1 and Userform2, you would want something like this:
Private Sub AddButton_Click()
Userform1/Userform2.Hide
End Sub
That would return you to your main macro, where you could follow up with:
If Userform1/Userform2.Definition.Value = Whatever Then
'if the add button was pressed it adds to doc etc
End If
right where your existing comments are. Note that you could set userChoice = Userform1.Definition.Value here, but you don't need to because Userform1.Definition.Value already contains the information you need to track.
Additional material:
Rather than using the default instance of your Userform1 and Userform2 by using .Show on them immediately without assigning new instances of them to variables, may I suggest creating Userform variables to contain New instances of them like this:
Dim UnknownDefinition As Userform1
Set UnknownDefinition = New Userform1
UnknownDefinition.Show
Or if you want to get really optimal, you could follow more of the approach recommended here on how to make a properly instanced, abstracted userform:
Rubberduck VBA: How to create a properly instanced, abstracted userform
And now with bonus quotes from the post's author, #Mathieu Guindon:
make Definition a proper property, with the Property Let mutator changing the label value on top of changing its private backing field's value; avoid accessing form controls outside the form, treat them as private even if VBA makes them public.
Calling code wants data, not controls. You can extract properties/data, but not controls, into a dedicated model class.

Why does Excel VBA prompt me for a Macro name when I press Run Sub

I have the following code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim RR As Range
Dim TestArea As Range
Dim foremenList As Range
Dim workerList As Range
Dim workers As Range
Dim Foremen As Range
Dim i As Integer
Dim R As Range
Dim EmplList() As Variant
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
Set foremenList = Sheet90.Range("V24:V30")
Set RR = Sheet90.Range("AA25:AA46")
i = 0
For Each R In RR.Cells
If Len(R.Value) > 0 Then
EmplList(i) = R.Value
i = i + 1
End If
Next R
Dim ValidStr As String
Set ValidStr = Join(EmplList, ",")
With Sheet90.Range("b26").Validation
.Delete
.Add xlValidateList, xlValidAlertStop, _
xlBetween, "1,2,3"
End With
Sheet90.Range("b40").Value = "Test"
End Sub
But when I press run to test it, it prompts me for a macro name.
Additionally, it does not trigger on Worksheet_Changeany more.
Is this an error (i.e. I forgot a semicolon or something) that consistently triggers Excel VBA to behave like this? If so, what should I look for in the future?
The reason you can't run this one with the Run Sub button is because it requires a parameter. If you want to run this standalone, one possibility is to run it in the Immediate Window so you can manually pass in the parameter. Since this one is expecting a more complex data type (range) you may want to create a small sub to call it so that you can properly create your range and pass that in. Then you can use the Run Sub on this sub which will call your other one.
As far is it not triggering on Worksheet_Change, I am not able to tell what is causing it just from what you posted. However, you do need to make sure that it is located on the code page for the worksheet you are trying to run it from. If you need the same one to run from multiple sheets, you should put it into a module and call it from each sheet's Worksheet_Change method.
You can't press F5 or the run button to run triggered code. You would have to make a change in the sheet where this code is located in order for the code to run. Also, if this code is not located in Sheet90, then you won't see anything happen because this code only makes changes to Sheet90. Lastly, to make sure events are enabled, you can run this bit of code:
Sub ReEnable_Events()
Application.EnableEvents = True
End Sub
Note that you will still have to enable macros.
The problem stems from two lines:
Set ValidStr = Join(EmplList, ",")
was not a valid use of the Set keyword (It's a string and not an object), and
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
apparently has too many arguments.
According to Microsoft, it should be a single string argument like:
Set TestArea = Sheet90.Range("b4:q8, b15:q19, b26:q30")
Commenting both of these out made the code run fine both with the run sub button, and on the event.
The "Name Macro" dialog is some kind of error indicator, but I still don't know what it means, other than Code Borked