Why is Object type instead of Sheet type used for variable declaration in the following VBA code? - vba

The following code gets user back to the old sheet if a Chart is activated, and it shows how many data points are included in the Chart before getting back. And I wonder why the variable Sh is defined as Object rather than Sheet in the two event-handler procedures. Same for the variable OldSheet.
Dim OldSheet As Object
Private Sub Workbook_SheetDeactivate(ByVal Sh As Object)
Set OldSheet = Sh
End Sub
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Dim Msg As String
If TypeName(Sh) = "Chart" Then
Msg = "This chart contains "
Msg = Msg & ActiveChart.SeriesCollection(1).Points.Count
Msg = Msg & " data points." & vbNewLine
Msg = Msg & "Click OK to return to " & OldSheet.Name
MsgBox Msg
OldSheet.Activate
End If
End Sub

Because there is no such thing as a 'Sheet' in Excel.
Notice the event is SheetActivate, not WorksheetActivate - the concept of a "sheet" encompasses several types that have nothing in common, other than the ability to be "activated". There is no Sheet type in the Excel object model - the Workbook.Sheets collection contains various types of objects, including Chart and Worksheet objects.
The Sh parameter in the SheetActivate event has to be an Object, because there is no common interface between a Chart and a Worksheet.
So you need do what you did: verify the type of the object instead of assuming you're dealing with a Chart or a Worksheet object.
Instead of using the TypeName function and thus stringly-typed type checks, you should use the TypeOf operator instead:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
If TypeOf Sh Is Excel.Worksheet Then
Debug.Print "Worksheet!"
ElseIf TypeOf Sh Is Excel.Chart Then
Debug.Print "Chart!"
Else
Debug.Print "Something else!"
End If
End Sub
The parameter being Object allows future versions to activate a "sheet" of a type that couldn't be dreamed of at the time the event declaration was written in the IWorkbookEvents hidden interface that every Workbook object implements.

The short version of the answer is that it has to be declared as Object. The events are being "fired" through a COM source sink, and that returns an IDispatch pointer (known in VBA as Object) to anything that has a subscribed callback function. The ByVal Sh As Object parameter is passed to the callback function so that the event handler can determine which object was responsible for raising the event. It's declared in the Excel type library on dispinterface WorkbookEvents like this:
[id(0x00000619), helpcontext(0x0007ad30)]
void SheetActivate([in] IDispatch* Sh);
Even without considering the COM plumbing of its implementation, it has to be declared as Object because the Sheets collection holds both Worksheet and Chart objects, and the event will fire if either type of tab is activated. The two types don't share a common interface, but they do both source the same event. That means in order to pass the source object to the event handler, it has to be passed as late bound (IDispatch). The assumption is that the handler will determine what type of object it was passed and take the appropriate action based on the type of the sender.

Related

PowerPoint VBA - Passing Shapes by value seems to be happening by reference

For part of a VBA script I'm putting together I want to iterate through all the shapes on the current slide and insert another shape on top of each.
I have a first subroutine, GetShapes(), that gets all the shapes on the current slide and then passes them by value to a second subroutine, LabelShapes(), which adds the new shapes on top.
However, the new shapes seem to show up in the Shapes object that was passed. It seems like this should not be the case as it was passed by reference.
WARNING, the below will quickly lockup PowerPoint
Sub GetShapes()
Dim ss As Shapes
Set ss = Application.ActiveWindow.View.Slide.Shapes
Call LabelShapes(ss)
End Sub
Sub LabelShapes(ByVal ss As Shapes)
Dim s As Shape
For Each s In ss
Debug.Print s.Name
Application.ActiveWindow.View.Slide.Shapes.AddShape _
Type:=msoShapeRectangle, Left:=50, Top:=50, Width:=15, Height:=15
Next
End Sub
I imagine I can get around this by using a special naming convention for my new shapes and then filtering them out. Maybe there is a better way? But, really I would just like to understand why this isn't behaving the way I expect.
Not sure exactly what you're trying to do, but it's a common misunderstanding that passing object references ByVal would magically create a copy of the object.
Passing an object reference ByVal means you're passing a copy of the object pointer, as opposed to a reference to that very same object pointer.
In both cases, you're passing an object pointer that's pointing to the exact same object, so when you .AddShape, you're altering the very same shape collection you're in the middle of iterating.
Passing an object reference ByVal does NOT pass a copy of the object. If you want to pass a copy, you need to make a copy.
This might help clarify:
Public Sub DoSomething()
Dim obj As Object
Set obj = New Collection
TestByVal obj 'pass a copy of the object reference
Debug.Assert Not obj Is Nothing
TestByRef (obj) 'force a copy of the object reference (despite ByRef)
Debug.Assert Not obj Is Nothing
TestByRef obj 'pass a reference to the object pointer
Debug.Assert Not obj Is Nothing ' << assert will fail here
End Sub
Private Sub TestByVal(ByVal obj As Object)
Set obj = Nothing ' only affects the local copy
End Sub
Private Sub TestByRef(ByRef obj As Object)
Set obj = Nothing ' DANGER! call site will see this
End Sub
The solution is to use the ShapeRange object, which "represents a shape range, which is a set of shapes on a document."
Note from the Shapes documentation:
If you want to work with a subset of the shapes on a document — for example, to do something to only the AutoShapes on the document or to only the selected shapes — you must construct a ShapeRange collection that contains the shapes you want to work with.
Sub GetShapes()
Dim ss As ShapeRange
Set ss = Application.ActiveWindow.View.Slide.Shapes.Range
LabelShapes ss
End Sub
Sub LabelShapes(ByVal ss As ShapeRange)
Dim s As Shape
For Each s In ss
Debug.Print s.Name
Application.ActiveWindow.View.Slide.Shapes.AddShape _
Type:=msoShapeRectangle, Left:=50, Top:=50, Width:=15, Height:=15
Next
End Sub

