Dynamic Update of Excel Chart with Delays - vba

I read a data line from an instrument using Excel VBA.
I would like to plot the data dynamically on an Excel Active Chart, IMMEDIATELY after it has been read.
I need to wait and read the data every 5 seconds and, in the mean time, I "sleep", either through the VBA Application.Wait command, or through the Kernel32 Sleep command.
In either case the Active Chart does NOT get updated. The complete plot shows up only after the LAST "sleep".
Any suggestions will be appreciated.
Here is the simplified code
Sub New_Data(indx)
Dim x As Integer
While True
x = Read_Instrument(1)
y = Read_Instrument(2)
Cells(indx, 1) = x
Cells(indx, 2) = y
ActiveSheet.ChartObjects.Item(1).Activate
ActiveChart.FullSeriesCollection(1).XValues = "=Sheet1!$A$1:$A$" & indx
ActiveChart.FullSeriesCollection(1).Values = "=Sheet1!$B$1:$B$" & indx
indx = indx + 1
Sleep 5000 'Use the KERNEL32 Sleep function for 5000 milliseconds
Wend
End Sub

Wait and Sleep will keep VBA running, so the screen will not updated.
Try using Application.OnTime, along these lines (in a standard code module, i.e. Module1).
Sub refreshMyChart()
Dim indx as long: indx = sheet1.Cells(Rows.Count, 1).End(xlUp).offset(1)
Cells(indx, 1) = Read_Instrument(1)
Cells(indx, 2) = Read_Instrument(2)
With Sheet1.ChartObjects(1).FullSeriesCollection(1)
.XValues = "=Sheet1!$A$1:$A$" & indx
.Values = "=Sheet1!$B$1:$B$" & indx
End With
''''' Now Schedule the same routine for 5 seconds later''''''''''
Application.OnTime Now + TimeSerial(0,0,5), "refreshMyChart"
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
End Sub
p.s.: be aware that this data cannot grow indefinitely.. You should define some way to stop or to delete old rows to keep the number of displayed data rows reasonable.

Excellent! Thank you very much.
I was not aware of the .OnTime construct
Application.OnTime Now + TimeSerial(0, 0, 5), "refreshMyChart"
and the fact that VBA allows recursive routines.
As for ending the loop, there is code in there, which I did not post because it was not relevant to the issue. Here it is
If Int(current_index / nmeasure) * nmeasure > finalval Then
MsgBox ("End of Measurement")
Exit Sub
End If
Thanks again.

Related

Automation error the server threw an exception on PPT SetSourceData

