Draw table according to user provided width and height - excel

I am very new with VBA in Excel. What I want to accomplish is this. When a user enters a length of say 5, then 5 columns must be outlined red. Then also when a user enters a width of say 6, then 6 rows must be outlined red. Example:
I have this code thus far:
On worksheet change:
Private Sub Worksheet_Change(ByVal Target As Range)
If (Target.Address = "$A$2") Then
Call Draw2DTankl
ElseIf (Target.Address = "$B$2") Then
Call Draw2DTankw
End If
End Sub
Draw2DTankl:
Sub Draw2DTankl()
On Error Resume Next
Cells(2, 4).Value = ""
Dim x As Range
Set x = Worksheets("Sheet1").Cells
x.Borders.LineStyle = xNone
Range("A1") = "Length"
Dim Length As Integer
Length = CInt(Cells(2, 1).Value)
If (Length > 30) Then
MsgBox "A length of a maximum 30 is allowed"
Exit Sub
End If
If (Length < 0) Then
MsgBox "Invalid length value entered"
Exit Sub
End If
Dim Rws As Long, Rng As Range, r As Range
If (Length > 0) Then
Rws = 20
Set Rng = Range(Cells(20, "H"), Cells(Rws, 8 + Length - 1))
For Each r In Rng.Cells
With r.Borders
.LineStyle = xlContinuous
.Weight = xlMedium
.ColorIndex = 3
End With
Next r
End If
If (Err.Number <> 0) Then
MsgBox Err.Description
End If
End Sub
Draw2DTankw:
Sub Draw2DTankw()
On Error Resume Next
Cells(2, 4).Value = ""
Dim x As Range
Set x = Worksheets("Sheet1").Cells
x.Borders.LineStyle = xNone
Range("B1") = "Width"
Dim Width As Integer
Width = CInt(Cells(2, 2).Value)
If (Width > 30) Then
MsgBox "A width of a maximum 30 is allowed"
Exit Sub
End If
If (Width < 0) Then
MsgBox "Invalid Width value entered"
Exit Sub
End If
Dim Col As Long, Rng As Range, r As Range
If (Width > 0) Then
Col = 21
Set Rng = Range(Cells(21, "H"), Cells(Col, 8 + Length - 1))
For Each r In Rng.Cells
With r.Borders
.LineStyle = xlContinuous
.Weight = xlMedium
.ColorIndex = 3
End With
Next r
End If
If (Err.Number <> 0) Then
MsgBox Err.Description
End If
End Sub
Please help me. My code doesn't work. The length works, but that brakes when I change the width.
Entering my length draws:
Which is correct. But then if I enter the width of 6 this happens: (my length also dissapears)
I apologize for this long post!

It looks like in the Draw2DTankw you have Width declared above but in the rng you are using length
Dim Width As Integer Width = CInt(Cells(2, 2).Value)
Set Rng = Range(Cells(21, "H"), Cells(Col, 8 + Length - 1))
I've modified your code to draw both height and width by extending the range to include the width. This worked with I test it.
Private Sub Worksheet_Change(ByVal Target As Range)
If (Target.Address = "$A$2") Or (Target.Address = "$B$2") Then
DrawTable
End If
End Sub
Sub DrawTable()
On Error Resume Next
Cells(2, 4).Value = ""
Dim x As Range
Set x = ActiveSheet.Cells
x.Borders.LineStyle = xNone
Range("A1") = "Length"
Dim Length As Integer
Length = CInt(Cells(2, 1).Value)
'Combined Width sections
Dim Width As Integer
Width = CInt(Cells(2, 2).Value)
If (Length > 30) Then
MsgBox "A length of a maximum 30 is allowed"
Exit Sub
ElseIf (Width > 30) Then
MsgBox "A width of a maximum 30 is allowed"
Exit Sub
ElseIf (Length < 0) Then
MsgBox "Invalid length value entered"
Exit Sub
ElseIf (Width < 0) Then
MsgBox "Invalid Width value entered"
Exit Sub
End If
Dim Rws As Long, Rng As Range, r As Range
If (Length > 0) Then
Rws = 20
'Added width to cells(rws)
Set Rng = Range(Cells(20, "H"), Cells(Rws + Width - 1, 8 + Length - 1))
For Each r In Rng.Cells
With r.Borders
.LineStyle = xlContinuous
.Weight = xlMedium
.ColorIndex = 3
End With
Next r
End If
If (Err.Number <> 0) Then
MsgBox Err.Description
End If
End Sub

