Want to display the processing progress using a Label to show a counter of sorts - vb.net

Its a label printing app. Desire to show progress via a counter displaying in a Label. ie. would like it to
look like this... 1000 increments to 1001 increments to 1002, 1003 etc. Pretty simple pgm so obviously processing from start to finish is instantaneous. Thus end number pops up right away rather than flashing a sequence of sorts. Don't need number to be readable but just simulating the process. Since it is a printing program, there is time for this display. At the end of processing the final number should be on the screen. Thought maybe I could use a timer to tick off before updating label. My coding is obviously not correct. Here it is. Any suggestions are appreciated. I am a 79 YO tinkerer programmer (worked back in the COBOL days) so be kind LOL.
Private Sub Button2_Click_1(sender As Object, e As EventArgs) Handles Button2.Click
' Print button
Dim toPrint As Integer = Me.NumericUpDown2.Value
Dim RetVal As Object
Dim TopPos As String = "375,200"
Dim InfoPos As String = "360,260"
startNo = TextBox2.Text
For index As Integer = 1 To toPrint
RetVal = RDP.PrintRawData("^XA^LL450^PQ" & arrCust(4) & "^CFB,30^FO" & TopPos & "^FD" & arrCust(5) & StartNo & "^FS^FO" & InfoPos & "^FD" & arrCust(2) & "^FS^XZ")
Timer1.Interval = 1000
Timer1.Start()
startNo += 1
Next
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Label4.Text = startNo
End Sub

The problem is the loop never yields control back to the underlying windows event message pump, and so there's never a chance to process any paint messages to redraw the label until after everything is finished.
To fix this we first need to resolve two conflicting statements:
from start to finish is instantaneous
and
Since it is a printing program, there is time for this display.
Only one of those two statements can be true. They are mutually exclusive.
If the first statement is true, I'd change the timer to tick more often (every 200 milliseconds or so; faster is just wasteful) and put up with an ugly Application.DoEvents call in the loop so the label can have a chance to repaint. (Also: start the timer once, before entering the loop, and not on each iteration).
If the latter statement is true, I'll echo the usual warnings about avoiding Application.DoEvents (there are a lot of "gotchas" with this method). Instead, I'd do it the right way, and use a BackgroundWorker component.

Related

How can I speed up VB copy

