I have searched on the web quite extensively, but have not managed to find any similar experiences. Any ideas?
I have a simple subroutine in VBA that changes a control cell. This control cell is used by the formulae in another sheet. As the code changes the control values, Excel uses increasingly more and more ram to the point that excel comes to a halt.
Basically I have a sheet setup that has 3000 rows and 330 columns. In each cell of the sheet the same formula below is populated:
=sum(sheet1!F8:INDEX(sheet1!F8:F$3000,Control!$D$1))
So in cells A1 you would have the formula above and in cell say B1 you would have:
=sum(sheet1!F**9**:INDEX(sheet1!F9:F$3000,Control!$D$1))
The control value that the code changes is Control!$D$1 and therefore changing control say from 2 to 4 will result in calculating running Sums from 2 to 4 consecutive rows in sheet1.
Note the code starts by setting a high value in the control cell (200) and works its way down to 2. The memory usage increase is therefore really baffling me.
The code I have is:
For i = 200 To 1 Step -1
Application.Calculation = xlCalculationManual
ClearClipboard 'sets cutcopymode to false
Range("Control!d1").Value = i
Application.Calculation = xlCalculationAutomatic
Next i
Finally I have tried the following alternatives and none of them suited me:
Doing all calculations in VBA arrays: Since VBA is not multithreaded, this is painfully slow (the point is to take advantage of my cpu cores using formulae in excel worksheets)
Setting screenupdating = false, enablevents = false, cutcopymode = false have no significant improvements
Converting formulae to values and reentering formulae by VBA: It again slows down the calculations
Manually reducing the number of processors: Defeats the purpose of my method as I need fast calculations
Is this an excel bug?
I have done some more research on your problem. The way your running sum function is built a lot of intermediate ranges have to be created:
=sum(sheet1!F8:INDEX(sheet1!F8:F$3000,Control!$D$1))
First, sheet1!F8:F$3000, second the result of the INDEX() function, third the argument to SUM().
Instead, I propose to only construct one range (the bare minimum) using the OFFSET() function. It lends itself to the task as the control parameter is a scalar and OFFSET() creates a new range from a range and scalars.
The running sum then is
=SUM(OFFSET(sheet1!$A$1;ROW()-1;COLUMN()-1;Control!$D$1;1))
where the control value merely sets the size of the range to be summed. In my tests with 2000 x 14 cells and 200 loops there was no increase in memory consumption to be observed. As both methods (INDEX() and OFFSET()) were quite fast for a sheet that size I cannot make any assumptions about calculation time (but see below for hints).
I have added the ROW() and COLUMN() functions to make the formula self-adjusting - the start address for the sum will be the identical address of the cell the formula is in. This way, all formulas on the sheet are exactly identical and do not have to be changed depending on their location.
As to the runtime, I would suggest you leave changing the Calculation Mode out of the loop (i.e. as automatic). I have noticed a huge overhead of several seconds for each change of mode. After changing the control cell all dependent cells have to be calculated once, and setting the mode to manual will not change that.
Lastly, if you try this formula with your original data, test whether leaving out the ClearClipboard command will have any effect. Ultimately it will have to call a Windows function thus leaving the Excel process environment, and I cannot see why it is needed here anyway except for cosmetic purposes.
Related
I've got a macro which generates a list from a spreadsheet, filters out things we need and don't need and formats it at the end. For filtering out information I am using a simple if formula which is inputted by my macro:
MyRange4.Formula = "=IF(M2<H2+1,""yes"",""no"")"
MyRange4.AutoFill Destination:=Formula3
When you run the macro, it is slowing it down a bit because it has to calculate for over 2000 lines, but it looked fine to me for a while, until we realized some of the lines we should have on the list are not there. When looking into it, I have filtered on the line we wanted, and the if formula was showing "no" which is obviously why it's not got excluded, but after 2-3 second the sheet recalculated and it's changed to "yes". Is there anyway I could force this to happen before the macro filters on the "yes" ones only so I am sure it's all correct?
Could this do the trick for you:
'Above this line: First bit of your code untill calculating formula's
Worksheets("Sheet1").UsedRange.Columns("A:A").Calculate 'Change sheetname and column accordingly
If Not Application.CalculationState = xlDone Then
DoEvents
End If
'Below this line: Second bit of your code, filtering the data
This is the code I have in a [very involved] spreadsheet someone made at work:
Sub ClearSheet()
'
' Macro5 Macro
'
'
Range("E9,E2:F7,C14:I39,Q41:Q55,N14:N39,N41:N55").Select
Range("Q14").Activate
Range("E9,E2:F7,C14:I39,C41:I55,Q41:Q55,N14:N39,N41:N55,L41:L55").Select
Range("Q41").Activate
Selection.ClearContents
I have never so much as glanced at an excel macro before, so I had to look some things up. I get that the first range is selected and then Q14 becomes the active cell. Then that is done again, with some overlapping sections, and Q41 is made into the active cell. All to have the selections just be cleared out. I'm sure this is a simple question but I don't understand what the point is of the .Activates, or why someone would separate the sections that need to be cleared into two separate segments? From my very limited understanding, I thought Activate was something like focus, where that is now that cell that has focus for ease of use on the users side. But what good is that if the focus changes from the first cell to the second cell in a millisecond?
All I know is that I need these cells:
E9,E2:F7,C14:I39,N14:N39,C41:I55,L41:L55,N41:N55,Q41:Q55
to clear out when this code is run, and if this code is doing something in addition to that, what is it?
Is this just poorly written or am I too ignorant to understand? ~the novel~
Use
Range("E9,E2:F7,C14:I39,N14:N39,C41:I55,L41:L55,N41:N55,Q41:Q55").ClearContents
Better still specify the workbook and worksheet to do this in e.g.
ThisWorkbook.Worksheets("Sheet1").Range("E9,E2:F7,C14:I39,N14:N39,C41:I55,L41:L55,N41:N55,Q41:Q55").ClearContents
Using sheet 1 as an example. You want to be sure to be in the right sheet before clearing stuff out. If you don't specify, and leave as just range, then the currently Active sheet is used.
In the code you talked about the each selection was shifting focus from the prior making the prior selections redundant.
Using Select, in particular, is not generally a good thing, it means 'touching' the sheet which incurs potentially unnecessary performance overhead.
As mentioned in comments, and indicated by ' Macro5 Macro, this is, at least in part, likely all, macro generated code. Macro meaning "many". Many instructions in this case. The macro recorder is verbose to say the least. It records everything your are doing including scrolling, mistakes in range selections etc. It is a good learning tool, and can often give useful insights into some objects and methods. The valuable skill is learning which elements to keep and how to turn this verbose code into structured programming.
The way you interpret Select and Activate is correct, one is for the actual selection and the other is somewhat to focus.
Select as the method name suggest selects the object. This method is not limited to Range Objects alone but is shared by most of the objects in Excel. Some of the examples:
Range("A1").Select '/* selecting a Range Object */
Worksheets("Sheet1").Select '/* selecting a Sheet Object */
Activate on the other hand works when you already selected an object.
Activates a single cell, which must be inside the current selection. To select a range of cells, use the Select method.
So what happens when you activate a cell not in the current selection?
It becomes the selected cell and as you've said, Excel executes the Select first and then the Activate in mili or nano or pico seconds (God knows how fast) interval.
In Range Objects the use of Select and Activate is almost interchangeable. But you have to take note that there will be difference always with Selection and ActiveCell. For example:
Range("A1:B10").Select
Range("B5").Activate
Debug.Print Selection.Address
Debug.Print ActiveCell.Address
This means that you can actually do stuff (e.g. format, clear, add formula, add text etc.) on all cells you activate within the current selection but still preserves what Selection object points to.
There are cases that activating the object is vital. For example you want to select multiple worksheets like below and then select Range("A1") of Sheet3.
Worksheets(Array("Sheet1", "Sheet3", "Sheet5")).Select
Worksheets("Sheet3").Activate '/* vital */
Worksheets("Sheet3").Range("A1").Select
Above is the correct select command for multiple worksheet selection and selecting a range within 1 of the worksheets selected. But without the Activate part, there is a chance that it will return:
Run-time error '1004': Select method of Range class failed
because the first sheet in the array will always be the activated sheet object after the select. Now, how to avoid this troubles? Simple, avoid using select and activate. ~the novel sequel~
This is actually an improvement for this previous topic.
Context:
Excel 2003, Windows 7 Professional SP1.
Working on workbook A, which contains a cell with conditional formatting calling a personal function. This function refers to a defined name of workbook A.
Workbook B is opened or edited (not only activated). It can be any workbook.
We activate back workbook A.
Problem:
The conditional formatting causes various bugs if workbook B's window overlaps workbook A's. In other words, when at some point workbook A gets behind workbook B.
Depending on the window state (whether Excel is in full screen or not, whether ActiveWindow.WindowState = xlNormal or xlMaximized, whether the window is narrow or wide ...), the bugs can be :
Mainly: cells calling personal (volatile) functions that refer to defined names don't calculate when switching back to workbook A. This is a huge problem, as I need to force calculation with Ctrl+Alt+F9.
Display bugs often occur, notably when workbooks A and B are floating next to each other, overlapping each other. Cell content vanish, ghosts cells of the ones with conditional formatting appear, the screen freezes, window border trails (W98 style) are displayed when trying to move around one of the workbooks ...
In Workbook_Activateof workbook A, breakpoints don't stop the code (though debug.print and MsgBox work) and you can't use Application or ActiveWindow methods. Application.CalculateFull sometimes causes all the cells' content to disappear until they are selected (using Ctrl+A, for example).
How to solve or avoid this problem?
I will answer to myself below, if it can be of some interest for anyone!
Solution 1:
Just don't use personal functions referring to defined cells names in conditional formatting formulas.
This solution solves every problem, but it's not acceptable when you have to have complex conditional formatting.
Solution 2:
Bug sample
Bug sample (solved)
Create a named cell anywhere in workbook A, for example call it "CondFormat". Set its value to "TRUE".
This will be the boolean driving the call of personal functions in your conditional formatting formulas.
Edit every conditional formatting this way:
=MyFunctionWithCellNames($A1)
becomes:
=IF(CondFormat,MyFunctionWithCellNames($A1))
Automatically toggle the boolean when you activate or deactivate the workbook:
Private Sub Workbook_Activate()
[CondFormat] = True
End Sub
Private Sub Workbook_Deactivate()
ThisWorkbook.Names("CondFormat").RefersToRange = False
End Sub
Note 1: sometimes, [CondFormat] isn't toggled when leaving the workbook. The ThisWorkbook.Names syntax makes it work all the time.
Note 2: we have to use one of the cells for the boolean CondFormat, because only macros can access to VBA global variables.
Note 3: I spent a lot of time figuring this out, I hope it will be useful for others. I think that most problems with conditional formatting can only be solved this way in Excel 2003, as Sheet.EnableFormatConditionsCalculation method only appeared in Excel 2007.
First, I must admit that I am completely newbie here and in VBA mostly too. Thank you for this forum! :)
I have program that reacts on SheetChange to save the data. However, if there is formula added by macro, it doesn't cause the SheetChange. Copying and inserting as values isn't possible - there are 7k rows and the program acts on every SheetChange (colors and other stuff) - the time is not bearable.
Is there any possibility to have EnableEvents = False (to turn of getting SheetChange), then specify the Range of the changed cells (always rather the whole column - there are 2 columns only that interest me) and then let the program save data. The coloring of the cells and so on would remain (this coloring and so on has to stay in the program)
Is it even possible that it would work? If it could, how should I tell the macro that specific Range has SheetChange?
Apologies if the question is totally stupid.
Thank you very much for reading at least.
In the sheet change event, just specify that it should only save when Target is within the specified range. For the formulas, use the Calculate event and repeat essentially the same code.
So, if you only want it to save when the changed cell is in the first column and only within a certain row range (for example), add If Target.Column = 1 And Target.Row > 5 And Target.Row <= 10 Then to your change event.
For the Formula issue, add the following routine
Private Sub Worksheet_Calculate()
'your code here
End Sub
I've setup a formula that combines numbers from multiple worksheets and then compares it against another number from worksheet.
I am using the INDIRECT function to reference the sheets as well as COLUMN and ROW to adjust the numbers to the corresponding coordinates when I drag it across.
However, some of the figures don't always exist in the worksheets so a #VALUE error is returned. How Can I change it so a blank cell is shown if this happens?
My Current formula: =INDIRECT("'"&$C$11&"'!R"&ROW(E29)&"C"&COLUMN(E29),FALSE)-SUM(INDIRECT("'"&$C$11&"'!R"&ROW(C29)&"C"&COLUMN(C29),FALSE),INDIRECT("'"&$C$13&"'!R"&ROW(E29)&"C"&COLUMN(E29),FALSE))
Use =IFERROR(<your original formula>, "") which will replace any error with "" but passes any other result through.
But do bear in mind the degradation in spreadsheet stability: INDIRECT makes spreadsheets brittle enough on its own: your hiding any error output could be dangerous.