Excel VBA: Pass `Target` Variable from Click Event to Userform - excel

I am building an Excel 2016 Userform using VBA and need to collect the row and column of the cell from which the form is opened. I open the form on a cell double click with Worksheet_BeforeDoubleClick and then initialize the Userform with UserForm_Initialize(). I would like to pass the Target of the double click event to UserForm_Initialize() but am not sure how to. This forum thread addresses this issue, but the provided solutions did not work for me.
Here is my Worksheet_BeforeDoubleClick:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Column = Target.Column
Row = Target.Row
'Find the last non-blank cell in column B(2)
lRow = Cells(Rows.Count, 2).End(xlDown).Row
'Find the last non-blank cell in row 2
lCol = Cells(2, Columns.Count).End(xlToRight).Column
If Not Intersect(Target, Range(Cells(3, 3), Cells(lRow, lCol))) Is Nothing Then
Cancel = True
EdgeEntryForm.Show
End If
End Sub
And my UserForm_Initialize():
Private Sub UserForm_Initialize()
Dim Column As Long, Row As Long 'I would like to fill these with the Target values
MsgBox ("Row is " & Row & " Column is " & Column)
'Description.Caption = "Fill out this form to define a network edge from " & Cells(2, Row).Value & " to " & Cells(Column, 2).Value
End Sub

As suggested in my comments, one way would be to just use the ActiveCell and assign that to a variable.
Alternatively, if you do want to pass it as a variable, you can do it with a bit of a workaround, by having a global variable to temporarly hold that information:
In your worksheet code:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
'.....
With UserForm1
Set .rngTarget = Target
.Show
End With
'.....
End Sub
In your userform:
Public rngTarget As Range
Private Sub UserForm_Activate()
'....
If Not rngTarget Is Nothing Then
MsgBox ("Row is " & rngTarget.Row & " Column is " & rngTarget.Column)
Else
MsgBox "something went wrong with assigning rngTarget variable"
End If
'....
End Sub
EDIT: I was trying initially to propose something similar to #MathieuGuindon's answer, but was failing due to my limited knowledge on the difference between initialise and activate (thanks Mathieu).
I've updated the answer to make use of the global variable at userform level, rather than use one from a module.