Some background, I had a VBA loop creating PPT slides with various filters/views on an Excel pivot table. It was working (after I added DoEvents). I have recently added functionality to create a new PPT file from scratch with multiple sides before they are populated with the data. It's not working anymore.
Two theories:
1) Somehow the memory got bogged down in the new PPT file creation loop and now the data population loop is erroring out.
2) Something about how the default chart is formatted is messed up. If I edit the charts manually, save, and populate, there is no error. However if I create and then automatically try to populate, there's an error.
Due to complexity of the scripts, the loop to create the slides is completely separate from the loop to reopen and populate the slides.
Here's the section that errors out:
'Paste the final temp dataset into PPT
Range("A1000").Activate
tempdata = Range(Selection, Selection.Offset(months, categories - 1)).Value
Set oChart = oPres.Slides(pages(b)).Shapes(metrics(a)).Chart
oChart.ChartData.Activate
Set wb = oChart.ChartData.Workbook
Set ws = wb.Worksheets(1)
ws.Range("A1:Z1000").ClearContents
ws.Range("A1", Range("A1").Offset(months, categories - 1)).Value = tempdata
'Let code catch up
Application.Wait (Now + TimeValue("00:00:02"))
DoEvents
'Redraw the selected dataset of the chart based on the # of categories and rows
oChart.SetSourceData Source:="='Sheet1'!$A$1:" & toChar(categories + 0) & months + 1, PlotBy:=xlColumns
Despite using both Application.Wait and DoEvents, it is still hanging up.
This is purely a timing issue because if I click Debug and continue running the code with no changes, it works fine. I am also using late binding (maybe?) through the Set Object statement and at the end of the loop I always Set oChart = Nothing.
Sometimes it works to write DoEvents multiple times, but as the process has gotten more complex, even this doesn't work. I'm all out of ideas. Any suggestions?
'Let code catch up
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
DoEvents
'Redraw the selected dataset of the chart based on the # of categories and rows
oChart.SetSourceData Source:="='Sheet1'!$A$1:" & toChar(categories + 0) & months + 1, PlotBy:=xlColumns
You may try:
Using Sleep, with this line at the top of your module (outside of your function):
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Then add this line in place of, or in addition to, DoEvents:
Sleep 1 ' Pause for 1 ms
See:
https://stackoverflow.com/a/3891017/2707864
See also:
https://www.myonlinetraininghub.com/pausing-or-delaying-vba-using-wait-sleep-or-a-loop
Using loops with DoEvents:
Dim PauseTime, Start, Finish, TotalTime
PauseTime = 4 ' Set duration.
Start = Timer ' Set start time.
Do While Timer < Start + PauseTime
DoEvents ' Yield to other processes.
Loop
Finish = Timer ' Set end time.
TotalTime = Finish - Start ' Calculate total time.
See:
https://www.mrexcel.com/forum/excel-questions/36052-when-how-use-doevents-solved-post166114.html#post166114
See also:
https://www.myonlinetraininghub.com/pausing-or-delaying-vba-using-wait-sleep-or-a-loop
Using combinations thereof, which can improve performance of your system depending on the wait time.
Public Sub WaitSeconds(intSeconds As Integer)
On Error GoTo PROC_ERR
Dim datTime As Date
datTime = DateAdd("s", intSeconds, Now)
Do
Sleep 100
DoEvents
Loop Until Now >= datTime
PROC_EXIT:
Exit Sub
PROC_ERR:
MsgBox "Error: " & Err.Number & ". " & Err.Description, , "modDateTime.WaitSeconds"
Resume PROC_EXIT
End Sub
See:
http://www.fmsinc.com/microsoftaccess/modules/examples/avoiddoevents.asp
#sancho.s, thanks for your help. So it turns out the error had nothing to do with DoEvents. I had been using that as a sloppy fix without understanding its functionality. Given that, none of the three options worked. I spent all day trying various combinations with no success. Instead, I had to brute force close the embedded PPT workbook, set oChart to Nothing, reinstantiate oChart, reopen the workbook, and close it again.
This made the process 2x slower (but no slower than forcing it to wait on a timer??), and it completely eliminated all errors. Apparently it just didn't like pasting the raw data and reselecting the data the first time the workbook was opened. No idea why.
Sub UpdateChart(ByVal a As Integer, ByVal b As Integer, ByVal months As Integer, ByVal categories As Integer, ByRef pages() As Integer, ByRef metrics() As String, ByVal oPres As Object, ByVal legend_flag As Boolean)
Dim tempdata As Variant
'Paste the final temp dataset into PPT
tempdata = Range(Worksheets("calc").Range("A1000"), Worksheets("calc").Range("A1000").Offset(months, categories - 1)).Value
If legend_flag Then
Set oChart = oPres.Slides(pages(b)).Shapes("legend").Chart
Else
Set oChart = oPres.Slides(pages(b)).Shapes(metrics(a)).Chart
End If
oChart.ChartData.Activate
Set wb = oChart.ChartData.Workbook
Set ws = wb.Worksheets(1)
ws.Range("A1:Z1000").ClearContents
ws.Range(ws.Range("A1"), ws.Range("A1").Offset(months, categories - 1)).Value = tempdata
'Close workbook
wb.Close
Set oChart = Nothing
If legend_flag Then
Set oChart = oPres.Slides(pages(b)).Shapes("legend").Chart
Else
Set oChart = oPres.Slides(pages(b)).Shapes(metrics(a)).Chart
End If
oChart.ChartData.Activate
'Redraw the selected dataset of the chart based on the # of categories and rows
oChart.SetSourceData Source:="='Sheet1'!$A$1:" & toChar(categories + 0) & months + 1, PlotBy:=xlColumns
'Close workbook
oChart.ChartData.Workbook.Close
Set oChart = Nothing
Exit Sub
End Sub
I also put the code snippet in a subroutine and added Exit Sub at the end to hard reset all parameters in an earlier attempt that didn't work. So all objects and parameters have definitely been cleared for good measure.
Does anyone have any ideas why the object definition/open workbook was tripping up like that? And why DoEvents doesn't actually work for this problem?

Word VBA 2016, table: no response when setting shading of cells with loop

