how to put arrow shape inside cells using VBA - excel

i'm working on a macro to generate comparison between numbers and i have the specific task to add some shapes near the text of every cell.
I tried to figure out how to calculate x, y, width, height in order to nicely place them in the cell, but without success.
I asked about that in a previous question and also obtaines a nice answer, but since i have a specific request i need the vba way to do this.
Any suggestions?
Dim s As Shape, sh As Worksheet
Set sh = ActiveSheet
If arrType = "Up" Then
Set s = sh.Shapes.AddShape(msoShapeUpArrow, x, y, width, height)
Else
Set s = sh.Shapes.AddShape(msoShapeDownArrow, x, y, width, height)
End If

Try the next adapted code, please. It happens I know your previous question on this theme, too:
Sub Compare_numbers()
Dim sh As Worksheet, i As Long, lastRow As Long
Dim arrA, txt As String
Set sh = ActiveSheet
lastRow = sh.cells(rows.count, "L").End(xlUp).row
For i = 2 To lastRow
If sh.cells(i, "L").Value = sh.cells(i, "M").Value Then
sh.cells(i, "N").Value = "they are equal"
arrA = isArrow(sh.Range("N" & i), "")
ElseIf sh.cells(i, "L").Value > sh.cells(i, "M").Value Then
With sh.cells(i, "N")
.Value = "L is greater than M ."
.EntireColumn.AutoFit
End With
arrA = isArrow(sh.Range("N" & i), "Up")
If arrA(0) = "OK" Then
If arrA(1) <> "Up" Then
insertArrow sh.Range("N" & i), "Up"
End If
Else
insertArrow sh.Range("N" & i), "Up"
End If
Else
With sh.cells(i, "N")
.Value = "L is greater than M ." 'Used this solution to Autofit on the larger text...
.EntireColumn.AutoFit
.Value = "L is less than M ."
End With
arrA = isArrow(sh.Range("N" & i), "Down")
If arrA(0) = "OK" Then
If arrA(1) <> "Down" Then
insertArrow sh.Range("N" & i), "Down"
End If
Else
insertArrow sh.Range("N" & i), "Down"
End If
End If
Next i
End Sub
It needs the following Sub inserting the appropriate arrow:
Sub insertArrow(rng As Range, arrType As String)
Dim sh As Worksheet, s As Shape
Dim leftP As Double, topP As Double, W As Double, H As Double
Set sh = rng.Parent
W = 8: H = 12 'set the arrow width and height (you can change them)
leftP = rng.left + rng.width - W - 1 'calculate the horiz position
topP = rng.top + (rng.height - H) / 2 'calculate the vert position
If arrType = "Up" Then
Set s = sh.Shapes.AddShape(msoShapeUpArrow, leftP, topP, W, H)
Else
Set s = sh.Shapes.AddShape(msoShapeDownArrow, leftP, topP, W, H)
End If
s.Name = s.Name & "-" & rng.Address 'add the cell address to be able
'to bring back the arrows moved by mistake
s.LockAspectRatio = msoFalse: s.placement = xlMoveAndSize
End Sub
And the next Function able to check if a shape is an arrow and what type:
Function isArrow(rng As Range, typeArr As String) As Variant
Dim s As Shape, sh As Worksheet, arr
Set sh = rng.Parent 'extract the range sheet where it belongs
For Each s In sh.Shapes
If s.TopLeftCell.Address = rng.Address Then 'match the range address with the shape TLCell address
If left(s.Name, 2) = typeArr Or left(s.Name, 4) = typeArr Then
isArrow = Array("OK", typeArr): Exit Function
Else
If left(s.Name, 2) = "UP" Or left(s.Name, 4) = "Down" Then
isArrow = Array("OK", IIf(typeArr = "Up", "Down", "Up"))
s.Delete: Exit Function
End If
Exit For
End If
End If
Next
isArrow = Array("No", "")
'the function creates an array able to 'tell' if the shape is an arrow and its type
End Function
Unfortunately, there is no event able to be triggered by the cell sizes change. But, try the next event, which act when you double click a cell. Please, copy it in the sheet code module, where you need to insert arrows:
Option Explicit
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Dim lastR As Long, s As Shape, i As Long, addr As String
'bring back the arrows moved by mistakes:
For Each s In Me.Shapes
If left(s.Name, 2) = "Up" Or left(s.Name, 4) = "Down" Then
addr = Split(s.Name, "-")(UBound(Split(s.Name, "-")))
If addr <> s.TopLeftCell.Address Then
s.left = Me.Range(addr).left + 10
s.top = Me.Range(addr).top + 1
End If
End If
Next
'last row on the column to be processed (N:N):
lastR = Me.Range("N" & Me.rows.count).End(xlUp).row
Me.Range("L:N").VerticalAlignment = xlCenter 'to look nicer
For i = 2 To lastR
arrangeSh Me.Range("N" & i)
Next i
End Sub
Sub arrangeSh(rng As Range)
Dim sh As Shape
For Each sh In rng.Parent.Shapes
If sh.TopLeftCell.Address = rng.Address Then
'bring back the row height if is less then the arrow height:
If rng.height < 12 Then rng.EntireRow.height = 12
sh.width = 8: sh.height = 12 'reset the arrow dimensions
'reposition the arrows:
sh.top = rng.top + (rng.height - sh.height) / 2
sh.left = rng.left + rng.width - sh.width - 1
Exit For
End If
Next
End Sub
It will firstly check if the arrow has been moved by mistake and bring it back on the 'mother' cell, then place it centered, at 1 point from the right cell side.
The event code can be placed in a sub, let us say reArrangeShapes, the event will have a single line reArrangeShapes, and this sub may be called from different sheet events (Calculate, Activate, Deactivate etc.). Doing its job when the events in discussion are triggered.
The subs can be optimized using Appliction.ScreenUpdating = True and Application.Calculation = xlCalculationManual at the beginning followed by Appliction.ScreenUpdating = True and and Application.Calculation = xlCalculationAutomatic at the end.
If something unclear, please, do not hesitate to ask for clarifications...
Edited:
In order to better understand what's happening the isArrow function can be tested using the next sub. You should select a cell (having or not an arrow and run the code:
Sub testIsArrow()
Debug.Print isArrow(ActiveCell, "Up")(0)
End Sub
What does it return in Immediate Window (being in IDE, press Ctrl + G)?
You must understand that if the active cell keeps an arrow not being "Up" type, the shape will be deleted...

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