The form is shown modally, so ActiveCell isn't going to change on you, and should be safe to use in the form's code-behind.
The problem with that, is that you've now tied the form to ActiveSheet/ActiveCell, and now in order to test anything you need to Select or Activate a cell.
If the form code only needs to know about the cell's Address, then it shouldn't be given a Range (give it a Range and it can access any cell in any sheet in any workbook in the Application instance) - that's the principle of least knowledge at play. But this is obviously example code, so let's go with a Range:
Option Explicit
Private internalWorkingCell As Range
Public Property Get WorkingCell() As Range
Set WorkingCell = internalWorkingCell
End Property
Public Property Set WorkingCell(ByVal value As Range)
Set internalWorkingCell = value
End Property
Now your form code can use WorkingCell or internalWorkingCell to do its thing, and no global variable needs to float around;
With New UserForm1 ' Initialize handler runs here
Set .WorkingCell = Target
.Show ' Activate handler runs here
End With
The WorkingCell belongs to the form - it has no business being in global scope.
Careful with the Initialize handler in forms - especially when you use its default instance (i.e. when you don't New it up): you don't control when that handler runs, the VBA runtime does; UserForm_Initialize will run the first time the form instance is referenced (in your case, immediately before the .Show call), and then never again unless the instance is destroyed (clicking the red X button would do that).
A MsgBox call in the Initialize handler will run before the form is shown; you probably want to move that code to the Activate handler before it causes problems.

Related

How to trigger an event by a change to a listobject?

Excel 365.
When the user changes the value of a cell in a certain column of my Excel table (Listobject), I can use the Worksheet_Change event to trigger more code. I would use something like:
If Not Intersect(Target, Listobjects(1).listcolumns(2).DataBodyRange) Is Nothing Then
...to tell that one of these cells was changed. But how do I tell which cell it was?
On a releated note: Is there a way for Worksheet_Change to tell when a new row or column is added to the Listobject?
Sorry in advance if I misunderstood what you asked.
As Target.Address returns its cell location on the sheet, you can translate it to context of listobject by offsetting it with the location of first cell of the table
With me.Listobjects(1)
Debug.Print .DataBodyRange(Target.Row - .Range.Row + 1, _
Target.Column - .Range.column + 1).Address(0,0)
End with
secondly, if you can store the information of the initial table to some variables when opening the workbook, then you can compare the information every time workbook_change event takes place.
P/S: it is quite risky to leave a sheet that already has worksheet event macros in place like this to be unprotected and changed without restriction.
In a Module,
Dim start_LROW&, start_LCOL& 'Variable is declared first
Sub run_when_Open()
With sheet1.ListObjects(1)
start_LROW = .ListRows.Count
start_LCOL = .ListColumns.Count
End With
End Sub
in Workbook_Open event under ThisWorkbook module,
Private Sub Workbook_Open()
Call Module1.run_when_OPEN
End Sub
in Workbook_Change event under Sheet module,
Private Sub Worksheet_Change(ByVal Target As Range)
With Me.ListObjects(1)
If Not Intersect(Target, .DataBodyRange) is Nothing Then
If .ListRows.Count <> start_LROW Or _
.ListColumns.Count <> start_LCOL Then
Debug.Print "changed" 'Trigger some codes
start_LROW = .ListRows.Count 'update the new information to be compared for next trigger.
start_LCOL = .ListColumns.Count
End If
End If
End With
End Sub

Excel VBA add-in, is Worksheet_SelectionChange possible?

I have an add-in that creates a report on a file, the report has a pivot table like this:
The above is now sorted on the leftmost column. To make it easier for the user I want to add a Worksheet_SelectionChange event code that if I click on a cell with "Kolli" or "Vikt" in this range visible in the image sort the table on this column.
The sorting is not the issue, but can a add-in file notice selection change?
I would need something like a ACTIVEsheet_SelectionChange, and then read the Target so that it's the correct workbook, sheet and range.
Is that even possible? Or do I need to somehow write the code in the "target" workbook worksheet?
Please, try the next way:
Put the next declaration on top of the add-in ThisWorkbook code module (in the declarations area):
Public WithEvents appEvHandler As Application
Put in Workbook_Open event the next code, to activate the event handler:
Private Sub Workbook_Open()
Set appEvHandler = Application 'this code line can be placed in any standard event, when need to activate the `appEvHandler` events
End Sub
Copy this new event code in ThisWorkbook code module:
Private Sub appEvHandler_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
If Sh.Parent.Name <> ThisWorkbook.Name Then
MsgBox Sh.Parent.Name & " workbook, sheet " & Sh.Name & " changed selection to " & Target.Address & "..."
Else
Debug.Print "Selection changed in this workbook..."
End If
End Sub
You can also filter the sheet (name) where the event to do something, on a similar mechanism.
I could also suggest the specific event code, but I could not understand what "I click on a cell with "Kolli" or "Vikt" in this range visible" does mean. No rows headers, I cannot understand which range to be the one triggering what you need... I mean to restrict the range where the event to be used. Is that row part of the table header? Anyhow, this part should be easy to handle, I think.
In case anyone else is looking for a code to sort a pivot table on column header when you click on it this is the code I use now thanks to FaneDuru.
Private Sub appEvHandler_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
If Sh.Parent.Name <> ThisWorkbook.Name Then
If Sh.Name = "Resultat" And Sh.Range("B3").Value = "Plockare" And Sh.Range("B4").Value = "Snitt" And Sh.Range("B5").Value = "Per timme" And Sh.Range("A11").Value = "Namn" And Sh.Range("B11").Value = "Plockare" Then
If Target.Row = 11 Then
If Target.Column > 2 And Target.Column <= Sh.Cells(4, 2).End(xlToRight).Column Then
On Error Resume Next
Sh.PivotTables("Pivottabell1").PivotFields("Kvittering av").AutoSort xlDescending, Target.Value, Sh.PivotTables("Pivottabell1").PivotColumnAxis.PivotLines(Target.Column - 2), 1
On Error GoTo 0
End If
End If
End If
End If
End Sub
The Pivot table is placed in cell B11, that is why target.column -2 is correct for me.
If yours is in A, then you probably want target.column -1
And the header is on row 11, that is why the code only reacts to target.row = 11

vba cope variable module

when i change avalue in B2:B10 in need the message "update"
When running Insert_row it should not give that message.
The code works but when I place the procedure Insert_row in another module the public variable is not known ?
How can i solve this.
Public blockchange As Boolean
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("B2:B10")) Is Nothing And blockchange = False Then
MsgBox "Update"
End If
End Sub
Sub Insert_rows()
blockchange = True
Dim LastRow As Integer
LastRow = 3 * ActiveSheet.UsedRange.Rows.Count
For i = 3 To LastRow Step 3
Cells(i, 1).EntireRow.Insert
Cells(i, 1).EntireRow.Insert
Next i
blockchange = False
End Sub
Some thoughts:
(1) As SJR wrote, put global variables into a regular module.
(2) If you insist, you can have a global variable also in the code of the sheet. In this case you access it by putting the codename of the sheet as prefix, eg Sheet1.blockchange. The codename of a sheet is the internal name of a sheet, you see it in the VBA editor in the project window or if you open the property window (F4), it is displayed in the first row as (name). This name doesn't change if a sheet is renamed, you can change it only in the VBA editor.
(3) For your specific case, it's maybe better not to use a variable but simply to prevent the events to fire while the Insert-code is running. You do this with the statement Application.EnableEvents = False. Just don't forget to put Application.EnableEvents = True at the end of the code, maybe with an error handler to prevent that the statement is not executed when an error occurs.