In Word 2016 VBA I want to set the shading of each cell of a table with a loop.
This seems to work for tables up to a size of about 15*15. With tables such as 20*20 or larger Word does not respond any more. Program execution seems to be correct though when using single step. I tried this for tables of ca. 50*50. ScreenRefresh and ScreenUpdating seem to have no influence.
In the code example, setting the shading of each cell to the same background color is only for demonstration, finally I want to apply more complex settings.
Sub TableCells_SetBackgroundColors()
' Set background color for each cell in Word table
' Application does not respond if table is larger than about 20*20
' debug single step works in any case
'Application.ScreenUpdating = False
Dim i, k, cntCol, cntRow As Integer
cntCol = 15 ' 20 is not ok
cntRow = 15 ' 20 is not ok
If ActiveDocument.Tables.Count <> 0 Then
ActiveDocument.Tables(1).Delete
End If
ActiveDocument.Tables.Add Range:=Selection.Range, _
numRows:=cntRow, _
NumColumns:=cntCol
Dim myTable As Word.Table
Set myTable = Selection.Tables(1)
With myTable.Borders
.InsideLineStyle = wdLineStyleSingle
.OutsideLineStyle = wdLineStyleSingle
End With
For i = 1 To cntRow Step 1
For k = 1 To cntCol Step 1
myTable.Cell(i, k).Shading.BackgroundPatternColor = wdColorRed
'Application.ScreenRefresh
Next k
Next i
'Application.ScreenUpdating = True
End Sub
Intro: The guy who commented here. Your problem occurs because the execution of the code takes to long doing stuff where to application itself doesn't do any events as far as I know. If this takes longer then a specific timespan the application just says it's not responding anymore. On my machine for example the application doesn't respond anymore even with only 15 rows and columns. There is one method that prevents this from happening: DoEvents. Below is your code with 3 more lines I added which works georgously on my machine. Below the code is a little more explanation.
Sub TableCells_SetBackgroundColors()
' Set background color for each cell in Word table
' Application does not respond if table is larger than about 20*20
' debug single step works in any case
'Application.ScreenUpdating = False
Dim i, k, cntCol, cntRow As Integer
cntCol = 21 ' 20 is not ok
cntRow = 21 ' 20 is not ok
If ActiveDocument.Tables.Count <> 0 Then
ActiveDocument.Tables(1).Delete
End If
ActiveDocument.Tables.Add Range:=Selection.Range, _
numRows:=cntRow, _
NumColumns:=cntCol
Dim myTable As Word.Table
Set myTable = Selection.Tables(1)
With myTable.Borders
.InsideLineStyle = wdLineStyleSingle
.OutsideLineStyle = wdLineStyleSingle
End With
For i = 1 To cntRow Step 1
'New
Application.StatusBar = "Row " & i & " of " & cntRow
'New
For k = 1 To cntCol Step 1
'New and important
DoEvents
'New and important
myTable.Cell(i, k).Shading.BackgroundPatternColor = wdColorRed
Next k
Next i
'New
Application.StatusBar = False
'New
End Sub
More Explanation: So for some reason Word is very slow with looping through all the cells of a table and applying some shading to them. This triggers the behaviour I described above. To prevent the application from not responding I inserted the line DoEvents in your column loop, so that the application "realizes that it is still alive" during every cell iteration. I did not test how much of an performance cost the DoEvents method has in this case but if you find it to be significant you can try to move DoEvents to the row loop and see if you are still fine. As for the other two lines with the StatusBar, these are not necessary to prevent the application from not responding but I find them very usefull because they prevent the user/you/me from worrying that the application crashed. It will tell you in the statusbar in which row the code currently is.
The statusbar during execution:
#Xam Eseerts
Thank you for your answer which solves the problem.
(It is still surprising how slow Word seems to work here. For my task of creating a big colorful table I finally switched to Excel).

1 sec delay lasts almost 2 sec

I am writing a code to copy data from one spreadsheet to an other one in every second. I have tried Application.Wait and Sleep but they blocked both spreadsheets so I decided to use a do until loop. It works but 1 sec lasts almost 2 sec and I don't know why. So I left only the loop in the code but the test gave the same result (it took ca 95 sec). Any suggestion? Here is the code:
Sub Test()
Dim Delay As Date
cell = 1
For i = 1 to 60
Workbooks("Data").Worksheets("Sheet1").Range("C" & cell).Value = cell
cell = cell +1
Delay = Now() + TimeValue("00:00:01")
Do Until Now() >= Delay
Do Events
Loop
Next i
End Sub
That is only an approximate delay because you really have no idea of what else is going through the message queue and being processed by the DoEvents command (one word btw). An alternative would be to call the procedure from within itself with the Application.OnTime method.
Sub timed_routine()
Application.Interactive = False
ThisWorkbook.Worksheets("Sheet1").Cells(1, 1) = 1
Application.Interactive = True
'Debug.Print Timer
Application.OnTime Now + TimeSerial(0, 0, 1), "timed_routine"
End Sub
With the Debug.Print Timer command uncommented and active, this routine was cycling in about 1.015 seconds.

