Bind many control events to same handler, get sender - vba

I'm building a form Access with a load of TextBox controls on it. The GotFocus() event for every one of them will be exactly the same:
Private Sub Text1_GotFocus()
Text1.BorderColor = RGB(100, 100, 255)
...
End Sub
Private Sub Text2_GotFocus()
Text2.BorderColor = RGB(100, 100, 255)
...
End Sub
'... ad infinitum
This is, naturally, a maintanence nightmare and aesthetically a vast wad I have to keep scrolling past. I can throw BorderColor = RGB(100, 100, 255) etc into a function and have every handler call that function, but I'm still left with 3-line identical blocks for every single TextBox - throw in LostFocus and other events that are handled identically regardless of the TextBox, and it just becomes silly.
So the sensible thing to do would be to have a single AllTextBoxes_GotFocus() method, and have every TextBox's On Got Focus event point to that. Two problems though:
I cannot see any functions or subs I've defined in VBA in the drop-down in the form designer, only [Event Procedure] (which generates the standard Private Sub Text1_GotFocus() method) and any Macros in the application. Which is... odd. Considering Macros have a RunCode option for calling VBA functions, it seems a bizarre round-the-houses way of calling code, having to get the control to call the macro to call the code. Surely there's a better way (and I think Macros can only call functions in modules, not on forms).
I'm not sure how to get the sender, so I can set the appropriate control's border. VB.NET passes in the sender and event args in its events: Private Sub Text1_GotFocus(ByVal sender As System.Object, ByVal e As System.EventArgs), but VBA does not.
How can I handle mutliple events with a single handler, and get the sender of the event within the handler?

Here is how I do it but know this will overwrite any custom GotFocus events
Private Sub Form_Load()
On Error Resume Next
Dim ctrl As Control
For Each ctrl In Me.Form.Controls
If ctrl.ControlType = acTextBox Then
ctrl.GotFocus = "=changeColor('" & ctrl.name & "',100,100,255)"
'For LostFocus
ctrl.LostFocus = "=changeColor('" & ctrl.name & "')"
End If
Next ctrl
End Sub
Function changeColor(field As String, Optional red AS Integer =0 ,green AS Integer =0,blue As Integer = 0)
Me.Form.Controls(field).BorderColor = RGB(red, green, blue)
End Function

Related

VB: How to manually call or delay events?

Background: I'm new to vb, coming from javascript.
My assignment is to use the txtBox.LostFocus event to do some validation. The problem is I need to cancel the validation step if the user intends to press either two of three buttons.
Illustration:
Code
Dim lblInputBox As Label
lblInputBox.Text = "Name:"
Dim txtInputBox As TextBox
Dim btnClear As Button
btnClear.Text = "Clear"
Dim btnSayName As Button
btnSayName.Text = "Say Name"
Dim btnExit As Button
btnExit.Text = "Exit"
' Some boolean to determine what the next action is
Dim UserIntentDetected = False
' When the user moves focus away from the textbox
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus
' I want to be able to detect the next focus, but this is where I'm going wrong
If btnExit.GotFocus Or btnClear.GotFocus Then
UserIntentDetected = True
Else
UserIntentDetected = False
End If
' Only call validate if there is no intent to quit or clear
If Not UserIntentDetected Then
Call validate()
End If
' Reset the intent boolean
UserIntentDetected = False
End Sub
' Validate subroutine
Private Sub validate()
' **Fixed description**
' User moved focus away from txtbox and doesn't intend to clear or exit
Console.WriteLine("You're NOT INTENDING to clear or exit")
End Sub
I've tried to add the two button's GotFocus event to the input box's LostFocus event handler, but there were bugs with the event firing multiple times in a loop.
Eg:
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus, btnExit.GotFocus, _
btnClear.GotFocus
... (code follows)
These attempts are entirely wrong, but from a javascript background, although also entirely wrong, I could hack something like this..
... (assume same element structure from vb)
var eventQueue = [];
var userIntentDetected = false;
txtInputBox.addEventListener("blur", function(event){
// Set a timeout to manually trigger the last event's click method in the eventQueue
setTimeout(function(){
if (eventQueue.length > 0 && userIntetDetected)
runIntendedHandler(eventQueue[eventQueue.length -1]);
}, 500);
})
// Both event listeners listen for click and stop default actions,
// set intent boolean to true, and add it's event object to a queue
btnExit.addEventListener("click", function(event){
event.preventDefault();
userIntentDetected = true;
eventQueue.push(event);
});
btn.addEventListener("click", function(event){
event.preventDefault();
userIntentDetected = true;
eventQueue.push(event);
});
// Validation should occur only if the user moves focus to an element
// that IS NOT either the btnExit or btnClear
function runIntendedHandler(event){
if (event.target.id = "btnExit")
// run exit functions
code...
else if (event.target.id = "btnClear")
// run clear functions
code..
userIntentDetected = false;
}
What is the proper way to work with events in vb and how would I go about detecting the next event in the queue before triggering an action? could the RaiseEvent statement help?
UPDATE 3: The answer was a lot easier than I made it seem. Apparently, you can use the btn.Focused property to check the next focus of an element from within the txtInputBox.LostFocus event handler... Go figure!
UPDATE 2: There's been a lot of confusion as to what exactly was needed, and a lot of that was my fault in describing the validation subroutine. I've changed some of the element names and added an image to sum up all of the information that was given to me by my instructor.
UPDATE 1: #TnTinMn has provided the closest working answer that can be used with a minor alteration.
Example follows:
Private LastActiveControl As Control = Me ' initialize on form creation
Protected Overrides Sub UpdateDefaultButton()
' Just added an IsNot condition to stay inline with the requirements
If (LastActiveControl Is txtNumberOfDrinks) AndAlso
((ActiveControl Is btnClear) OrElse (ActiveControl Is btnExit)) Then
Console.WriteLine("Your intent to press either btnClear or btnExit has been heard...")
' Validation happens only if the user didn't intend to click btnClear or btnExit
ElseIf (LastActiveControl Is txtNumberOfDrinks) AndAlso
((ActiveControl IsNot btnClear) OrElse (ActiveControl IsNot btnExit)) Then
Console.WriteLine("You didn't press either btnClear or btnExit.. moving to validation")
validateForm()
End If
LastActiveControl = ActiveControl ' Store this for the next time Focus changes
End Sub
Thank you all!
Winform's event order is confusing at best. For a summary, see Order of Events in Windows Forms.
Assuming I have interpreted your goal correctly, I would not respond to the txtInputBox.LostFocus event but rather override a little known Form method called UpdateDefaultButton. This method is called when the Form.ActiveControl property changes and effectively gives you an ActiveControl changed pseudo-event. If the newly ActiveControl is a Button, this method also executes before the Button.Click event is raised.
Since you want to call your validation code only when the focus changes from txtInputBox to either btnPromptForName or btnExit, you can accomplish that with something like this.
Public Class Form1
Private LastActiveControl As Control = Me ' initialize on form creation
Protected Overrides Sub UpdateDefaultButton()
If (LastActiveControl Is tbInputBox) AndAlso
((ActiveControl Is btnPromptForName) OrElse (ActiveControl Is btnExit)) Then
ValidateInput()
End If
LastActiveControl = ActiveControl ' Store this for the next time Focus changes
End Sub
Private Sub ValidateInput()
Console.WriteLine("You're either intending to prompt for name or exit")
End Sub
Private Sub btnPromptForName_Click(sender As Object, e As EventArgs) Handles btnPromptForName.Click
Console.WriteLine("btnPromptForName_Click clicked")
End Sub
Private Sub btnExit_Click(sender As Object, e As EventArgs) Handles btnExit.Click
Console.WriteLine("btnExit clicked")
End Sub
End Class
Edit: I forgot to mention, that since UpdateDefaultButton runs before the Button.Click event is raised, it is possible wire-up the click event handler only if validation succeeds. You would then remove the event handler as part of the Button.Click handler code.
It looks like the solution was a lot easier than I thought. The intended (instructor's) way to do this is by checking for alternate "btn.Focused" properties within the txtInputBox.LostFocus event handler eg:
' When the user moves focus away from the textbox
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus
' Check if the clear or exit button is focused before validating
' Validate only if btnClear and also btnExit IS NOT focused
If Not btnExit.Focused AndAlso Not btnClear.Focused Then
Call validateForm()
End If
End Sub
Thank you #TnTinMn for the UpdateDefaultButton and showing me an alternate way to track events. This was very helpful too.

Something like Controls("lbl" & i) for PowerPacks RectangleShape?

I am doing something for my VB.NET class in high school and I previously found on the internet a way to use Controls("lbl" & i).Text = "Example" to quickly change/set multiple labels.
Now we are doing somewhat simple animations (I like to take it a step further) and I want to have a timer that makes it so every rectangle that gets created with a key press starts moving through a timer. If that's confusing basically what I want to do is (incorrect syntax of course because I'm using controls() as my example):
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
For i = 0 to createdRectangleAmount
Controls("rec" & i).left += 1
Next
End Sub
Also as one last side note, the timer only gets enabled AFTER a rectangle as been created (rectangle 0), so this wouldn't produce errors, outside of the obvious misuse of Controls()
Get a specific control collection with filter. OfType asks for what control type you are looking for. The Where predicate does some filtering with condition expression as below). This also assumes that all controls are located in the forms control collection and not in any other container(panel, groupbox, etc...). As always check your MSDN
Dim recs = Me.Controls.OfType(Of RectangleShape)().
Where(Function(r) r.Name.StartsWith("rec"))
For Each rec In recs
rec.SetBounds(recBounds.Left + 1, rec.Bounds.Top, rec.Width, rec.Height)
Next

