This question already has answers here:
Collection Object - ByRef - ByVal
(1 answer)
Do I need to pass a worksheet as ByRef or ByVal?
(1 answer)
Closed 1 year ago.
Private Sub Worksheet_Change(ByVal Target As Range)
End Sub
On the internet byRef and byVal are differentiated by saying byRef points to a reference and byVal creates a new reference and copies the values from the original. So when you change the passed variable it does not effect the original variable.
But if this is accurate why are the worksheet events such as change and selection_change use byVal. Wouldn't that mean the code should not be able to manipulate the values of the ranges that are being selected. After all byVal should not be able to change the original values only the copies it creates. Yet if you write something like,
Private Sub Worksheet_Change(ByVal Target As Range)
Target.Font.Color = VBA.ColorConstants.vbBlue
End Sub
Anything written in the selected range will actually change their color. So what is really going on here?
You must know that ByVal still passes references. Even if you get a copy to a referenced object.
Inside of the procedure/event called ByVal you can use the referenced object and change its properties. Such a simple event, works based on what I stated in my first item:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Target.Interior.Color = vbYellow
End Sub
You practically work with the same object instance (in memory). Only the reference is copied. You can use it inside the sub called ByVal.
The behavior of the variable acts as you tried stating when you use a custom variable and expect to afect somehow (or not) the initial variable value but after calling other Subs handling it.
Please, look to the next example and see how the initial variable is changed in case of the two different processing Subs:
Sub testByValSub()
Dim x As String
x = "myString"
Debug.Print x
changeStrBV x
Debug.Print "After ByVal: " & x
changeStrBR x
Debug.Print "After ByRef: " & x
End Sub
Sub changeStrBV(ByVal x)
x = x & " and something else"
'do whatever you want with x, but only here...
End Sub
Sub changeStrBR(ByRef x)
x = x & " and something else"
End Sub
Related
When I call a sub and pass a variable, after the sub ends the passed variable is changed. How do I make it not do that?
So in the example the debug.print should be 3 not 8
sub main()
i = 3
SomeSub i
debug.print i
end sub
sub SomeSub(j)
j=j+5
end sub
Just use the ByVal keyword to pass on the value
Sub SomeSub(ByVal j As Long)
j = j + 5
End Sub
Further reading
Using ByRef and ByVal
When we pass a simple variable to a procedure we can pass using ByRef
or ByVal.
ByRef means we are passing the address of the variable. If the
variable changes in the procedure the original will also be changed.
ByVal means we are creating a copy of the variable. If the variable
changes in the procedure the original will not be changed.
I want to find a way to dynamically add hyperlinks to my Excel-Sheet and run macros depending on some cell contents. But neither the HYPERLINK-formula nor the regular hyperlink feature in Excel allow you to call macros directly from the worksheet. Looking for that problem online will always retrieve the option to use the Worksheet_FollowHyperlink event. But for my purpose this option is not suitable as you either have to write your macro to like "if target.range.address = A1 call macroA elseif target.cell = A2 call macro ...." etc... This solution is way too static in my opinion as you have to "hardwire" too much in your Worksheet_FollowHyperlink code. Furthermore you have to prepare the hyperlinks via VBA to change the address and subaddress to "" to avoid unwanted selection changes or error popups from excel (because some adress could not be found).
The =HYPERLINK()-formula looks way more interesting since you can dynamically create it wherever and whenever needed. It also works fine as a column-function inside a table which is what I actually want to do: Have a column filled with hyperlinks inside a table that will run macros with some given parameters depending on the other contents in each table data row. This would not work with regular hyperlinks at all as the user has to copy & paste them manually into every single row.
Sadly the =HYPERLINK()-formula also offers no option to run a macro directly with the given parameters (at least none that I could find). It will not even fire the Worksheet_FollowHyperlink event so it appears to be a dead end at this point.
Interesting feature I found during my trial and error + internet research:
=HYPERLINK("#TestMe", "Some text here...") will open the VBA-editor and jump directly to my TestMe() sub. Yet it will not be called!
What could be the solution to this problem?
Create Hyperlinks dynamically in a table data column
Call a macro depending on the data row contents
I had the idea to use the Workbook_SheetSelectionChange event to monitor if a cell with a HYPERLINK-formula was selected and it turned out very well.
First revision of my code:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
Dim MacroName As String
If Target.Cells.Count > 1 Then Exit Sub
If Target.Formula Like "=HYPERLINK(LEFT(""|""*""|"",*),*)" Then
MacroName = Split(Target.Formula, """|""")(1)
MacroName = VBA.Trim(Replace(MacroName, "&", ""))
MacroName = Sh.Evaluate(MacroName)
Application.Run Macro
End If
End Sub
It requires to have a cell with the following formula:
=HYPERLINK(LEFT("|" & A1 & "|", 0), "Run Macro in A18") where cell A1 contains the name of some macro I want to run. The name of the macro could also be hardwired in the formula.
Note: the LEFT(..., 0) part is needed so the address of the hyperlink will appear empty to excel when clicking it. Otherwise it will bother you with an error popup for not finding the target.
Unfortunately the SelectionChange event also fires when selecting a cell with return-key, tab-key or arrow keys. To filter these out, you will need the following API-call:
Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vkey As Integer) As Boolean
This function checks if a key is pressed at the moment it gets called.
Source is this unresolved question: How to run code when clicking a cell?
The next evolution of the code above now looks like this:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
If GetAsyncKeyState(vbKeyTab) _
Or GetAsyncKeyState(vbKeyReturn) _
Or GetAsyncKeyState(vbKeyDown) _
Or GetAsyncKeyState(vbKeyUp) _
Or GetAsyncKeyState(vbKeyLeft) _
Or GetAsyncKeyState(vbKeyRight) _
Or Target.Cells.Count > 1 _
Or VBA.TypeName(Sh) <> "Worksheet" _
Then Exit Sub
Dim Macro As String
If Target.Formula Like "=HYPERLINK(LEFT(""|""*""|"",*),*)" Then
Macro = Split(Target.Formula, """|""")(1)
Macro = VBA.Trim(Replace(Macro, "&", ""))
Macro = Sh.Evaluate(Macro)
Application.Run Macro
End If
End Sub
This now will filter out all selection changes done by key commands.
Yet there is one more step to take as I had to notice there seems to be a flaw when changing a cell above or left of my hyperlink and hit return key or tab key. For some reason the GetAsyncKeyState will return false for both keys so my code would continue to run.
So for these situations I had to create a little dirty work around. You will need the Workbook_SheetChange event to set a switch which temporarily disables the Workbook_SheetSelectionChange event.
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
RecentSheetChange = True
Application.OnTime VBA.DateAdd("s", 0.1, Now), "ResetRecentSheetChange"
End Sub
'Code inside a new module:
Option Explicit
Option Private Module
Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vkey As Integer) As Boolean
Public RecentSheetChange As Boolean
Private Sub ResetRecentSheetChange()
RecentSheetChange = False
End Sub
The final code in ThisWorkbook now looks like this:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
If GetAsyncKeyState(vbKeyTab) _
Or GetAsyncKeyState(vbKeyReturn) _
Or GetAsyncKeyState(vbKeyDown) _
Or GetAsyncKeyState(vbKeyUp) _
Or GetAsyncKeyState(vbKeyLeft) _
Or GetAsyncKeyState(vbKeyRight) _
Or Target.Cells.Count > 1 _
Or VBA.TypeName(Sh) <> "Worksheet" _
Or RecentSheetChange _
Then Exit Sub
Dim Macro As String
If Target.Formula Like "=HYPERLINK(LEFT(""|""*""|"",*),*)" Then
Macro = Split(Target.Formula, """|""")(1)
Macro = VBA.Trim(Replace(Macro, "&", ""))
Macro = Sh.Evaluate(Macro)
Application.Run Macro
End If
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
RecentSheetChange = True
Application.OnTime VBA.DateAdd("s", 0.1, Now), "ResetRecentSheetChange"
End Sub
Adding parameter features to the hyperlink is only a small step from here.
Your thoughts?
I've simplified my question to make it as generic as possible.
Say I would like to create a Sub, which takes a collection, and a datatype of objects expected to be contained in that collection, and prints a specified parameter to the immediate window. The sub would need to take as inputs a collection (e.g. ThisWorkbook.Worksheets), a datatype (e.g. Worksheet), and I would think optionally a property (e.g. Name)
The sub would look something like:
Sub PrintMembers(ByVal myCol as Collection, ByVal datatype as ????, ByVal property as ????)
dim myObj as datatype?
for each in myCol
debug.print myObj.property?
next
End Sub
Which I could then call with:
Call PrintMembers(ThisWorkbook.Worksheets, Worksheet, "Name")
or something along those lines, which would output:
Sheet1
Sheet2
Sheet3
I'm not exactly sure what you're asking but it seems as though you might be looking for the CallByName() function:
Public Sub RunMe()
PrintMembers ThisWorkbook.Worksheets, "Name"
End Sub
Private Sub PrintMembers(col As Object, prop As String)
Dim item As Object
For Each item In col
Debug.Print CallByName(item, prop, VbGet)
Next
End Sub
So I'm pretty familiar with referencing worksheet ranges for worksheet events such as double click. In this case though, I'm looking to reference when the row header gets double clicked instead of a cell. It would still be specific to a worksheet but I've been unsuccessful thus far.
I have multiple ranges that do different events on double clicks so I use code similar to the example below:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Dim rWatchRange As Range
Dim sWatchRange As Range
Set rWatchRange = Range("A5:A1000")
'I somehow need it to recognize if the row header is
'double clicked between row 5 and 1000 to fire off the second sub
Set sWatchRange = Range("5:1000")
If Not Application.Intersect(Target, rWatchRange) Is Nothing Then
Run "aFormattingSub"
End If
If Not Application.Intersect(Target, sWatchRange) Is Nothing Then
Run "aSubToInsertNewLineAndGroupWithRowAbove"
End If
End Sub
I'm not sure if there is a worksheet reference, application reference or a setting in Excel that I'm aware of that can do this.
The DoubleClick Event does not fire when the headers are doubleclicked. I don't think there is any trivial way around this - you have to live with the events as they are provided.
I think there are still enough room to implement more functionality.
To give you some more ideas, you could do different things on a double click or right click with ctrl held down.
An example that reacts to the right click with ctrl held down, and only when entire rows are selected:
Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
If (GetKeyState(KeyCodeConstants.vbKeyControl) And &H8000) And _
Selection.Address = Selection.EntireRow.Address Then
Cancel = True
' ... code
End If
End Sub
(the And &H8000 is necessary to react only to currently present and ignore previous keypresses)
Import the API function in a module:
Public Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
Im having trouble takign my assigned variable and offseting it. What am I doing wrong?
Public Sub SampleBox_Change()
Dim str As Integer
If (SampleBox.ListIndex > -1) Then
str = SampleBox.List(SampleBox.ListIndex)
End If
End Sub
Public Sub Samplesdel(str As Integer)
Range(Range("BA1").EntireColumn, Range("BA1").Offset(0, -str).EntireColumn).Select
End Sub
Public Sub CommandButton1_Click()
Application.Run "Samplesdel"
End Sub
So the variable (str) is a whole number. I would like to use this number to select a certain number of columns (from BA1 to "left however many columns valued as str"). so if the user selects 8 i would like to select BA1 and left 8 columns.
The combobox is in a userform where the code for the assigned variable is set.
I would like to use the assigned variable in a macro (where i used the select function).
so the variable str gets assigned from a userform combobox. I would then like to pass this variable to a macro where i use it in the offset function.
I just used your snippets of code here, I'll assume you know what you're trying to accomplish with this. As mentioned in another answer, I think it's an issue of scope; it doesn't seem like there's a good reason you can't combine your statements as follows:
Public Sub SampleBox_Change()
Dim str As Integer
If (SampleBox.ListIndex > -1) Then
Range(Range("BA1").EntireColumn, Range("BA1").Offset(0, SampleBox.ListIndex).EntireColumn).Select
End If
End Sub
To add some unsolicited feedback, naming a variable that is an integer "str" will be very confusing to anyone else that has to read your code, the name "str" implies "string", a different data type.
James, you are setting the value of str here correct?
Public Sub SampleBox_Change()
Dim str As Integer
If (SampleBox.ListIndex > -1) Then
str = SampleBox.List(SampleBox.ListIndex)
End If
End Sub
The scope for str will only be this function and it will not be able to be accessed by any other function.
What value are you passing to Samplesdel as the parameter?
Application.Run "Samplesdel"