I'm running the following loop successfully when the number of items is low. However, when run against a larger list on the ListView, it seems to be taking way too long. I tested it with a list of 8,700 files and it took about two hours to complete. Is there something I can do to speed this up? I guess that removing the check for the Cancel button would help but I would like to keep that there for usability. As I've mentioned in earlier posts, I'm pretty new to Visual Basic so please provide lots of explanation with your suggestions. Thanks. Here's the code:
For i As Integer = 0 To m_CountTo
' Has the background worker be told to stop?
If BackgroundWorker1.CancellationPending Then
' Set Cancel to True
e.Cancel = True
Exit For
End If
'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.
LVFiles.Items(i).Selected = True
strSourceFilePath = LVFiles.SelectedItems(i).SubItems(0).Text
strDestFilePath = LVFiles.SelectedItems(i).SubItems(3).Text
My.Computer.FileSystem.CopyFile(strSourceFilePath, strDestFilePath, overwrite:=False)
' Report The progress of the Background Worker.
BackgroundWorker1.ReportProgress(CInt((i / m_CountTo) * 100))
' Me.LabelStatus.Text = FormatPercent((i + 1) / (intLVIndex + 1), 2) ' Show Percentage in Label
SetLabelText_ThreadSafe(Me.LabelStatus, FormatPercent(i / m_CountTo, 2))
Next
The Backgroundworker encapsulates a new thread. You cannot directly access controls that are created in another thread. If you do you will get an InvalidOperationException because of a cross-thread operation. The Backgroundworker however offers some functionality to share data (or access to controls) between threads. You should use them.
Private Sub StartBGW_Click(sender As Object, e As EventArgs) Handles StartBGW.Click
Dim dict As New Dictionary(Of String, String)
For i As Integer = 0 To m_CountTo
dict.Add(Me.LVFiles.Items(i).SubItems(0).Text,
Me.LVFiles.Items(i).SubItems(3).Text)
Next
Me.BackgroundWorker1.RunWorkerAsync(dict)
End Sub
First we prepare a dictionary that contains the source as Key and the target as Value. This object is given to the BackgroundWorker as a parameter.
Now comes the essential part:
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim counter As Integer = -1
Dim dict = DirectCast(e.Argument, Dictionary(Of String, String))
For Each kvp In dict
counter += 1
' Has the background worker be told to stop?
If Me.BackgroundWorker1.CancellationPending Then
' Set Cancel to True
e.Cancel = True
Exit For
End If
'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.
My.Computer.FileSystem.CopyFile(kvp.Key, kvp.Value, overwrite:=False)
' Report The progress of the Background Worker.
Me.BackgroundWorker1.ReportProgress(CInt((counter / m_CountTo) * 100), counter)
Next
End Sub
We don't access the ListView anymore. Instead we use the dictionary that is given to us as a parameter through e.Argument. Theres also a slight difference in the BackgroundWorker1.ReportsProgress line. There's a second parameter I have used to pass the current index to the ProgressChanged event which can be obtained via e.UserState.
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Me.LVFiles.Items(Convert.ToInt32(e.UserState)).Selected = True
Me.LabelStatus.Text = e.ProgressPercentage.ToString
End Sub
This event is designed to be raised with a SynchronizationContext of the calling thread, in this case the UI thread. Here we can safely access any control and update them. The index is passed as e.UserState, so we can access the relevant item and set their Selected property to true.
The biggest improvement comes from the change of Me.LVFiles.SelectedItems(i).SubItems(0).Text to Me.LVFiles.Items(i).SubItems(0).Text. I'm not a professional, but it seems that SelectedItems isn't a real list. Instead it iterates through every item using the SendMessage API until the desired index is reached. This is why it takes longer the higher your index is. Everytime it starts with the first item and iterates through them. Lot of operations.
The second improvement is the separation of code that access UI controls. It's all done in one method now. More clear and readable.
Update: #Enigmativity mentioned that SelectedListViewItemCollection implements IList and therefore is a real list. Even though it has no underlying list containing all selected items like you have in ListViewItemCollection. My point was to say, that accessing a single element is more complicated.

Using progress bars in vb to show a specific time frame

I want to use progress bars for my project throughout, since it is very much necessary, for the main part of this game I am creating, however, to get a GOOD grade at A Level, you need to be able to show variation and to not have a large amount of data replication, is there any, remotely easy understandable way to allow for a button when pressed to allow for the progress bar to be completed for me in a 5 second time period. Please let me know. If you need any code It may be rather extensive a long, because I went a really unorthodox way about this originally. But just don't really want to have about 15-20 timers in the end product.
Private Sub ButtonClick2_Click(sender As Object, e As EventArgs) Handles ButtonClick2.Click
money = money + (4 * LevelMultiplier2)
label_avail_money.Text = Math.Round(money, 2).ToString("N2")
Public Class Form1
Dim money As Decimal = 0
Dim LevelMultiplier2 As Decimal = 1
Basically this is what is for this button, all I need for is for 1. For the Calculation to be ran 5 seconds prior to when the button is pressed and also to have a progress bar running simultaneously with the button press too. Hope this helps, also putting this into some form of code now may help me more to resolve this issue :)
You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Forms and add Imports System.Reactive.Linq - then you can do this:
Private Sub ButtonClick2_Click(sender As Object, e As EventArgs) Handles ButtonClick2.Click
ButtonClick2.Enabled = False
Observable _
.Interval(TimeSpan.FromSeconds(5.0 / 100.0)) _
.Take(100) _
.ObserveOn(Me) _
.Subscribe(
Sub(x) ProgressBar1.Value = x + 1,
Sub()
money = money + (4 * LevelMultiplier2)
label_avail_money.Text = Math.Round(money, 2).ToString("N2")
ButtonClick2.Enabled = True
End Sub)
End Sub
That code nicely animates the progress bar and then fills in the label_avail_money.Text value.

Converting String to a DateTime or TimeValue

