slow cell formatting using vba? - vba

Disclaimer: I am relatively new to vba and macros.
I have written a macro to update value and formatting in some individual cells after reading and parsing a json through http and the process is very slow, so I broke down the code into different portions to see where the bottleneck might be. Turns out the cell updating is the problem, I have the following code:
Sub test()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Application.EnableCancelKey = False
t = Timer
With Range("A1")
.Font.Italic = True
.Interior.ColorIndex = 37
.Value = 3412
End With
Debug.Print Timer - t
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Application.EnableCancelKey = True
End Sub
the debug print is about 0.3 to 0.5 sec... I have afterwards further wrapped the timer around each of the italic, colorIndex, and value lines and they all turns out take about 0.015 sec each... I have tried searching online how to make the code more efficient, hence the screenupdating toggles as well as no selection, but 0.5 sec still seem a bit slow in updating a cell to me.
please note that I am not whining, I just want to know if I am doing the right thing here. Is there a more efficient way to implement the formatting and value changes that I posted here, or is it just a fact that excel takes this amount of time to update the cell? I am just very curious because the json reading and parsing that I also implemented are significantly faster than this.
Also I have tested this script on at least 3 computers and they all take around the same time so I don't think it's an individual computer problem. And I used excel 2007 and 2010 to test.

I assume you are wanting to format more than a single cell? If so, it will be faster to create a range reference to all the cells requiring the same format (it need not be contiguous), then apply the required format to that range object in one step
Following example demo's creating a range reference, and applying format in one go
Option Explicit
Private Declare Function GetTickCount Lib "kernel32" () As Long
Sub Demo()
Dim t As Long
Dim n As Long, i As Long
Dim m As Long
Dim ws As Worksheet
Dim cl As Range
Dim rSearch As Range
Dim rResult As Range
Set ws = ActiveSheet ' or another sheet...
Set rSearch = ws.Range("A1:A1000")
' note, this is an inefficient loop, can be made much faster
' details will depend on the use case
For Each cl In rSearch
' determine if cell is to be formatted
If cl.Row Mod 2 = 0 Then
' add cl to Result range
If rResult Is Nothing Then
Set rResult = cl
Else
Set rResult = Application.Union(rResult, cl)
End If
End If
Next
Debug.Print "Result Range includes ", rResult.Cells.Count, "Cells"
t = GetTickCount
' Apply format
With rResult
.Font.Italic = True
.Interior.ColorIndex = 37
.Value = 3412
End With
Debug.Print (GetTickCount - t) / 1000, "seconds"
End Sub

Related

Autofit.rowheight depending on text(string) in each cell of that row