Related

Yes/No boxes in VBA

I have an array of shapes created in a for loop and want to assign simple code to each of them as "yes/no" buttons.
The code that creates the array of buttons is as follows:
Dim i As Integer
Dim j As Integer
Dim k As Integer
For i = 1 To 3
For j = 2 To 17
ActiveSheet.Shapes.addshape(msoShapeRectangle, Cells(j, i).Left + 0, _
Cells(j, i).Top + 0, Cells(j, i).Width, Cells(j, i).Height).Select
Next j
Next i
I would like to be able to assign code to each of the shapes as they are created but do not know how. What I want the code to do for each shape looks like the below. I want the shapes to react when clicked and cycle through yes/no/blank text in each of the shapes. The general logic of the code is below
value = value +1
if value = 1, then "yes" and green
if value = 2, then "no" and red
if value = 3, then value = 0 and blank and grey
Thank you in advance for your help
You can do something like this:
Option Explicit
Sub Tester()
Dim i As Long, j As Long, k As Long
Dim addr As String, shp As Shape
For i = 1 To 3
For j = 2 To 17
With ActiveSheet.Cells(j, i)
Set shp = .Parent.Shapes.AddShape(msoShapeRectangle, .Left + 0, _
.Top + 0, .Width, .Height)
With shp.TextFrame2
.VerticalAnchor = msoAnchorMiddle
.TextRange.ParagraphFormat.Alignment = msoAlignCenter
End With
shp.Name = "Button_" & .Address(False, False)
End With
shp.Fill.ForeColor.RGB = RGB(200, 200, 200)
shp.OnAction = "ButtonClick"
Next j
Next i
End Sub
'called from a click on a shape
Sub ButtonClick()
Dim shp As Shape, capt As String, tr As TextRange2
'get a reference to the clicked-on shape
Set shp = ActiveSheet.Shapes(Application.Caller)
Set tr = shp.TextFrame2.TextRange
Select Case tr.Text 'decide based on current button text
Case "Yes"
tr.Text = ""
shp.Fill.ForeColor.RGB = RGB(200, 200, 200)
Case "No"
tr.Text = "Yes"
shp.Fill.ForeColor.RGB = vbGreen
Case ""
tr.Text = "No"
shp.Fill.ForeColor.RGB = vbRed
End Select
End Sub
Just to visualize my idea regarding using the selection change event instead of buttons:
The area that should be the clickable range is named clickArea - in this case B2:D17.
Then you put this code in the according sheet module
Option explicit
Private Const nameClickArea As String = "clickArea"
Private Enum bgValueColor
neutral = 15921906 'gray
yes = 11854022 'green
no = 11389944 'red
End Enum
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
'whenever user clicks in the "clickArea" the changeValueAndColor macro is triggered
If Not Intersect(Target.Cells(1, 1), Application.Range(nameClickArea)) Is Nothing Then
changeValueAndColor Target.Cells(1, 1)
End If
End Sub
Private Sub changeValueAndColor(c As Range)
'this is to deselect the current cell so that user can select it again
Application.EnableEvents = False: Application.ScreenUpdating = False
With Application.Range(nameClickArea).Offset(50).Resize(1, 1)
.Select
End With
'this part changes the value and color according to the current value
With c
Select Case .Value
Case vbNullString
.Value = "yes"
.Interior.Color = yes
Case "yes"
.Value = "no"
.Interior.Color = no
Case "no"
.Value = vbNullString
.Interior.Color = neutral
End Select
End With
Application.EnableEvents = True: Application.ScreenUpdating = True
End Sub
And this is how it works - with each click on one of the cells value and background color are changed. You have to click on the image to start anmimation.
To reset everything I added a hyperlink that calls the reset action (and refers to itself)
Add this code to the sheets module
Private Sub Worksheet_FollowHyperlink(ByVal Target As Hyperlink)
clearAll
End Sub
Private Sub clearAll()
With Application.Range(nameClickArea)
.ClearContents
.Interior.Color = neutral
End With
End Sub

