Excel autofit row height doesn't work on meged cells with word wrap - excel

I'm programmatically inserting some text into merged cells in a row. I have Wrap Text set and want the row height to expand as necessary to accommodate multiple lines of text. I was programmatically applying AutoFit once the cells had been filled but that didn't work. I subsequently found a Knowledge Base article saying the AutoFit doesn't work for merged cells! I can try to compute the row height required to accommodate the number of lines of wrapping text. But I don't really want to climb into calculating character widths etc. Any ideas gratefully appreciated.
Question credit goes to David (I had the exact same question, just reposting here for posterity) source

I found a VB macro here that will simulate the autofit of any merged cells on the active sheet. Source credits parry from MrExcel.com
Sub AutoFitMergedCellRowHeight()
Dim CurrentRowHeight As Single, MergedCellRgWidth As Single
Dim CurrCell As Range
Dim ActiveCellWidth As Single, PossNewRowHeight As Single
Dim StartCell As Range, c As Range, MergeRng As Range, Cell As Range
Dim a() As String, isect As Range, i
'Take a note of current active cell
Set StartCell = ActiveCell
'Create an array of merged cell addresses that have wrapped text
For Each c In ActiveSheet.UsedRange
If c.MergeCells Then
With c.MergeArea
If .Rows.Count = 1 And .WrapText = True Then
If MergeRng Is Nothing Then
Set MergeRng = c.MergeArea
ReDim a(0)
a(0) = c.MergeArea.Address
Else
Set isect = Intersect(c, MergeRng)
If isect Is Nothing Then
Set MergeRng = Union(MergeRng, c.MergeArea)
ReDim Preserve a(UBound(a) + 1)
a(UBound(a)) = c.MergeArea.Address
End If
End If
End If
End With
End If
Next c
Application.ScreenUpdating = False
'Loop thru merged cells
For i = 0 To UBound(a)
Range(a(i)).Select
With ActiveCell.MergeArea
If .Rows.Count = 1 And .WrapText = True Then
'Application.ScreenUpdating = False
CurrentRowHeight = .RowHeight
ActiveCellWidth = ActiveCell.ColumnWidth
For Each CurrCell In Selection
MergedCellRgWidth = CurrCell.ColumnWidth + MergedCellRgWidth
Next
.MergeCells = False
.Cells(1).ColumnWidth = MergedCellRgWidth
.EntireRow.AutoFit
PossNewRowHeight = .RowHeight
.Cells(1).ColumnWidth = ActiveCellWidth
.MergeCells = True
.RowHeight = IIf(CurrentRowHeight > PossNewRowHeight, _
CurrentRowHeight, PossNewRowHeight)
End If
End With
MergedCellRgWidth = 0
Next i
StartCell.Select
Application.ScreenUpdating = True
'Clean up
Set CurrCell = Nothing
Set StartCell = Nothing
Set c = Nothing
Set MergeRng = Nothing
Set Cell = Nothing
End Sub

Related

How can I speed up this For Each loop in VBA?

