Loop through row in range and apply conditional formatting based on cell in that row, to an entire column - excel

I've got the following table in an Excel sheet, called Teams;
TEAM
TLA
C
COLOUR
Ferrari
FER
FER
EE161F
Renault
REN
REN
00B0F0
WilliamsF1
WIL
WIL
000066
The value in the C column is taken directly from the TLA column with =[#TLA]. The value in the COLOUR column is what I want the text and background colour set to when I run the macro. I also want this conditional formatting to apply to the entire column and not just that specific cell. I've got the first part working with the following sub;
Sub SetConditionalFormatting()
Dim rng As Range
Dim row As Range
Dim position As Long
Dim colourColumnIndex As Integer
Dim tlaColumnIndex As Integer
Set rng = Range("Teams")
colourColumnIndex = rng.ListObject.ListColumns("COLOUR").Range.Column
tlaColumnIndex = rng.ListObject.ListColumns("C").Range.Column
For Each row In rng
Dim colorCell As Range
Dim tlaCell As Range
Dim hex As String
Dim color
Set colorCell = Cells(row.row, colourColumnIndex)
Set tlaCell = Cells(row.row, tlaColumnIndex)
hex = colorCell.Value
color = RGB(Application.Hex2Dec(Left(hex, 2)), Application.Hex2Dec(Mid(hex, 3, 2)), Application.Hex2Dec(Right(hex, 2)))
tlaCell.FormatConditions.Delete
tlaCell.FormatConditions.Add Type:=xlCellValue, Operator:=xlEqual, Formula1:=tlaCell.Value
tlaCell.FormatConditions(1).Interior.color = color
tlaCell.FormatConditions(1).Font.color = color
tlaCell.FormatConditions(1).Borders.color = RGB(19, 21, 29)
tlaCell.FormatConditions(1).StopIfTrue = False
Next
End Sub
However this only applies the conditional formatting to that specific cell (so $C$2 for example). What I need is the formatting to be applied to $C$2:$C$4, like it would if I'd select the entire C column and then manually copy/paste the formatting to other tables.
I've added tlaCell.FormatConditions(1).ModifyAppliesToRange Range("Teams[C]") as a final call as an attempt to make this work, but instead of applying formatting once for reach row to the entire column, it applies the formatting as seen in the first screenshot. What I need instead is for the "applies to" range to be set as in the second screenshot. Any idea how I can accomplish this?
Edit: managed to get it to work thanks to Foxfire's suggestion, this is the code I ended up with;
Sub SetConditionalFormatting()
Dim rng As Range
Dim row As Range
Dim position As Long
Dim colourColumnIndex As Integer
Dim tlaColumnIndex As Integer
Dim formattingColumn As Range
Set rng = Range("Teams")
Dim colours
Set colours = CreateObject("Scripting.Dictionary")
colourColumnIndex = rng.ListObject.ListColumns("COLOUR").Range.Column
tlaColumnIndex = rng.ListObject.ListColumns("C").Range.Column
Set formattingColumn = Range("Teams[C]")
formattingColumn.FormatConditions.Delete
For Each row In rng
Dim colorCell As Range
Dim tlaCell As Range
Dim hex As String
Set colorCell = Cells(row.row, colourColumnIndex)
Set tlaCell = Cells(row.row, tlaColumnIndex)
hex = colorCell.Value
If Not colours.Exists(tlaCell.Value) Then
colours.Add Key:=tlaCell.Value, Item:=hex
End If
Next
Dim tla As Variant
Dim index As Integer
index = 1
For Each tla In colours.Keys
hex = colours(tla)
Dim color
color = RGB(Application.Hex2Dec(Left(hex, 2)), Application.Hex2Dec(Mid(hex, 3, 2)), Application.Hex2Dec(Right(hex, 2)))
formattingColumn.FormatConditions.Add Type:=xlCellValue, Operator:=xlEqual, Formula1:=tla
formattingColumn.FormatConditions(index).Interior.color = color
formattingColumn.FormatConditions(index).Font.color = color
formattingColumn.FormatConditions(index).Borders.color = RGB(19, 21, 29)
formattingColumn.FormatConditions(index).StopIfTrue = False
index = index + 1
Next
End Sub
I'll see if I can clean it up a bit, but this works how it's supposed to.

I made an example based on your data using a dictionary.
Sub test()
Dim i As Long
Dim LR As Long
Dim FormatRng As Range
Dim Dic As Object
Dim MyKey As Variant
Dim hex As String
Dim Mycolor As Variant
Set Dic = CreateObject("Scripting.Dictionary")
LR = Range("A" & Rows.Count).End(xlUp).Row 'last used row in column A
Set FormatRng = Range("B2:B" & LR) 'the range where I want to apply my CF rules
FormatRng.FormatConditions.Delete
For i = 2 To LR '2 is the first row where my data is
'loop to create a Dicionary of unique items of C,COLOUR values
If Dic.Exists(Range("C" & i).Value) = False Then Dic.Add Range("C" & i).Value, Range("D" & i).Value
Next i
'loop trough dictionary to apply cf rules to FormatRng
i = 1
For Each MyKey In Dic.Keys
hex = Dic(MyKey)
Mycolor = RGB(Application.Hex2Dec(Left(hex, 2)), Application.Hex2Dec(Mid(hex, 3, 2)), Application.Hex2Dec(Right(hex, 2)))
With FormatRng
.FormatConditions.Add Type:=xlCellValue, Operator:=xlEqual, Formula1:=MyKey
.FormatConditions(i).Interior.Color = Mycolor
.FormatConditions(i).Font.Color = vbWhite
.FormatConditions(i).Borders.Color = RGB(19, 21, 29)
.FormatConditions(i).StopIfTrue = False
End With
i = i + 1
Next MyKey
Set Dic = Nothing
Set FormatRng = Nothing
End Sub
The output I get:
Excel VBA Dictionary – A Complete
Guide

Related

Change color of cells if the value matches values of other worksheets values in a column

So here's the code. I have a calendar with dates in B4:H9. I want to change the color of the cells if the those dates are in a list (column, on different worksheet).
This might be a bit heavy to run if there are many different dates in the worksheet, but that doesn't matter.
What am I doing wrong here? It keeps giving me different error codes, when trying different things.
Sub check_Click()
Dim area As Range
Dim item1 As Range
Dim item2 As Range
Dim sheet As Worksheet
Dim columnlist As Range
sheet = Range("E2").Value
area = Range("B4:H9")
columnlist = Worksheets(sheet).Range("A2:A" & Rows.Count)
For Each item1 In area
For Each item2 In columnlist
If item1.Value = item2.Value Then
item1.Interior.ColorIndex = RGB(255, 255, 0)
End If
Next item2
Next item1
End Sub
As SuperSymmetry mentioned, when you define objects (e.g. ranges, sheets) you need to use the Set keyword. I will not get into that explanation. However few things that I would like to mention...
Try and give meaningful variable names so that you can understand what are they for.
Work with objects so that your code knows which sheet, which range are you referring to.
No need of 2nd loop. Use .Find to search for your data. It will be much faster
To set RGB, you need .Color and not .ColorIndex
Is this what you are trying? (Untested)
Option Explicit
Sub Check_Click()
Dim rngData As Range
Dim rngReference As Range
Dim aCell As Range
Dim matchedCell As Range
Dim ws As Worksheet
Dim lastRow As Long
Dim worksheetName As String
'~~> Change the sheet name accordingly
worksheetName = ThisWorkbook.Sheets("Sheet1").Range("E2").Value
Set ws = ThisWorkbook.Sheets(worksheetName)
With ws
'~~> Find the last row in Col A
lastRow = .Range("A" & .Rows.Count).End(xlUp).Row
'~~> Set your range
Set rngData = .Range("B4:H9")
Set rngReference = .Range("A2:A" & lastRow)
'~~> Loop through your data and use .Find to check if the date is present
For Each aCell In rngData
Set matchedCell = rngReference.Find(What:=aCell.Value, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False, _
SearchFormat:=False)
If Not matchedCell Is Nothing Then
'~~> Color the cell
matchedCell.Interior.Color = RGB(255, 255, 0)
End If
Next aCell
End With
End Sub
This should do the trick, I don't like leaving ranges without their sheet, but since I believe you are using a button, there should be no problem:
Option Explicit
Sub check_Click()
'We are going to use a dictionary, for it to work you need to:
'Go to Tools-References-Check the one called: Microsoft Scripting Runtime
Dim DatesToChange As Dictionary: Set DatesToChange = LoadDates
Dim area As Range: Set area = Range("B4:H9")
Dim item As Range
For Each item In area
If DatesToChange.Exists(item.Value) Then
item.Interior.Color = RGB(255, 255, 0)
End If
Next item
End Sub
Private Function LoadDates() As Dictionary
Set LoadDates = New Dictionary
Dim arr As Variant: arr = ThisWorkbook.Sheets(Range("E2")).Range("A:A")
Dim i As Long
For i = 2 To UBound(arr)
'This here will break the loop when finding an empty cell in column A
If arr(i, 1) = vbNullString Then Exit For
'This will add all your dates in a dictionary (avoiding duplicates)
If Not LoadDates.Exists(arr(i, 1)) Then LoadDates.Add arr(i, 1), 1
Next i
End Function
When you define objects (e.g. ranges, sheets) you need to use the Set keyword
Set area = Range("B4:H9")
Set columnlist = Worksheets(sheet).Range("A2:A" & Rows.Count)
Worksheets() accepts either an Integer or a String. Therefore, sheet should be of Type String
Dim sheet As String
You're also setting columnlist to the whole column in the sheet so you're looping hundreds of thousands more times unncessarily. Change it to
With Worksheets(sheet)
Set columnlist = .Range(.Range("A2"), .Range("A" & Rows.Count).Offset(xlUp))
End With
The above should fix the errors in your code and make it run a little faster. However, there's still big room for improvment in the efficiency of the code. For example, instead of changing the colour inside the loop, you should build a range and set the colour one time after the loop.
Also consider resetting the colour at the beginning of the code with
area.Interior.Pattern = xlNone
I would personally go with conditional formatting as #SiddharthRout suggested in the comments.
Edit following comment
Here's my rendition
Sub check_Click()
Dim dStart As Double
dStart = Timer
Dim rngCalendar As Range
Dim vCalendar As Variant
Dim shtDates As Worksheet
Dim vDates As Variant, v As Variant
Dim i As Long, j As Long
Dim rngToColour As Range
' Change the sheet name
With ThisWorkbook.Sheets("Calendar")
Set rngCalendar = .Range("B4:H9")
vCalendar = rngCalendar.Value
Set shtDates = ThisWorkbook.Sheets(.Range("E2").Value)
End With
With shtDates
vDates = .Range(.Range("A2"), .Range("A" & Rows.Count).End(xlUp)).Value
End With
For i = 1 To UBound(vCalendar, 1)
For j = 1 To UBound(vCalendar, 2)
For Each v In vDates
If v <> vbNullString And v = vCalendar(i, j) Then
If rngToColour Is Nothing Then
Set rngToColour = rngCalendar.Cells(i, j)
Else
Set rngToColour = Union(rngToColour, rngCalendar.Cells(i, j))
End If
Exit For
End If
Next v
Next j
Next i
rngCalendar.Interior.Pattern = xlNone
If Not rngToColour Is Nothing Then
rngToColour.Interior.Color = RGB(255, 255, 0)
End If
MsgBox "Time taken: " & Format(Timer - dStart, "0.0000s")
End Sub
With a list of 2500 dates it took 0.0742s on my machine.

VBA: Loop through merged cells and apply formatting for alternate rows

I've used VBA to filter out values from a different sheet and I'm thinking of how best to format it for readability.
I've merged similar values and would like to select the corresponding rows for each alternating merged cell and apply a color fill.
Here is a visual for reference:
And this is the code I've used to get to the current state.
Dim lRow As Long
lRow = Cells(Rows.Count, "B").End(xlUp).Row
Application.DisplayAlerts = False
For i = lRow To 7 Step -1
If Cells(i, 2) = Cells(i - 1, 2) Then
Range(Cells(i, 2), Cells(i - 1, 2)).Merge
End If
Next i
Application.DisplayAlerts = True
Is there a way of inserting formatting within the loop or otherwise? I'm also open to other ways of making the table more readable.
PS: The image I've attached is just for reference. The actual table I'm working with has tons of rows and columns so readability is important.
Except for the merging of cells the code below does what you want. Instead of merging the code effectively hides the duplicate item titles.
Option Explicit
Sub FormatData()
' 28 Feb 2019
Const CaptionRow As Long = 1
Const FirstDataRow As Long = 3 ' assuming row 2 to contain subtitles
Const FirstDataClm As String = "B" ' change as appropriate
Const DescClm As String = "D" ' change as appropriate
Dim Desc As Variant, PrevDesc As Variant
Dim Col() As Variant, ColIdx As Boolean
Dim FontCol As Long
Dim Rng As Range
Dim Rl As Long, Cl As Long ' last Row / Column
Dim R As Long
Rl = Cells(Rows.Count, DescClm).End(xlUp).Row
Cl = Cells(CaptionRow, Columns.Count).End(xlToLeft).Column
Col = Array(15261367, 15986394) ' sky, pale: change as required
FontCol = Cells(FirstDataRow, FirstDataClm).Font.Color
Application.ScreenUpdating = False
For R = FirstDataRow To Rl
Desc = Cells(R, DescClm).Value
If Desc = PrevDesc Then
Set Rng = Rng.Resize(Rng.Rows.Count + 1)
Else
If Not Rng Is Nothing Then
SetColouring Rng, DescClm, Col(Abs(ColIdx)), FontCol
ColIdx = Not ColIdx
End If
Set Rng = Range(Cells(R, FirstDataClm), Cells(R, Cl))
End If
PrevDesc = Desc
Next R
SetColouring Rng, DescClm, Col(Abs(ColIdx)), FontCol
Application.ScreenUpdating = True
End Sub
Private Sub SetColouring(Rng As Range, _
ByVal C As String, _
ByVal Col As Long, _
ByVal Fcol As Long)
' 28 Feb 2019
Dim R As Long
With Rng
.Interior.Color = Col
.Font.Color = Fcol
For R = 2 To .Rows.Count
.Cells(R, Columns(C).Column - .Column + 1).Font.Color = Col
Next R
End With
End Sub
There are some constants at the top of the code which you can modify. Note also that the font color you use in the sheet is presumed to be found in the first used cell of the sheet as specified by the constants.
Observe that the entire code runs on the ActiveSheet. I strongly urge you to change that bit and specify a sheet, preferably both by its name and the workbook it is in. If you regularly use the code as published above its just a matter of time before you apply it to a worksheet which gets damaged as a result.

setting dynamic range vba

I have been searching the web for a reason why my range won't work when referred to later on, but am a loss still. Any help is greatly appreciated!
What I am attempting to do is set a range based on a potentially moving last row (I import data and each week it grows). However, when I get to the Set ExpenseNameRange, for some reason it is not considering it to be a range.
I need this to use later for a range in a sumproduct formula.
Below is my code. Any help is greatly appreciated!
Dim Profitability As Worksheet
Dim Time As Worksheet
Dim Expense As Worksheet
Dim ExpenseValueRange As Range
Dim ExpenseNameRange As Range
Dim ExpenseDateRange As Range
Dim e As Integer
Dim d As Integer
Set Carryover = ThisWorkbook.Worksheets("2016 Carryover Forecast")
Set Profitability = ThisWorkbook.Worksheets("Profitability")
Set Time = ThisWorkbook.Worksheets("SYNC Time")
Set Expense = ThisWorkbook.Worksheets("SYNC Expense")
finalrowexpense = Expense.Range("A100000").End(xlUp).Row
**Set ExpenseNameRange = Expense.Range(Cells(2, 12), Cells(finalrowexpense, 12))**
Set ExpenseDateRange = Expense.Range(Cells(2, 19), Cells(finalrowexpense, 19))
Set ExpenseValueRange = Expense.Range(Cells(2, 23), Cells(finalrowexpense, 23))
For e = 37 To 63
employeename = Carryover.Cells(e, 33).Value
For d = 34 To 41
If employeename <> "" Then
ExpenseSum = Application.WorksheetFunction.SumProduct(Month(ExpenseDateRange) = Month(Cells(35, d)), ExpenseNameRange = employeename)
ExpenseSum = employeename.Offset(0, d).Value
Consider:
With Expense
Set ExpenseNameRange = Range(.Cells(2, 12), .Cells(finalrowexpense, 12))
Set ExpenseDateRange = Range(.Cells(2, 19), .Cells(finalrowexpense, 19))
Set ExpenseValueRange =Range(.Cells(2, 23), .Cells(finalrowexpense, 23))
End With
because, by itself, Cells() refers to the ActiveSheet.
EDIT#1:
Also you must fix the Cells() in the line with SUMPRODUCT().
EDIT#2:
If the Cells() are qualified, the Range() does not need to be:
Sub dural()
Dim r As Range
Sheets("Sheet1").Activate
Sheets("Sheet1").Select
With Sheets("Sheet2")
Set r = Range(.Cells(1, 1), .Cells(2, 2))
MsgBox r.Parent.Name
End With
End Sub
Thanks for the awesome answers! I managed to follow the rabbit hole far enough and came up with something else that worked. It also looked like Sumifs was a much better option over sumproduct based on the reading I did. Sumifs don't call for arrays or any other special factors.
This is my looping code if anyone is interested.
Sub Organize_Expenses()
Dim Carryover As Worksheet
Dim Profitability As Worksheet
Dim Time As Worksheet
Dim Expense As Worksheet
Dim ExpenseValueRange As Range
Dim ExpenseNameRange As Range
Dim ExpenseMonthRange As Range
Dim ExpenseYearRange As Range
Dim VendorNameRange As Range
Dim e As Integer
Dim d As Integer
Dim a As Integer
Dim b As Integer
Set Carryover = ThisWorkbook.Worksheets("2016 Carryover Forecast")
Set Profitability = ThisWorkbook.Worksheets("Profitability")
Set Time = ThisWorkbook.Worksheets("SYNC Time")
Set Expense = ThisWorkbook.Worksheets("SYNC Expense")
finalrowexpense = Expense.Range("A100000").End(xlUp).Row
Set ExpenseNameRange = Expense.Range("L2:L" & finalrowexpense)
Set ExpenseMonthRange = Expense.Range("AC2:AC" & finalrowexpense)
Set ExpenseValueRange = Expense.Range("w2:w" & finalrowexpense)
Set ExpenseYearRange = Expense.Range("AD2:AD" & finalrowexpense)
For d = 1 To 8
For e = 37 To 56
Employeename = Carryover.Cells(e, 33).Value
If Employeename <> "" Then
ExpenseSum = Application.WorksheetFunction.SumIfs(ExpenseValueRange, ExpenseNameRange, Employeename, ExpenseMonthRange, d, ExpenseYearRange, "2016")
Carryover.Cells(e, 33).Offset(0, d).Value = ExpenseSum
End If
Next e
Next d
End Sub

Variables in Conditional Formatting Formula1

I'm trying to use variables in the FormatCondition Formula1 property. The variables will be cell references. However, I can't get the syntax right. The two bits I'm having trouble with in the code below are: "=(C$3:J$10=""CM"")" and "=($C3:$J10=""RM"")".
The aim of this is to highlight a column with CM in a certain cell, and to highlight a row with RM in a certain cell. The number of columns and rows will increase and decrease, hence the use of variables.
Or if this isn't the right way or the best way, alternatives would be appreciated.
The code is:
Private Sub Workbook_Open()
Application.ScreenUpdating = False
'Rows
Dim iRowA As Integer, iRowB As Integer, iRowC As Integer
Dim iRowDataStart As Integer, iRowLast As Integer
'Columns
Dim iColX As Integer, iColY As Integer, iColZ As Integer
Dim iColDataStart As Integer, iColLast As Integer
'Ranges
Dim rAll As Range
Dim rRowB As Range, rColY As Range
Dim rRowMark As Range, rColMark As Range
'String
Dim sString As String
'Assign values, normally these would be variable values, not assigned
iRowA = 1: iRowB = 2: iRowC = 3
iRowDataStart = 4: iRowLast = 10
iColX = 1: iColY = 2: iColZ = 3
iColDataStart = 4: iColLast = 10
'Set ranges
Set rAll = Range(Cells(iRowA, iColX), Cells(iRowLast, iColLast))
Set rRowB = Range(Cells(iRowB, iColZ), Cells(iRowLast, iColLast))
Set rColY = Range(Cells(iRowC, iColY), Cells(iRowLast, iColLast))
Set rRowMark = Range(Cells(iRowC, iColZ), Cells(iRowLast, iColLast))
Set rColMark = Range(Cells(iRowC, iColZ), Cells(iRowLast, iColLast))
'Delete all CF currently in the worksheet
With rAll
.FormatConditions.Delete
End With
'Format column with Column Mark
sString = "=(C$3:J$10=""CM"")"
With rRowB
.FormatConditions.Add _
Type:=xlExpression, _
Formula1:=sString
.FormatConditions(.FormatConditions.Count).SetFirstPriority
With .FormatConditions(1)
.Interior.Color = RGB(196, 189, 151)
.StopIfTrue = False
End With
End With
'Format row with Row Mark
sString = "=($C3:$J10=""RM"")"
With rColY
.FormatConditions.Add _
Type:=xlExpression, _
Formula1:=sString
.FormatConditions(.FormatConditions.Count).SetFirstPriority
With .FormatConditions(1)
.Font.ColorIndex = 2
.Interior.Color = RGB(127, 127, 127)
.StopIfTrue = False
End With
End With
Range("A1").Select
Application.StatusBar = False
Application.CutCopyMode = False
End Sub
You just need to dynamically set your ranges by getting last row and column of your data where you can find many examples here like this one. Something like:
Dim r As Range
Dim lr As Long, lc As Long
Dim formula As String
With Sheet1 '~~> change to your actual sheet
lr = .Range("C" & .Rows.Count).End(xlUp).Row '~~> based on C, adjust to suit
lc = .Cells(3, .Columns.Count).End(xlToLeft).Column '~~> based on row 3
Set r = .Range(.Cells(3, 3), .Cells(lr, lc))
formula = "=(" & r.Address & "=""CM"")"
'~~> formatting code here
End With
Or you can try what I've posted here about Conditional Formatting which of course can be automated as I posted HERE and HERE. Something like:
formula = "=C3=""CM"""
[C3].FormatConditions.Add xlExpression, , formula
With [C3].FormatConditions(1)
.Interior.Color = RGB(196, 189, 151)
.ModifyAppliesToRange r
End With
HTH.

Copy a Range using variables as arguments

I'm new to VBA and am having issues with Range syntax and what are acceptable arguments.
the purpose of this code is as follows:
user inputs value into cell D5 on Sheet2
User activates code with button
searches "configs" sheet for value
copies corresponding range after locating value
pastes range back into Sheet2
the range I am attempting to copy paste is a block that starts with the selected cell (D5) on sheet "Configs", and continues until an empty cell is found.
Sub search()
Dim GCell As Range,
Dim box As Integer
Dim Avio As String
Dim Sheet2 As Worksheet, Configs As Worksheet
Dim rw1 As String, rw2 As String
Set Configs = ActiveWorkbook.Sheets("Configs")
Set Sheet2 = ActiveWorkbook.Sheets("Sheet2")
Avio = Range("D5").Value
Set GCell = Configs.Cells.Find(Avio)
box = 0
LoopX:
box = box + 1
If GCell.Offset(box, 0).Value = "" Then
rw1 = GCell.Offset(1, -1).Address
rw2 = GCell.Offset(box, 2).Address
Configs.Range("rw1:rw2").Copy <-- this syntax doesnt seem to work...
Sheet2.Range("Avio.Offset(1,0)").Paste <-- I know this is wrong, but I would like the range to be pasted just below the selected cell on Sheet2
Else: GoTo LoopX
End If
End Sub
Is this helping?
Sub search()
Dim GCell As Range
Dim box As Integer
Dim Sheet2 As Worksheet, Configs As Worksheet
Dim rw1 As String, rw2 As String
Set Configs = ActiveWorkbook.Sheets("Configs")
Set Sheet2 = ActiveWorkbook.Sheets("Sheet2")
Dim rngAvio As Range
Set rngAvio = Sheet2.Range("D5")
Set GCell = Configs.Cells.Find(rngAvio.Value)
box = 0
Do While (GCell.Offset(box, 0).Value <> "")
box = box + 1
rw1 = GCell.Offset(1, -1).Address
rw2 = GCell.Offset(box, 2).Address
Configs.Range(rw1 & ":" & rw2).Copy rngAvio.Offset(1, 0)
Loop
End Sub

Resources