How to use WITH with an array of ranges

I need to set the borders for a bunch of ranges.
This is how I do it:
For n = 1 to record_num
With ThisWorkbook.Sheets("Sheet1").Range("A" & (n-1)*3 + 1 & ":C" & (n-1)*3 + 2)
With .Borders(xlEdgeLeft)
.LineStyle = xlContinuous
.ThemeColor = 6
.TintAndShade = -0.499984740745262
.Weight = xlThick
End With
... some other border codes
End With
Next n
Because For ... Next is always slow, I'm thinking of putting the ranges in an array and set the borders all at once.
With ThisWorkbook.Sheets("Sheet1")
For n = 1 to record_num
Set cellArray(i) = .Range("A" & (n-1)*3 + 1 & ":C" & (n-1)*3 + 2)
Next i
End With
This loop works fine. I then tried to do a With cellArray(), With Range(cellArray()), With ThisWorkbook.Sheets("Sheet1").Range(cellArray()) and they all failed with the error message of "Method 'Range' of object '_Global' failed".
What is the proper syntax of doing it?
Format With Offset
Range.Union
Option Explicit
Sub formatWithOffset()
Dim record_num As Long
' e.g.;
record_num = 5
Dim rng As Range
Set rng = ThisWorkbook.Worksheets("Sheet1").Range("A1:C2")
Dim tRng As Range
Dim n As Long
For n = 1 To record_num
If Not tRng Is Nothing Then
Set tRng = Union(tRng, rng.Offset((n - 1) * 3))
Else
Set tRng = rng
End If
Next n
If Not tRng Is Nothing Then
With tRng
With .Borders(xlEdgeLeft)
.LineStyle = xlContinuous
.ThemeColor = 6
.TintAndShade = -0.499984740745262
.Weight = xlThick
End With
End With
End If
End Sub
If you want to use With with an array, you need to iterate through each element of the array individually, as the With operator only operates on a single object.
This assigns the ranges to the cellarray:
Dim cellarray(1 To 5) As Range
With ThisWorkbook.Sheets("Sheet1")
For n = 1 To 5
Set cellarray(n) = .Range("A" & (n - 1) * 3 + 1 & ":C" & (n - 1) * 3 + 2)
Next n
End With
and then run through the elements of cellarray, and use With to apply the styles.
For Each c In cellarray
With c
.Interior.ColorIndex = 12
End With
Next c

Merge cells with same year in a row