VBA - Out Of Memory Caused by Workbook.Save?

I have the following Sub. The script causes some information to be written to a table of fixed length every 15 seconds. The results are then graphed to create a real-time moving graph feeling.
The problem is that when I uncomment the line where it saves the workbook, I notice in the task manager that Excel uses more memory (about 3000KB), and holds it. Eventually, Excel runs out of memory and crashes. I have tried setting my objects and stuff to Nothing, but that doesn't help. It's all due to the save line, but I don't really know why. How can I save, and avoid the memory problem?
Sub Record()
Dim BookValueTable As ListObject: Set BookValueTable = Range("bookvalue_table").ListObject
If Sheets("Orders").LoggingButton.Value And _
BookValueTable.Range(BookValueTable.ListRows.Count + 1, 1) <> Now Then
Dim NewRow As ListRow: Set NewRow = BookValueTable.ListRows.Add(AlwaysInsert:=True)
NewRow.Range(1, 1) = Now
NewRow.Range(1, 2) = Sheets("Orders").Range("total_dollar_pnl")
While BookValueTable.ListRows.Count >= 28800
BookValueTable.ListRows(1).Delete
Wend
ThisWorkbook.RefreshAll
If Hour(Now) = 17 Then
Application.OnTime TimeValue("18:00:00"), "Record"
Else
Application.OnTime Now + TimeValue("00:00:" & 15 - (Second(Now) Mod 15)), "Record"
End If
'Workbooks("Book1.xlsm").Save
End If
Set BookValueTable = Nothing
Set NewRow = Nothing
End Sub

How should I Notify the user during a long process?

I have some long processes that require notifications to the user at successive stages, so that he doesn't get to believe Excel has crashed down.
How could I display asynchronous messages to the user in Excel, using VBA ?
You can use the status bar in Excel to do this:
Application.StatusBar = "status message"
Here is an example on how to implement this: http://www.vbaexpress.com/kb/getarticle.php?kb_id=87
Below is the code from the site (added line break to make is easier to read):
Sub StatusBar()
Dim x As Integer
Dim MyTimer As Double
'Change this loop as needed.
For x = 1 To 250
'Dummy Loop here just to waste time.
'Replace this loop with your actual code.
MyTimer = Timer
Do
Loop While Timer - MyTimer < 0.03
Application.StatusBar = _
"Progress: " & x & " of 250: " & Format(x / 250, "Percent")
DoEvents
Next x
Application.StatusBar = False
End Sub
UPDATE:
I do want to add that updating the status bar will result in a sizable hit in performance (quite a bit actually), so you should only update it in approriate intervals. Here's an example of what I mean (I use MOD here to ensure we only increment each 1000):
Sub test()
Dim i As Long
Dim temp As String
For i = 1 To 1000000
temp = "testing 123, testing 123"
If i Mod 1000 = 0 Then
Application.StatusBar = "Processing " & i & "/1,000,000..."
End If
Next
Application.StatusBar = "Ready"
End Sub
Also note that you want to reset the text to "Ready" otherwise it'll be left as if it were in the loop.
I've stuck with Walkenbach's progress form for my addins
The following article has a number of ways of doing this: http://oreilly.com/pub/h/2607
I think the best bet for you would be to show a progress form. This can include a progress bar and text updates to reassure the user.
Something I once did was to create an extra tab called "Running".
After each time consuming loop, I add the following code with updated text information.
Although the text sometimes changes too fast, the changing color bar shows the user that the script is still running. You have to define AlertColor first with a value of 6.
Sheets("Running").Select 'Show the running page
Range("B18").Value = "Importing ABC......"
Cells(18, 2).Interior.ColorIndex = AlertColour
AlertColour = AlertColour + 1
If AlertColour > 8 Then AlertColour = 6
Application.ScreenUpdating = True
Application.ScreenUpdating = False
I don't know how far you want to go with your solution, but you can utilise RTD function. That way you could put a status message directly in the worksheet. But it would require development of a COM Automation server, which is not complicated (can be written in .NET or VB6 or C++ or Delphi), but causes problems once in production (deployment, support, code control etc.)