I have a problem trying to convert a String to a DateTime or TimeValue.
I made a little Windows Form to use as an example. Please take a look at this to see the Form:
As you can see there is a TextBox1.
Also a Timer1 with an interval of 1000 ms.
What I want is to be able to fill in a time in the textbox (like this format: 22:30:00) and when your desktop reaches this time, it will display a messagebox.
I tried several old posts and tutorials with similar problems but I can't seem to find the solution. Can anyone make a quick example using my screenshot perhaps?
This is the code someone else suggested.
Dim tsValue As TimeSpan = TimeSpan.Zero
If TimeSpan.TryParse(TextBox1.Text.Trim, tsValue) Then
If Date.Now.TimeOfDay = tsValue Then
MessageBox.Show(String.Format("Messagebox test {0}", tsValue.ToString))
End If
End If
But for some reason it does not work when I place it in my Timer.
Keep a variable with the target time (assuming it passes validation) and have your timer check if the target time matches the current time. Make sure your timer updates the date when the day changes (12:00 AM).
Private m_dteTime As DateTime
Private m_dteCurrentDate As Date = DateTime.Now.Date
Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles TextBox1.TextChanged
'Validation
Try
Dim strHours As String = TextBox1.Text.Substring(0, 2)
Dim strMinutes As String = TextBox1.Text.Substring(3, 2)
Dim strSeconds As String = TextBox1.Text.Substring(6, 2)
If IsNumeric(strHours) AndAlso IsNumeric(strMinutes) And IsNumeric(strSeconds) Then
m_dteTime = DateTime.Now.Date.AddHours(Convert.ToDouble(strHours)).AddMinutes(strMinutes).AddSeconds(strSeconds)
Else
m_dteTime = DateTime.MinValue
End If
Catch ex As Exception
End Try
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
If DateTime.Now.Date > m_dteCurrentDate Then
m_dteCurrentDate = DateTime.Now.Date
End If
If m_dteTime.Date = DateTime.Now.Date AndAlso m_dteTime.Hour = DateTime.Now.Hour AndAlso _
m_dteTime.Minute = DateTime.Now.Minute AndAlso m_dteTime.Second = DateTime.Now.Second Then
MsgBox("The time is now.")
End If
End Sub
The reason your code is not working is you're using "=" operator to compare two values which are highly unlikely to be equal. The thing is, you're entering the time value without milliseconds in your TextBox control, but the TimeOfDay function returns current time including milliseconds. Therefore, your message box never gets shown.
Now, there are multiple ways to get around this, but let's keep it simple. If you change "=" to "<=", it will work, but in that case your message box will continue to appear with each timer iteration, so you would have to disable timer right after or just before you show a message box. Depending on what are you trying to achieve, this could or could not be the solution for you.
If you need to keep the timer running, you can remove the millisecond part of the current time before comparing, which will give you one second to do the comparing. Also, in this case you should reduce timer interval to a lower value (500ms should be enough).
Also, if I may suggest, instead of TextBox, you should use DateTimePicker control with Format property set to Time and ShowUpDown property set to True. It will make your life easier as it will automate validation and also avoid uneeded conversions.

Picturebox location change 20 times per second not redrawing

I'm trying to make a few images do the nice slidey thingy that I've seen lots of Microsoft applications use. The one where the movement starts slow speeds up half way there and then comes to a nice slow stop in it's new location. I've got all the calculations figured out, getting and setting the picture box locations, Confirmation using console.writeline that the image locations are correct, and even a test run that works in a simplified format.
But in the full blown version It's not repainting the image. In fact, it looks like nothing has happened at all while the script is running. I've tried Me.Refresh(), Invalidate(), Timer.Enabled = True/False, and Me.Update(). None of which have worked. The last step is the most frustrating: I'm calling my SetPanelLocation() method at the end to ensure that the panel ends up in the final location regardless of if the movement worked. Nothing happens on this call either, even though immediately after this routine fails I can call the same method from another user event and it starts working again like nothing was wrong.
I'm creating my own PictureBox class called clsFeedImageBox which inherits PictureBox that includes this functionality (along with other features). Each image is only 300x225 pixels so they're not massive images that take a lot of time to redraw. Each instance of this class is in a common Forms.SplitterPanel. I use a lot of comments out of habit so i left them in here, maybe they'll add some light.
Public Class clsFeedImgBox
Inherits PictureBox
Private iRank As Integer 'rank in whatever feed this file gets put in
Private iRankTarget As Integer 'rank to move to when rank feed event starts
Private iTopStart As Integer 'starting top location before feed event
Private iTopTarget As Integer 'final Top location after feed event
Private WithEvents tMyTimer As New System.Timers.Timer
Private WithEvents oParent As FeedBase 'splitter panel, all location info comes from the parent
Public Sub New(ByRef sender As FeedBase, ByVal rank as Integer)
'set objects
oParent = sender
'set .Image property to pre-made thumbnail
Image.FromFile(ThumbPath) 'ThumbPath is a property which is set by this point (some code has been removed)
'setup initial position
setPanelLocation(rank)
'set autosize
Me.SizeMode = PictureBoxSizeMode.StretchImage
'set Image Scroll timer interval to 20 fps (1000 / 20 = 50)
tMyTimer.Interval = 50
End Sub
Public Sub scroll(ByVal newRank As Integer)
'setPanelLocation(newRank) <== this works, timed movements don't
iRankTarget = newRank
iTopStart = Me.Top
iTopTarget = oParent.ImgTop(newRank) 'gets an integer for the new Top location
tMyTimer.Start()
End Sub
Private Sub myScrollStep() Handles tMyTimer.Elapsed
'tMyTimer.Enabled = False 'this idea with the enabled = True at the end didn't work
iTickCount += 1
Dim iScrollPerc As Integer 'scroll % between Start and End * 100
iScrollPerc = oParent.ScrollStep(iTickCount, Rank) 'this part works
Console.WriteLine(strThumbName & " scrollPerc: " & iScrollPerc.ToString)
If iScrollPerc >= 100 Then
'scroll event complete
Console.WriteLine(strThumbName & " SetFinalLocation")
Me.setPanelLocation(iRankTarget) '<== This line doesn't work here, but works when called by other means
'stop Feed updates
tMyTimer.Stop()
'reset iTickCount for next movement
iTickCount = 0
Else
'scrolling still going
Dim newTop As Integer
newTop = Math.Round(iTopTarget - (((100 - iScrollPerc) * (iTopTarget - iTopStart)) / 100)) 'this part works
'Console.WriteLine(strThumbName & " TopTarget: " & newTop)
Me.Top = newTop 'Nothing happens here
End If
'Me.Left = oParent.ImgLeft
'Me.Width = oParent.ImgWidth
'Me.Height = oParent.ImgHeight 'that didn't work
'Me.Refresh() 'this didn't work
'Invalidate() 'this didn't do much good either
'Me.Update() 'Aaaaand no cigar, time for StackOverflow
'tMyTimer.Enabled = True
End Sub
Public Sub setPanelLocation(ByVal rank As Integer)
iRank = rank
Me.MyRePaint()
End Sub
Public Sub MyRePaint()
'repaint image box with everything in it's current rank
Me.Left = oParent.ImgLeft
Me.Top = oParent.ImgTop(iRank)
Me.Width = oParent.ImgWidth
Me.Height = oParent.ImgHeight
End Sub
End Class
What gives? There must be some inner workings of VB.NET that will help me figure this out. I'm using VS 2012 and Win8
You could make a WPF application and use a Slider control instead of "manually" making a slider with planes, picture boxes, etc, etc.

Want to Call Same BackgroundWorker Multiple Times without using Application.DoEvents