Add two Characters Objects together so as to concatenate their text but retain formats from each

I am adding the contents of cells to a shape object. The contents are all text, but each cell may have different formatting. I would like to be able to preserve this formatting when adding the content of the cells to the shape, so that a bold cell will appear as such and so on.
I have been trying to take the current Shape.TextFrame.Characters object and add the new Range("TargetCell").Characters object to it, for each target cell in my source range.
Is there a simple way to force two .Characters objects together, so the text concatenates and the formatting changes to reflect the source at the boundary of the new text - I see the .Characters.Insert(string) method, but that only inserts the text, not the formatting. Every time I add a new cell to the output list, I need to recalculate where each portion of text has what formatting, which is proving to be difficult.
I was trying along these lines, but keep coming into difficulties trying to get or set the .Characters(n).Font.Bold property.
Private Sub buildMainText(Target As Range, oSh As Shape)
On Error GoTo 0
Dim chrExistingText As Characters
Dim chrTextToAdd As Characters
Dim chrNewText As Characters
Dim o As Characters
Dim i As Integer
Dim isBold As Boolean
Dim startOfNew As Integer
i = 0
With oSh.TextFrame
Set chrExistingText = .Characters
Set chrTextToAdd = Target.Characters
Set chrNewText = chrTextToAdd
chrNewText.Text = chrExistingText.Text & chrTextToAdd.Text
startOfNew = Len(chrExistingText.Text) + 1
.Characters.Text = chrNewText.Text
For i = 1 To Len(chrNewText.Text)
If i < startOfNew Then
If chrExistingText(i, 1).Font.Bold Then
.Characters(i, 1).Font.Bold = True
Else
.Characters(i, 1).Font.Bold = False
End If
Else
If chrNewText(i - startOfNew + 1, 1).Font.Bold Then
.Characters(i, 1).Font.Bold = True
Else
.Characters(i, 1).Font.Bold = False
End If
End If
Next i
End With
End Sub
Here is an example which takes a single cell and appends it to a shape; preserving, shape's and range's formattings. In the example below, we will preserve BOLD (B), ITALICS (I) and UNDERLINE (U). Feel free to modify the code to store more formatting attributes.
LOGIC:
The maximum length of characters you can have in a shape's textframe is 32767. So we will create an array (as #SJR mentioned in the comments above) say, TextAr(1 To 32767, 1 To 3), to store the formatting options. The 3 columns are for B,U and I. If you want to add more attributes then change it to the relevant number.
Store the shape's formatting in an array.
Store the cells's formatting in an array.
Append the cell's text to the shape.
Loop through the array and re-apply the formatting.
CODE:
I have commented the code but if you have a problem understanding it then simply ask. I quickly wrote this so I must confess that I have not done extensive testing of this code. I am assuming that the cell/shape doesn't have any other formatting other than B, I and U(msoUnderlineSingleLine). If it does, then you will have to amend the code accordingly.
Option Explicit
Sub Sample()
Dim ws As Worksheet
'~~> Change this to the relevant sheet
Set ws = Sheet1
AddTextToShape ws.Range("F3"), ws.Shapes("MyShape")
End Sub
'~~> Proc to add cell range to shape
Sub AddTextToShape(rng As Range, shp As Shape)
'~~> Check for single cell
If rng.Cells.Count > 1 Then
MsgBox "Select a single cell and try again"
Exit Sub
End If
Dim rngTextLength As Long
Dim shpTextLength As Long
'~~> Get the length of the text in the supplied range
rngTextLength = Len(rng.Value)
'~~> Get the length of the text in the supplied shape
shpTextLength = Len(shp.TextFrame.Characters.Text)
'~~> Check if the shape can hold the extra text
If rngTextLength + shpTextLength > 32767 Then
MsgBox "Cell text will not fit in Shape. Choose another cell with maximum " & _
(32767 - shpTextLength) & " characters"
Exit Sub
End If
Dim TextAr(1 To 32767, 1 To 3) As String
Dim i As Long
'~~> Store the value and formatting from the shape in the array
For i = 1 To shpTextLength
With shp.TextFrame.Characters(i, 1)
With .Font
If .Bold = True Then TextAr(i, 1) = "T" Else TextAr(i, 1) = "F"
If .Italic = True Then TextAr(i, 2) = "T" Else TextAr(i, 2) = "F"
If .Underline = xlUnderlineStyleSingle Then TextAr(i, 3) = "T" Else TextAr(i, 3) = "F"
End With
End With
Next i
'~~> Store the value and formatting from the range in the array
Dim j As Long: j = shpTextLength + 2
For i = 1 To rngTextLength
With rng.Characters(Start:=i, Length:=1)
With .Font
If .Bold = True Then TextAr(j, 1) = "T" Else TextAr(j, 1) = "F"
If .Italic = True Then TextAr(j, 2) = "T" Else TextAr(j, 2) = "F"
If .Underline = xlUnderlineStyleSingle Then TextAr(j, 3) = "T" Else TextAr(j, 3) = "F"
j = j + 1
End With
End With
Next i
'~~> Add the cell text to shape
shp.TextFrame.Characters.Text = shp.TextFrame.Characters.Text & " " & rng.Value2
'~~> Get the new text length of the shape
shpTextLength = Len(shp.TextFrame.Characters.Text)
'~~> Apply the formatting
With shp
For i = 1 To shpTextLength
With .TextFrame2.TextRange.Characters(i, 1).Font
If TextAr(i, 1) = "T" Then .Bold = msoTrue Else .Bold = msoFalse
If TextAr(i, 2) = "T" Then .Italic = msoTrue Else .Italic = msoFalse
If TextAr(i, 3) = "T" Then .UnderlineStyle = msoUnderlineSingleLine _
Else .UnderlineStyle = msoNoUnderline
End With
Next i
End With
End Sub
IN ACTION