I have an Worksheet_Change macro that hides/unhides rows depending on the choice a user makes in a cell with a data validation list.
The code takes a minute to run. It's looping over c.2000 rows. I'd like it to take closer to a few seconds so it becomes a useful user tool.
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
'Exit the routine early if there is an error
On Error GoTo EExit
'Manage Events
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.EnableEvents = False
'Declare Variables
Dim rng_DropDown As Range
Dim rng_HideFormula As Range
Dim rng_Item As Range
'The reference the row hide macro will look for to know to hide the row
Const str_HideRef As String = "Hide"
'Define Variables
'The range that contains the week selector drop down
Set rng_DropDown = Range("rng_WeekSelector")
'The column that contains the formula which indicates if a row should
'be hidden c.2000 rows
Set rng_HideFormula = Range("rng_HideFormula")
'Working Code
'Exit sub early if the Month Selector was not changed
If Not Target.Address = rng_DropDown.Address Then GoTo EExit
'Otherwise unprotect the worksheet
wks_DailyPlanning.Unprotect (str_Password)
'For each cell in the hide formula column
For Each rng_Item In rng_HideFormula
With rng_Item
'If the cell says "hide"
If .Value2 = str_HideRef Then
'Hide the row
.EntireRow.Hidden = True
Else
'Otherwise show the row
.EntireRow.Hidden = False
End If
End With
'Cycle through each cell
Next rng_Item
EExit:
'Reprotect the sheet if the sheet is unprotected
If wks_DailyPlanning.ProtectContents = False Then wks_DailyPlanning.Protect (str_Password)
'Clear Events
Application.ScreenUpdating = True
Application.DisplayAlerts = True
Application.EnableEvents = True
End Sub
I have looked at some links provided by other users on this website and I think the trouble lies in the fact I'm having to iterate through each row individually.
Is it possible to create something like an array of .visible settings I can apply to the entire range at once?
I'd suggest copying your data range to a memory-based array and checking that, then using that data to adjust the visibility of each row. It minimizes the number of interactions you have with the worksheet Range object, which takes up lots of time and is a big performance hit for large ranges.
Sub HideHiddenRows()
Dim dataRange As Range
Dim data As Variant
Set dataRange = Sheet1.Range("A13:A2019")
data = dataRange.Value
Dim rowOffset As Long
rowOffset = IIf(LBound(data, 1) = 0, 1, 0)
ApplicationPerformance Flag:=False
Dim i As Long
For i = LBound(data, 1) To UBound(data, 1)
If data(i, 1) = "Hide" Then
dataRange.Rows(i + rowOffset).EntireRow.Hidden = True
Else
dataRange.Rows(i + rowOffset).EntireRow.Hidden = False
End If
Next i
ApplicationPerformance Flag:=True
End Sub
Public Sub ApplicationPerformance(ByVal Flag As Boolean)
Application.ScreenUpdating = Flag
Application.DisplayAlerts = Flag
Application.EnableEvents = Flag
End Sub
Another possibility:
Dim mergedRng As Range
'.......
rng_HideFormula.EntireRow.Hidden = False
For Each rng_Item In rng_HideFormula
If rng_Item.Value2 = str_HideRef Then
If Not mergedRng Is Nothing Then
Set mergedRng = Application.Union(mergedRng, rng_Item)
Else
Set mergedRng = rng_Item
End If
End If
Next rng_Item
If Not mergedRng Is Nothing Then mergedRng.EntireRow.Hidden = True
Set mergedRng = Nothing
'........
to increase perfomance you can populate dictionary with range addresses, and hide or unhide at once, instead of hide/unhide each particular row (but this is just in theory, you should test it by yourself), just an example:
Sub HideHiddenRows()
Dim cl As Range, x As Long
Dim dic As Object: Set dic = CreateObject("Scripting.Dictionary")
x = Cells(Rows.Count, "A").End(xlUp).Row
For Each cl In Range("A1", Cells(x, "A"))
If cl.Value = 0 Then dic.Add cl.Address(0, 0), Nothing
Next cl
Range(Join(dic.keys, ",")).EntireRow.Hidden = False
End Sub
demo:

Merge cells when cell value match (different column row value)