I'm running in to a problem that I was able to fix with Application.DoEvents, but don't want to leave that in because it might introduce all sorts of nasty problems.
Background:
Our app is primarily a desktop app that makes many calls to a web service. We control everything but changes to the overall system design are not going to be seriously considered. One of those calls, Calculate, is used very often, and occasionally can take a few minutes to process all the data to return valid results.
Previously this call to Calculate was done synchronously and thus would block the UI leaving the user to wonder if the app had frozen or not, etc. I've successfully moved all the long wait calls to a BackgroundWorker and then made a simple Waiting screen that would cycle through a "Calculating..." animated message.
Now the problem arises when our UI code tries to call the calculate routine again prior to the first one finishing. I would get a "This BackgroundWorker is currently busy and cannot run multiple instances..." message. Which I thought should be controlled by the resetEvent.WaitOne() calls. It did not so I thought maybe another event controlling access to the entire routine would help, so I added the calcDoneEvent. This still did not fix the problem, but would cause it to block indefinitely on the 2nd call to Calculate's calcDoneEvent.WaitOne() call. Then on a whim I added the Application.DoEvents to the bottom of Calculate and viola, problem solved.
I don't want to leave that .DoEvents in there because I've read it can cause problems that later are very difficult to track down. Is there a better way to handle this situation?
Thanks in advance..
Private WithEvents CalculateBGW As New System.ComponentModel.BackgroundWorker
Dim resetEvent As New Threading.AutoResetEvent(False)
Dim calcDoneEvent As New Threading.AutoResetEvent(True)
Public Sub Calculate()
calcDoneEvent.WaitOne() ' will wait if there is already a calculate running.'
calcDoneEvent.Reset()
' setup variables for the background worker'
CalculateBGW.RunWorkerAsync() ' Start the call to calculate'
Dim nMsgState As Integer = 0
' will block until the backgorundWorker is done'
Do While Not resetEvent.WaitOne(200) ' sleep for 200 miliseconds, then update the status window'
Select Case nMsgState
Case 1
PleaseWait(True, vbNull, "Calculating. ")
Case 2
PleaseWait(True, vbNull, "Calculating.. ")
Case 3
PleaseWait(True, vbNull, "Calculating... ")
Case 4
PleaseWait(True, vbNull, "Calculating....")
Case Else
PleaseWait(True, vbNull, "Calculating ")
End Select
nMsgState = (nMsgState + 1) Mod 5
Loop
PleaseWait(False, vbNull) 'make sure the wait screen goes away'
calcDoneEvent.Set() ' allow another calculate to proceed'
Application.DoEvents() ' I hate using this here'
End Sub
Private Sub CalculateBGW_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles CalculateBGW.DoWork
Try
'make WS Call, do data processing on it, can take a long time..'
'No Catch inside the DoWork for BGW, or exception handling wont work right...'
'Catch'
Finally
resetEvent.Set() 'unblock the main thread'
End Try
End Sub
Private Sub CalculateBGW_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles CalculateBGW.RunWorkerCompleted
'If an error occurs we must check e.Error prior to touching e.Result, or the BGW'
'will possibly "eat" the exception for breakfast (I hear theyre tasty w/ jam)'
If Not (e.Error Is Nothing) Then
'If a Web Exception timeout, retry the call'
If TypeOf e.Error Is System.Net.WebException And _
e.Error.Message = "The operation has timed out" And _
intRetryCount < intRetryMax Then
' Code for checking retry times, increasing timeout, then possibly recalling the BGW'
resetEvent.Reset()
CalculateBGW.RunWorkerAsync() 'restart the call to the WS'
Else
Throw e.Error ' after intRetryMax times, go ahead and throw the error up higher'
End If
Else
Try
'normal completion stuff'
Catch ex As Exception
Throw
End Try
End If
End Sub
You declared:
Private WithEvents CalculateBGW As New System.ComponentModel.BackgroundWorker
Dim resetEvent As New Threading.AutoResetEvent(False)
Dim calcDoneEvent As New Threading.AutoResetEvent(True)
as private fields of the containing class. Notice that this way, all calls to RunWorkerAsync() are referred to the same object instance of the BackgroundWorker class (that is, to the same object). That is why it is "busy". This code is built to hold only one BackgroundWorker at a given time.
If you mean to allow the UI code to call the Calculate() method whenever it needs to, you should declare CalculateBGW as a local variable within the Calculate() method, thus creating a new instance of the BackgroundWorker class with every call (and they will run asynchronosly). This means you'll have to add and remove the event handlers inside Calculate() as well, using AddHandler and RemoveHandler.
There are several approaches to updating the UI on the progress, but it is suggested to use the BackgroundWorker.ProgressChanged event and BackgroundWorker.ReportProgress method.
Use the BackgroundWorker.RunWorkerCompleted event as a callback trigger, reporting the UI that the calculation is completed, thus triggering the needed code to represent the result. This approach eliminates the need to maintain a thread looping around bossing the calculation thread - thereby eliminating the need for DoEvents(). It lets the calculation thread inform its boss when its done working, instead of having the boss checking the worker's status and going to sleep over and over.