Disable a specific form control button from running its assigned macro even if it is clicked

I would like to be able to prevent a specific button ("Button 4925") from running its assigned macro even when it is clicked. Basically, when you click it, it would either do nothing or show a message that says " This is an essential item that cannot be deleted"
In other words, I would like to be able to exit the sub only if the clicked button is in cell A12. Otherwise, run the code as normal. I don't know how to do that considering that I am a very beginner in VBA.
Some information:
The button is a form control button. Not an Active X one. It gets copied and pasted by another macro on the sheet. The assigned macro is written under a Standard Module.
The assigned macro function is to delete a relative range of rows. Here is the code:
Sub Delete_Button()
' Delete_Button Macro
' Step 1: Select the cell under the clicked button
Dim r As Range
Dim s As Object
Set r = ActiveSheet.Buttons(Application.Caller).TopLeftCell
r.Select
' Step 2: delete all buttons relative to the selected cell from step 1
StartCell = ActiveCell.Offset(-5, 0).Address
EndCell = ActiveCell.Offset(0, 0).Address
For Each s In ActiveSheet.DrawingObjects
If Not Intersect(Range(StartCell, EndCell), s.TopLeftCell) Is Nothing Then
s.Delete
End If
Next s
' Step 3: delete the rows relative to the selected cell from step 1
ActiveCell.Offset(-7, 0).Rows("1:9").EntireRow.Select
Selection.Delete Shift:=xlUp
ActiveCell.Offset(-4, 0).Range("A1").Select
End Sub
You must 'tell' to the code, in a way, that it must not delete the range.
So, I would suggest you to create a Private variable on top of the module keeping the button code (in the declarations area):
Private stopButCode As Boolean
Than, you must make this variable True. Use a Check box, or a piece of code in another control to make it True.
The Button code must be adapted in a way like following:
If Not stopButCode Then
'delete whatever is to be deleted
Else
MsgBox "Deletion not allowed..."
Exit Sub
End If
Edited:
If you want the code not working only if the button will be on cell "A12", you can use adapt your code as following:
Dim r As Range
Dim s As Object
Set r = ActiveSheet.Buttons(Application.Caller).TopLeftCell
If r.Address = "$A$12" then Exit Sub
'here follows your existing code...

