How to set properties of userform inside function - vba

On the UserForm_Activate() code, I want to call a function (which has not been created yet) that will set scroll properties of the Userform. See code below.
Private Sub UserForm_Activate()
MakeFormResizable
End Sub
On the Modules4 sheet, I have the following code:
Public Sub MakeFormResizable()
Me.ScrollHeight = .Height
Me.ScrollWidth = .Width
End Sub
When I run the code that I have pasted, I get the error "Compile error: Invalid or unqualified reference."

It will not work as Me is local ….
Perhaps you want to pass the Userform object to a Public sub, as follows:
Public Sub MakeFormResizable(ByRef aUserform As MSForms.UserForm)
With aUserform
'Do your stuff
End With
End Sub

Got it!
Private Sub UserForm_Activate()
Dim MyObject
Set MyObject = Me
MakeFormResizable MyObject
End Sub
Public Sub MakeFormResizable(MyObject)
With MyObject
.ScrollHeight = .Height
End If
End With
End Sub

Related

VBA CommandBarButton does not run code OnAction

Once the button is pressed it does not perform the subroutine defined OnAction method.
I have checked all the Security options in Access are enabled and have written the same code in different ways.
I have tried to run a function with the OnAction method instead.
Private Sub Check_Status_Click()
Dim cmdBAR As CommandBar
Dim cmdButton1 As CommandBarButton
Set cmdBAR = CommandBars.Add(, msoBarPopup, False, True)
Set cmdButton1 = cmdBAR.Controls.Add(msoControlButton)
cmdButton1.Caption = "Dale"
cmdButton1.OnAction = "Dale"
cmdBAR.ShowPopup
'Clean
Set cmdBAR = Nothing
Set cmdButton1 = Nothing
End Sub
Public Sub Dale()
MsgBox "hola"
End Sub
I dont get any error, just it is not doing anything even the menu shows up.
Actually OnAction subroutine needs to be
public sub
in public module
So you should change your code to something like this:
...
cmdButton1.Caption = "Dale"
cmdButton1.OnAction = "Dale"
cmdBAR.ShowPopup
...
And place your sub into some public module:
Public Sub Dale()
MsgBox "hola"
End Sub

Handling custom events using modeless form & user defined classes