VBA - Reference an object by using a variable

Not sure how to reference the worksheet object with a variable that changes each time a sheet is activated.
The point is to reference a cell value based on the last worksheet that was activated (this code affects Sheet1 which does not set the variable when activated)
--Module1
Public MyWS as String
--Sheet3 (Deactivation)
MyWS = Sheet3.Codename
--Sheet2 (Deactivation)
MyWS = Sheet2.Codename
--Sheet1
Sheet1.Range("A3").Value = MyWS.Range("A3").Value
Updated:
Thanks for all the guidance but your instructions are not working for my project at least.
Sheet5.Range("C4").Value = Worksheets(MyWS).Range("A2").Value
Subscript out of range error when the above code is executed on Sheet5 deactivate.
MyWS is declared as a public string.
MyWS is assigned the Sheet5.CodeName string when Sheet5 is activated. Sheet5 exists and that is the unmodified codename of the sheet. I can not use the user defined name of the sheet because that can change.
Public MyWS As String declares a String variable, not an object.
CodeName
The CodeName property returns a String that contains an identifier that VBA uses to generate a project-scoped object variable for a Worksheet; in the properties toolwindow (F4), that's the (Name) property.
This is how such code is legal:
Sheet1.Range("A3").Value = 42
Because Sheet1 has a code name string that returns Sheet1. Note that this identifier isn't necessarily the sheet's name (it is by default though), which the user can change at any time without accessing the Visual Basic Editor.
So if you rename the "Sheet1" tab/sheet to "Summary", but don't change its code name, then it will still be Sheet1 in code - so these two instructions do exactly the same thing:
Sheet1.Range("A3").Value = 42
ThisWorkbook.Worksheets("Summary").Range("A3").Value = 42
Now, if you want an object variable holding a reference to a worksheet that exists at compile-time, you already have one - Sheet1 is exactly that.
If you added a worksheet a run-time (doesn't exist at compile-time), then there's no such project-scope object variable for that sheet; that's when you need to declare your own, and assign it with the Set keyword:
Dim someSheet As Worksheet
Set someSheet = ThisWorkbook.Worksheets.Add
ActiveSheet
The Excel object model also has the ActiveSheet object, which returns whatever sheet is currently active.
Sheet1.Range("A3").Value = ActiveSheet.Range("A3").Value
Notice the explicit qualifiers. If it's written in a standard module (.bas), this code is equivalent:
Sheet1.Range("A3").Value = Range("A3").Value
If it's written in the code-behind of a specific worksheet module, then the above code will instead be doing this:
Sheet1.Range("A3").Value = Me.Range("A3").Value
Where Me is whatever the specific worksheet module you're in is, so if you're writing that code in a worksheet module, you will want to explicitly qualify the Range member call with the ActiveSheet object.
Worksheet Events
If you need to execute code when a worksheet is activated, you can handle the SheetActivate event in the ThisWorkbook module:
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Dim sheet As Worksheet
If TypeOf Sh Is Worksheet Then
Set sheet = Sh
Else
'Sh is not a worksheet. could be a chart sheet, or something else.
Exit Sub
End If
Debug.Print sheet.Name & " activated!"
End Sub
If you need to handle the Activated event of a specific worksheet that exists at compile-time, you need an event handler for it in that worksheet's code-behind:
Private Sub Worksheet_Activate()
Debug.Print Me.Name & " activated!"
End Sub
If you need to handle that event for a worksheet that is created at run-time, you need a WithEvents object variable in a class module (.cls):
Private WithEvents MySheet As Worksheet
And then you can write a handler for MySheet_Activate in that module, but that's more advanced stuff and I'm barely scratching the surface here, but that should get you going :)
With ActiveSheet as mentioned in the comments is really the best solution.
However, if you want to do it "your way", write these Activate events in every worksheet:
Private Sub Worksheet_Activate()
lastWS = Me.Name
End Sub
Then lastWs would be the name of the ActiveSheet. And you would be able to refer to it like this Worksheets(lastWs). Thus:
Sheet1.Range("A3").Value = Worksheets(lastWs).Range("A3").Value

