I have 50 datasheets in the project, and nobody remembers to run the save macro when going to another sheet. The bright idea is to use a private sub Worksheet_Deactivate to do the necessary calculations when they select another worksheet. In addition to the 50 datasheets, there are two more worksheets in the workbook for which the calculation must not run. It would be nice if the sub could be put in "Worksheets" rather than replicated 50 times in individual worksheets, but the two other worksheets need to be excluded from processing.
Problem is, the sub defaults to the deactivating worksheet (such as an unqualified "Range.Value =" in the macro code), but the active worksheet is now the worksheet being navigated TO. So any ActiveXXXXX statement directs to the wrong worksheet. Worksheet.Name is disallowed.
Datasheets are numbered 1 to 50. What is needed is a statement early in the deactivate sub similar to
If DeactivatingWorksheet(X) = "BasicInfo" Or "Constants" Then GoTo EndSub
where X is the value of the deactivating worksheet. Of course, X is known only to Excel at the moment of processing.
I can't seem to figure out how to refer to the deactivating worksheet in the macro's IF statement. Any ideas?
Use Workbook_SheetDeactivate(ByVal sh as Object) instead of Worksheet_Deactivate(). The Workbook-level event supplies the name of the sheet being departed, even though in both cases the ActiveSheet has already changed when when event fires. Use sh just like a worksheet variable - sh.Name, sh.ProtectionMode, etc.
Now you don't need 50 subs; just one. Another thing that this allows is, you can "abort" the change to the now ActiveSheet by sh.Activate to the old one (but turn off events or you'll have a lovely infinite loop).
Me also gives the old sheetname and works for the worksheet event, if you still want to go that way. Me is the old one, ActiveSheet is the new one.
If you are using Worksheet_Deactivate and this calls a subroutine in a seperate module, you can pass the name of the deactivating worksheet to the subroutine.
For instance, if your subroutine is something like:
Sub test()
ActiveSheet.Range("whatever") = "something"
ThisWorkbook.Save
End Sub
And you call it from the worksheet like:
Private Sub Worksheet_Deactivate()
Module1.test()
End Sub
You can add a parameter to the subroutine to take the worksheet name, and add a test:
Sub test(worksheetname as string)
If worksheetname <> "dontsavethistab" then
ActiveSheet.Range("whatever") = "something"
'or... you could also do:
Sheets(worksheetName).Range("Whatever") = "something"
ThisWorkbook.Save
End If
End Sub
And call it from your Worksheet_Deactivate event like:
Private Sub Worksheet_Deactivate()
Module1.test (Me.Name)
End Sub
If you wanted to get a little cleaner, instead of the worksheet name you could pass the worksheet object:
Private Sub Worksheet_Deactivate()
Module1.test(Me)
End Sub
Sub test(ws as worksheet)
If ws.name <> "dontsavethistab" then
ws.Range("Whatever") = "something"
ThisWorkbook.Save
End If
End Sub
This way you have the entire worksheet object to do with as you please in your subroutine.
Related
I have a worksheet with:
Private Sub Worksheet_Activate()
MsgBox "Ran"
End Sub
I have a button on my toolbar that I made. What I want it to do is trigger this method on the currently selected WorkSheet.
I figured I could do Call ActiveWorksheet.Activate or Call Worksheet.Activate but while these seem to execute without errors, the method is not called.
As a workaround I considered adding a public DoActivate method, but it seems a bit lame and I would likely have to fiddle with CallByName to get it to work (and developers would have to remember to implement this method on every worksheet).
Is there a reason why my Activate method is not calling manually via the above code, or a suitable workaround to get what I'm looking for?
Move your code to a new Sub called OnActivate
Private Sub Worksheet_Activate()
OnActivate
End Sub
Private Sub OnActivate() 'or Public if you call from another module
MsgBox "Ran"
End Sub
The Worksheet_Activate() event handler can be called manually from inside the module by Worksheet_Activate like any other sub (although this is IMO not a nice way to do it)
If you want to ensure all worksheets have the same method, then you can make them Implement an interface: e.g.
Class module: IActivateHandler
Public Sub OnActivate()
End Sub
Then in Sheet1, 2, 3 etc:
Implements IActivateHandler
Private Sub IActivateHandler_OnActivate()
MeOnActivate
End Sub
Private Sub Worksheet_Activate()
MeOnActivate
End Sub
Private Sub MeOnActivate()
MsgBox "Ran"
End Sub
And the button:
Private Sub Button1_Click()
Dim sheetToCall As IActivateHandler
' Debug.Assert TypeOf ActiveSheet Is IActivateHandler
Set sheetToCall = ActiveSheet 'gets the IActivateHandler of the sheet, ensuring it is defined. Will error if it isn't
sheetToCall.OnActivate 'runs the IActivateHandler_OnActivate() method of sheet1
End Sub
You can call the activating event of any active sheet (without knowing its name) in this way:
Create the next event in ThisWorkbook code module. Or, simple copy the following code. Take care that such an event does not already exist:
Public Sub Workbook_SheetActivate(ByVal Sh As Object)
MsgBox Sh.Name & " activated..."
'the necessary code here...
End Sub
Then, call it from a standard module in the next way:
ThisWorkbook.Workbook_SheetActivate ActiveSheet
If you want excepting some sheets, you can adapt the event code to do it:
If sh.Name <> "MySheetName" then
MsgBox Sh.Name & " activated..."
'the necessary code here...
End if
If many sheets should be excepted, an array of sheet names should be built and use Application.Match to differentiate between the sheets to use their event and the other ones.
Edited:
If you need a piece of code written in an add-in (or any macro enabled workbook), able to catch the Activate event of a sheet in any (other) open workbook, you should proceed in the next way:
Copy the next declaration on top of the add-in ThisWorkbook code module (in the declarations area):
Public WithEvents appEvHandler As Application
In the same code module, copy the next code:
Private Sub appEvHandler_SheetActivate(ByVal sh As Object)
If sh.Parent.Name <> ThisWorkbook.Name Then
MsgBox sh.Parent.Name & " workbook, sheet " & sh.Name & " activated..."
Else
Debug.Print "changed in this workbook..."
End If
End Sub
Copy also the next Sub, which will activate the event:
Sub activateAppEvHandler()
Set appEvHandler = Application 'It can be placed in `Workbook_Open` event to be run when workbook opens
End Sub
If you want to inactivate it (for some reason...), use the next Sub:
Sub InactivateAppEvHandler()
Set appEvHandler = Nothing
End Sub
Please, test it and send some feedback. I must confess I am not sure I correctly understood what you need. I was asking for a scenario to be followed, but I tried imagining that this is what you want...
I have simple code which is moving worksheet "Data" to after activated worksheet.
Sub Workbook_SheetActivate(ByVal Sh As Object)
Worksheets("Data").Move After:=Worksheets(Sh.Name)
Worksheets(Sh.Name).Activate
End Sub
I am using
Worksheets(Sh.Name).Activate
because without this line worksheet "Data" remains selected after the move which is not the intention.
The problem I have is when this code is run it takes about 2-3 seconds for excel to think about it before seeing result.
I don't understand why. Without chaining these 2 operations together it takes milliseconds. Could someone please explain how to improve this and why is this happening?
This should be quicker:
Sub Workbook_SheetActivate(ByVal Sh As Object)
Dim nam As String
nam = Sh.Name
Application.EnableEvents = False
Worksheets("Data").Move After:=Worksheets(nam)
Worksheets(nam).Activate
Application.EnableEvents = True
End Sub
This happens because of recursive calling: when you use .Activate, then your Sub Workbook_SheetActivate is called again and you're stuck in an endless loop.
If you simply want to deselect the data range, you can use Cells(1,1).Select and you can directly use Sh instead of using Worksheets(Sh.Name), since they're equivalent.
So your final code would be:
Sub Workbook_SheetActivate(ByVal Sh As Object)
Worksheets("Data").Move After:=Sh
Cells(1,1).Select
End Sub
Hope this helps.
Thanks for your input guys. Turning off/on events helped to get out of the loop.
Application.EnableEvents = False
Worksheets("Data").Move After:=Sh
sh.Activate
Application.EnableEvents = True
I should have probably explained what the code was being used for in my question.
The user had 50+ Worksheets in the Workbook (please don't ask me why :)). They had one in particular ("Data") they wanted to be able to click in and out whilst working with other worksheets. So this code was simply causing the Worksheet "Data" to "follow them"
The line:
sh.Activate
Was used to get back to the Worksheet they have just clicked on as otherwise they were getting stuck on "Data" Worksheet.
Trying to get a sub from a module to run within another worksheet. In a sense to stop using redundant ranges and keep it streamlined.
i.e.
-Module object-
Public sub method1()
{
Range("B4:B23") = ""
Range("C4:C23") = ""
'Empties these ranges...
}
-worksheet(s)-
sub project)
{
with sheet1 (or on any sheet 2,3,4,5... etc.)
Call module1.method1
'but this method only works on the module object, not in the context of the 'specified worksheet where it is needed
End with
Ideally to clear the data in ranges by using method1 in ANY worksheet. Every reference I tried just runs the module1 method without any effect or makes a useless reference to the method or worksheet. Just trying to save on code space by not writing direct references to every sheet which is formatted identical.
You can do this with a sub (ClearCells) that accepts a variable number of arguments via the ParamArray keyword. Then you can simply call the ClearCells sub and pass it the worksheet objects you want to clear the same ranges in, as in the DoClear sub. You can add more ranges as needed to the Union function in the GetRanges function.
Sub DoClear()
ClearCells Sheet1, Sheet3
End Sub
Sub ClearCells(ParamArray wkshts() As Variant)
Dim vWs As Variant
Dim ws As Worksheet
For Each vWs In wkshts
Set ws = vWs
GetRanges(ws).Clear
Next vWs
End Sub
Function GetRanges(ws As Worksheet) As Range
With ws
Set GetRanges = Union(.Range("B4:B23"), _
.Range("C4:C23"))
End With
End Function
Or assuming you are calling the method from the sheet you want to clear, you can just use ActiveSheet:
Public Sub Method1()
ActiveSheet.Range("B4:B23").Clear
ActiveSheet.Range("C4:C23").Clear
End Sub
I'm somewhat newer to VBA, and this particular action seems like it may be out of my current scope of knowledge.
Is there a way to code VBA to have it actively select the same cell on all worksheets as the current cell selected? I have a model I've put together to allow my team to enter data simultaneously regarding product SKUs in Column A on Sheet1, but due to the large amount of information that we enter per item, I used multiple sheets
For example, if I have cell H4 selected on Sheet1, is it possible to have all other sheets active cell H4 upon switching to the other worksheets?
This is what I've come up with so far on a test workbook, but it does not seem to work:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Select Case LCase(Sh.Name)
Case Is = "sheet1", "sheet2", "sheet3"
If CurRow > 0 Then
With Application
.EnableEvents = False
.Goto Sh.Cells(CurRow, CurCol), Scroll:=True
Sh.Range(ActCellAddr).Select
.EnableEvents = True
End With
End If
End Select
End Sub
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
Select Case LCase(Sh.Name)
Case Is = "sheet1", "sheet2", "sheet3"
CurRow = ActiveWindow.ScrollRow
CurCol = ActiveWindow.ScrollColumn
ActCellAddr = ActiveCell.Address
End Select
End Sub
I've located this code below:
Excel VBA code to allow the user to choose the same cell on every sheet
But this requires the user actually enter the cell they'd like to have selected. I am looking for it to be automatic.
Any tips or suggestions? Any help is greatly appreciated.
You can post the following to every sheet in your workbook.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Set CurrWS = ActiveSheet
For Each WS In ThisWorkbook.Worksheets
WS.Activate
WS.Range(Target.Address).Select
Next
CurrWS.Activate
End Sub
Every time you select a cell, it will cycle through all the worksheets and select the same cell there. The downside to this is obvious: if you have too many sheets, it's going to be tedious. The other problem is that it's going to cycle through everything. So it might mess up some other sheets if you're going to use this for data entry.
Otherwise, if it's just selecting the cell, then this is harmless though the flicker can be noticeable at times, based on how many sheets you have.
Not as elegant as one would want, but it works. Good luck and let us know if this helps.
Worth noting there is a workbook-level event handler which handles the same event, so you only need to add the code once to the ThisWorkbook code module:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, _
ByVal Target As Range)
Sh represents the ActiveSheet.
Probably also worth disabling events while you're selecting the ranges on the other sheets, or that will re-trigger your event handler (don't forget to turn event handling back on before exiting your code!)
This approach will test for hidden sheets. It selects all non-hidden sheets, selects the target cell then returns to the original sheet. It works pretty fast even if you have many many tabs.
targetcell = ActiveCell.Address
OriginSheet = ActiveSheet.Name
Dim ws As Worksheet
For Each ws In Sheets
If ws.Visible = True Then ws.Select (False)
Next ws
range(targetcell).Select
Sheets(OriginSheet).Select
In a worksheet I can attach code to an event like so:
Private Sub Worksheet_Calculate()
' .. code here
End Sub
How can I get a reference to the worksheet the event was generated in?
I want to have a 100% secure way, so I don't have to worry about code breaking when the name worksheet changes etc.
ActiveSheet is not correct because it is not guaranteed that any sheet is active upon (re)calculate.
You can use caller:
Application.Caller.Worksheet.Name
You can use the Codename property:
Private Sub Worksheet_Calculate()
Debug.Print Me.CodeName
End Sub
By default it's the same name as the worksheet, but you can change it in the Properties Window of the VBE - it's the Name property. The user can't change it. (Copying a sheet adds a number to the name, e.g., Sheet1 becomes Sheet11.
You can also use it in the workbook-level event:
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
If Sh.CodeName = "TheSheetICareAbout" Then
Debug.Print Sh.Name
End If
End Sub
It's hard to be more helpful as you don't say how you'll use the reference.
Some more information on code names:
http://www.ozgrid.com/VBA/excel-vba-sheet-names.htm