Hopefully this is an easy one. I have a series of charts in MS Excel that point to data on the same worksheet. The data on the worksheet is calculated using a VBA function. When the data is updated by the VBA function the new numbers are not reflected in the charts that are pointing to them. I tried calling Application.Calculate, but that didn't do the trick. Any thoughts?
UDPATE:
I was able to duplicate this issue on a much smaller scale. Here's how:
Create a new workbook
Rename Sheet 1 to "Summary"
Rename Sheet 2 to "Data"
Open the Summary sheet in the VBA editor and paste the following code:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Parent.Range("worksheetDate") = Target Then
Application.CalculateFull
End If
End Sub
Create a new VBA module
Paste the following code into the new VBA module (I apologize - I can't get Stack Overflow to format this correctly for the life of me - this is the best I could get it to do):
.
Function getWeekValue (weekNumber As Integer, valuesRange As Range) As Integer
Dim aCell As Range
Dim currentDate As Date
Dim arrayIndex As Integer
Dim weekValues(1 To 6) As Integer
currentDate = ThisWorkbook.Names("worksheetDate").RefersToRange.Value
arrayIndex = 1
For Each aCell In valuesRange
If month(currentDate) = month(ThisWorkbook.Sheets("Data").Cells( _
aCell.Row - 1, aCell.Column)) Then
weekValues(arrayIndex) = aCell.Value
arrayIndex = arrayIndex + 1
End If
Next
getWeekValue = weekValues(weekNumber)
End Function
.
Modify the Data worksheet to match the following image:
Select Cell B1 and name the range "worksheetDate"
Duplicate rows 1 through 3 in the following image:
In row 4, under the "Week X" headers, enter the following formula
.
= getWeekValue(1, Data!$A$2:$M$2)
incrementing the first argument to the getWeekValue function by one for each week (e.g., pass 1 for Week 1, 2 for Week 2, 3, for Week 3, etc.
Create a bar graph using cells A3 through E4 as the data
Change the date in cell B2 to a date between 10/1/2010 and 12/31/2010, choosing a month other than the month that is currently in the cell. For example, if the date is 12/11/2010, change it to something like 11/11/2010 or 10/11/2010. Note that both the data and chart update correctly.
Modify the date in cell B2 gain. Note that the data updates, but the chart does not.
Oddly, after a period of time (several minutes) has elapsed, the chart finally updates. I'm not sure if this is because I have been performing other activities that triggered the update or because Excel is triggering an update after several minutes.
Just figured out the solution to this issue as I was suffering from the same.
I've just added "DoEvents()" prior to printing or exporting and the chart got refreshed.
example
Sub a()
Dim w As Worksheet
Dim a
Set w = Worksheets(1)
For Each a In w.Range("a1:a5")
a.Value = a.Value + 1
Next
DoEvents
End Sub
at the end of my changes I close the workbook and reopen it. that seems the easiest and most reliable way to update everything for me.
For example:
Sub a()
Dim w As Worksheet
Dim a
Set w = Worksheets(1)
For Each a In w.Range("a1:a5")
a.Value = a.Value + 1
Next
w.ChartObjects(1).Chart.Refresh
End Sub
This solution worked for me. For the offending worksheet add:
Private Sub Worksheet_Activate()
Dim rngSelection As Range
Dim objChartObject As ChartObject
Dim objChart As Chart
Dim objSeriesCollection As SeriesCollection
Dim objSeries As Series
Dim strFormula As String
Set rngSelection = Selection
For Each objChartObject In Me.ChartObjects
Set objChart = objChartObject.Chart
Set objSeriesCollection = objChart.SeriesCollection
For Each objSeries In objSeriesCollection
strFormula = objSeries.Formula
objSeries.Delete
Set objSeries = objSeriesCollection.NewSeries
objSeries.Formula = strFormula
Next objSeries
Next objChartObject
rngSelection.Select
End Sub
It's possible that the issue is the argument list of getWeekValue, which includes only the week number and the data stream.
If you add a third argument, worksheetDate, then Excel's recalculation engine will be hit on the side of the head with the fact that getWeekValue uses the value held in worksheetDate. In your current implementation, this fact is held only in the VBA code, where it is probably invisible to the recalculation engine.
I write this so hedgingly because I am not privy to the inner workings of the recalculation engine. (Maybe someone who knows about this better than I can comment on my speculation) But I did do a test, in which getWeekValue does have that third argument, and the chart does recalculate properly. Nice added benefit of this approach: you can remove all that other VBA event management. -HTH
I've found that calling this Sub works...
Sub DoAllEvents()
DoEvents
DoEvents
End Sub
BUT
Microsoft cautions about being caught with the next DoEvents executing before the first DoEvents completes, which can happen depending on how often it's called without a delay between calls. Thus DoEvents appears to be acting as a type of non maskable interrupt, and nesting non maskable interrupts can cause the machine to freeze for multiple reasons without any recovery other than reboot.
(Note: If one is not calling the routine above, often and quickly, nesting may not
be an issue.)
Using the following Sub below, which I modified from their suggestion, prevents this from happening.
Sub DoAllEvents()
On Error GoTo ErrorCheck
Dim i
For i = 1 To 4000 ' Start loop. Can be higher, MS sample shows 150000
'I've found twice is enough, but only increased it to four or 4000.
If i Mod 1000 = 0 Then ' If loop has repeated 1000 times.
DoEvents ' Yield to operating system.
End If
Next i
Exit Sub
ErrorCheck:
Debug.Print "Error: "; Error, Err
Resume Next
End Sub
I appears that the number of DoEvents needed is based on the number of background tasks running on your machine, and updating the graph appears to be a background task for the application. I only needed two DoEvents because I call the routine frequently; however, I may end up upping it later if needed.
I also keep the Mod at 1000 so to not change the lag between each DoEvents as Microsoft suggests, preventing nesting. One possible reason you might want to increase the number from 2000 to a higher number is if you system does not update the graph. Increasing this number allows the machine to handle larger numbers of background events that DoEvents might encounter through multiple calls as they are probably on a stack, and the DoEvents event is only allowed to run a specific number of cycles before marking its place in the stack to allow unhandled events and returning, leaving them to be handled on the next call. Thus the need for multiple calls. Changing this to their example of 150000 doesn't appear to slow the machine too much, to play it safe you might want to make it 150000.
Note: the first example Sub with two DoEvents is probably safe depending on how often you call the Sub, however, if called too often, your machine might freeze up. Your call. ;-)
PS: DoEvents will become one of your best calls if you create a lot of nested loops and the program doesn't behave as expected. Fortunately, this is available in all apps that use VBA!
Running Excel 2019.
Added the following to the macro code:
ActiveSheet.ChartObjects(1).Chart.Refresh
DoEvents
The chart now updates during macro execution
UDF getWeekValue has to be marked as volatile.
Function getWeekValue (weekNumber As Integer, valuesRange As Range) As Integer
Application.Volatile '!!
Dim aCell As Range
Dim currentDate As Date
'...
Just an idea: in your Worksheet_Change Sub, insert as the first line:
Application.EnableEvents = False
in order to avoid self-firing events....
Of course set it back to True at the end of the Sub.
Related
I have real-time data streaming from another application into Excel and making =RTD() calls successfully in a complex workbook.
Trying to (a) compare historical data as of a moment in time to current/live data, and, (b) run stats on snapshots taken. Say I have RTD data in 4 rows and 8 columns A1:H4.
The "freeze": Is it possible to push a button (to run a macro) and render, say, row 2 (A2:H2) 'inert', so it's no longer RTD? Basically, copy then-current values and paste them into the same cells (A2:H2) as values, removing the RTD formulas so that the data stops changing at that moment.
The "snapshot": Make a copy of the 3rd row, A3:H3, and paste it into A5:H5 periodically by timer, and have the copy become 'inert', i.e., no longer RTD or linked to RTD in row 5 while row 3 remains RTD. And place a timestamp for this action in cell I5 next to the copy.
This should also theoretically enable me to chart the snapshot data. I can't get RTD data to chart - expected that chart would update as streaming data changed, but charts won't display at all. So that's a secondary but related issue. It would appear that if I can snapshot the data and make it inert, I should be able to chart.
I've tried all kinds of manual processes from paste special/values, go to / special / formulas, highlight, etc. to using a third party tool. I've looked for VBA macros and found one which I'm pasting below, but I can't get it to work (the macro runs on cells to which it's directed, does not error out, but nothing happens). And even if it did work, it would only solve part of the problem as per above.
I do not speak VBA but write enough code in other languages that I should be able to figure out what it does and mod it, if someone is able to offer the base functionality. Any help would be greatly appreciated.
'This code should go in a regular module sheet, and won't work properly if installed anywhere else. _
The next (Dim) statement must occur before any subs or functions.
Dim NextTime As Double
Sub RecordData()
Dim Interval As Double
Dim cel As Range, Capture As Range
Interval = 5 'Number of seconds between each recording of data
Set Capture = Worksheets("Sheet1").Range("A1:A5") 'Capture this column of data
With Worksheets("Sheet2") 'Record the data on this worksheet
Set cel = .Range("A2") 'First timestamp goes here
Set cel = .Cells(.Rows.Count, cel.Column).End(xlUp).Offset(1, 0)
cel.Value = Now
cel.Offset(0, 1).Resize(1, Capture.Cells.Count).Value = Application.Transpose(Capture.Value)
End With
NextTime = Now + Interval / 86400
Application.OnTime NextTime, "RecordData"
End Sub
Sub StopRecordingData()
On Error Resume Next
Application.OnTime NextTime, "RecordData", , False
On Error GoTo 0
The above code is initiated when the workbook is opened, and stopped when the workbook is closed by code in ThisWorkbook code pane.
'These subs must go in ThisWorkbook code pane. They won't work at all if installed anywhere else!
Private Sub Workbook_BeforeClose(Cancel As Boolean)
StopRecordingData
End Sub
Private Sub Workbook_Open()
RecordData
End Sub
End Sub
I am a complete novice, this is my first VBA code (necessity is mother of . . . inept coding by novice).
Problem: Why is my code not updating in real-time? Or in any time at all? Can it be fixed? Do I need to somehow put all 16 sheets worth of VBA code into a "module" or do some other trick to fix it?
Background:
I have VBA code "behind" multiple "client" spreadsheets in a workbook. The code allows cell colors to transfer to a master "all clients" spreadsheet. The reason I needed the VBA code was that there was a function (and INDEX function) already in the color-filled cells.
The code was not working properly, so I figured out that the references were wrong and edited one of the sheets' VBA code to ensure I had the references right. They were correct. But even getting those edited references in that one sheet's code to work correctly took a bunch of clicking around and saving and reopening the document.
I then needed to fix the code in all the other sheets, starting with one of them. I can't for the life of me get anything to happen even though I made the correct edit. I should have seen colors change, but nothing happened.
Google search led me to the news that just putting code "behind" spreadsheets often doesn't work. One reference said I should place it in a module. But I have no idea how to do that across all of my 16 client sheets.
I'm also working over Remote Desktop which is probably not helping. I could probably send myself the workbook if needed.
Below is my code (one sheet's worth). The references are different across sheets so that the various client's data (in vertical columns) populates on the correct horizontal rows of the master sheet. Along with that data are the colors that this VBA code is supposed to help render onto the master sheet.
This is the "Glen" spreadsheet's VBA code, Glen's data that needs to be color coded identically on the "WeeklyRatingsAllClients" sheet (ending up in the BD6:CH6 range and BD7:CH7 range) is in the Q4:Q38 range and the U4:U38 range. The other sheets are the exact same except that in the next person's sheet the BD6:CH6 range and BD7:CH7 ranges will update to become BD8:CH8 range and BD9:CH9 and so on sequentially (next client is 10, 11; next is 12, 13 etc.).
If it matters to anyone, I got the original code here and modified it for my needs: https://www.extendoffice.com/documents/excel/4071-excel-link-cell-color-to-another-cell.html
Also, I make a long comment on above page under "Sara" dated 3 months ago that describes more about the code/purpose and shows how I modified the example code for my purpose and it worked--it's just not working now (probably not useful if you already know this stuff well, like I don't).
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim xRg As Range
Dim xCRg As Range
Dim xStrAddress As String
xStrAddress = "WeeklyRatingsAllClients!$BD$6:$CH$6"
Set xRg = Application.Range(xStrAddress)
Set xCRg = Me.Range("$Q$4:$Q$38")
On Error Resume Next
For xFNum = 1 To xRg.Count
xRg.Item(xFNum).Interior.Color = xCRg.Item(xFNum).Interior.Color
Next
xStrAddress = "WeeklyRatingsAllClients!$BD$7:$CH$7"
Set xRg = Application.Range(xStrAddress)
Set xCRg = Me.Range("$U$4:$U$38")
On Error Resume Next
For xFNum = 1 To xRg.Count
xRg.Item(xFNum).Interior.Color = xCRg.Item(xFNum).Interior.Color
Next
End Sub
Perhaps use the Workbook.SheetSelectionChange event, something like the following. Note that this can definitely be refactored.
Make sure to add this code in the ThisWorkbook module.
Change "Bob", "Fred", "Joe" to the sheet names in question (in order), and add more Cases as needed, always increasing the offsetNum by 2 from the previous Case.
There's a mismatch in the number of cells on the main sheet vs the client sheet. U4:U38 would be 35 cells, but BD6:CH6 is only 31... more an FYI.
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
Dim offsetNum As Long
Select Case Sh.Name
Case "Glen"
offsetNum = 0
Case "Bob"
offsetNum = 2
Case "Fred"
offsetNum = 4
Case "Joe"
offsetNum = 6
Case Else
Exit Sub
End Select
Dim allClientsSheet As Worksheet
Set allClientsSheet = Me.Worksheets("WeeklyRatingsAllClients")
Dim mainColorRange As Range
Set mainColorRange = allClientsSheet.Range("BD6:CH6").offset(offsetNum)
Dim sourceColorRange As Range
Set sourceColorRange = Sh.Range("Q4:Q38")
Dim i As Long
For i = 1 To mainColorRange.Rows(1).Cells.Count
mainColorRange.Rows(1).Cells(i).Interior.Color = sourceColorRange.Cells(i).Interior.Color
Next
Set sourceColorRange = Sh.Range("U4:U38")
For i = 1 To mainColorRange.Rows(2).Cells.Count
mainColorRange.Rows(2).Cells(i).Interior.Color = sourceColorRange.Cells(i).Interior.Color
Next
End Sub
I encounter a weird problem that I believe is related to Excel behavior, rather than to my code.
I have a global variable named "numEtape", which is an integer. My code consists in several steps where the user has to type data on a sheet, then press a button which saves the data in an array and increments the "numEtape", before going to the next step.
The code (simplified) looks like this :
Dim numEtape As Integer
Sub AjoutEssai()
numEtape = 2
UFPreAjoutInfos.Show 'Unrelated Userform that asks user for more informations, but doesn't modify "numEtape" or call any other macro
Call InterfaceFiller
End Sub
Sub InterfaceFiller()
Dim rangedubtn As Range
Dim btnPrecedent As Button
Select Case numEtape
Case 2
'Change value of some cells
Case 3
'Change value of some cells
Case 4
'Change value of some cells
Case Is >= 5
'Change value of some cells
Case Else
Debug.Print "Error"
End Select
Set rangedubtn = Sheets("Interface").Range("M3")
Set btnPrecedent = Sheets("Interface").Buttons.Add(rangedubtn.Left, rangedubtn.Top,rangedubtn.Width, rangedubtn.Height)
With btnPrecedent
.OnAction = "mSuivant"
.Caption = "Suivant"
.Name = "btnSuivant"
End With
End Sub
Sub mSuivant()
numEtape = numEtape + 1
Call InterfaceFiller
End Sub
I don't think the code itself is important, what I can expect from it, since I first call AjoutEssai(), is for numEtape to always be greater than 2.
However, when during the steps the user opens and close other excel/office files (that don't have any vba code/macros in it), excel seems to empty numEtape, which makes the Select Case go to the Case Else.
When does excel remove global variables from memory, and is there a way to prevent this behavior from happening?
Public numEtape As Long
A viable option is to use the word public like public numEtape As Long.
Then the variable will be saving its value for as long as the Excel workbook is opened. In older versions of VBA the word was Global (What is the difference between Dim, Global, Public, and Private as Modular Field Access Modifiers?)
Dim numEtape As Long
For using Dim outside of Sub or Function, the variable will be emptied, after the code is over. Take this snippet only:
Dim numEtape As Long
Sub MainTest()
numEtape = 23
End Sub
Once you run it and you hit End Sub the variable would be emptied as well. Check after running the MainTest():
Sub PrintingVariable()
Debug.Print numEtape
End Sub
If you want to save the value, there are 2 basic ways that work:
Write the value in an excel cell
Write the value in a database
Every time I try and execute my code it says "object required."
I want a MsgBox to open if one a date on my birthday list matches today.
The birthday list extends from b2 to b100 and I want to look through to find the birthday.
This is a piece of code I took from a YouTube video.
The MsgBox pops up every time I open the workbook.
Private Sub Workbook_Open()
Dim cl As Range
Set cl = ThisWorbook.Sheets("Birthdays").Range("B2:B100")
If IsDate(cl) Then
If Now >= cl Then
MsgBox "Somebody's had a birthday!"
End If
End If
End Sub
You are getting Object Required error because of a typo ThisWorbook should be ThisWorkbook
It is very normal to encounter such errors. So always use Option Explicit. I have covered it in To ‘Err’ is Human
So Can I have it added to my code automatically?
Yes you can. To have it added to all new files you create, simply select "Tools" -> "Options" in the VBE, and tick the "Require Variable Declaration" box.
Note: This will effect only new files that you create. You will need to add it yourself to existing files.
I basically just want my excel to create a msgbox when I open it, if one of the date on my birthday list matches today.
You can use Application.WorksheetFunction.CountIf to check if there is today's date in a range.
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
Dim matchFound As Long
Set ws = ThisWorkbook.Sheets("Birthdays")
Set rng = ws.Range("B2:B100")
matchFound = Application.WorksheetFunction.CountIf(rng, Date)
If matchFound > 0 Then
MsgBox "Birthday Found"
Else
MsgBox "Birthday Not Found"
End If
End Sub
Screenshot
cl is a Range object representing 99 dalmatians cells, each encapsulating a Variant value.
The IsDate function is happy to take a Variant, but doesn't know what to do with 99 of them.
Because Range has a hidden default property, you can use it as if it were a value - but, especially for someone that's just beginning to learn VBA, it makes for confusing, implicit, "magic" code that says one thing, and does another.
If IsDate(cl.Value) Then
The implicit Range.Value member call here, yields a Variant representing the cell's value itself if the range represents only a single cell, otherwise (i.e. if the range is for more than one cell) it yields a Variant pointing to a 2D Variant array (in this case 99x1) that's holding every single value.
IsDate wants one value, so if we have 99 of them, we need a loop. But here's the thing: the last thing we want to do is iterate individual cells, get their Value, and verify that - because that would be very slow.
So instead, we grab that 2D Variant array, and iterate that.
If cl.Count = 1 Then
'single-cell range: not a 2D array
If Now >= cl.Value Then
MsgBox "Somebody's had a birthday on " & Format(cl.Value, "yyyy-mm-dd")
End If
Exit Sub
End If
Dim values As Variant
values = cl.Value
Dim currentRow As Long
For currentRow = LBound(values, 1) To UBound(values, 1)
Dim currentCol As Long
For currentCol = LBound(values, 2) To UBound(values, 2)
Dim currentValue As Variant
currentValue = values(currentRow, currentCol)
If IsDate(currentValue) Then
If Now >= currentValue Then
MsgBox "Somebody's had a birthday on " & Format(currentValue, "yyyy-mm-dd")
Exit Sub
End If
End If
Next
Next
Right now the msgbox just pops up every time I open the excel whether a birthday matches or not.
Sounds like your actual code has On Error Resume Next somewhere - that makes VBA ignore any run-time errors and merrily keep running, ...and you definitely don't want that. Rule of thumb, never use On Error Resume Next to side-step an error. Execution is normally halted when there's an "object required" error: an unconditional MsgBox popping means execution is allowed to continue in an error state, and that can't be a good thing.
As Sid found out, the type mismatch is caused by a typo -- this shouldn't be allowed to happen: make sure every module you ever type any code in says Option Explicit at the top, and it'll never happen again... for your early-bound code (late-bound code is still vulnerable to typos, but that's another story for another time).
Lastly, note that several of the above mentioned issues would have been reported by Rubberduck's code inspections (disclaimer: I manage this open-source project) - including the typo, the implicit default member calls, and the absence of Option Explicit.
I have two spreadsheets... when one gets modified in a certain way I want to have a macro run that modifies the second in an appropriate manner. I've already isolated the event I need to act on (the modification of any cell in a particular column), I just can't seem to find any concrete information on accessing and modifying another spreadsheet (this spreadsheet is located on a different LAN share also... the user has access to both, though).
Any help would be great. References on how to do this or something similar are just as good as concrete code samples.
In Excel, you would likely just write code to open the other worksheet, modify it and then save the data.
See this tutorial for more info.
I'll have to edit my VBA later, so pretend this is pseudocode, but it should look something like:
Dim xl: Set xl = CreateObject("Excel.Application")
xl.Open "\\the\share\file.xls"
Dim ws: Set ws = xl.Worksheets(1)
ws.Cells(0,1).Value = "New Value"
ws.Save
xl.Quit constSilent
You can open a spreadsheet in a single line:
Workbooks.Open FileName:="\\the\share\file.xls"
and refer to it as the active workbook:
Range("A1").value = "New value"
After playing with this for a while, I found the Michael's pseudo-code was the closest, but here's how I did it:
Dim xl As Excel.Application
Set xl = CreateObject("Excel.Application")
xl.Workbooks.Open "\\owghome1\bennejm$\testing.xls"
xl.Sheets("Sheet1").Select
Then, manipulate the sheet... maybe like this:
xl.Cells(x, y).Value = "Some text"
When you're done, use these lines to finish up:
xl.Workbooks.Close
xl.Quit
If changes were made, the user will be prompted to save the file before it's closed. There might be a way to save automatically, but this way is actually better so I'm leaving it like it is.
Thanks for all the help!
Copy the following in your ThisWorkbook object to watch for specific changes. In this case when you increase a numeric value to another numeric value.
NB: you will have to replace Workbook-SheetChange and Workbook-SheetSelectionChange with an underscore. Ex: Workbook_SheetChange and Workbook_SheetSelectionChange the underscore gets escaped in Markdown code.
Option Explicit
Dim varPreviousValue As Variant ' required for IsThisMyChange() . This should be made more unique since it's in the global space.
Private Sub Workbook-SheetChange(ByVal Sh As Object, ByVal Target As Range)
' required for IsThisMyChange()
IsThisMyChange Sh, Target
End Sub
Private Sub Workbook-SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
' This implements and awful way of accessing the previous value via a global.
' not pretty but required for IsThisMyChange()
varPreviousValue = Target.Cells(1, 1).Value ' NB: This is used so that if a Merged set of cells if referenced only the first cell is used
End Sub
Private Sub IsThisMyChange(Sh As Object, Target As Range)
Dim isMyChange As Boolean
Dim dblValue As Double
Dim dblPreviousValue As Double
isMyChange = False
' Simple catch all. If either number cant be expressed as doubles, then exit.
On Error GoTo ErrorHandler
dblValue = CDbl(Target.Value)
dblPreviousValue = CDbl(varPreviousValue)
On Error GoTo 0 ' This turns off "On Error" statements in VBA.
If dblValue > dblPreviousValue Then
isMyChange = True
End If
If isMyChange Then
MsgBox ("You've increased the value of " & Target.Address)
End If
' end of normal execution
Exit Sub
ErrorHandler:
' Do nothing much.
Exit Sub
End Sub
If you are wishing to change another workbook based on this, i'd think about checking to see if the workbook is already open first... or even better design a solution that can batch up all your changes and do them at once. Continuously changing another spreadsheet based on you listening to this one could be painful.