How to implement "CanRaiseEvent" property in VB6? - vb6

Reference Link to "CanRaiseEvent" property: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.canraiseevents?view=netframework-4.8#System_Windows_Forms_Control_CanRaiseEvents
Given that events in VB6 are recursive, I would like to disable a given controls event, while in a event handler sub.
For example, currently, if in a TextBox change event the contents of the TextBox are changed (say to "test") another event will fire causing the state of the current event to be stacked and the TextBox change event will be called again. I can prevent the event from repeating the code with a simple "If" (e.g. If Text1.Text = "test" Then Exit Sub). This will immediately exit the second event and return to the state of the first event to continue processing. However, I would prefer to disable the TextBox event on entering the initial event. It would appear that the property cited in the reference link would accomplish this, but it is not recognized by VB6.
Is there something I need to declare first to use the "CanRaiseEvent" property in VB6?
Thanks X

The typical way this is done is with a Static ... Boolean variable:
Sub SomeEvent
Static inHere As Boolean
If inHere Then
Exit Sub
Else
inHere = True
End If
.....
inHere = False
End Sub

You can do it kind of like this, but it might not be worth the ffort. (And by the way, I don't have the VB6 IDE on this PC so this will be un-syntax checked but it should get you started.
Within your Form (and for this example let's say the textbox you care about is txtFirstName):
Private WithEvents mFirstNameEventListener As Textbox
Private Sub mFirstNameEventListener_TextChanged() 'or whatever the event is called in VB6
DisableEvents
'do work here, like forcing to upper-case etc
EnableEvents
End Sub
Sub Form_Load()
EnableEvents
End Sub
Sub Form_Unload()
DisableEvents
End Sub
Private Sub EnableEvents()
Set mFirstNameEventListener = txtFirstName
End Sub
Private Sub DisableEvents()
Set mFirstNameEventListener = Nothing
End Sub
Also note, you would not use the standard VB6 stubbed out event handler too. you would only do it this way. (Otherwise both event handlers would get called)

Related

How to pass arguments to an event listener in LibreOffice Basic?

I am writing a macro to automate tasks on a spreadsheet in LibreOffice Calc.
One of those tasks is simply to add the numbers contained in a given cell range and write the total in the appropriate cell when one of these cells is edited. (The cells actually contain text: the names of different services. The program then fetches the number of hours associated with each service's name to add them all up.)
Editing such a cell triggers the Modify_modified(oEv) event listener.
The listener then calls the subroutine UpdateTotalHoursOfAgent(calendarSize, allServices, agentTopleftCell) which performs the task described above.
The problem is that arguments calendarSize and allServices, which are defined in other places in the code, are out of scope in the event listener.
I do not know how to pass those arguments to the listener.
I tried using global variables instead even though it is frowned upon, but I suspect that they reach the end of their lifetime when the main program's execution is complete, and are not available anymore when a cell is edited afterwards.
How can I pass arguments calendarSize and allServices to the UpdateTotalHoursOfAgent subroutine when Modify_modified(oEv) is triggered?
Here's part of the code used to create the event listener (found on a forum):
Private oListener, cellRange as Object
Sub AddListener
Dim sheet, cell as Object
sheet = ThisComponent.Sheets.getByIndex(0) 'get leftmost sheet
cellRange = sheet.getCellrangeByName("E4:J5")
oListener = createUnoListener("Modify_","com.sun.star.util.XModifyListener") 'create a listener
cellRange.addModifyListener(oListener) 'register the listener
End Sub
Sub Modify_modified(oEv)
' *Compute agentTopleftCell*
REM How to obtain calendarSize and allServices from here?
UpdateTotalHoursOfAgent(calendarSize, allServices, agentTopleftCell)
End Sub
Sub Main
' *...code...*
Dim allServices As allServicesStruct
Dim calendarSize As calendarStruct
AddListener
' *...code...*
End Sub
I tried using global variables...
Probably you did not do it correctly. Here is how to set a global variable.
Private oListener, cellRange as Object
Global AllServices
Type allServicesStruct
svc As String
End Type
Sub AddListener
Dim sheet, cell as Object
sheet = ThisComponent.Sheets.getByIndex(0) 'get first sheet
cellRange = sheet.getCellrangeByName("E4:J5")
oListener = createUnoListener("Modify_","com.sun.star.util.XModifyListener")
cellRange.addModifyListener(oListener)
End Sub
Sub Modify_modified(oEv)
MsgBox AllServices.svc
End Sub
Sub Main
Dim allServicesLocal As allServicesStruct
allServicesLocal.svc = "example"
AllServices = allServicesLocal
AddListener
End Sub
Result:
This was adapted from my answer at https://stackoverflow.com/a/70405189/5100564

How to create a Static member inside a form in Visual Basic 6?

I need to keep some object static (without change ) inside a form in vb 6. The object is passing to the form before form.Show vbModeless code block. There is a list inside this form and when item click event fires, the object becomes Nothing.
In my application, there is a class and a form. I need to use an object of the class inside the form. In the main calling class there's a sub method to load the form and call it's aainitialization method too. I pass the SelectStyleDlg object to the form as follows.
Below I have mentioned the sub method that I am using in the calling class.
Public Sub ShowTheDialog()
With frmSelectStyle
.aaInitialize SelectStyleDlg:=SelectStyleDlg
.Show vbModeless
End With
End Sub
Now I am going to mention the code in the form.
Option Explicit
Private mobjSelectStyleDlg As SelectStyleDlg
Public Static Sub aaInitialize(ByRef SelectStyleDlg As SelectStyleDlg)
Set mobjSelectStyleDlg = SelectStyleDlg
End Sub
Private Sub lvwStyles_ItemClick(ByVal Item As MSComctlLib.ListItem)
If Not mobjSelectStyleDlg Is Nothing
MsgBox "Object is not nothing"
Else
MsgBox "Object is nothing"
End If
End Sub
When item click event fires the mobjSelectStyleDlg object becomes nothing.
Please help me. Thank you.
I don't think you mean static, I think you just need a class instance variable.
Within your form:
Private mSelectStyleForm As frmSelectStyle
Private Sub cmdSelect_Click()
If mSelectStyleForm Is Nothing Then
Set mSelectStyleForm = New frmSelectStyle
End If
Call mSelectStyleForm.aaInitialize(SelectStyleDlg:=SelectStyleDlg)
Call mSelectStyleForm.Show(vbModeless) 'Or Modal
End Sub
Private Sub MyForm_Unload()
If Not mSelectStyleForm Is Nothing Then
'depending on if you unload the form earlier or not, do that too here
Set mSelectStyleForm = Nothing
End If
End Sub

Handling GotFocus / LostFocus events on all TextBoxes in a Form VB6

I would like to create a handler which listens GetFocus / LostFocus events for all TextBoxes in a Form using VB6 how can I achieve that?
What i tried so far:
Option Explicit
Dim Cnt As Control
Private WithEvents Txt As VB.TextBox
Private Sub Form_Load()
For Each Cnt In Me.Controls
If TypeOf Cnt Is TextBox Then
Set Txt = Cnt
End If
Next Cnt
End Sub
Private Sub Txt_GotFocus()
Txt.BackColor = &H80000018
End Sub
Private Sub Txt_LostFocus()
Txt.BackColor = &H80000005
End Sub
but this only works for one TextBox in the Form
this only works for one TextBox in the Form because Txt can only refer to one textbox at a time.
One way to have a common handler is to create your Texboxes as a control array. Give them all the same name (ie txtBox). VB will automatically make an array of them. You can control their order in the array using the Index property. Now, your LostFocus will look like this:
Private Sub txtBox_LostFocus(Index As Integer)
txtBox(Index).Backcolor = &H80000005
End Sub
If you need to change what you do based on WHICH textbox it is, use the Index to tell which one it is. NOTE: Control arrays are quite handy, but they disappear in VB.NET. There are some equivalent methods but I would not get too attached to the exact way they work.
For more complex ops, the several events can call a common procedure passing the control as an argument.

How to check if CURRENT event is not during the OPEN event of form

I have an event procedure which is to be run when user selects a certain record.
If I put it in an ON CURRENT event, it works.
However, I don't need it to run when the form is being opened. That is, when the form is opened and the records are loaded onto the form, my event procedure is called up for every
record that is loaded. It slows my program down as this is unnecessary.
My question is, how do I check that the event is not run during the "loading" of the records.
something like:
on_current_event do:
if event is not during on_load then
do this
end if
end proc
Why not declare a form level boolean variable and set it to true when the form has finished loading?
In your OnCurrent event you can then do something like:
If Variable then
Do Stuff
Else
Don't do stuff
End If
This is probably not addressing the underlying issue however...
In your form's code:
Private isLoading As Boolean
Private Sub Form_Open(Cancel as Boolean)
isLoading = True
End Sub
Private Sub Form_Current()
If isLoading Then
isLoading = False
Exit Sub
End If
End Sub
That way the first occurence of the OnCurrent event when the form is opening will be bypassed.
That being said, it looks to me that this is not your real underlying problem but you will need to give more information (open another question) if you want other people to try to help.

How to gracefully exit from the middle of a nested subroutine when user cancels?

(I'm using VB6 but I imagine this comes up in most other languages.)
I've got a GUI button that calls a routine that takes a minute or two to complete. I want impatient users to be able to click on the button a second time to have it gracefully exit out of the routine at any point.
I used a static variable to make this work pretty well (see code below), but I'm cleaning up the project and I want to put the For/Next loop into its own function, since it's required in several different places in the project.
But doing that would break my static flag embedded in the for/next, so I need to make some changes. Before I do something hare-brained with public (global) variables, I thought I'd ask what other (smarter, perhaps actually CS educated) people have done when faced with this problem.
So basically my question is how do I replicate this:
Private Sub DoSomething_Click()
Static ExitThisSub As Boolean ' Needed for graceful exit
If DoSomething.Caption = "Click To Stop Doing Something" Then
ExitThisSub = False ' this is the first time we've entered this sub
Else ' We've re-entered this routine (user clicked on button to stop it)
ExitThisSub = True ' Set this so we'll see it when we exit this re-entry
Exit Sub '
End If
DoSomething.Caption = "Click To Stop Doing Something"
For i = 0 To ReallyBigNumber
Call DoingSomethingSomewhatTimeConsuming
If ExitThisSub = True Then GoTo ExitThisSubNow
DoEvents
Next
' The next line was missing from my original example,
' prompting appropriate comments
DoSomething.Caption = "Click To Do Something"
Exit Sub
ExitThisSubNow:
ExitThisSub = False ' clear this so we can reenter later
DoSomething.Caption = "Click To Do Something"
End Sub
When I move the for/next loop to its own function?
I'm thinking I'll change ExitThisSub to a public variable QuitDoingSoManyLongCalculations that will exit the new for/next sub and then the DoSomething_Click in the same way.
But I always feel like an amateur (which I am) when I use global variables - is there a more elegant solution?
Well you could declare the variable at module level in the forms as private.
That is not a global but a module level variable.
Then you could pass it to the function you create and check it in the function.
But be careful with the DoEvents. It basically means allow the windows message loop to process messages. This means that not only can the user click your button again, they can close the form and do other things. So when you are in this loop you'll need to set a module level variable anyway as you'll need to check for it in a QueryUnload of the form and in any event handlers.
You can also use the Tag property of the control itself to store a flag of sorts. But I don't consider that more elegant.
I also prefer to use two different buttons. Just hide one and show the other. That way your cancel code and run code are separated in different event handlers.
To expand on my answer here is some sample code that handles the unloading aspect.
Here if you stop via the x it prompts you. If you kill via task manager, it dies gracefully.
Option Explicit
Private Enum StopFlag
NotSet = 0
StopNow = 1
StopExit = 2
End Enum
Private m_lngStopFlag As StopFlag
Private m_blnProcessing As Boolean
Private Sub cmdGo_Click()
Dim lngIndex As Long
Dim strTemp As String
m_lngStopFlag = StopFlag.NotSet
m_blnProcessing = True
cmdStop.Visible = True
cmdGo.Visible = False
For lngIndex = 1 To 99999999
' check stop flag
Select Case m_lngStopFlag
Case StopFlag.StopNow
MsgBox "Stopping - Last Number Was " & strTemp
Exit For
Case StopFlag.StopExit
m_blnProcessing = False
End
End Select
' do your processing
strTemp = CStr(lngIndex)
' let message loop process messages
DoEvents
Next lngIndex
m_lngStopFlag = StopFlag.NotSet
m_blnProcessing = False
cmdGo.Visible = True
cmdStop.Visible = False
End Sub
Private Sub cmdStop_Click()
m_lngStopFlag = StopFlag.StopNow
End Sub
Private Sub Form_Load()
m_blnProcessing = False
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
Select Case UnloadMode
Case vbFormControlMenu, vbFormCode
If m_blnProcessing Then
Cancel = True
If MsgBox("Unload Attempted - Cancel Running Process?", vbOKCancel + vbDefaultButton1 + vbQuestion, "Test") = vbOK Then
m_lngStopFlag = StopFlag.StopExit
End If
End If
Case Else
m_lngStopFlag = StopFlag.StopExit
Cancel = True
End Select
End Sub
You need some kind of shared variable so that your for loop and your button can communicate. I'd put the for loop (and its associated code) in a command object. My VB is rusty but I think you can declare Modules with their own 'global' variables and functions. You can move all the code into a module and just check the global variable as you do now.
My main concern with the code sample you've posted has nothing to do with user-cancelling but rather everything else: you check your running state by reading the button text instead of doing that the other way around (set the button text because of the running state, which should be stored in a variable); you use a GOTO to exit your for loop instead of a break (does VB have breaks?) and you put your cleanup code outside the normal flow, when it seems to me that it could be run regardless of whether the user cancelled or not.
One possible alternative is to offload your heavy-work function to a new Thread. Then you can either directly kill that thread if the user wants to cancel, or you can send a message to the thread.
Toggling via button name, as you are doing above, is a pretty commonly seen trick though, and pretty safe if you only have a couple of button states.
I've always used a global boolean variable like bUserPressedCancel, along with DoEvents within a loop. Elegant, smelegant, it works.
I agree with Mr Shiny that testing against the value of a caption is not a great idea. If you change the text on the button in the designer, you'll break the code. Better not to rely on the wording of the text for your code to work.
Works until you need to do localization or any other thing else that require the UI to be changed independently of the logic. I would use the Tag property or a private module level variable . Then you can vary the caption independently from the logic.
If it was me I'd use 2 buttons - one to GO and one to STOP. The STOP button is made visible when you click GO. The Click event for STOP simply hides itself - that's it.
Your loop can then simply check to see if the STOP button is still visible. If it's not, that means it was clicked and you should break out.
Your controls are static objects with form scope...

Resources