VBA Excel - Call macro using buttons

I have written the following macro that calls a Private Sub Worksheet_Calculate() if I confirm that I want to run it. Please find below the first part of the code:
Sub test()
Dim result As VbMsgBoxResult
result = MsgBox("Run Macro?", vbYesNo, "Excel VBA")
If result = vbYes Then
Call Worksheet_Calculate
End If
End Sub
The second code, which is Private Sub Worksheet_Calculate(), shall copy the result of the calculation performed in B2 and paste it in C2 as well as add a timestamp to D2. Columns C and D are populated as values in B2 change, so that I get a list of results and timestamps in C3 and D3, C4 and D4 etc. Here is the code:
Private Sub Worksheet_Calculate()
Dim lastrow As Long
lastrow = Worksheets(1).Cells(Rows.Count, 2).End(xlUp).Row
With Worksheets(1).Cells(lastrow, 2)
.Offset(1, 0) = Cells(2, 1).Value
.Offset(1, 1) = FormatDateTime(Now, vbLongTime)
End With
End Sub
The problem that I am facing is related to the fact that my Public Sub Worksheet_Calculate() is called only once and whenever I recalculate the value of B2 nothing happens.
Is there a way to a) keep second code activated or b) have a button(s) or a tickbox that would activate/deactivate the second code?
Hope it makes sense, thanks in advance!
Worksheet Calculate Event
This is what you put into Module1, or what ever you will call your Module. This is a normal module created with Add Module. I've left the name of the test program intact, but I changed the other because it is used for worksheet events, particularly the Worksheet Calculate event. The test program and the variable blnCalculation enable or disable the whole thing, so in the beginning nothing will be happening before you enable it. You can create a button for it and in its Click event just add Module1.test or just run it from Macros.
Option Explicit
Public TargetValue As Variant
Public blnCalculation As Boolean
Sub test()
Dim result As VbMsgBoxResult
result = MsgBox("Run Macro?", vbYesNo, "Excel VBA")
If result = vbYes Then
blnCalculation = True
ActiveSheet.Calculate
Else
blnCalculation = False
End If
End Sub
Sub SheetCalculate()
Dim lastrow As Long
With ActiveSheet
lastrow = .Cells(Rows.Count, 2).End(xlUp).Row
With .Cells(lastrow, 2)
.Offset(1, 0) = .Parent.Cells(2, 1).Value
.Offset(1, 1) = FormatDateTime(Now, vbLongTime)
End With
End With
End Sub
And the following is what you put into each sheet code aka Object module, which you get by double-clicking on it in VBE. The Worksheet Calculate event happens each time Excel 'decides' to calculate a worksheet, which is in your case probably guaranteed because you say that there is a formula in B2, so the event will happen every time B2 gets calculated. But it won't run the SheetCalculate program unless the value has changed which is checked with the TargetValue variable.
The Worksheet Activate event happens on a worksheet e.g. each time when you select it in the tab. In this case it is used to pass the new B2 value from the newly selected worksheet to the TargetValue variable.
Option Explicit
Private Sub Worksheet_Activate()
If Module1.blnCalculation Then _
Module1.TargetValue = Me.Range("B2").Value
End Sub
Private Sub Worksheet_Calculate()
If Module1.blnCalculation Then
If Me.Range("B2").Value <> Module1.TargetValue Then
Module1.SheetCalculate
End If
End If
End Sub

Resources