Convert excel macro from defined range, to operate on selected cells only?

I have a macro that does what I want but it runs on a set range of cells. I have been trying to make it run only on cells that are selected (highlighted) by the user. I've tried various combinations of defining the range using Dim Rng as Range and also Selection method. Me:No VBA experience to speak of, some python experience.
Working code (defined range)
Sub NoHalve()
'
' Macro to remove less-than sign and report only the LOR formatted grey and underlined .
' x = columns, y = rows
For x = 1 To 200
For y = 2 To 3000
If Left(Cells(y, x), 1) = "<" Then
Cells(y, x) = (Right(Cells(y, x), Len(Cells(y, x)) - 1))
Cells(y, x).Select
Selection.Font.ColorIndex = 16
Selection.Font.Underline = xlUnderlineStyleSingle
End If
Next y
Next x
End Sub
This is my attempt to make it run on user selected cells that gives me object required error for the r.Select line:
Sub NoHalve_selection()
Set Rng = Selection
For Each r In Rng
If Left(r, 1) = "<" Then
r = (Right(r, Len(r) - 1))
r.Select
Selection.Font.ColorIndex = 16
Selection.Font.Underline = xlUnderlineStyleSingle
End If
Next
End Sub
You're nearly there
Sub NoHalve_selection()
Dim r As Range, Rng As Range
Set Rng = Selection
For Each r In Rng.Cells ' .Cells is implied in For Each r in Rng
With r 'Using With block is more efficient as it does fewer lookups to Excel
If Left$(.Value, 1) = "<" Then ' .Value uses the With block (so is the same as r.Value). Value is the default property of a Range
.Value = Mid$(.Value, 2)
.Font.ColorIndex = 16
.Font.Underline = xlUnderlineStyleSingle
End If
End With
Next
End Sub
Original post for comparison
Sub NoHalve_selection()
Dim r As Range, Rng As Range
Set Rng = Selection
For Each r In Rng
If Left(r, 1) = "<" Then
r = (Right(r, Len(r) - 1))
r.Font.ColorIndex = 16
r.Font.Underline = xlUnderlineStyleSingle
End If
Next
End Sub
This should be relatively easy by replacing the hard coded numbers with Selection.Columns.Count and Selection.Rows.Count.
Sub NoHalve()
'
' Macro to remove less-than sign and report only the LOR formatted grey and underlined .
' x = columns, y = rows
For x = 1 To Selection.Columns.Count
For y = 2 To Selection.Rows.Count
If Left(Cells(y, x), 1) = "<" Then
Cells(y, x) = (Right(Cells(y, x), Len(Cells(y, x)) - 1))
Cells(y, x).Font.ColorIndex = 16
Cells(y, x).Font.Underline = xlUnderlineStyleSingle
End If
Next y
Next x
End Sub
If you are making a simple text substitution (removing a sign), as long as that sign is left most then I'd also suggest maybe using something that doesn't rely so heavily on the position of the characters in a string. So something like replace:
Cells(y, x) = replace(Cells(y, x),"<","",,1)
Also I don't believe the line Cells(y, x).Select is required and may change the active selection unnecessarily.