Making a button.click event do two different things

I'm working on a simple VB.NET program (just using winforms) and am really terrible at UI management. I'd like to have a single button that starts a process, and then have that same button stop the process.
I'm thinking about having the main form initiate a counter, and the Click event iterate the counter. Then it does a simple check, and if the counter is even it will do thing A and odd does thing B.
Is there a better way, aside from using two buttons or stop/start radio buttons?
I've done that exact thing one of two ways. You can use a static variable or toggle the text of the button.
Since your button has two functions, Good design requires you to indicate that to the user. The following code assumes the Button's text is set in Design Mode to "Start", and the code to start and stop your process is in the Subs StartProcess and EndProcess.
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Button1.Text ="Start" Then
StartProcess()
Button1.Text="End"
Else
EndProcess()
Button1.Text="Start"
End IF
End Sub
EDIT
The above solution is fine for a single-language application developed by a small number of developers.
To support multiple languages, developers typically assign all text literals from supporting files or databases. In larger development shops, with multiple programmers, using a display feature of the control for flow-control may cause confusion and regression errors. In those cass, the above technique wouldn't work.
Instead, you could use the Tag property of the button, which holds an object. I would typically use a Boolean, but I used a string just to make more clear as to what's going on.
Public Sub New()
'Initialize the Tag
Button1.Tag="Start"
End Sub
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Button1.Tag.ToString="Start" Then
StartProcess()
Button1.Tag="End"
Else
EndProcess()
Button1.Tag="Start"
End IF
End Sub
This is example in pseudo-code. I don't guarantee that names of methods and event are exactly match real names. But this should provide you a design that you could use for responsive form.
Lets say, your process is running on separate tread, using BackgroundWorker.
You setup your worker and start process
Class MyForm
private _isRunning as boolean
private _bgWorker as BackgroundWorker
sub buton_click()
If Not _isRunning Then
_isRunning = true;
StartProcess()
Else
StopProcess()
End if
end sub
sub StartProcess()
' Setup your worker
' Wire DoWork
' Wire on Progress
' wire on End
_bgWorker.RunWorkerAsync()
End sub
sub StopProcess()
if _isRunning andAlso _bgWorker.IsBusy then
' Send signal to worker to end processed
_bgWorker.CancelAsync()
end if
end sub
sub DoWork()
worker.ReportProgress(data) ' report progress with status like <Started>
' periodically check if process canceled
if worker.canceled then
worker.ReportProgress(data) ' report progress with status like <Cancelling>
return
end if
' Do your process and report more progress here with status like <In Progress>
' and again periodically check if process canceled
if worker.canceled then
worker.ReportProgress(data) ' report progress with status like <Cancelling>
return
end if
worker.ReportProgress(data) ' report progress with status like <Ending>
end sub
sub ReportProgress(data)
if data = <some process state, like "Started"> then
btnProcess.Text = "End Process"
end if
End sub
sub ReportEndOfProcess
btnProcess.Text = "Start Process"
_isRunning = false
end sub
End Class
Here you can pinpoint the names of methods and events
You have to substitute identifiers with real names and create you state or data object, which will carry information from background thread to UI thread, and also an Enum Status that can be part of your custom state object. This should work once translated into real code
Just want to show another approach for this task
Use .Tag property for your own purpose
If .Tag Is Nothing (by default in designer) then start process
If not Nothing -> stop process
Public Sub Button1_Click(ByVal Sender as Object, ByVal e as System.EventArgs)
If Me.Button1.Tag Is Nothing Then
StartProcess()
Me.Button1.Tag = New Object()
Me.Button1.Text = "End"
Else
EndProcess()
Me.Button1.Tag = Nothing
Me.Button1.Text = "Start"
End
End Sub

Find Timers by Name

