Excel VBA add-in, is Worksheet_SelectionChange possible? - excel

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

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: Pass `Target` Variable from Click Event to Userform

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.

Using VBA userform to select ranges on multiple sheets - sheet changes back to original activesheet

I have a userform which has multiple RefEdit controls. I need the user to select ranges from multiple sheets and the userform has to be complete before the rest of the code can run.
Issue: The activesheet is "Sheet1" when the userform is initiated. Each time I select a range on "Sheet2" and click into the next RefEdit the visible Excel sheet returns to "Sheet1". I'd like the sheet to remain on "Sheet2", since clicking between the sheets significantly increases the time it takes to select the data.
Because I need the userform to be completed before continuing with my code, using "vbModeless" doesn't appear to work.
I've tried to step through the userform events which appeared to be relevant but none were activated when I entered the RefEdit, selected the data, or left the RefEdit.
Thanks in advance for any help!
Edit: Using some input from the responses and doing some more research I think I've figured out the problem and a work around.
RefEdit events such as Change or Exit (I tried all of them I think) don't appear to trigger when a change occurs in the control. So I couldn't write code to manipulate the activesheet when I changed the control. A workaround found here: http://peltiertech.com/refedit-control-alternative/ uses a textbox and inputbox to simulate a RefEdit control and will actually trigger when changes are made! Code is below. To add other "RefEdit" controls you should repeat the code in the Userform_Initialize event for each control, then add another TextBox1_DropButtonClick and update TextBox1 to the name of the new control. In use when the control updates the workbook jumps to the previous activesheet and then returns the desired activesheet. Not as smooth as I'd like but much better than it was.
Code:
Private Sub CancelButton_Click()
Unload Me
End
End Sub
Private Sub OKButton_Click()
UserForm1.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
End
End Sub
Private Sub UserForm_Initialize()
Me.TextBox1.DropButtonStyle = fmDropButtonStyleReduce
Me.TextBox1.ShowDropButtonWhen = fmShowDropButtonWhenAlways
End Sub
Private Sub TextBox1_DropButtonClick()
Dim ASheet As String ' Active sheet
Me.Hide
'Use input box to allow user to select a range
On Error Resume Next
Me.TextBox1.Value = Application.InputBox("Select the range containing your data", _
"Select Chart Data", Me.TextBox1.Text, Me.Left + 2, _
Me.Top - 86, , , 0)
On Error GoTo 0
'Check if there is a sheet name - if the range selected is on the activesheet the output of the inputbox doesn't have a sheet name.
If InStr(1, Me.TextBox1.Value, "!", vbTextCompare) > 0 Then ' there is a sheet name
ASheet = Replace(Split(Me.TextBox1.Value, "!")(0), "=", "") ' extract sheet name
Else ' there is no sheet name
Me.TextBox1.Value = "=" & ActiveSheet.Name & "!" & Replace(Me.TextBox1.Value, "=", "") ' add active sheet name to inputbox output
ASheet = ActiveSheet.Name
End If
Worksheets(ASheet).Activate ' set the active sheet
Me.Show
End Sub
Have you tried something as simple as:
Sheets("Sheet2").Select
somewhere in the beginning of your form code ?
Since you haven't posted your code, it's hard to provide a good answer.
Hope this helps a little :)
This form module worked for me.
Private Sub CommandButton1_Click() 'Cancel Button
Unload Me
End Sub
Private Sub CommandButton2_Click() 'GO Button
Dim newSheet As Worksheet
abc = Split(RefEdit1.Value, "!")
cbn = abc(0)
Unload Me
Set newSheet = Worksheets(abc(0))
newSheet.Activate
End Sub

How to reference a specific sheet within VBA Code in Excel 2013

This works perfectly, but I only want it to reference a single sheet instead of every sheet in the workbook
Private Sub Worksheet_Change()
Dim LastRow As Long, c As Range
Application.EnableEvents = False
LastRow = Cells(Cells.Rows.Count, "AA120").End(xlUp).Row
On Error Resume Next
For Each c In Range("AA5:AA120" & LastRow)
If c.Value = "0" Then
c.EntireRow.Hidden = True
ElseIf c.Value > "0" Then
c.Activate
c.EntireRow.Hidden = False
End If
Next
On Error GoTo 0
Application.EnableEvents = True
End Sub
As i can see you only need to insert this code in one sheet which ever you want. Rightclick on sheet name(tab) and select "view code", then paste this code. BUT! If this runs on every sheet, then you also need to delete code from "This Workbook" --> (Alt+F11) and select "This workbook", then erase this code from there.
You have to use
Private Sub Worksheet_Change(ByVal Target As Range)
instead.
According to the great Chip Pearson, "The events and their procedure declarations are fixed. You must not alter the name or parameter list of an event procedure." (I do not have a system with Office to test it).
You would insert your Sub in a sheet module, and it will work only on that sheet.

Circular Reference with drop-down list

Is it possible in MS. Excel or VBA to have a circular reference with a drop-down list?
Here is what I am after: I want to generate on two sheets (sheet 1, sheet 2) a drop down list that says either "Complete" or "Incomplete." If I change sheet 1 from Complete to Incomplete, I want sheet 2 to say the same thing, but I also want vice versa
(If I change sheet 2 from Complete to Incomplete, I want sheet 1 to change).
Is this possible?
Acting on a change in any of the worksheets' B5 range seems a likely way to proceed but the individual Worksheet_Change event macros have some limitations.
The code has to be repeated across many worksheet code sheets and any modifications have to be cloned across the same. New worksheets require the sub procedure to be incorporated into their own code sheets.
Without disabling events before writing new values, each worksheet receiving a new value is going to initiate its own Worksheet_Change event macro which in turn will rewrite values which will trigger more events. A cascade event failure is almost sure to happen.
By exchanging the Worksheet_Change event macro for the more universal Workbook_SheetChange event macro located in the ThisWorkbook code sheet, all of the code can be localized to a single location. Adjustments are made in a single place and new worksheet will automatically be added to the queue of worksheets to process. They can easily be added to the array of worksheet not to process as well.
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
If Target.Address = "$B$5" And Sh.Name <> "Sheet3" Then
On Error GoTo bm_Safe_Exit
Application.EnableEvents = False
Dim w As Long
For w = 1 To Worksheets.Count
With Worksheets(w)
'skip this worksheet and Sheet3
If CBool(UBound(Filter(Array(Sh.Name, "Sheet3"), _
.Name, False, vbTextCompare))) Then
.Range("B5") = Target.Value
'.Range("B5").Interior.ColorIndex = 3 '<~~testing purposes
End If
End With
Next w
End If
bm_Safe_Exit:
Application.EnableEvents = True
End Sub
Any worksheet that is not to receive an update to the value in its own B5 cell can be added to the array used in the Filter function. Currently, Sheet3 and the worksheet that initiated the Workbook_SheetChange event are excluded.
I would create a hidden sheet that contains the range for input and the linked cell. Then link both drop downs to the list and the linked cell. Then when you change one it will change the other. The key here is the Linked Cell. This is assuming Excel 2013, using the Form control Combo Box.
Please have a look at #Jeeped's answer as it is the most efficient answer.
After a little trial and error I've gotten this to work with a cell with a "Data Validation" dropdown menu. In my test case, I had 3 sheets with a Data Validation list in cell $B$5 on each worksheet linked to a list on a hidden sheet to populate the list, the sheet with the options list was "Sheet3" and did not contain a data validation list.
The Code below needs to be copied to every sheet module.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim ws As Worksheet
If Target.Address = "$B$5" Then
For Each ws In ThisWorkbook.Worksheets
If Not ws.Name = Me.Name And Not ws.Name = "Sheet3" Then
If Not ws.Range(Target.Address) = Me.Range(Target.Address) Then
ws.Range(Target.Address) = Me.Range(Target.Address)
End If
End If
Next ws
End If
End Sub
This is fairly easy with activeX comboboxes on the sheets
On the workbook module add the code below to populate the comboboxes
Private Sub Workbook_Open()
With ThisWorkbook
With .Worksheets("Sheet1").ComboBox1
.AddItem "Complete"
.AddItem "Incomplete"
End With
With .Worksheets("Sheet2").ComboBox1
.AddItem "Complete"
.AddItem "Incomplete"
End With
End With
End Sub
On the "Sheet1" Module add
Private Sub ComboBox1_Change()
If Me.ComboBox1 = "Complete" Then
ThisWorkbook.Worksheets("Sheet2").ComboBox1.Value = "Complete"
ElseIf Me.ComboBox1 = "Incomplete" Then
ThisWorkbook.Worksheets("Sheet2").ComboBox1.Value = "Incomplete"
End If
End Sub
On the "Sheet2" Module add
Private Sub ComboBox1_Change()
If Me.ComboBox1 = "Complete" Then
ThisWorkbook.Worksheets("Sheet1").ComboBox1.Value = "Complete"
ElseIf Me.ComboBox1 = "Incomplete" Then
ThisWorkbook.Worksheets("Sheet1").ComboBox1.Value = "Incomplete"
End If
End Sub

Resources