I would like to write a Excel vba to merge cells according to their values and a reference cell in another column. Like the picture attached.
I have over 18000 Lines, with many of variation.
All the values within the line are in order rank.
enter image description here
This is the code that I based my VBA
Sub MergeCells()
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Dim rngMerge As Range, cell As Range
Set rngMerge = Range("B2:C10")
MergeAgain:
For Each cell In rngMerge
If cell.Value = cell.Offset(1, 0).Value And IsEmpty(cell) = False Then
Range(cell, cell.Offset(1, 0)).Merge
GoTo MergeAgain
End If
Next
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
Edit Minor upgrade to allow merged ranges to be extended enabling merge updates.
Merge Vertically Adjacent Cells with Equal Values.
Save in a regular module.
Be sure the constants (Const) come before any other code in the module.
Consider adding a guard to ensure this only runs against the worksheet
it is intended for (see how to after the code).
Run the macro from the Alt-F8 Macro Dialogue.
NB Like most macros, this will wipe the Excel undo buffer.
It cannot be undone with a Ctrl-Z. (The only options are to revert to last saved
or manually edit to the way it was before.)
Copy/Paste
Private Const LastCol = 20
Private Const LastRow = 20
Public Sub Merge_Cells()
Dim r As Range
Dim s As Range
Dim l As Range
Dim c As Long
Dim v As Variant
For c = 1 To LastCol
Set s = Nothing
Set l = Nothing
For Each r In Range(Cells(1, c), Cells(LastRow, c))
v = r.MergeArea(1, 1).Value
If v = vbNullString Then
DoMerge s, l
Set s = Nothing
Set l = Nothing
ElseIf s Is Nothing Then
Set s = r
ElseIf s.Value <> v Then
DoMerge s, l
Set s = r
Set l = Nothing
Else
Set l = r
End If
Next r
DoMerge s, l
Next c
End Sub
Private Sub DoMerge(ByRef s As Range, ByRef l As Range)
If s Is Nothing Then Exit Sub
If l Is Nothing Then Set l = s
Application.DisplayAlerts = False
With Range(s, l)
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
.Merge
End With
Application.DisplayAlerts = True
End Sub
Consider finding the last column and last row programmatically.
If the merge should start after row 1:
For Each r In Range(Cells(1, c), Cells(LastRow, c))
^
Change the 1 to the correct row number or replace with an added const variable.
To guard other worksheets, use the tab name (recommend renaming the tab first):
For Each r In Worksheets(TabName).Range(Cells(1, c), Cells(LastRow, c))
^^^^^^^^^^^^^^^^^^^^
Make this edit to the same line as the starting row edit.
And add Private Const TabName = "The Merge Tabs Name" ' Spaces ok
to the top of the Module with the other Const (constants).
Or place the name directly in the code: Worksheets("The Merge Tabs Name").
Add this into a module, select your range of data (excluding headers), run the macro and see if it works for you.
Public Sub MergeRange()
Dim rngData As Range, lngRow As Long, lngCol As Long, strTopCell As String
Dim strBottomCell As String, strThisValue As String, strNextValue As String
Dim strThisMergeArea As String, strNextMergeArea As String
Set rngData = Selection
Application.DisplayAlerts = False
Application.ScreenUpdating = False
With rngData
For lngCol = 1 To .Columns.Count
strTopCell = ""
For lngRow = 1 To .Rows.Count
If strTopCell = "" Then strTopCell = .Cells(lngRow, lngCol).Address
strThisValue = .Cells(lngRow, lngCol)
strNextValue = .Cells(lngRow + 1, lngCol)
If lngCol > 1 Then
strThisMergeArea = .Cells(lngRow, lngCol - 1).MergeArea.Address
strNextMergeArea = .Cells(lngRow + 1, lngCol - 1).MergeArea.Address
If strThisMergeArea <> strNextMergeArea Then strNextValue = strThisValue & "."
End If
If strNextValue <> strThisValue Or lngRow = .Rows.Count Then
strBottomCell = .Cells(lngRow, lngCol).Address
With rngData.Worksheet.Range(strTopCell & ":" & strBottomCell)
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
.MergeCells = True
End With
strTopCell = .Cells(lngRow + 1, lngCol).Address
End If
Next
Next
End With
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
There's one trick to this which is able to be changed and that is that it will also group based off the prior column. You can see an example of what I'm talking about in cell C19 ...
... it has worked out that the previous column had a grouping that stopped at that point, therefore, the 1 isn't carried through and grouped to the next lot, it stops and is grouped there. I hope that makes sense and I hope it gives you what you need.
Another thing, this code here will attempt to demerge all of your previously merged data.
Public Sub DeMergeRange()
Dim rngData As Range, lngRow As Long, lngCol As Long, objCell As Range
Dim objMergeArea As Range, strMergeRange As String, strFirstCell As String
Dim strLastCell As String, objDestRange As Range
Set rngData = Selection
Application.DisplayAlerts = False
Application.ScreenUpdating = False
With rngData
For lngCol = 1 To .Columns.Count
For lngRow = 1 To .Rows.Count
Set objCell = .Cells(lngRow, lngCol)
If objCell.Areas(1).MergeArea.Cells.Count > 1 Then
strMergeRange = objCell.Areas(1).MergeArea.Address
objCell.MergeCells = False
strFirstCell = Split(strMergeRange, ":")(0)
strLastCell = Split(strMergeRange, ":")(1)
Set objDestRange = .Worksheet.Range(.Worksheet.Range(strFirstCell).Offset(1, 0).Address & ":" & strLastCell)
.Worksheet.Range(strFirstCell).Copy objDestRange
End If
Next
Next
End With
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
A note, my suggestion is to ensure you have the original source data saved to another workbook/sheet as a backup before running any code over the top of it.
If it stuffs with your data then it will be a right royal pain to undo manually.

Filtering Row based off checkbox