Okay I'm working with visual studio and I've hit a bit of a snag. The basic situation is I have a bunch of buttons and timers that correspond to each other. For example when button1 is clicked Timer1 should start.
Currently I'm using one method to handle all of the button clicks. Which identifies the CR (1, 2, 3, etc...) and constructs a string for the name of the correct Timer that goes along with it, dim timername as string = "timer" & cr.ToString. Then when I use Me.Controls(cr).Enabled = True it returns an a null pointer error.
I know the issue has to do with the identification of the timer, suggestions?
You can't identify a control using a string (well, not easily). Try this.
Private Sub ButtonX_Click(sender As Object, e As EventArgs) Handles Button1.Click, Button2.Click ' etc.
Dim vButton = DirectCast(sender, Button)
Select Case vButton.Name
Case "Button1"
Timer1.Start ' Or stop, or whatever
Case "Button2"
Timer2.Start
End Select
End Sub
You can also compare the button object itself using If vButton Is Button1, but that gets messy in VB (I remember having to use GetType and stuff like that).
However, if your code is as simple as my example, why not just use separate handlers for each button?!!
A Timer is a Component not a Control so it will not be located in the Control Collection. This is a case where it is probably better to not use a common button click handler since it is not simplifying anything.
However, everything which inherits from Object, such as a Button, has a Tag property which you can use to associate things with that object. In form load:
Button1.Tag = Timer1
Button2.Tag = Timer2
Button3.Tag = Timer3
Then the click event:
Private Sub ButtonX_Click(... etc ) Handles Button1.Click, Button2.Click ...
Dim thisBtn As Button = CType(sender, Button)
Dim thisTmr As Timer = Ctype(thisBtn.Tag, Timer)
thisTmr.Start
End Sub

Control zoom level of WinForms using mouse scroll wheel and Ctrl in VB.NET

If I have a winform, may I know how can I control the zoom level of the font in the application (as well as the application window itself obviously) by using Ctrl + Mouse Scroll Wheel? I see there is a Delta in the Scroll Wheel event, but not sure how that works. Is there any code sample that I can look into?
I suspect that you can just test:
(VB.NET):
If (ModifierKeys And Keys.Control) = Keys.Control Then
(C#):
if( (ModifierKeys & Keys.Control) == Keys.Control )
to check if the control key is down.
You'll have to handle the KeyDown and KeyUp event in order to determine whether or not Ctrl key is being held down. This value should be stored at class-level because it will be used by other subroutines besides the KeyDown and KeyUp events.
You then write code to handle the form's MouseWheel event. Scrolling downwards (towards you) causes a negative value for the Delta property of the MouseEventArgs. Scrolling upwards is obviously the reverse. The value of the Delta property is always currently 120.
Microsoft's reason for this value is as follows:
Currently, a value of 120 is the standard for one detent. If higher resolution mice are introduced, the definition of WHEEL_DELTA might become smaller. Most applications should check for a positive or negative value rather than an aggregate total.
In your context you'll just check for the sign of the Delta and perform an action.
Here is a sample code implementing basic 'zoom' functionality:
Public Class Form1
Enum ZoomDirection
None
Up
Down
End Enum
Dim CtrlIsDown As Boolean
Dim ZoomValue As Integer
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
ZoomValue = 100
End Sub
Private Sub Form1_KeyDown_KeyUp(ByVal sender As Object, _
ByVal e As KeyEventArgs) _
Handles Me.KeyDown, Me.KeyUp
CtrlIsDown = e.Control
End Sub
Private Sub Form1_MouseWheel(ByVal sender As Object,
ByVal e As MouseEventArgs) _
Handles Me.MouseWheel
'check if control is being held down
If CtrlIsDown Then
'evaluate the delta's sign and call the appropriate zoom command
Select Case Math.Sign(e.Delta)
Case Is < 0
Zoom(ZoomDirection.Down)
Case Is > 0
Zoom(ZoomDirection.Up)
Case Else
Zoom(ZoomDirection.None)
End Select
End If
End Sub
Private Sub Zoom(ByVal direction As ZoomDirection)
'change the zoom value based on the direction passed
Select Case direction
Case ZoomDirection.Up
ZoomValue += 1
Case ZoomDirection.Down
ZoomValue -= 1
Case Else
'do nothing
End Select
Me.Text = ZoomValue.ToString()
End Sub
End Class
Read on the following for more information about your question:
MSDN: Control.KeyDown Event
MSDN: Control.KeyUp Event
MSDN: Control.MouseWheel Event
MSDN: MouseEventArgs Class
For CrystalReportViewer1
Just put CrystalReportViewer1.Zoom(ZoomValue)
instead of the line Me.Text = ZoomValue.ToString() in the Sub Zoom