Word VBA Userform, Getting Control Object By Name fails sometimes

I have a userform “myUserForm” with dozens of controls whose TypeName() is “CheckBox”. I just hate having dozens of _Click() routines named like “Private Sub Chk1_Click()”, so in order to manage the quantity of _Click() routines, I simplified and made them nearly identical:
Private Sub Chk1_Click()
ProcessClickFor ("Chk1")
End Sub
Private Sub Chk2_A_Click()
ProcessClickFor ("Chk2_A")
End Sub
Private Sub Chk3_Z_Click()
ProcessClickFor ("Chk3_Z")
End Sub
ProcessClickFor() does most of the work.
Sub ProcessClickFor(anyCheckBox As String)
Dim cbControl As Object
Set cbControl = ControlByName(anyCheckBox)
If cbControl.Value Then
cbControl.Value = True
End If
End Sub
Later, when I want to work with any control, I can get the Control object by name, like:
Dim aControl As Object
Set aControl = ControlByName(“Chk3”)
MsgBox “The control named “ & cbControl.Name & “ is “ & cbControl.Visible
Function ControlByName(sName) As Object
Dim objectified As Object
For Each objectified In myUserForm.Controls
If objectified.Name = sName Then
Set ControlByName = objectified
Exit Function
End If
Next objectified
End Function
This works fine, almost, but it FAILS on the same four controls on myUserForm every time.
The failure “mode” is that ControlByName() seems to return successfully, but the first use of the returned control (such as my MsgBox) gives the error:
"Run-time error '91': Object variable or With block variable not set".
I verified that the spelling of the defined control names matches the names in my _Click() routines. Dozens of similarly designed CheckBox controls work perfectly. Could it have to do with the length of the CheckBox names or the number of “_” characters in the CheckBox names? Could there be a corrupt character in a CheckBox name? Can you think of other things for me to try?

VBA: existing form object not detected?