I need to merge the cells one above the months.
Cells Should be merged from 01 to 12 showing year in cell.
Look for the picture for more clarification.
I have below code, but which show months after run in cell row1.
My idea is to convert above cells to years through vba and apply merge same year at the end.
which is shown in desired output.
Note.
ROW 4 and 5 are just my thinking, which will help year to merge.
Dim a(), i As Long, j As Long, m As Long, x As Range
With Range("b1:qaz1")
.MergeCells = False
.ClearContents
a() = .Offset(1).Value
m = Month(a(1, 1))
j = UBound(a, 2)
Set x = .Cells(1)
For i = 2 To j
If m <> Month(a(1, i)) Or i = j Then
With Range(x, .Cells(i - IIf(i = j, 0, 1)))
.MergeCells = True
.HorizontalAlignment = xlCenter
End With
x.Value = Format(DateSerial(2000, m, 1), "MMMM")
m = Month(a(1, i))
Set x = .Cells(i)
End If
Next
End With
End Sub
After running new program output look like
Since you have true dates in your caption row the month and year can be extracted from there. However, the code below converts dates that might have been created using formulas to hard dates before processing them.
Sub MergeCaptionsByYear()
' 031
Const CapsRow As Long = 1 ' change to suit
Const StartClm As Long = 2 ' change to suit
Dim Rng As Range ' working range
Dim Tmp As Variant ' current cell's value
Dim Cl As Long ' last used column
Dim Cstart As Long ' first column in Rng
Dim C As Long ' working column
Dim Yr As Integer ' year
Cl = Cells(CapsRow, Columns.Count).End(xlToLeft).Column
Range(Cells(CapsRow, StartClm), Cells(CapsRow, Cl)).Copy
Cells(CapsRow, StartClm).PasteSpecial xlValues
Application.CutCopyMode = False
C = StartClm - 1
Application.DisplayAlerts = False
Do
Tmp = Cells(CapsRow, C + 1).Value
If Not IsDate(Tmp) And (C <> Cl) Then
MsgBox "Cell " & Cells(CapsRow, C + 1).Address(0, 0) & _
" doesn't contain a date." & vbCr & _
"This macro will be terminated.", _
vbInformation, "Invalid cell content"
Exit Do
End If
If (Yr <> Year(CDate(Tmp))) Or (C = Cl) Then
If Yr Then
Set Rng = Range(Cells(CapsRow, Cstart), _
Cells(CapsRow, C))
With Rng
.Merge
.HorizontalAlignment = xlCenter
.NumberFormat = "yyyy"
End With
SetBorder Rng, xlEdgeLeft
SetBorder Rng, xlEdgeRight
End If
If C > (Cl - 1) Then Exit Do
Cstart = C + 1
Yr = Year(Tmp)
End If
C = C + 1
Loop
Application.DisplayAlerts = True
End Sub
Private Sub SetBorder(Rng As Range, _
Bord As XlBordersIndex)
' 031
With Rng.Borders(Bord)
.LineStyle = xlContinuous
.ColorIndex = xlAutomatic
.TintAndShade = 0
.Weight = xlMedium ' xlThin
End With
End Sub
Assuming the months range is "B5:AH5"
Sub test()
Dim monthsRng As Range
Set monthsRng = Range("B5:AH5")
monthsRng.Cells(1, 1).Offset(-1, 0).Select
For j = 1 To Int((monthsRng.Cells.Count / 12) + 2)
If ActiveCell.Offset(1, 0) <> 0 Then
For i = 1 To 12
ActiveCell.Value = Year(ActiveCell.Offset(1, 0))
If Year(ActiveCell.Offset(1, i)) = ActiveCell Then
Selection.Resize(1, i + 1).Select
Else
Exit For
End If
Next
With Selection
.HorizontalAlignment = xlCenter
.MergeCells = True
End With
Selection.Offset(0, 1).Select
Else
Exit For
End If
Next
End Sub
Replacing the inner for loop with below code will work irrespective of whether the dates in the Range("B5:AH5") in above procedure are formatted as dates or not.
For i = 1 To 12
ActiveCell.Value = Right(Format(ActiveCell.Offset(1, 0), "DD.MM.YYYY"), 4)
If Right(Format(ActiveCell.Offset(1, i), "DD.MM.YYYY"), 4) = Format(ActiveCell, Text) Then
Selection.Resize(1, i + 1).Select
Else
Exit For
End If
Next
However, in any case you need to format the output in excel as number (without 1000 separator and decimal places) and not date.

Set cell color based on current value

How can my code be made shorter?
If a user fills the cell with color yellow then if its value is 0 then it will turn to red and it will popup a message box, then if its value is > 0 it will back again to yellow, then if the user enters value of > 0 in the "no fill up cell" it will turn grey and back to no fill up if I input 0 this code is for column L only I need to make this for column M, N and O also.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim ws As Worksheet
Set ws = ActiveSheet
On Error GoTo ExitSub
'WEEK 0
'For Task Not done
With ws.Cells(15, 12)
If Not (Application.Intersect(Range("L15"), Target) Is Nothing) Then
If .Interior.ColorIndex = 6 And .Value < 1 Then
MsgBox "Project Delay!"
Range("L15").Interior.ColorIndex = 3
Else
If Range("L15").Interior.ColorIndex = 3 And .Value > 0 Then
Range("L15").Interior.ColorIndex = 6
End If
End If
'For overlapped Task
If .Interior.ColorIndex = -4142 And .Value > 0 Then
MsgBox "Overlap!"
Range("L15").Interior.ColorIndex = 16
Else
If Range("L15").Interior.ColorIndex = 16 And .Value < 1 Then
Range("L15").Interior.ColorIndex = -4142
End If
End If
End If
End With
On Error GoTo ExitSub
'For Task Not done
With ws.Cells(17, 12)
If Not (Application.Intersect(Range("L17"), Target) Is Nothing) Then
If .Interior.ColorIndex = 6 And .Value < 1 Then
MsgBox "Project Delay!"
Range("L17").Interior.ColorIndex = 3
Else
If Range("L17").Interior.ColorIndex = 3 And .Value > 0 Then
Range("L17").Interior.ColorIndex = 6
End If
End If
'For overlapped Task
If .Interior.ColorIndex = -4142 And .Value > 0 Then
MsgBox "Overlap!"
Range("L17").Interior.ColorIndex = 16
Else
If Range("L17").Interior.ColorIndex = 16 And .Value < 1 Then
Range("L17").Interior.ColorIndex = -4142
End If
End If
End If
End With
End Sub
Please try this code. As far as I understood your intentions it should do what you want.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim Tmp As Long
With Target
If .Cells.CountLarge > 1 Then Exit Sub
If (.Column >= Columns("L").Column) And (.Column <= .Columns("O").Column) Then
Tmp = Val(.Value)
Select Case .Row
Case 15
.Interior.ColorIndex = IIf(Tmp, 6, 3)
If Tmp = 0 Then
MsgBox "Project Delay!", _
vbCritical, "Attention required!"
End If
Case 17
.Interior.ColorIndex = IIf(Tmp, 16, -4142)
If Tmp Then
MsgBox "Enter a value of zero.", _
vbExclamation, "Overlap!"
End If
End Select
End If
End With
End Sub
I have kept the syntax simple so that you ought to be able to tweak it where it needs tweaking. Good luck!