I have some VBA code that I was intending to use to filter a row instead of a column. I have some issues though that I can't seem to work out, at this point I have confused myself.
Sub FilterRow()
Dim b As Object, cs As Integer
Set b = ActiveSheet.CheckBoxes(Application.Caller)
With b.TopLeftCell
cs = .Row
End With
Dim rng As Range, cel As Range, totalRng As Range
If ActiveSheet.AutoFilterMode = True Then
ActiveSheet.AutoFilterMode = False
End If
For Each cel In totalRng
If cel.Row <> cs Then
cel.EntireColumn.Hidden = True
Else
cel.EntireColumn.Hidden = False
End If
Next cel
End Sub
The goal is to have the code check columns C:DX of that specific row for a value, if a value is NOT found then to hide that column. Ultimately eliminating any blank cells in that particular row.
So assuming FilterRow is the macro that you've assigned to the checkbox, the following will do what I think you're trying to do:
Sub FilterRow()
Dim b As Object, cs As Integer
Set b = ActiveSheet.CheckBoxes(Application.Caller)
With b.TopLeftCell
cs = .row
End With
Dim rng As Range, cel As Range, totalRng As Range
Set totalRng = Range("C" & cs & ":DX" & cs)
If b.Value = 1 Then
If ActiveSheet.AutoFilterMode = True Then
ActiveSheet.AutoFilterMode = False
End If
For Each cel In totalRng
If cel.Value = "" Then
cel.EntireColumn.Hidden = True
Else
cel.EntireColumn.Hidden = False
End If
Next cel
Else
' Set up autofilter here
totalRng.EntireColumn.Hidden = False
End If
End Sub
Since this macro gets triggered when the checkbox is either checked or unchecked, I changed the logic so that it unhides all the columns when the check box is unchecked (ie reverses what it did when checked).

VBA Excel Merging of Cells based on a Specific cell value

I would like to automate the merging of cells based by column for multiple columns based on the information in a specific column.
Based on the below picture the values in column c will determine the number of rows that need to be merged together for Columns A through K. With each change in the value in Column C - the merging would begin again.
Private Sub MergeCells_C()
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Dim rngMerge As Range, cell As Range
Set rngMerge = Range("C1:C1000") 'Set the range limits here
MergeAgain:
For Each cell In rngMerge
If cell.Value = cell.Offset(1, 0).Value And IsEmpty(cell) = False Then
Range(cell, cell.Offset(1, 0)).Merge
GoTo MergeAgain
End If
Next
Application.DisplayAlerts = True
Application.ScreenUpdating = True
End Sub
This worked for me:
Sub MergeCellsByC()
Dim c As Range, sht As Worksheet, currV
Dim n As Long, rw As Range, r As Range
Set sht = ActiveSheet
Set c = sht.Range("C4") 'adjust to suit....
currV = Chr(0) 'start with a dummy value
Do
If c.Value <> currV Then
If n > 1 Then
Set rw = c.EntireRow.Range("A1:K1") 'A1:K1 relative to the row we're on...
Application.DisplayAlerts = False
'loop across the row and merge the cells above
For Each r In rw.Cells
r.Offset(-n).Resize(n).Merge
Next r
Application.DisplayAlerts = True
End If
currV = c.Value
n = 1
Else
n = n + 1 'increment count for this value
End If
If Len(c.Value) = 0 Then Exit Do 'exit on first empty cell
Set c = c.Offset(1, 0) 'next row down
Loop
End Sub

Excel VBA - Using shapes as toggle buttons

I'm trying to use a shape instead of a button to toggle hiding rows with blank cells (according to conditions). Is it even possible?
Sub ToggleChevron3_Click()
Dim rng As Range, cell As Range
Set rng = Range("A1:C100")
Application.ScreenUpdating = False
With rng
For Each cell In rng
If cell.Offset(0, 4).Value = "" Then ' Condition 1
If cell.Value = "" Then ' Condition 2
ActiveSheet.Shapes("Chevron 3").cell.EntireRow.Hidden _
= Not ActiveSheet.Shapes("Chevron 3").cell.EntireRow.Hidden
End If
End If
Next
End With
Application.ScreenUpdating = True
End Sub
Yes, it is possible. The code to accomplish what I think you are looking for is below. Both pieces of code below assume you want to just click a button to hide / unhide the rows, depending on the current state.
Sub ToggleChevron3_Click()
Application.ScreenUpdating = False
Dim rng As Range, cell As Range
'Set rng = Range("A1:C100") 'do you really want to loop through every cell in columns A through C
Set rng = Range("A1:A100")
For Each cell In rng
If Len(cell.Offset(, 4).Value) = 0 And Len(cell.Value) = 0 Then
Dim bToggle As Boolean
bToggle = cell.EntireRow.Hidden
cell.EntireRow.Hidden = Not bToggle
End If
Next
Application.ScreenUpdating = True
End Sub
However, there is alternative that is cleaner code and faster execution, as long as filtering is okay for you.
Sub ToggleChevron3_Click()
Application.ScreenUpdating = False
Dim bToggle As Boolean
bToggle = ActiveSheet.AutoFilterMode
If bToggle Then
ActiveSheet.AutoFilterMode = False
Else
Dim rng As Range
Set rng = Range("A1:E100") 'used E because you had an offset of 4 columns
With rng
.AutoFilter 5, "<>"
.AutoFilter 1, "<>"
End With
End If
Application.ScreenUpdating = True
End Sub

Resources