I have a macro (Excel 2010) and a textbox with name CSVExport in the sheet. Here is the code:
Option Explicit
Sub Export()
Dim exportRangeStr As String
Dim currSheet As Worksheet
Set currSheet = Worksheets("Skill Experience")
exportRangeStr = currSheet.CSVExportRange.Value
Debug.Print 1
When I try to run it I get an error "Compile error: Method or data member not found" with .CSVExportRange highlighted. But when I comment that line out and set a breakpoint in the last line I see in the watch window that currSheet has the CSVExportRange field and all its data properly stored. What am I doing wrong?
The generic Worksheet class does not have a CSVExportRange member. You either need to declare your currSheet variable as an Object, or using the specific code name for the worksheet in question (or the relevant interface if you've set one up).

Why would VBA TypeOf operator fail

I have been fighting with an Excel 2007 problem for several days now. Below is a listing of all facts I can think of that might be relevant:
IDetailSheet is a class declared in the VBA project with several methods, and which throws an error in its Class Initializer so that it cannot be instantiated (making it abstract).
Option Explicit is set in all modules.
Ten worksheets in the VBA project implement IDetailSheet and compile cleanly (as does the entire project).
CDetailSheets is a class declared in the VBA project that wraps a Collection object and exposes the Collection object as a Collection of IDetailSheet. It also exposes some additional methods to perform certain methods of IDetailSheet on all collection menmbers.
In its Class initializer (called from the Workbook_ Open event handler and assigned to a global variable), CDetailSheet executes the following code to populate the private collection DetailSheets:
Dim sht as EXCEL.WorkSheet
For Each sht in ActiveWorkbook.Worksheets
If TypeOf sht is IDetailSheet Then
Dim DetailSheet as IDetailSheet
Set DetailSheet = sht
DetailSheets.Add DetailSheet, DetailSheet.Name
End If
Next sht
In certain Ribbon call-backs the following code is run:
If TypeOf ActiveWorkbook.ActiveSheet is IDetailSheet Then
Dim DetailSheet as IDetailSheet
Set DetailSheet = ActiveWorkbook.ActiveSheet
DetailSheet.Refresh *[correction]*
End If
All ActiveX controls have been removed from the Workbook, after having been identified with other stability issues (There were a few dozen originally). A Fluent Interface Ribbon has been created to replace the functionality originally associated with the ActiveX controls.
There is a Hyperion add-in from the corporate template, but it is not used in this workbook.
When all is said and done, the following symptom occurs when the workbook is run:
Any number of instances of IDetailSheet are recognized in the CDetailSheets Initializer by TypeOf Is, from 1 (most common) to occasionally 2 or 3. Never zero, never more than 3, and most certainly never the full 10 available. (Not always the same one, though being near the front of the set seems to increase likelihood of being recognized.)
Whichever instances of IDetailSheet implementation are discovered in the CDetailSheets initializer (and as near as I can determine, only such instances) are also recognized by TypeOf ... Is in the Ribbon call-back.
Can anyone explain why most of the TypeOf ... Is operations are failing? Or how to fix the issue?
I have resorted to manually creating v-tables (i.e. big ugly Select Case ... End Select statements) to get the functionality working, but I actually find it rather embarrassing to have my name beside such code. Besides which, I can see that being a future maintenance nightmare.
Thinking that it might be a stale p-code issues, I went to the extent of deleting the Project.Bin file from the expanded XLSM zip, and then manually importing all the VBA code back in. No change. I also tried adding the project name to all the usages of IDetailSheet to make them miFab.IDetailSheet, but again to no avail. (miFab is the project name.)
There are a few ways you could cheat using CallByName. You're going to have to work around this bug one way or another.
A quick dirty example
Every sheet that starts with an implementing line should have a public GetType function.
I attached the "TestSheet" sub to a button on my ribbon. It puts the returned type name in cell A1 to demonstrate the function.
Module1
'--- Start Module1 ---
Option Explicit
Public Sub TestSheet()
Dim obj As Object
Set obj = ActiveSheet
ActiveSheet.[A1] = GetType(obj)
End Sub
Public Function GetType(obj As Object) As String
Dim returnValue As String
returnValue = TypeName(obj)
On Error Resume Next
returnValue = CallByName(obj, "GetType", VbMethod)
Err.Clear
On Error GoTo 0
GetType = returnValue
End Function
'--- End Module1 ---
Sheet1
'--- Start Sheet1 ---
Implements Class1
Option Explicit
Public Function Class1_TestFunction()
End Function
Public Function GetType() As String
GetType = "Class1"
End Function
'--- End Sheet1 ---
I found this question after posting my own similar issue as TypeOf fails to work with Excel workbook's ActiveSheet that implements interface
I don't have a definitive explanation, but I think I do have a workaround.
I suspect it is because [the code] is implementing an interface on Sheet1 or Chart, and is extending Sheet1/Chart1, but Sheet1 is already extending Worksheet (and Chart1 is already extending Chart).
In my testing, I am able to force VBA to return the real value of TypeOf by firstly accessing a property of the sheet. That means, doing something ugly like:
'Explicitly access ThisWorkbook.ActiveSheet.Name before using TypeOf
If TypeOf ThisWorkbook.Sheets(ThisWorkbook.ActiveSheet.Name) Is PublicInterface Then
If you're not trusting TypeOf, plough on and ignore errors:
Dim sht as EXCEL.WorkSheet
For Each sht in ActiveWorkbook.Worksheets
'If TypeOf sht is IDetailSheet Then
Dim DetailSheet As IDetailSheet
On Error Resume Next
Set DetailSheet = sht
On Error GoTo 0
If Not DetailSheet Is Nothing Then
DetailSheets.Add DetailSheet, DetailSheet.Name
End If
Next sht
If this doesn't work, the worksheets really aren't IDetailSheet at that time at least.