Range.Characters object doesn't work as expected in a loop

Below is a program in Excel VBA that creates a progress indicator. I've tried to make the progress indicator as simple as possible, yet it still looks elegant by using Unicode characters: full block and thin space.
Private Sub Play_Click()
Dim iCounter As Long, iRow As Long, nRow As Long, _
Block As String, Progress As Long, iChar As Long
Columns(1).ClearContents
With Cells(2, 4)
.ClearContents
.Font.Color = vbBlue
nRow = 100
For iRow = 1 To nRow
For iCounter = 1 To 100
Cells(iRow, 1) = iCounter
Next
Progress = Int(iRow / 10)
If Progress = iRow / 10 Then
Block = Block & ChrW(9608) & ChrW(8201)
'------------------
'Option statements
'------------------
End If
.Value = Block & " " & iRow & " %"
Next
End With
End Sub
I'd like to have the progress indicator looks like this
where the full blocks are always green-colored and the percentage number is always blue-colored while the program is running. But using these three option statements,
Option 1
.Characters(, 2 * Progress - 1).Font.Color = vbGreen
Option 2
For iChar = 1 To Len(.Value)
If Mid$(Text, iChar, 1) = ChrW(9608) Then
.Characters(iChar, 1).Font.Color = vbGreen
End If
Next
Option 3
GreenBlue 2 * Progress - 1
---------------------
Sub GreenBlue(GreenPart As Integer)
Select Case GreenPart
Case 1 To 19
Cells(2, 4).Characters(, GreenPart).Font.Color = vbGreen
End Select
End Sub
I kept getting the following output
What is the correct way to get the output like the first picture?
Whenever you replace the value of the cell, all of the new content will pick up its formatting from the first character being replaced, so the whole content will be green: need to first set the color back to blue if you want the numeric part to be blue
Private Sub Play_Click()
Dim iCounter As Long, iRow As Long, nRow As Long, _
Block As String, Progress As Long, iChar As Long, x As Long
Columns(1).ClearContents
With Cells(2, 4)
.ClearContents
.Font.Color = vbBlue
nRow = 100
For iRow = 1 To nRow
For iCounter = 1 To 100
Cells(iRow, 1) = iCounter
Next
Progress = Int(iRow / 10)
If Progress = iRow / 10 Then
Block = Block & ChrW(9608) & ChrW(8201)
End If
Application.ScreenUpdating = False 'reduce flashing during update
.Value = Block & " " & iRow & " %"
.Font.Color = vbBlue
If Len(Block) > 0 Then
.Characters(1, InStr(.Value, " ")).Font.Color = vbGreen
End If
Application.ScreenUpdating = True
'add some delay...
For x = 1 To 1000
DoEvents
Next x
Next
End With
End Sub