For loop end variable doesn't change

I've written a very simple loop that goes through a table column and colors negative values red, positive values green and removes empty rows.
The problem occurs when rows are deleted. I update the value of the RowCount, and compensate i to check the same row again since a row was just deleted. If I have a column with 10 rows of which 2 are empty, they are deleted. I would expect the For i = 1 to RowCount to stop at 8, but it continues to 9 and produces an error because it then tries to delete the nonexistent 9th row.
What do I need to do so the loop stops at 8 instead of continuing (to I assume the initial value of the RowCount.
Sub ColourFilledCells()
Dim Table1 As ListObject
Set Table1 = ThisWorkbook.Worksheets(1).ListObjects(1)
Dim i As Lon, RowCount As Long
RowCount = Table1.ListRows.Count
For i = 1 To RowCount
If Not Table1.DataBodyRange(i, 1) = Empty Then
With Table1.DataBodyRange(i, 1)
If .Value < 0 Then
.Interior.Color = RGB(255, 0, 0)
ElseIf .Value > 0 Then
.Interior.Color = RGB(0, 255, 0)
Else
.ColorIndex = 0
End If
End With
ElseIf Table1.DataBodyRange(i, 1) = Empty Then
Table1.ListRows(i).Delete
RowCount = RowCount - 1
i = i - 1
End If
Next i
End Sub
To avoid issues with Delete affecting to For loop, count backwards.
Your code, refactored (Plus a few suggestions)
For i = RowCount to 1 Step -1
If Not isempty( Table1.DataBodyRange(i, 1)) Then
With Table1.DataBodyRange(i, 1)
If .Value < 0 Then
.Interior.Color = vbRed
ElseIf .Value > 0 Then
.Interior.Color = vbGreen
Else
.ColorIndex = xlColorIndexNone
End If
End With
Else
Table1.ListRows(i).Delete
End If
Next i
Try this code :
Sub ColourFilledCells()
Dim Table1 As ListObject
Dim uRng As Range
Set Table1 = ThisWorkbook.Worksheets(1).ListObjects(1)
Dim i As Long, RowCount As Long
RowCount = Table1.ListRows.Count
For i = 1 To RowCount
If Not Table1.DataBodyRange(i, 1) = Empty Then
With Table1.DataBodyRange(i, 1)
If .Value < 0 Then
.Interior.Color = RGB(255, 0, 0)
ElseIf .Value > 0 Then
.Interior.Color = RGB(0, 255, 0)
Else
.ColorIndex = 0
End If
End With
ElseIf Table1.DataBodyRange(i, 1) = Empty Then
If uRng Is Nothing Then
Set uRng = Table1.ListRows(i).Range
Else
Set uRng = Union(uRng, Table1.ListRows(i).Range)
End If
End If
Next i
If Not uRng Is Nothing Then uRng.Delete xlUp
End Sub

Resources