I'm trying to display the progress of various routines on a modeless form, by having those routines raise custom events detailing their progress. The form should handle those events to display appropriate information.
The problem is that although RaiseEvent is called, the event handlers don't then do anything.
The intended result of the following code is the two debug.prints would be called whenever an event is raised by triggerTest.
The only success I've had is with raising the error within the userform, by CommandButton1_Click in the following code. The form's event handler then kicks in (rather redundantly, but perhaps that means I'm on the right path).
Thanks
Event class clsChangeProgressTrigger
Option Explicit
Public Enum geProgressStatus
geProgressStatusComplete = -1
geProgressStatusRestart = -2
End Enum
Public Event ChangeProgress(dProgress As Double, sProcedure As String)
'
Public Sub Update(dProgress As Double, sProcedure As String)
RaiseEvent ChangeProgress(dProgress, sProcedure)
End Sub
Public Sub Complete(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusComplete, sProcedure)
End Sub
Public Sub Restart(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusRestart, sProcedure)
End Sub
User form frmOutput
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub CommandButton1_Click()
Call mProgressTrigger.Update(12.34, "SomeValue")
End Sub
Private Sub CommandButton2_Click()
Call modZTest.triggerTest
End Sub
Private Sub UserForm_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Form Event Handled"
End Sub
Event test class clsEventTest
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Class Event Handled"
End Sub
Private Sub Class_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Test wrapper in public module modZTest
Public Sub triggerTest()
Application.EnableEvents = True
' Instantiate Trigger class for this routine
' Dim cChangeProgressTrigger As clsChangeProgressTrigger
Set gChangeProgressTrigger = New clsChangeProgressTrigger
' Instantiate Event Test class, which should handle raised event
Dim cEventTest As clsEventTest
Set cEventTest = New clsEventTest
' Instantiate user form, which should handle raised event
Set gfrmOutput = New frmOutput ' Modeless form, gfrmOutput has global scope
gfrmOutput.Show
Stop
' Raise an event
Call gChangeProgressTrigger.Complete("SomeValue")
' Tidy Up
Set gfrmOutput = Nothing
Set gChangeProgressTrigger = Nothing
Set cEventTest = Nothing
End Sub
Thanks Dee, that helped me along to the solution.
With this declared as global scope:
Public gChangeProgressTrigger As clsChangeProgressTrigger
I had to change the class / form level initialisations as follows:
Private Sub UserForm_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
and
Private Sub Class_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
Then the event handlers fired as desired.

Creating A Custom Event With VBA

I'm trying struggling to understand how to create a custom event using class modules in VBA.
I've put together the simple following example. You put a value in A1 and B1 and then re activate the sheet to calculate the sum of the two and then I hoped an event would fire to warn of calculation, but nothing happens.
I'd be very grateful for any help solving this example.
Class module cCalc:
Dim m_wks As Worksheet
Public Event BeforeCalc()
Property Set Worksheet(wks As Worksheet)
Set m_wks = wks
End Property
Public Sub Calc()
Dim dVal1 As Double
Dim dVal2 As Double
With m_wks
dVal1 = .Range("A1").Value
dVal2 = .Range("B1").Value
RaiseEvent BeforeCalc
.Range("C1").Value = dVal1 + dVal2
End With
End Sub
In a module mGlobal:
Public gCalc As cCalc
In the code behind Sheet1:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set gCalc = New cCalc
Set gCalc.Worksheet = ActiveSheet
gCalc.Calc
End Sub
You can't declare event-driven classes in modules. You'll need to set the cCalc reference in gModule equal to the object you declared WithEvents in Sheet1. Change your code in Sheet1 to what i wrote below and it will work:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc 'Instantiate the WithEvents object above
Set mGlobal.gCalc = calcEvent 'Set the object declared in gModule
Set mGlobal.gCalc.Worksheet = ActiveSheet
mGlobal.gCalc.Calc
End Sub
Note that this is using the variable you put in gModule... The event that is actually called is still calcEvent_BeforeCalc(), which is good as this way you can have n number of objects defined in gModule that would all fire off the same event code when the event is triggered.
To simplify the code, you could always just write:
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc
Set calcEvent.Worksheet = ActiveSheet
calcEvent.Calc
End Sub

Calling OnTime method in a Custom Class

I'm trying to use the .OnTime method in a class module, but can't figure out how to call a procedure in the class. All of the .OnTime examples I've seen refer to using the method from a standard code module rather than a custom class. Is there any way of calling a procedure in the class module rather than a standard code module?
#Alex P: Updated to include code. Here is the Class Module:
Option Explicit
Public Sub Test()
MsgBox "Success"
End Sub
Private Sub Class_Initialize()
Application.OnTime EarliestTime:=Now + TimeValue("00:00:03"), _
Procedure:="Test"
End Sub
And the Standard Module:
Option Explicit
Public Sub TestOnTime()
Dim OnTime As CCOnTime
Set OnTime = New CCOnTime
End Sub
I've also tried Procedure:="CClass.Test"
You can do it, but the call-back needs to be bounced back into the object from a Standard Module or a Worksheet Module or Thisworkbook.
Here is an example that pulses a value in a worksheet cell.
The timer is (almost) encapsulated in the cOnTime Class.
A cOnTime Object is instantiated in the host worksheet, whose code module can have a property to set the pulse time as well as the call-back routine.
If you protect the sheet, it will start pulsing and you can stop it by un-protecting the sheet.
If you navigate away from the host sheet, the timer is killed and if you navigate back it re-starts (as long as the sheet is protected).
Class cOnTime
Option Explicit
Const DEFPulseTime = "PulseTime"
Const DEFearliestTime As Long = 5
Const DEFlatestTime As Long = 15
Public WithEvents wb As Workbook
Public ws As Worksheet
Private DoWhen As String
Public mPulseTime As Long
Public mNextTime As Double
Property Let callBackDoWhen(cb As String)
DoWhen = "'" & wb.Name & "'!" & ws.CodeName & "." & cb 'e.g. 'wb Name.xlsm'!Sheet1.kickdog
End Property
Private Function PulseTime() As Long
On Error Resume Next
PulseTime = CallByName(ws, DEFPulseTime, VbGet)
If Err.number <> 0 Then
PulseTime = DEFearliestTime
End If
On Error GoTo 0
End Function
Property Get designMode() As Boolean
designMode = Not ws.ProtectContents
End Property
Public Sub kickDog()
Const myName As String = "kickDog"
Dim psMessage As String
If ws Is ActiveSheet And Not designMode Then
mNextTime = Now + TimeSerial(0, 0, mPulseTime)
On Error Resume Next
Application.OnTime mNextTime, DoWhen
On Error GoTo 0
End If
Exit Sub
End Sub
Public Sub killDog()
If ws Is Nothing Or mNextTime = 0 Then Exit Sub
On Error Resume Next
Application.OnTime mNextTime, DoWhen, , False
On Error GoTo 0
End Sub
Private Sub Class_Initialize()
Dim errorContext As String
On Error GoTo enableAndExit
Set wb = ActiveWorkbook
Set ws = ActiveSheet
On Error GoTo 0
callBackDoWhen = DEFDoWhen
callBackPulseTime = DEFPulseTime
mPulseTime = PulseTime
kickDog
Exit Sub
enableAndExit:
If Err <> 0 Then
If ws Is Nothing Then
errorContext = "ws"
ElseIf wb Is Nothing Then
errorContext = "wb"
End If
End If
End Sub
Private Sub Class_Terminate()
Const myName As String = "Class_Terminate"
On Error Resume Next
killDog
Set ws = Nothing
Set wb = Nothing
Exit Sub
End Sub
Private Sub wb_WindowActivate(ByVal Wn As Window)
wb_Open
End Sub
Private Sub wb_WindowDeactivate(ByVal Wn As Window)
killDog
End Sub
Private Sub wb_BeforeClose(Cancel As Boolean)
killDog
End Sub
Private Sub wb_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
If SaveAsUI Then killDog
End Sub
In Worksheet Module
Option Explicit
Const cPulseTime As Long = 1
Dim mOnTime As cOnTime
Property Get PulseTime() As Long
PulseTime = cPulseTime
End Property
'****************************************
'Timer call-back for cOnTime
Public Sub kickDog()
' Code to execute on timer event
'******************************************
On Error Resume Next
Me.Cells(1, 1) = Not Me.Cells(1, 1)
On Error GoTo 0
'******************************************
Debug.Print "woof!!"
mOnTime.kickDog
End Sub
Private Sub Worksheet_Activate()
Me.Cells(1,1) = False
Set mOnTime = New cOnTime
End Sub
Private Sub Worksheet_Deactivate()
On Error Resume Next
Set mOnTime = Nothing
End Sub
You are asking for magic - VBA is a comprehensive tool but it is not magic.
The reason is that every Class module is simply a template which can be instantiated any number of times in the application code. Excel could not hope to correctly guess which particular instantiation of the Class module is the correct one on which to invoke the method. You are responsible for making this decision and managing the references to the appropriate Class instance.
Ah you say - But there is no private data/references being used by the method I want called. It is a static method. Well the answer to that is that VBA does not support static methods on Class modules, only on Standard modules. Any method that you wish to declare to the environment as being static is declared as being static by being included in a standard module.
So, place your call-back method in a Standard module, and declare a private member that holds a reference to the particular instance of the Class that you wish to handle the event.
One alternative route which isn't specified by others here but, I think, would theoretically work is:
Register VBA object as an active object with a specified GUID
Launch a PowerShell (or binary) daemon to connect to the object with specified GUID and call a specified method on that object every n seconds.
Pros:
Totally encapsulated
Cons:
Launches external process
Method likely has to be public (unless connection points can be abused)
Potentially crash-prone
I haven't implemented such a solution yet but intend to on stdCOM of stdVBA library if everything works out.

Programmatically adding a commandbutton to a userform

In excel vba I have added a commandbutton to userform... like below
Set ctrl = Me.Controls.Add( _
bstrProgID:="Forms.CommandButton.1", _
Name:="CommandButton1", Visible:=True)
Now I wanted to know how would I tell it what to do when it is clicked?
This is one of those techniques that vba will let you do, but you probably shouldn't. For all the same reasons you shouldn't use code that alters your code.
That said, here is how to do what you want. First insert a class module and name it DynBtn, then paste this code into it:
Private WithEvents mobjBtn As MSForms.CommandButton
Private msOnAction As String
''// This has to be generic or call by name won't be able to find the methods
''// in your form.
Private mobjParent As Object
Public Property Get Object() As MSForms.CommandButton
Set Object = mobjBtn
End Property
Public Function Load(ByVal parentFormName As Object, ByVal btn As MSForms.CommandButton, ByVal procedure As String) As DynBtn
Set mobjParent = parentFormName
Set mobjBtn = btn
msOnAction = procedure
Set Load = Me
End Function
Private Sub Class_Terminate()
Set mobjParent = Nothing
Set mobjBtn = Nothing
End Sub
Private Sub mobjBtn_Click()
CallByName mobjParent, msOnAction, VbMethod
End Sub
Now to use this in your form, create a blank user form and paste this code into it:
Private Const mcsCmdBtn As String = "Forms.CommandButton.1"
Private mBtn() As DynBtn
Private Sub UserForm_Initialize()
Dim i As Long
ReDim mBtn(1) As DynBtn
For i = 0 To UBound(mBtn)
Set mBtn(i) = New DynBtn
Next
''// One Liner
mBtn(0).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn1", True), "DoSomething").Object.Caption = "Test 1"
''// Or using with block.
With mBtn(1).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn2", True), "DoSomethingElse").Object
.Caption = "Test 2"
.Top = .Height + 10
End With
End Sub
Public Sub DoSomething()
MsgBox "It Worked!"
End Sub
Public Sub DoSomethingElse()
MsgBox "Yay!"
End Sub
Private Sub UserForm_Terminate()
Erase mBtn
End Sub