Having Trouble passing a Cell object? (i could be wrong)

First off thank you very much. Over the last few months (i believe) my coding has progressed drastically. Any and all criticize is always welcome (rip me apart).
Recently I started to try to use different Subs (I dont quite understand when to use functions etc, but i figure it is good structure practice for when i figure it out.
I am hitting a Run-time 424 Error with the following bit of code in Sub ownerCHECK
Sub OccupationNORMALIZATION()
Dim infoBX As String
' initialize variables
LRow = ActiveSheet.UsedRange.Rows.Count
LCol = ActiveSheet.UsedRange.Columns.Count
STATUScounter = LRow
Do While infoBX = ""
infoBX = InputBox("Enter Occupation Column", "Occupation Column")
Loop
restaurCHECK (infoBX)
Application.ScreenUpdating = True
Application.StatusBar = ""
End Sub
-
Sub restaurCHECK(infoBX As String)
Dim RestaurantS(), RestaurantDQs() As Variant
Dim i, LRow, LCol, STATUScounter As Long
Dim rRng As Range
LRow = ActiveSheet.UsedRange.Rows.Count
LCol = ActiveSheet.UsedRange.Columns.Count
STATUScounter = LRow
RestaurantS = Array("estaur", "food", "cafe", "beverage", "waiter", "waitr", _
"waitstaff", "wait staff", "grill") 'array list of target occupations
RestaurantDQs = Array("fast", "pub", "import", "packing", "processing", "packag", _
"retired", "anufact", "distrib") ' disqualifying words for Restaurante category
Set rRng = Range(infoBX & "2:" & infoBX & LRow)
Application.ScreenUpdating = False
For Each cell In rRng
ownerCHECK (cell)
For i = LBound(RestaurantS) To UBound(RestaurantS)
If InStrRev(cell.Value, UCase(RestaurantS(i))) > 0 Then
cell.Offset(, 1) = "Restaurants"
cell.Interior.Color = 52479
End If
Debug.Print cell.Value
Next
For i = LBound(RestaurantDQs) To UBound(RestaurantDQs)
If InStrRev(cell.Value, UCase(RestaurantDQs(i))) And cell.Interior.Color = 52479 Then
cell.Interior.Color = 255
cell.Offset(, 1) = ""
End If
Next
STATUScounter = STATUScounter - 1
Application.StatusBar = "REMAINING ROWS " & STATUScounter & " tristram "
Next cell
End Sub
-
Sub ownerCHECK(str_owner As Range)
Dim owner() As Variant
owner() = Array("owner", "shareholder", "owns ")
For i = LBound(owner) To UBound(owner)
If InStrRev(str_owner, UCase(owner(i))) > 0 Then
cell.Offset(, 2) = "Owner"
End If
Next
End Sub
I can see a couple of issues in ownerCHECK():
"cell" is not defined (unless it's global)
you shouldn't use "cell" as a variable name (internal VBA property)
check validity of incoming range
.
Option Explicit
Sub ownerCHECK(ByRef rngOwner As Range)
If Not rngOwner Is Nothing Then
Dim owner() As Variant
owner() = Array("OWNER", "SHAREHOLDER", "OWNS ")
For i = LBound(owner) To UBound(owner)
If InStrRev(UCase(rngOwner), owner(i)) > 0 Then
rngOwner.Offset(, 2) = "Owner"
End If
Next
End If
End Sub

Resources