Bear in mind:
Different column.width,
each cell has different string(length)
All Text must appear!
Its not easy because you can't tell how much space each line has?
Not cut the biggest cell (like, when I do that manually sometimes the last line is cut off)
(like autofit, because autofit just looks at one cell(.select) not all cells in that row, if there is another cell with more content it get's cut off)
If someone has a hint for me, would be awesome!
Here is my code so far:
Sub dynamicRowFit() 'start of the sub
Dim ws As Worksheet
Dim aRng As Range 'maybe we just need one range?
Dim bRng As Range
Dim defRow As Integer
Dim defColmn As Integer
Set defRow = 'enter the row to start(as Integer)
Set defColmn = 'enter the Column to start (as Integer)
Application.ScreenUpdating = False 'helps to prevent screenfreezes
Application.Calculation = xlCalculationManual 'defines window
Set ws = Worksheets("Sheet1")
Set aRng = ws.Range(ws.Cells(defRow, defCulmn), ws.Cells(ws.Rows.Count, 1).End(xlUp))
'this is a defined loop with defined height, I need a dynamic solution:
For Each bRng In aRng
If Len(bRng) <= 90 Then 'when lengh of string is smallerOrequal to 90
bRng.EntireRow.RowHeight = 15
Else
bRng.EntireRow.AutoFit
End If
Next bRng
Application.Calculation = xlCalculationAutomatic 'till all cells emty
Application.ScreenUpdating = True'Enables the update of screen
End Sub 'end of sub
So, it also depends on the .size(type size(I'll have just one 11) and font (each font can be different in size, means space needed for each character).
I did my best to define my question, tags, etc. as good as possible ;-)

Optimize vba code when it comes to processing a high number of rows

Greeting all! I wrote a code that allows me to compare two EXCEL worksheets for same values; here it is:
Sub compare()
Dim i As Integer
Dim j As Integer
Dim oldVal1 As Variant
Dim oldVal2 As Variant
Dim newVal1 As Variant
Dim newVal2 As Variant
Dim count As Integer
Const equal = "equal"
Dim WKB As Workbook
Dim OldWS As Worksheet
Dim NewWS As Worksheet
Dim DiffWS As Worksheet
Const OldWSName = "Sheet1"
Const NewWSName = "Sheet2"
Const DiffWSName = "Sheet3"
Set WKB = ActiveWorkbook
Set OldWS = WKB.Worksheets(OldWSName)
Set NewWS = WKB.Worksheets(NewWSName)
Set DiffWS = WKB.Worksheets(DiffWSName)
Dim OldRow As Long
Dim NewRow As Long
Call OptimizeCode_Begin
oldRow = OldWS.Cells(Rows.Count, 1).End(xlUp).Row
newRow = NewWS.Cells(Rows.Count, 1).End(xlUp).Row
count = 1
For i = 2 To oldRow
oldVal1 = OldWS.Cells(i, 1).Value
oldVal2 = OldWS.Cells(i, 4).Value
For j = 2 To newRow
newVal1 = NewWS.Cells(j, 1).Value
newVal2 = NewWS.Cells(j, 4).Value
If (oldVal1 = newVal1) And (oldVal2 = newVal2) Then
count = count + 1
DiffWS.Cells(count, 1).Value = equal
DiffWS.Cells(count, 2).Value = oldVal1
DiffWS.Cells(count, 3).Value = oldVal2
End If
Next j
Next i
Call OptimizeCode_End
DiffWS.Activate
'Reset variables
Set WKB = Nothing
Set OldWS = Nothing
Set NewWS = Nothing
Set DiffWS = Nothing
Application.ScreenUpdating = True
MsgBox ("Your data has been compared!")
End Sub
This code is preceded by the variables definitions, long list that I chose not to paste in here. But basically, oldVal1 is the first value compared from OldWS worksheet and oldVal2 the second one from the same worksheet. Those values are being compared with newVal1 and newVal2 from NewWS worksheet (second worksheet). Same values are copied over to DiffWS (third worksheet) with an additional column on the left for the status equal, hence DiffWS.Cells(count + 1, 2).Value = oldVal1.
I've added the following functions to optimize the code and make it run fast when it comes to comparing 2 worksheets of at east 100000 rows:
Sub OptimizeCode_Begin()
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
EventState = Application.EnableEvents
Application.EnableEvents = False
PageBreakState = ActiveSheet.DisplayPageBreaks
ActiveSheet.DisplayPageBreaks = False
End Sub
AND
Sub OptimizeCode_End()
ActiveSheet.DisplayPageBreaks = PageBreakState
Application.EnableEvents = EventState
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
End Sub
The execution is faster on a low number of rows I must admit, but it just doesn't work when the worksheets grow bigger. My EXCEL crashes when I run my code.
Any other optimization tips I should know of? because I am at loss as to how to do this. My code would be useless if no solution is available, and I'd better know it now and think about a way other than EXCEL to compare my data.
Thanks in advance for shedding light on this.
Your code sample doesn't indicate how you're opening each of the workbooks. In my experience, almost any time Excel crashes, it's because of a memory issue, and often that's because instances of Excel are being opened in the background, and then not .Close'd properly, or perhaps the excel objects not being Set to Nothing (perhaps from repeated execution of the code due to errors).
If this is the case, then Ctrl+Alt+Del → Task Manager → Processes will show multiple instances of Excel, and the easiest fix is to reboot, and then, of course, fix the handling of the Excel object in your code.
If the goal is to compare two worksheets, then perhaps a better question is why you are attempting to re-create functionality that already exists existing solutions available, most likely even built-in to your copy of Office.
Depending on your version, you may already have a utility installed.
For example, if you're running Office Pro Plus 2013, you can use Microsoft Spreadsheet Compare to run a report on the differences.
More information:
Basic tasks in Spreadsheet Compare
I'm running Excel 2016 from an Office 365 Subscription. I've never had a need to compare spreadsheets, but out of curiosity, I just:
Hit the Windows Key
Start typing: spreadsheet compare
Sit back and let the professionally built analysis/merge tool do it's job.
If all else fails, there are a number of other (3rd-Party) free and paid utilities available as well (such as xlCompare).

VBA - Trim function : reduce time of operation / freezing

I have written code in VBA that removes some potential spaces between characters. The code works pretty well but becomes really slow when the file contains thousands of rows. I'd like to know if it's possible to improve it, in order to reduce the time of operation, but also mainly to stop the file from freezing. Here is the code:
Sub Test()
Dim cell as Range
Dim sht As Worksheet
Dim LastRow As Long
Dim StartCell As Range
Dim areaToTrim As Range
Set sht = ThisWorkbook.Worksheets("SS upload")
Set StartCell = sht.Range("A14")
LastRow = sht.Cells(sht.Rows.Count, StartCell.Column).End(xlUp).Row
Set areaToTrim = sht.Range("B14:B" & LastRow)
For Each cell In areaToTrim
cell.Value = Trim(cell.Value)
Next cell
End Sub
The fastest way is to read the range into an array, trim it there and then write it back to the range:
Sub Test()
Dim sht As Worksheet
Dim LastRow As Long
Dim StartCell As Range
Dim areaToTrim As Range
Dim varArray() As Variant
Dim i As Long
Set sht = ThisWorkbook.Worksheets("SS upload")
Set StartCell = sht.Range("A14")
LastRow = sht.Cells(sht.Rows.Count, StartCell.Column).End(xlUp).Row
Set areaToTrim = sht.Range("B14:B" & LastRow)
varArray = areaToTrim ' Read range into array
For i = LBound(varArray, 1) To UBound(varArray, 1)
varArray(i, 1) = Trim(varArray(i, 1))
Next i
areaToTrim.Value = varArray ' Write array back to range
End Sub
No need to worry about Application.ScreenUpdating or Application.Calculation. Nice and simple!
If you are still worried about any responsiveness, put a DoEventsin the body of the loop.
You can prevent the freezing when you insert DoEvents in your loop.
And then execute it, say every hundredth time.
This will make the loop run a little slower, but allows the user to use the GUI meanwhile.
...
Dim cnt As Integer
For Each cell In areaToTrim
cell.Value = Trim(cell.Value)
cnt=cnt + 1
If cnt Mod 100 = 0 Then
DoEvents
End If
Next cell
...
You can play around with the number to optimize it for your needs.
DoEvents brings also some problems with it. A good explanation about DoEvents can be found here.
Try like this, to reduce screenupdating. This is a piece of code, that I always use, thus some of the commands are probably a bit too much for the current question, but they can be still useful.
As a second point - do not declare a variable with the name Cell, you can suffer a bit from this later. Declare it rngCell or myCell or anything else, which is not part of the VBE variables.
Public Sub TestMe()
Call OnStart
'YourCode
Call OnEnd
End Sub
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
If you feel like it, you may save the range as an array and do the trim operation there. However, it may overcomplicate your code, if you are not used to work with arrays - Trim Cells using VBA in Excel

Using UNION and Ranges To Speed Up Deleting Columns? [duplicate]

This question already has answers here:
Improving the performance of FOR loop
(3 answers)
Closed 7 years ago.
Trying to use Union and ranges to speed up deleting empty columns, across all sheets in my workbook, except "AA" and "Word Frequency"
Sample workbook
Example of sheet before:
Example of sheet after (note, I will need to write separate script to shift keywords up, you can't see all the keywords, but only the columns with data in them are left):
In my search for a method to speed up deleting columns in a sheet if the column is empty (except the header), I was directed by #chrisneilsen to reference the thread Improving the performance of FOR loop.
That thread shed light on the fact that deleting columns individually slows down performance significantly. Script speed can be improved by defining a "master" range to include all the ranges (columns) to be deleted (by using Union), and then simply deleting the "master" range.
As a VBA noob, I used to following references to learn about Ranges, Union, UBound and LBound to understand the code in the thread mentioned above:
Excel-Easy.com: Using UBound and LBound, Dynamic Arrays (Using ReDim)
Youtube: Using UNION method to select (and modify) multiple ranges
My old (slow) script that works, but takes about about 3 hours to run across ~30 sheets and deleting ~100 columns each sheet:
Sub Delete_No_Data_Columns()
Dim col As Long
Dim h 'to store the last columns/header
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
Application.ScreenUpdating = False
For col = h To 5 Step -1
If Application.CountA(Columns(col)) = 1 Then Columns(col).Delete
Next col
End Sub
Almost working script (for one sheet), using the same approach as #chrisneilsen code in thread mentioned above. When I run it, it doesn't do anything, however #chrisneilsen noted there were 2 syntax errors (Column. instead of Columns.) and that I was mixing an implicit ActiveSheet (by using Columns without a qualifier) with an explicit sheet Worksheets("Ball Shaker"). Errors in code are commented below.
Sub Delete_No_Data_Columns_Optimized()
Dim col As Long
Dim h 'to store the last columns/header
Dim EventState As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
Dim columnsToDelete As Range
Dim ws as Worksheet '<<<<<<<<< Fixing Error (qualifying "Columns." properly)
On Error GoTo EH:
'Optimize Performance
Application.ScreenUpdating = False
EventState = Application.EnableEvents
Application.EnableEvents = False
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
PageBreakState = ActiveSheet.DisplayPageBreaks
ActiveSheet.DisplayPageBreaks = False
' <<<<<<<<<<<<< MAIN CODE >>>>>>>>>>>>>>
Set ws = ActiveSheet
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
'<<<<<<<<<<<<<< Errors corrected below in comments >>>>>>>>>>>>
For col = h To 5 Step -1
If Application.CountA(Column(col)) = 1 Then
'<<<<< should be Application.CountA(ws.Columns(col)) = 1
If columnsToDelete Is Nothing Then
Set columnsToDelete = Worksheets("Ball Shaker").Column(col)
'should be columnsToDelete = ws.Columns(col)
Else
Set columnsToDelete = Application.Union(columnsToDelete, Worksheets("Ball Shaker").Column(col))
'should be columnsToDelete = Application.Union(columnsToDelete, ws.Columns(col))
End If
End If
Next col
'<<<<<<<<<<<<<< End Errors >>>>>>>>>>>>>>>>
If Not columnsToDelete Is Nothing Then
columnsToDelete.Delete
End If
' <<<<<<<<<<<< END MAIN CODE >>>>>>>>>>>>>>
CleanUp:
'Revert optmizing lines
On Error Resume Next
ActiveSheet.DisplayPageBreaks = PageBreakState
Application.Calculation = CalcState
Application.EnableEvents = EventState
Application.ScreenUpdating = True
Exit Sub
EH:
' Handle Errors here
Resume CleanUp
End Sub
Working code that runs across all sheets in workbook, in about ~6 minutes (except "AA" and "Word Frequency" worksheets, which I don't need to format):
Option Explicit
Sub Delete_No_Data_Columns_Optimized_AllSheets()
Dim sht As Worksheet
For Each sht In Worksheets
If sht.Name <> "AA" And sht.Name <> "Word Frequency" Then
sht.Activate 'go to that Sheet!
Delete_No_Data_Columns_Optimized sht.Index 'run the code, and pass the sht.Index _
'of the current sheet to select that sheet
End If
Next sht 'next sheet please!
End Sub
Sub Delete_No_Data_Columns_Optimized(shtIndex As Integer)
Dim col As Long
Dim h 'to store the last columns/header
Dim EventState As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
Dim columnsToDelete As Range
Dim ws As Worksheet
Set ws = Sheets(shtIndex) 'Set the exact sheet, not just the one that is active _
'and then you will go through all the sheets
On Error GoTo EH:
'Optimize Performance
Application.ScreenUpdating = False
EventState = Application.EnableEvents
Application.EnableEvents = False
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
PageBreakState = ActiveSheet.DisplayPageBreaks
ActiveSheet.DisplayPageBreaks = False
' <<<<<<<<<<<<< MAIN CODE >>>>>>>>>>>>>>
h = ws.Range("E1").End(xlToRight).Column 'find the last column with the data/header
For col = h To 5 Step -1
If ws.Application.CountA(Columns(col)) = 1 Then 'Columns(col).Delete
If columnsToDelete Is Nothing Then
Set columnsToDelete = ws.Columns(col)
Else
Set columnsToDelete = Application.Union(columnsToDelete, ws.Columns(col))
End If
End If
Next col
If Not columnsToDelete Is Nothing Then
columnsToDelete.Delete
End If
' <<<<<<<<<<<< END MAIN CODE >>>>>>>>>>>>>>
CleanUp:
'Revert optmizing lines
On Error Resume Next
ActiveSheet.DisplayPageBreaks = PageBreakState
Application.Calculation = CalcState
Application.EnableEvents = EventState
Application.ScreenUpdating = True
Exit Sub
EH:
' Handle Errors here
Resume CleanUp
End Sub
Note: Trying to delete columns and shift to left, so columns with data inside will all be grouped together neatly after script is run.
Is this the best way to utilize Union and ranges for deleting columns? Any help would be greatly appreciated.
The special cells method actually will not serve you so well here. Instead, find the last row of data in your sheet and delete only the cells in the column up to the that row and shift everything to the left. This will be much faster than deleting an entire column!
Sub Delete_No_Data_Columns()
Dim col As Long, lRow as Long
Dim h as Long'to store the last columns/header
lRow = Range("E" & Rows.Count).End(xlUp).Row ' assumes column E will have last used row ... adjust as needed
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
For col = h To 5 Step -1
If Application.CountA(Columns(col)) = 1 Then
Range(Cells(2,col),Cells(lRow,col)).Delete shift:=xlToLeft
End If
Next col
Application.ScreenUpdating = False ' i think you want this at the beginning of the program, no?
End Sub

Efficiently assign cell properties from an Excel Range to an array in VBA / VB.NET

In VBA / VB.NET you can assign Excel range values to an array for faster access / manipulation. Is there a way to efficiently assign other cell properties (e.g., top, left, width, height) to an array? I.e., I'd like to do something like:
Dim cellTops As Variant : cellTops = Application.ActiveSheet.UsedRange.Top
The code is part of a routine to programmatically check whether an image overlaps cells that are used in a workbook. My current method of iterating over the cells in the UsedRange is slow since it requires repeatedly polling for the top / left / width / height of the cells.
Update: I'm going to go ahead an accept Doug's answer as it does indeed work faster than naive iteration. In the end, I found that a non-naive iteration works faster for my purposes of detecting controls that overlap content-filled cells. The steps are basically:
(1) Find the interesting set of rows in the used range by looking at the tops and heights of the first cell in each row (my understanding is that all the cells in the row must have the same top and height, but not left and width)
(2) Iterate over the cells in the interesting rows and perform overlap detection using only the left and right positions of the cells.
The code for finding the interesting set of rows looks something like:
Dim feasible As Range = Nothing
For r% = 1 To used.Rows.Count
Dim rowTop% = used.Rows(r).Top
Dim rowBottom% = rowTop + used.Rows(r).Height
If rowTop <= objBottom AndAlso rowBottom >= objTop Then
If feasible Is Nothing Then
feasible = used.Rows(r)
Else
feasible = Application.Union(used.Rows(r), feasible)
End If
ElseIf rowTop > objBottom Then
Exit For
End If
Next r
Todd,
The best solution I could think of was to dump the tops into a range and then dump those range values into a variant array. As you said, the For Next (for 10,000 cells in my test) took a few seconds. So I created a function that returns the top of the cell that it's entered into.
The code below, is mainly a function that copies the usedrange of a sheet you pass to it and then enters the function described above into each cell of the usedrange of the copied sheet. It then transposes and dumps that range into a variant array.
It only takes a second or so for 10,000 cells. Don't know if it's useful, but it was an interesting question. If it is useful you could create a separate function for each property or pass the property you're looking for, or return four arrays(?)...
Option Explicit
Option Private Module
Sub test()
Dim tester As Variant
tester = GetCellProperties(ThisWorkbook.Worksheets(1))
MsgBox tester(LBound(tester), LBound(tester, 2))
MsgBox tester(UBound(tester), UBound(tester, 2))
End Sub
Function GetCellProperties(wsSourceWorksheet As Excel.Worksheet) As Variant
Dim wsTemp As Excel.Worksheet
Dim rngCopyOfUsedRange As Excel.Range
Dim i As Long
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
wsSourceWorksheet.Copy after:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)
Set wsTemp = ActiveSheet
Set rngCopyOfUsedRange = wsTemp.UsedRange
rngCopyOfUsedRange.Formula = "=CellTop()"
wsTemp.Calculate
GetCellProperties = Application.WorksheetFunction.Transpose(rngCopyOfUsedRange)
Application.DisplayAlerts = False
wsTemp.Delete
Application.DisplayAlerts = True
Set wsTemp = Nothing
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Function
Function CellTop()
CellTop = Application.Caller.Top
End Function
Todd,
In answer to your request for a non-custom-UDF I can only offer a solution close to what you started with. It takes about 10 times as long for 10,000 cells. The difference is that your back to looping through cells.
I'm pushing my personal envelope here, so maybe somebody will have a way to to it without a custom UDF.
Function GetCellProperties2(wsSourceWorksheet As Excel.Worksheet) As Variant
Dim wsTemp As Excel.Worksheet
Dim rngCopyOfUsedRange As Excel.Range
Dim i As Long
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
wsSourceWorksheet.Copy after:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)
Set wsTemp = ActiveSheet
Set rngCopyOfUsedRange = wsTemp.UsedRange
With rngCopyOfUsedRange
For i = 1 To .Cells.Count
.Cells(i).Value = wsSourceWorksheet.UsedRange.Cells(i).Top
Next i
End With
GetCellProperties2 = Application.WorksheetFunction.Transpose(rngCopyOfUsedRange)
Application.DisplayAlerts = False
wsTemp.Delete
Application.DisplayAlerts = True
Set wsTemp = Nothing
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Function
I would add to #Doug the following
Dim r as Range
Dim data() as Variant, i as Integer
Set r = Sheet1.Range("A2").Resize(100,1)
data = r.Value
' Alternatively initialize an empty array with
' ReDim data(1 to 100, 1 to 1)
For i=1 to 100
data(i,1) = ...
Next i
r.Value = data
which shows the basic process of getting a range into an array and back again.