While programming a module in VBA that operates on copy-pasting a range based on formulae on a sheet, the currently activated sheet's Worksheet_Activate event is triggered.
I know that I can use Application.EnableEvents = False to disable the trigger, however, I would like to understand why the PasteSpecial operation for a Range object triggers the Worksheet_Activate method, as I cannot find any mention of the same on the Range.PasteSpecial documentation, or what actions trigger the Worksheet_Activate event.
For a reproducible example, please create a workbook with two sheets, Sheet1 and Sheet2, and insert code in Sheet1 as so:
Public Sub Worksheet_Activate()
MsgBox "Sheet 1 has been activated."
End Sub
Add a separate module with code as so:
Public Sub copy_and_paste_in_sheet2()
Set Rng1 = ThisWorkbook.Worksheets("Sheet2").Range("A1:A10")
Rng1.Copy
Set rng2 = ThisWorkbook.Worksheets("Sheet2").Range("B2:B12")
rng2.PasteSpecial xlPasteFormulas
End Sub
While Sheet1 is activated, run the copy_and_paste_in_sheet2 macro, and it will be evident that the Worksheet_Activate event for Sheet1 is triggered despite no apparent code in the second module explicitly doing so.
I would expect that notwithstanding any use of Select operations that require the Activate command to work, the PasteSpecial operation should not ideally trigger the Worksheet_Activate event. Could you please direct me to the relevant documentation for this behaviour?
It seems to be one of the odd things you may have when dealing with Excel: PasteSpecial quickly "activates" the sheet you are pasting to. You can see the screen flicker (at least I can, using a VM that is rather slow when it comes to screen updating). Also, if you put similar event code to sheet2, you will see that it is triggered also.
The strange thing is that in the triggered sheet you see still sheet1 as ActiveSheet. So the following code (as event routine of Sheet2) will print "activate 2 Sheet1"
Public Sub Worksheet_Activate()
Debug.Print "activate 2 " & ActiveSheet.Name
End Sub
I doubt that you will find any documentation for that behaviour. And your best bet is to accept this and (as you already wrote) use Application.EnableEvents = False
Related
(07/08/21 - I edited my text to update and sharpen the problem).
I have made an Excel VBA program that provides the conditional formatting of a large number of cells (which are formatted using the formulas option which refer to cell values in the target spreadsheet). The script and spreadsheet works fine, but I have a problem as immediately after I have run my script (or to be precise a particular input box script has been run) then ghost images appears. (I can easily replicate the issue including on different Windows machines.) The ghost images no longer happen if the user saves the sheet and then re-opens it. However, to me this is not a good solution and makes the program look poor in quality and trustworthiness!
I have a "first" routine that when run (via a button press) uses an Application.Inputbox - this allows the user to select a range of cells. These selection of cells are located in the target worksheet which is a different workbook to where the code is run from. Also, the selection of cells are located in a sheet that is not the front sheet of the workbook concerned.
I then have another second button which when pressed uses collected data and conditionally formats the target spreadsheet. However, after doing this button press I get ghost images appearing (which shows cells from selection made earlier from the first button press).
The screenshot below illustrates the occurrence - you can see that there is a table being shown from the second sheet on the top left-hand side of the sheet (despite not fitting the cells of screen 1!). I hope that makes sense.
Someone kindly below said that I needed to use:
Application.ScreenUpdating=False
and then return it to true at the end.
However, I still have the same ghost images occur and I note these happen after the script has been run.
From researching the topic, I found that this is a common issue from using the property Application.InputBox. If I run my second program without using the first one immediately before it (which has the Application.InputBox) then no ghost images appear. Therefore, I think it is pretty safe to assume the problem has come from this Application.InputBox! However, I have not been able to find a solution! I list below the code used for the first Application.InputBoxs routine.
Sub UserSelectsCells()
Dim rng As Range
Dim wks As Worksheet
Dim wkb As Workbook
If Range("C9") <> False Then
Workbooks.Open Filename:=Range("C9")
End If
On Error Resume Next
Set rng = Application.InputBox( _
Title:="Select Test Cells", _
Prompt:="Please Find The Cells In Your Workbook That Test Whether The User Has Answered The Questions Correctly" & vbCrLf & "Remember this may be in a different sheet in your workbook" & vbCrLf & "These cells must be in a single column", _
Type:=8)
On Error GoTo 0
'Test to ensure User Did not cancel
If rng Is Nothing Then
Workbooks("Version060821.xlsm").Activate
Exit Sub
End If
Workbooks("Version060821.xlsm").Activate
Range("C32").Value = rng.Parent.Parent.Name
Range("C33").Value = rng.Parent.Name
Range("C34").Value = rng.Address
Range("D35").HorizontalAlignment = xlLeft
Range("D35").Value = rng.Count
End sub
Can anyone please find a solution? As an idea, is it possible to somehow clean the memory before my second program is run?
I note that if there is a ghost images problem and I delete all of the conditional formating then the ghost images still appear. I think this is significant because the conditional formatting is linked to the ghost image cells that appear. So, to me this suggests there is some kind of microsoft bug?
I'm not exactly sure why these ghost screens pop up sometimes but I've found that preventing the screen from flashing during your code normally fixes the issue. You can do this by setting Application.ScreenUpdating to False and the beginning of your code. Just be sure to set it back to True at the end! Something like this:
Application.ScreenUpdating = False
[Your code]
Application.ScreenUpdating = True
edit:
After further research, it would appear this is an issue that has been already identified. The workaround below originally comes from here.
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Dim wSheet As Worksheet
On Error Resume Next
Application.ScreenUpdating = False
For Each wSheet In Worksheets
wSheet.Select
Range("A1").Select
Next
Application.ScreenUpdating = True
End Sub
This is definitely a dirty fix but if it works it works. An alternative solution was to scroll up and down using:
Private Sub worksheet_change(ByVal target As Range)
Application.ScreenUpdating = False
ActiveWindow.SmallScroll Down:=-100
ActiveWindow.SmallScroll Up:=100
Application.ScreenUpdating = True
End Sub
Please let me know what works best!
Not enough rep for a comment!
Alec, Workbook_BeforeClose is a workbook event, you don't call it like procedures.
Workbook_BeforeClose Event
From that documentation, "False when the event occurs. If the event procedure sets this argument to True, the close operation stops and the workbook is left open.". So if you add Cancel=True inside it, say, after an if statement check, you can stop the close operation.
Edit: In order to answer the question in comments.
The event is fired when you close the workbook, either from the X in the corner or from the menu, or if you have something like ActiveWorkbook.Close in your code.
You don't have to have a Cancel=True/False inside BeforeClose event's code, depends on if you want to control a premature closure of the workbook. It is required, say, if you were writing the event yourself instead of selecting it in VBA editor. Editor already inserts that parameter.
I've created VBA that manipulates the cells in my current sheet (triggered by a SelectionChange event handler).
How can I make this trigger in any of the sheets in my workbook? (The action should only react to, and modify, the sheet that's currently active.)
Here's an example: It takes the value of the current cell, and copies it to "A1":
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Cells(1, 1).Value = Cells(Target.Row, Target.Column).Value
End Sub
(The real code is rather more complex —and constant WiP—, so I don't really want to copy it to each and every sheet.)
Using Workbook_SheetSelectionChange instead of Worksheet_SelectionChange seems to work.
It should be noted though, that the script then has to be placed into the ThisWorkbook code window (instead of the one for Sheet1).
This macro works as intended, from a button on the Batch Input sheet:
Sub BatchTriggerOFF()
Sheets("Batch Input").Unprotect
Sheets("Batch Input").Range("G3:J3").Value = "Off"
Sheets("SQL LOGIC").Calculate
Sheets("Batch Input").Range("A12").Select
Sheets("Batch Input").Shapes.Range(Array("Group 12")).ZOrder msoSendToBack
Sheets("Batch Input").Protect
End Sub
However, when BatchTriggerOFF is called from a different sheet in the same workbook, the macro neither changes the Range("G3:J3").Value nor Shapes.Range(Array("Group 12")).ZOrder msoSendToBack. There is no error message.
If Sheets("SQL LOGIC").Range("B1") = "On" Then Call BatchTriggerOFF
I've tried unprotecting the Batch Input sheet beforehand, messing with Sheets("Batch Input").Activate, Sheets("Batch Input").Select, and even tried pasting the BatchTriggerOFF line by line VBA directly into the second macro, to no avail.
What is causing BatchTriggerOFF to seemingly not run when called from the second macro/sheet?
[...] something is inherently wrong with the second code I've provided, likely not actively running when the value in Range("B1") is changed?
Exactly. A procedure that's in a standard module needs something, somewhere to invoke it. Could be a shape or button on the worksheet, could be other VBA code, but something needs to invoke it somehow.
No procedure is going to just know to run when Range("B1") is changed on Sheets("SQL LOGIC"): you need to have code that's "triggered" when a cell is changed on that sheet.
The way to do this, is to handle the worksheet module's Change event. Find your "SQL LOGIC" sheet in the VBE's Project Explorer (Ctrl+R), double-click it. In the code-behind module for that specific worksheet, select Worksheet from the left-side dropdown at the top of the code pane; the right-side dropdown should say SelectionChange, and the VBE should have added a private procedure that looks like this:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
End Sub
Select Change from the right-side dropdown; the VBE creates a private procedure that looks like this:
Private Sub Worksheet_Change(ByVal Target As Range)
End Sub
Now delete the SelectionChange handler, you don't need it unless you need to track cells that the user has selected. Since we want to track cells that have changed, we'll use the Change worksheet event. This procedure will be invoked whenever the user or your code changes anything on that sheet.
Since we only care to run code when a specific cell is changed, we need a condition here, involving the Target parameter. Using the Application.Intersect function, we can get a Range object reference that's Nothing if the two specified ranges don't intersect; we can use this information to bail out if it's not B1 that changed:
If Application.Intersect(Me.Range("B1"), Target) Is Nothing Then Exit Sub
Any code written after that condition inside the Worksheet.Change event handler procedure, will only run after the value of cell B1 was modified - either by the user typing in a value, or by any other code writing to that cell (you need to toggle Application.EnableEvents off if you have to prevent firing that event when it's code doing the changes and you don't want the handler to run).
Now, it looks like cell B1 isn't going to change, rather, it looks like it contains a formula whose result might change after making changes to the "Batch Input" sheet.
If that's the case, then the Change event will not be fired when B1 recalculates and now evaluates to a new value, because the cell didn't change, only its result.
If that's your scenario, then you want to handle the Calculate worksheet event, and have that be your trigger:
Private Sub Worksheet_Calculate()
If Me.Range("B1").Value = "On" Then BatchTriggerOFF
End Sub
If you need your sub to be called from any (sheet) module, move it in a module! The function/sub in the sheet module cannot be called without specifying the module name where it belongs, like you will be able to do in a module.
I have a database which auto refreshes and updates the table from an external source every 15 minutes. I tried the following code which updates the PivotTable every time the source data is edit/added/deleted:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Worksheets("PIVOT TABLE WORKSHEET").PivotTables("PIVOT TABLE NAME").RefreshTable
End Sub
When i manually edit the source data, the PivotTables refresh accordingly. However, when the source data is updated automatically the PivotTables remain unchanged. Is there a way to make the Pivot tables refresh together with the database without the need of a user input?
You'll have better results if you use the Worksheet_Change event instead of the Worksheet_SelectionChange event, so that the procedure runs when data changes, not when you select a cell with the mouse or keyboard.
Worksheet_Change vs. Worksheet_SelectionChange events
Worksheet_SelectionChange fires when the selection changes on a worksheet.
For example, when the user clicks on a cell, or pushes an arrow key.
Worksheet_Change fires when cells on the worksheet are changed, either by the user or by an external link.
Note: Worksheet_Change does **not** occur when cells change during a re-calculation; use theCalculate` event to trap a sheet re-calculation.)
Depending on how the data is laid out in your worksheet(s), you may want to limit the execution of this procedure by checking which cell(s) were changed, which is easiest overall by comparing the event procedure's Target parameter to a specific cell or cell-range, using the Intersect function.
Caution: Beware of infinity!
When using code that changes cells within the area being "watched" by the Worksheet_Change event procedure, you risk entering into an infinite loop, since the change fires the event again, which changes cells again, etc.
(Click image to enlarge.)
There are several ways this could be avoided. This most common is to temporarily disable events with the Application.EnableEvents property while the Change event does what it needs to do. (Don't forget to re-enable events at the end of the procedure... see example below.)
Example:
Here's an untested example using all these points:
Private Sub Worksheet_Change(ByVal Target As Range)
Const cellsToWatch = "A1:D4"
With Worksheets("PIVOT TABLE WORKSHEET")
'exit the procedure if at least part of the changed cells were not within `A1:D4`
If Application.Intersect(.Range(cellsToWatch), Target) Is Nothing Then
'the change wasn't within `cellsToWatch`
Exit Sub
End If
Application.EnableEvents = False 'disable execution of this or other events
'----------Run your code here:--------------
.Calculate
.PivotTables("PIVOT TABLE NAME").RefreshTable
'-------------------------------------------
Application.EnableEvents = True 're-enable events
End With
End Sub
More Information:
MSDN : Worksheet_Change event (Excel)
MSDN : Worksheet_SelectionChange event (Excel)
MSDN : Application.Intersect method (Excel/VBA)
Office Support : Intercept function (Excel/Worksheet)
Stack Overflow : Stop Excel from firing Worksheet_Change before _BeforeSave?
MSDN : Application.EnableEvents property (Excel)
Problem: I have found many postings (including Microsoft's own support site) containing information on how to run a macro when certain cells change in Excel. I can get it to work, but I need to store that sub in my Personal.xlsb workbook and have it affect named worksheets in another workbook.
Background: I receive an Excel file from a third party often and run a series of routines on it, with all Macros stored in my Personal.xlsb hidden workbook. Part of the process requires me to "undo" many of the formatting changes I have made automatically when a user enters a date in a specific cell. I would like to call those "undo" subs on 5 separate worksheets as soon as a user does make a date entry in cells specified on any of the given 5 worksheets. However, all of the help on this that I have already read online has me add the code to the exact worksheet to which the changes need to occur. I want to store that code in a module in my Personal workbook so that it will run on any Workbook containing the worksheet names...similar to the way I have my other queries laid out.
Code so far:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
Set KeyCells = Range("W9")
If Not Application.Intersect(KeyCells, Range(Target.Address)) Is Nothing Then
MsgBox "You changed THE CELL!"
End If
End Sub
Question: How do I revise the sub in order to put it into effect in all of my workbooks containing the specified worksheets? Or is there a better option for me somewhere out there?
Since I faced the same problem, I worked my way through Chip Pearson's instructions and will provide you with a more comprehensive guide.
I can also really encourage you to read your way through Chip Pearson's great summary on events. I know it's a lot to digest, but it will greatly help you understand what we actually do here.
As an overview, these are the steps we are going to perform:
Add a class module for our event handlers (This is where the actual event handling code will go)
Add an event handler to our personal.xlsb's ThisWorkbook module to wire-up our event handler class
Enjoy infinite event handling loops because we changed the Target.Value :-P (optional)
Let's get started.
1. Add a class module
First, we need to create a cozy plaze to put all our event handlers. We will use a class module for this, since it provides some nice structure and you immediately know where to look for your application level event handlers.
In your personal.xlsb's VBA project create a new class by right clicking in the project browser -> insert -> class module.
Rename the class to CAppEventHandler by changing the name in the properties pane.
Add the source code listed below (both the setup part and the event handling part) to the class you just created. (Make sure to read the comments, because they add some additional information to what we are doing and why.)
2. Add the initializing event handler
Now we need to make sure our event handlers are "activated" when our personal.xlsb is opened (so anytime you open Excel :)).
To do this, double click your ThisWorkbook module and add the code below.
Now you are actually already good to go and can test your event handling. The only thing you will have to do beforehand (in order to trigger the "wiring-up" process) is restart Excel.
Notes
I added a check for the current sheet's name at the beginning of the event handler - so make sure your sheet is named "MySheet" :)
You can set a breakpoint inside the handler and watch it in action just like any other piece of VBA code
And a word of warning (aka 3. Enjoy infinite event handling loops)
When I tested this, I naively used Target.Value = "It wooooorks!" as the code to be executed if we changed the right cell on the right sheet. That was not good.
See the last snippet of code (also taken from Chip's post) for how to prevent such an event loop.
Source code
In the CAppEventHandler class (setup)
Option Explicit
' Declare our own reference to the Application object
' WithEvents is needed to capture the events of the application object
' (Note: The events 'bubble up' from the Worksheet to the Workbook and
' finally to the Application.
' So event handlers in the Sheet module are executed first
' (if any handlers are declared), then the ones in the Workbook
' module (again, if they are declared) and finally the ones
' in the Application module.)
Private WithEvents App As Application
' Whenever a new object of a class is instantiated, the _Initialize-Sub is called,
' that's why we use this Sub to get the reference to the current Application object
Private Sub Class_Initialize()
Set App = Application
End Sub
In the CAppEventHandler class (actual event handling)
' Here is the actual code executed whenever the event reaches the Application level
' (see above for the order of 'bubbling up' of the events) and hasn't been marked
' as handled before
Private Sub App_SheetChange(ByVal Sh As Object, ByVal Target As Range)
If Sh.Name <> "MySheet" Then
Exit Sub
End If
Dim rngKeyCells As Range
Set rngKeyCells = Sh.Range("A5")
If Intersect(rngKeyCells, Target) Is Nothing Then
Exit Sub
End If
' Do our actual work
MsgBox "It wooooorks!", vbInformation
' Note: If you want to change the contents of your keyCells, you will
' have to make sure to prevent an infinite event loop (i.e. using
' Application.EnableEvents = False because otherwise changing
' the value in your macro will trigger the event again
End Sub
In the ThisWorkbook module
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' The following code must be placed in the "ThisWorkbook" module of your personal.xlsb
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Option Explicit
Private OurEventHandler As CAppEventHandler
Private Sub Workbook_Open()
' Since we declared so in our _Initialize-Sub this wire-up the current
' Application object in our EventHandler class
Set OurEventHandler = New CAppEventHandler
End Sub
How to prevent event loops
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Target.Value = Target.Value + 1
Application.EnableEvents = True
End Sub
(from Chip Pearson's comprehensive post on events)