I have an excel vba macro that creates and formats a sales quote. I have a function that autofits merged cells. I use this function to autofit description fields as some of them are long and some are short. On a typical quote, this function is called around 40 times. The macro completes in under a second. If I run the exact same macro again (perhaps with different settings on how it will display) it takes upwards of 30-60 seconds. There's nothing in the rest of the macro that slows down with each run except for the following block:
Is there something that could be making this code run slower for the exact same set of inputs?
Sub AutoFit_Height(ByVal Target As Range)
Dim MergeWidth As Single
Dim cM As Range
Dim CWidth As Double
Dim NewRowHt As Double
With Target
.MergeCells = False
CWidth = .Cells(1).ColumnWidth
MergeWidth = 0
For Each cM In Target
cM.WrapText = True
MergeWidth = cM.ColumnWidth + MergeWidth
Next
'small adjustment to temporary width
MergeWidth = MergeWidth + Target.Cells.count * 0.66
.Cells(1).ColumnWidth = MergeWidth
.EntireRow.AutoFit
NewRowHt = .RowHeight
.Cells(1).ColumnWidth = CWidth
.MergeCells = True
.RowHeight = NewRowHt
End With
End Sub
Go to the start of the main body of code (the one that calls this subroutine) and add
Application.ScreenUpdating = False
as one of the first lines. Then at the end of that function, after this subroutine has been called, turn it back on:
Application.ScreenUpdating = True
This will keep it from redrawing the screen for every change in column/row size, resulting in a significantly faster run time.
Optimazing your script might start with
using xlsb file saving extension
look for any unecessary formatting by deleting unused rows and
columns and saving so to make the vertical and horizontal scroll bar
scroll just to the necessary data
Use named range as much as possible
This could be added to your script
At the start of the main module from where you call other modules or functions
With Application
.DisplayAlerts = False
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With
At the end of the main module task is complete
With Application
.DisplayAlerts = True
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
As you are manipulating many worksheet objects that's quite a cpu intensive process. A change in algorithm could improve time needed for this process. Maybe you should post the entire code so to help you further.
Cheers,
Pascal
http://multiskillz.tekcities.com
Autofit is painfully slow for me to. The only thing I can add to any other answer here is to put it in a separate module and only call it when necessary or time is available. Also, to implement last, once all other code is tested and working.
Related
I have an excel file with a VBA code (Not written by me)
How this code works is user enters a 6 digit number in a user form, the VBA then checks another sheet and if this 6 digit number is present on the worksheet.
If it does, it changes the stage, but if it doesn't it adds this 6 digit number to the worksheet
It used to work perfectly, but now because the excel file has grown in the number of rows, almost 6000 rows, this code is become very slow, takes up to 20 seconds to update the sheet
Can someone please help me speed this code up, or suggest another way to acheive it
The code is below
Private Sub cmdPSDUdate_Click()
Dim x
If (Me.PSDUDateRow = "") + (Me.PSDStageCB.ListIndex = -1) Then Exit Sub
With Sheets("psdata stage cals").ListObjects("PSDataStageCals")
x = Application.Match(Val(Me.PSDUDateRow), .ListColumns(1).DataBodyRange, 0)
If IsNumeric(x) Then
.ListRows(x).Range(2) = Me.PSDStageCB.Value
Else
.ListRows.Add.Range = Array(Val(Me.PSDUDateRow), Me.PSDStageCB)
End If
End With
Me.PSDUDateRow.Value = ""
Me.PSDStageCB.Value = ""
Me.PSDUDateRow.SetFocus
End Sub
Thanks in advance
Rahul
You could turn off screenupdating, automatic calculations etc
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Application.DisplayStatusBar = False
Application.EnableEvents = False
‘Place your macro code here
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.DisplayStatusBar = True
Application.EnableEvents = True
In general, there are two ways to speed up VBA code:
Write good code, that does not use Select, Activate, ActiveCell, Selection etc - How to avoid using Select in Excel VBA
Refer to these routines on the start and on the end of the code:
Public Sub OnEnd()
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.AskToUpdateLinks = True
Application.DisplayAlerts = True
Application.Calculation = xlAutomatic
ThisWorkbook.Date1904 = False
Application.StatusBar = False
End Sub
Public Sub OnStart()
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.AskToUpdateLinks = False
Application.DisplayAlerts = False
Application.Calculation = xlAutomatic
ThisWorkbook.Date1904 = False
ActiveWindow.View = xlNormalView
End Sub
(For improvement ideas, kindly make PullRequest)
I think that Calculation should be always set to xlAutomatic, as far as if you need xlCalculationManual to speed up, it is a good idea to refactor the code. Furthermore manual calculation is too risky.
The same goes for Date1904 - it is always set to False.
In addition to the tweaks suggested by Storax, your code is slow because you are bringing data cell-by-cell over the Excel/VBA divide.
Furthermore, you can radically speed up your MATCH function by using the Binary version of it. Have a read of http://dailydoseofexcel.com/archives/2015/04/23/how-much-faster-is-the-double-vlookup-trick/ and also try to minimise the amount of individual transfers you do across the Excel/VBA divide by either performing the lookups entirely within the Excel sheet (by using VBA to write the formula in the sheet and execute it there) or by bringing all the data into VBA in one go using variant arrays, performing your logic, and then by dumping it back in one go. Google "Efficient way to transfer data between Excel and VBA" or something similar. Also check out any articles from Charles Williams on the subject.
I don't see anything wrong with your code. Perhaps the Workbook itself is the culprit. Is it becoming huge and slow to open ?
If yes, try searching for 'cleanup excel file'.
Some results I found:
https://excelfilecleaner.codeplex.com/
https://support.microsoft.com/en-us/help/3070372/how-to-clean-up-an-excel-workbook-so-that-it-uses-less-memory
When crunching large chunks of data in Excel that requires frequent referencing of cells, it’s always much much faster to copy the data to an array (copy the entire worksheet if necessary), process the data within the array, and then write back to the worksheet if necessary. Copying data from worksheet to array is a one line command that is very very fast. Same with array to worksheet. Relatively speaking, referencing cells is a very time consuming process compared with referencing elements of an array.
I am using VBA to try and call a series of sub routines from the main sub routine. When I combine all of the subroutines with code similar to the following, I get #N/A for the cells that the formula is suppose to be ignoring.
Sub Main()
'Turn off autocalculation
Application.Calculation = xlCalculationManual
Application.DisplayStatusBar = False
'*********************************************************
'A bunch of other code
'*********************************************************
Call Sub_Routine1
Call Sub_Routine2
Call Sub_Routine3
Call Sub_Routine4
Call Sub_Routine5
Call Sub_Routine6
Call Sub_Routine7
'This is the sub routine that is not working correctly
Call Material_Formulas
'Turn back on the autocalculation function
Application.Calculation = xlAutomatic
'*********************************************************
'A bunch of other code
'*********************************************************
Application.DisplayStatusBar = True
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
When I remove the Material_Formulas subroutine from the Main sub routine and run it separately using the following script, it executes as it is suppose to and looks like the image bellow.
Private Sub Material_Formulas()
'Turn on manual calculation
Application.Calculation = xlCalculationManual
Dim lRow As Integer
Dim tiesMaterial As String
Dim result As String
lRow = Sheets("Material").Range("A2").End(xlDown).Row
lCol = Sheets("Material").Range("A2").End(xlToRight).Column
'Starts the count at column CU
endCount = lCol - 1
For c = 99 To endCount
For r = 3 To lRow
tiesMaterial = Cells(r, 87).Value
'Looks to see if the cells starting at CU2 contains a number and then iterates through each cell in row 3 to add a formula
If tiesMaterial = "TIES MATERIAL" Then
'Defines the unique ID and calendar year cells for the index-match-match function
materialID = Sheets("Material").Cells(r, "CQ").Address(False, False)
materialYear = Sheets("Material").Cells(2, c).Address(False, False)
'Starting in cell CU3 it adds the formula =INDEX(BOM_Summary_Array,MATCH(CQ3,BOM_Summary_ID,0),MATCH(CU2,BOM_Summary_Head,0))
Sheets("Material").Cells(r, c).Formula = "=INDEX(BOM_Summary_Array,MATCH(Material!" & materialID & ",BOM_Summary_ID,0),MATCH(Material!" & materialYear & ",BOM_Summary_Head,0))"
End If
Next r
Next c
'Turn on the auto calculation function
Application.Calculation = xlAutomatic
End Sub
What am I doing wrong? How does it run fine when I manually and independently select it but it fails when I combine it with the other sub routines?
Before anything else you need to improve your code. I can pretty much guarantee that this is likely happening because of poorly written code. For example:
materialID = Sheets("Material").Cells(r, "CQ").Address(False, False)
materialYear = Sheets("Material").Cells(2, c).Address(False, False)
Note that materialID and materialYear are never declared. This means they are of the type Variant (you need to add Option Explicit to the top of your code modules for this exact reason). The funny thing about Variants is they, you guessed it, vary. MaterialID could be a string, an int, a long, a decimal, a date, an array, a range, etc. We can assume that the address of the range is going into materialID, but we can't be certain.
Additionally, notice the pesky Sheets("Material"). What this actually is saying is ActiveWorkboook.Sheets("Material")`. Qualify your references otherwise you have pretty much no clue what is actually happening.
What can easily be happening in the case of your code is that the address is correctly going into materialID as a string, but it is the address from another workbook with a sheet named "Material". Unlikely, but possible.
What we do know is quite likely is that the ActiveWorkbook is changing in some capacity, likely in Sub_Routine7 (sidenote here, you must descriptively name subroutines otherwise your code is nowhere near maintainability).
Good luck with this, but I highly suggest not bothering to try to debug the situation until you have qualified your ranges, declared all your variables, and added Option Explicit.
I have the following code:
Application.ScreenUpdating = false
Set rng = Application.Selection.Range
For i = 0 To 25000
ActiveDocument.Fields.Add rng, WdFieldType.wdFieldExpression, "1+2", False
Next i
Application.ScreenUpdating = true
Is there a way to further increase its performance?
(Noticed that if I would hide the application Application.Visible = false its faster but I don't really want do that)
(The amount is an edge case, but I would still like to make it plausible and currently, it's taking around 30 minutes to complete, which no user will wait)
I am creating an Excel Spreadsheet graph using VBA like so:-
Private Sub Chart_Calculate()
Title$ = Range("charttitle")
ActiveChart.Axes(xlCategory).Select
With ActiveChart.Axes(xlCategory)
.MinimumScale = 0
.MaximumScale = 300
.MinorUnit = 10
.MajorUnit = 50
.Crosses = xlCustom
.CrossesAt = 0
.ReversePlotOrder = True
.ScaleType = xlLinear
.DisplayUnit = xlNone
End With
End Sub
I then attempt to set the series name, like this:-
ActiveChart.SeriesCollection(1).Name = CStr(some_value) & " Some Text"
When I add this line of code to the sub, it causes the Chart_Calculate function to be activated numerous times in rapid succession, causing the graph to flicker for a few seconds before stabilising.
Does anyone know what the matter is, and what I should do about it?
(This is Excel 2003 running on Windows XP.)
EDIT: PortlandRunner's suggestion cleared up the problem during the graph redrawing, but it still exists if the Graph is visible at startup. See also this.
Try inserting Application.Calculation = xlCalculationManual at the beginning of your code, and Application.Calculation = xlCalculationAutomatic at/near the end of your code.
My Excel tool performs a long task, and I'm trying to be kind to the user by providing a progress report in the status bar, or in some cell in the sheet, as shown below. But the screen doesn't refresh, or stops refreshing at some point (e.g. 33%). The task eventually completes but the progress bar is useless.
What can I do to force a screen update?
For i=1 to imax ' imax is usually 30 or so
fractionDone=cdbl(i)/cdbl(imax)
Application.StatusBar = Format(fractionDone, "0%") & "done..."
' or, alternatively:
' statusRange.value = Format(fractionDone, "0%") & "done..."
' Some code.......
Next i
I'm using Excel 2003.
Add a DoEvents function inside the loop, see below.
You may also want to ensure that the Status bar is visible to the user and reset it when your code completes.
Sub ProgressMeter()
Dim booStatusBarState As Boolean
Dim iMax As Integer
Dim i As Integer
iMax = 10000
Application.ScreenUpdating = False
''//Turn off screen updating
booStatusBarState = Application.DisplayStatusBar
''//Get the statusbar display setting
Application.DisplayStatusBar = True
''//Make sure that the statusbar is visible
For i = 1 To iMax ''// imax is usually 30 or so
fractionDone = CDbl(i) / CDbl(iMax)
Application.StatusBar = Format(fractionDone, "0%") & " done..."
''// or, alternatively:
''// statusRange.value = Format(fractionDone, "0%") & " done..."
''// Some code.......
DoEvents
''//Yield Control
Next i
Application.DisplayStatusBar = booStatusBarState
''//Reset Status bar display setting
Application.StatusBar = False
''//Return control of the Status bar to Excel
Application.ScreenUpdating = True
''//Turn on screen updating
End Sub
Text boxes in worksheets are sometimes not updated
when their text or formatting is changed, and even
the DoEvent command does not help.
As there is no command in Excel to refresh a worksheet
in the way a user form can be refreshed, it is necessary
to use a trick to force Excel to update the screen.
The following commands seem to do the trick:
- ActiveSheet.Calculate
- ActiveWindow.SmallScroll
- Application.WindowState = Application.WindowState
Put a call to DoEvents in the loop.
This will affect performance, so you might want to only call it on each, say, 10th iteration.
However, if you only have 30, that's hardly an issue.
#Hubisans comment worked best for me.
ActiveWindow.SmallScroll down:=1
ActiveWindow.SmallScroll up:=1
Specifically, if you are dealing with a UserForm, then you might try the Repaint method. You might encounter an issue with DoEvents if you are using event triggers in your form. For instance, any keys pressed while a function is running will be sent by DoEvents The keyboard input will be processed before the screen is updated, so if you are changing cells on a spreadsheet by holding down one of the arrow keys on the keyboard, then the cell change event will keep firing before the main function finishes.
A UserForm will not be refreshed in some cases, because DoEvents will fire the events; however, Repaint will update the UserForm and the user will see the changes on the screen even when another event immediately follows the previous event.
In the UserForm code it is as simple as:
Me.Repaint
This worked for me:
ActiveWindow.SmallScroll down:=0
or more simply:
ActiveWindow.SmallScroll 0
I couldn't gain yet the survey of an inherited extensive code. And exact this problem bugged me for months. Many approches with DoEnvents were not helpful.
Above answer helped. Placeing this Sub in meaningful positions in the code worked even in combination with progress bar
Sub ForceScreenUpdate()
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.Wait Now + #12:00:01 AM#
Application.ScreenUpdating = False
Application.EnableEvents = False
End Sub
This is not directly answering your question at all, but simply providing an alternative. I've found in the many long Excel calculations most of the time waiting is having Excel update values on the screen. If this is the case, you could insert the following code at the front of your sub:
Application.ScreenUpdating = False
Application.EnableEvents = False
and put this as the end
Application.ScreenUpdating = True
Application.EnableEvents = True
I've found that this often speeds up whatever code I'm working with so much that having to alert the user to the progress is unnecessary. It's just an idea for you to try, and its effectiveness is pretty dependent on your sheet and calculations.
On a UserForm two things worked for me:
I wanted a scrollbar in my form on the left. To do that, I first had to add an Arabic language to "Change administrative language" in the Language settings of Windows 10 (Settings->Time & Language->Change Administrative Language). The setting is actually for "Change the language of Non-Unicode Programs," which I changed to Arabic (Algerian). Then in the properties of the form I set the "Right to Left" property to True. From there the form still drew a partial ghost right scrollbar at first, so I also had to add an unusual timed message box:
Dim AckTime As Integer, InfoBox As Object
Set InfoBox = CreateObject("WScript.Shell")
'Set the message box to close after 10 seconds
AckTime = 1
Select Case InfoBox.Popup("Please wait.", AckTime, "This is your Message Box", 0)
Case 1, -1
End Select
I tried everything to get the screen to redraw again to show the first text box in it's proper alignment in the form, instead of partially underneath or at least immediately adjacent to the scrollbar instead of 4 pixels to the right where I wanted it. Finally I got this off another Stackoverflow post (which I now can't find or I would credit it) that worked like a charm:
Me.Frame1.Visible = False
Me.Frame1.Visible = True
In my case the problem was in trying to make one shape visible and another one invisible on a worksheet.
This is my approach to "inactivating" a button [shape] once the user has clicked it. The two shapes are the same size and in the same place, but the "inactive" version has dimmer colors, which was a good approach, but it didn't work, because I could never get the screen to update after changing .visible = FALSE to = TRUE and vice versa.
None of the relevant tricks in this thread worked. But today I found a solution that worked for me, at this link on Reddit
Essentially you just call DoEvents twice in immediate succession after the code that makes the changes. Now why? I can't say, but it did work.
I've been trying to solve this Force a screen update on a Worksheet (not a userform) for many years with limited success with
doevents and scrolling etc.. This CH Oldie solutions works best with a slight mod.
I took out the Wait and reset ScreenUpdating and EnableEvents back to true.
This works office excel 2002 through to office 365
Sub Sheet1Mess(Mess1 As String)
Sheet1.Range("A6").Value = Mess1
ForceScreenUpdate
End Sub
Sub ForceScreenUpdate()
Application.ScreenUpdating = True
Application.EnableEvents = True
' Application.Wait Now + #12:00:01 AM#
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub