Store range values in a VBA object - vba

I wrote a procedure which multiplies values present in a specified range (A1:B2) by a value passed as a parameter (the value written in the cell D1).
Now, how can I do in order to run again the same macro with a different parameter that will operate on the original values? I'm pretty sure I need some global variable which will tell my procedure that the macro has been run at least one time (I found how to do it) but I don't know how/where to store the original values.
For example, I have
Range("A1").Value = 1
Range("A2").Value = 1
Range("B1").Value = 1
Range("B2").Value = 1
Range("D1").Value = 2
Click the macro button and get the values in the range Range("A1:B2") as the following
Range("A1").Value = 2
Range("A2").Value = 2
Range("B1").Value = 2
Range("B2").Value = 2
Now, after manually editing the Range("D1").Value from 2 to 3 and clicking again the macro button i want my procedure return the following values
Range("A1").Value = 3
Range("A2").Value = 3
Range("B1").Value = 3
Range("B2").Value = 3
The minimalistic procedure could be structure as this
dim rng as Range
dim MyRange as Range
set MyRange = Range("A1:B2")
For each rng in MyRange
rng.value = rng.value*Range("D1").Value
next rng
I hope this is a good example. Thanks
Francesco

If I understood well, what you want is a way to track everytime you called your sub with the button and use this data within this very sub.
One way would be to create a public variable in a module (I usually create a variable module in my projects) and update this data in the sub you call with your button.
'Your public variable inside a module (VariablesModule)
Public param() As Variant
'Your general code
Call procedure(VariablesModule.param)
'Inside your sub
Sub procedure(byVal paramVal as Variant)
'(your code)
ReDim param(0 To n) As Variant
Set param(1) = ABCDEFG
End sub
A second way would be to simply store this data in a range and use/update it within your sub.

Related

What is the best way to automate copy and paste specific ranges in excel?

I am very new to VBA and there is a task I would like to automate and don't know where to start. I have a data set that looks like below.
Sample Data
What I'm trying to do is loop through column A and if it has something in it (will always be an email) select all rows until there is something in column A again. Copy and paste into new tab. So row 2-5 would copy and paste into a new tab. Then row 6-9 into a different new tab. Also row 1 would copy to each tab as well. I haven't been able to find code to help with this specific need and any help would be greatly appreciated.
I found this code and started modifying it but, it's nowhere close to what I need or working for that matter.
Sub split()
Dim rng As Range
Dim row As Range
Set rng = Range("A:A")
For Each row In rng
'test if cell is empty
If row.Value <> "" Then
'write to adjacent cell
row.Select
row.Copy
Worksheets("Sheet2").Activate
Range("A2").Select
row.PasteSpecial
Worksheets("Sheet1").Activate
End If
Next
End Sub
This code should provide what you need:
Sub Split()
Dim wb As Workbook
Set wb = ThisWorkbook
Dim ws As Worksheet
Set ws = wb.Worksheets(1) 'change sheet index or use Worksheets("Sheet1") method to use exact name
Dim rngBegin As Range
Dim rngEnd As Range
With ws
Dim rngHeader As Range
Set rngHeader = .Range("A1:H1") 'to copy headers over each time
Dim lRowFinal As Long
lRowFinal = .Range("C" & .Rows.Count).End(xlUp).Row 'assumes eventually last row of needed data will have an address1
Set rngEnd = .Range("A1") ' to begin loop
Set rngBegin = rngEnd.End(xlDown) 'to begin loop
Do
Set rngEnd = rngBegin.End(xlDown).Offset(-1)
Dim wsNew As Worksheet
Set wsNew = Worksheets.Add(After:=wb.Sheets(.Index))'always after current sheet, change as needed
.Range(.Cells(rngBegin.Row, 1), .Cells(rngEnd.Row, 8)).Copy wsNew.Range("A2")
wsNew.Range("A1:H1").Value = rngHeader.Value
Set rngBegin = rngEnd.End(xlDown)
Loop Until rngBegin.Row >= lRowFinal
End With
End Sub
Try to break your process into steps and determine rules on how to proceed. Then write out some pseudo-code (code like logic) to make sure it all makes sense.
You need some sort of loop, since you are going to treat each
group of rows in the same way.
You need some code that determines what cells are contained in each block
Code to take a block (given by step 2) and paste it into a new tab.
Your Pseudo Code might look like this:
' This is the main function that runs the whole routine
Sub Main()
Set headerRg = GetHeaderRg()
Do Until IsAtTheEnd(startRow) = True
Set oneBlock = GetNextBlock(startRow)
Call ProcessBlock(oneBlock)
startRow = startRow + oneBlock.Rows.Count
Loop
End Sub
' This function returns the header range to insert into the top
Function GetHeaderRg() As Range
' Write some code here that returns the header range
End Function
' This function determines whether we are at the end of our data
Function IsAtTheEnd(current_row as Long) as Boolean
' Write some code here that determines whether we have hit the end of our data
'(probably checks the first column to see if there is data)
End Function
' This function takes the startRow of a block and returns the whole block of Rows
Function GetNextBlock(startRow) As Range
' Write some code that returns the whole range you want to copy
End Function
' This sub takes a range to be processed and a header to print and prints
' it into a new tab
Sub ProcessBlock(BlockRg As Range, headerRg as Range)
Set targetSheet = thisWorkbook.Sheets.Add()
' Write some code that pastes the headerRg and BlockRg where you want it
End Sub
If you start to have more specific questions about syntax etc, we will be happy to help here!

VBA public variables

I am having trouble with the public i as intger portion of my code.
I am using i to keep the value of my current row so i can use this range across
my program. In my for loop it increments i so it will step through a column and search for v
however when i try using "i" in another set of code "i" no longer has a value.
I am not sure how global/public variables work in VBA or what is cause this error.
the problem occurs int Sub "yes" , and sub "no"
at the code
Cells(i,lcol).value=" ok "
and
Cells(i,lcol).value = " updated "
1st set of code is as follows, which gets my value for "i"
Public V As Integer
Public i As Integer
Private Sub Enter_Click()
Dim EmptyRow As Long
'Audit will only search Master Sheet
Worksheets("Master").Activate
'Find empty row value so we can use that for limit of search
With Sheets("Master")
EmptyRow = .Range("A" & Rows.Count).End(xlUp).Row + 1
End With
'i needs to be set to minimum limit
'Begin loop of search
For i = 12 To EmptyRow + 1
If Cells(i, 1).Value = V Then 'AssetNum.Value Then
'Go to compare userform to display
Compare.AssetDisplay.Value = AssetNum.Value
Compare.LocationDisply.Value = Cells(i - 1, 2).Value
Compare.Show
End If
Next i
'If i gets to emptyrow num then go to non found asset userform
Unload Me
NonFoundAsset.Show
End Sub
Private Sub UserForm_Initialize()
'Read in value from asset num to be comapre in loop
AssetNum.Value = V
End Sub
the second set of code im trying to call "i" using the public variable and it has no value
Private Sub No_Click()
Dim ws As Worksheet
Dim lcol As Long
'Make Master Sheet Active
Worksheets("Master").Activate
Set ws = ThisWorkbook.Sheets("Master")
'Finds next empty column
With ws
lcol = .Cells(11, .Columns.Count).End(xlToLeft).Column - 1
End With
'If the displayed location is not the same as the actual location "No" will be
'selected and the Change User Form will be displayed
'The value under the current audit column will be displayed as updated
Cells(i, lcol).Value = " Updated "
Unload Me
AuditChange.Show
End Sub
Private Sub Yes_Click()
Dim ws As Worksheet
Dim lcol As Long
'Make Master Sheet Active
Worksheets("Master").Activate
Set ws = ThisWorkbook.Sheets("Master")
'Finds next empty column
With ws
lcol = .Cells(11, .Columns.Count).End(xlToLeft).Column - 1
End With
'If the location displayed is correct "Yes" will be selected and
'The value "ok" will be displayed in the current audit column
Cells(i, lcol).Value = " Ok "
Unload Me
'Returns to Assetlookup to look for a new asset
Assetlookup.Show
End Sub
I appreciate any help, Im new to VBA and don't understand why this is not working.
I believe a public variabe in a UserForm is only available if the UserForm is running (loaded). To have a truely global variable, declare it in a normal module.
Probably the variable isn't available and VB can't find it in its scope. If Tools, Options, Require variable declarations is turned OFF, VB will create a new variable with that name in the current scope. Hence it looks as if it has "lost" its value.
Tip: don't call global variables something like i, j, n etc. These are typically used as local variables for loops and counters. Use a naming convention that makes clear the variable is global. I always prefix such a variable with g for 'global', e.g.:
Public giLoopCounter As Integer;
It depends where you declare it. You have to refer to that location. So if i is in UserForm1 and you are trying to use it from another form, reference it as.
Cells(UserForm1.i,lcol).value=" ok "
If you put
Option explicit
at the top of the form you are trying to call it from it would tell you that i by itself is not defined in that scope of you code.
EDIT: For additional comments from OP. Asked if i can be public in a click event.
To my knowledge, you can't have public/global variables in an event.
You will have to use a variable local
'Public variables are declared outside (above) all subs and functions
'This will be accessible by all subs functions and events in in the forms or sheets module or wherever it is
Public i As Integer
'This will be accessible by all subs functions and events in in the CURRENT sheet or form. It is private but to the current item
Private i As Integer
Private Sub CommandButton1_Click()
Dim j As count
'Do whatever it is to get that value.
j = 5
'You can access i to use it in you click event code
msgbox i * j
'Or you can set it in the event
i = j
End Sub

Detect change from nested formulas

I have a very complex workbook with many tabs. The tabs may have either normal data or formulas in various cells. In the case of formulas, the formulas may be nested from one sheet to the next (i.e. a formula on sheet1 refers to a formula on sheet2 which in turn refers to a formula on sheet3, etc.).
I have a hidden tab that contains the following: source sheet, source range, target sheet, and target range.
A named range has been created over these 4 fields and all applicable rows.
When we wish to save data to the database, we loop through every row in the range mapping and copy the data from the source sheet/range to the target sheet/range. After this, the applicable data is serialized into XML and sent to a web service to be saved.
The problem that we wish to resolve is that we want to mark a cell on a hidden sheet when a change is made by the user to a source range. Since formulas can be nested, the Worksheet_Change event does not pick up the change.
Since a change on one sheet may affect another sheet that is not the active sheet, the Workbook_SheetChange event does not catch the change either.
Is there any way form me to catch when a sheet defined in the mapping is changed, even if it is the result of a formula change several levels deep?
Edit
Thank you for your responses. I was attempting to find the fastest and least process intensive way to determine if data changes within a monitored range. The data may consist of actual data or of nested formulas.
My research showed that I could not actually achieve this result by taking range intersections as I could not detect if the data within a monitored range was modified. This is due to the fact that the monitored range may not be on the active sheet and also may contain formulas.
I have shown the method used to actually detect a change below. If there is any feedback on a better way to achieve the same result, I would appreciated it.
Worksheet_Change event will not work if a cell value is changed by a formula, you need Worksheet_Calculate.
Check out my example workbook here.
And Here for the WebPage of example codes
There is no "easy" way to detect if a nested formula has changed when the formula being monitored is not on the active sheet. While my hope was to detect the modified range and use an intersection of ranges to set a flag, this was not possible because the Worksheet_Change event does not work on formulas and the Workbook_SheetChange event only works on the active sheet. Since my workbooks have over 20+ tabs and 20 - 30 ranges being monitored, this approach does not work. This approach was desired for speed purposes.
Instead, the workbook will need to "check" to see if the current values are the same as the last time the save to database event was called. If not, a dirty flag will be set.
The code for this approach is provided below.
An example of the mapping range is shown in the picture below though in practice there are 20-30 rows comprising this range.
There are three other sheets where Sheet3 contains actual data in A1:H1 and Sheet2 has formulas pointing to Sheet3. Sheet1 has formulas pointing to Sheet2.
As the mapping range indicates, we are looking at a range on Sheet1, even though changes may be made to Sheet3.
The code used is as provided below.
Option Explicit
Public Sub DetermineIfEditOccurred()
Dim oMappingRange As Range
Dim szSourceTab As String
Dim szSourceRange As String
Dim oSourceRange As Range
Dim szTargetTab As String
Dim szTargetRange As String
Dim oTargetRange As Range
Dim oWorksheetSource As Worksheet
Dim oWorksheetTarget As Worksheet
Dim oRangeIntersection As Range
Dim nRowCounter As Long
Dim nCellCounter As Long
Dim szSourceValue As String
Dim szTargetValue As String
Dim oCell As Range
Dim bIsDirty As Boolean
If Range(ThisWorkbook.Names("DirtyFlag")).Value = 0 Then
Set oMappingRange = Range(ThisWorkbook.Names("Mapping"))
For nRowCounter = 1 To oMappingRange.Rows.Count
szSourceTab = oMappingRange(nRowCounter, 1)
szSourceRange = oMappingRange(nRowCounter, 2)
szTargetTab = oMappingRange(nRowCounter, 3)
szTargetRange = oMappingRange(nRowCounter, 4)
Set oWorksheetSource = ThisWorkbook.Worksheets(szSourceTab)
Set oWorksheetTarget = ThisWorkbook.Worksheets(szTargetTab)
Set oSourceRange = oWorksheetSource.Range(szSourceRange)
Set oTargetRange = oWorksheetTarget.Range(szTargetRange)
nCellCounter = 1
For Each oCell In oSourceRange.Cells
szSourceValue = oCell.Value
If szSourceValue = "#NULL!" Or _
szSourceValue = "#DIV/0!" Or _
szSourceValue = "#VALUE!" Or _
szSourceValue = "#REF!" Or _
szSourceValue = "#NAME?" Or _
szSourceValue = "#NUM!" Or _
szSourceValue = "#N/A" Then
szSourceValue = ""
End If
szTargetValue = GetCellValueByPosition(oTargetRange, nCellCounter)
If szSourceValue <> szTargetValue Then
Range(ThisWorkbook.Names("DirtyFlag")).Value = 1
bIsDirty = True
Exit For
End If
nCellCounter = nCellCounter + 1
Next
If bIsDirty Then
Exit For
End If
Next
End If
End Sub
Public Function GetCellValueByPosition(oRange As Range, nPosition As Long) As String
Dim oCell As Range
Dim nCounter As Long
Dim szValue As String
nCounter = 1
For Each oCell In oRange
If nCounter = nPosition Then
szValue = oCell.Value
Exit For
End If
nCounter = nCounter + 1
Next
GetCellValueByPosition = szValue
End Function
The Workbook_SheetChange event is as follows:
Option Explicit
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Call DetermineIfEditOccurred
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
If Sh.Name <> "MAPPING" Then
Call DetermineIfEditOccurred
End If
End Sub

Use VBA functions and variables in a spreadsheet

I'm new to Excel VBA. I am trying to use a VBA function I found online that enables the user to use goalseek on multiple cells at a time. How do I call the function in a spreadsheet and how do I point to the cells that are supposed to be associated with the variables in the function (e.g. Taddr, Aaddr, gval). Do I have to write the cell values and ranges in the code itself and just run it that way?
Maybe I should redefine the function so that it takes these variables as input, so I can write a formula like =GSeekA(Taddr,Aaddr,gval)
Option Explicit
Sub GSeekA()
Dim ARange As Range, TRange As Range, Aaddr As String, Taddr As String, NumEq As Long, i As Long, j As Long
Dim TSheet As String, ASheet As String, NumRows As Long, NumCols As Long
Dim GVal As Double, Acell As Range, TCell As Range, Orient As String
' Create the following names in the back-solver worksheet:
' Taddr - Cell with the address of the target range
' Aaddr - Cell with the address of the range to be adjusted
' gval - the "goal" value
' To reference ranges on different sheets also add:
' TSheet - Cell with the sheet name of the target range
' ASheet - Cell with the sheet name of the range to be adjusted
Aaddr = Range("aaddr").Value
Taddr = Range("taddr").Value
On Error GoTo NoSheetNames
ASheet = Range("asheet").Value
TSheet = Range("tsheet").Value
NoSheetNames:
On Error GoTo ExitSub
If ASheet = Empty Or TSheet = Empty Then
Set ARange = Range(Aaddr)
Set TRange = Range(Taddr)
Else
Set ARange = Worksheets(ASheet).Range(Aaddr)
Set TRange = Worksheets(TSheet).Range(Taddr)
End If
NumRows = ARange.Rows.Count
NumCols = ARange.Columns.Count
GVal = Range("gval").Value
For j = 1 To NumCols
For i = 1 To NumRows
TRange.Cells(i, j).GoalSeek Goal:=GVal, ChangingCell:=ARange.Cells(i, j)
Next i
Next j
ExitSub:
End Sub
GSeekA is a Subprocedure, not a Function. Subprocedures cannot be called from worksheet cells like Functions can. And you don't want to convert GSeekA into a function. Functions should be used to return values to the cell(s) from which they're called. They shouldn't (and often can't) change other things on the sheet.
You need to run GSeekA as a sub. Now the problem becomes how you get user provided information into the sub. You can use InputBox to prompt the user to enter one piece of information. If you have too many, InputBox becomes cumbersome.
You can create areas in the spreadsheet where the user must enter information, then read from that area. That's how it's set up now. It's reading cells named asheet and tsheet. As long as those named ranges are present, the code works.
Finally, you can create a UserForm that the user will fill out. That's like putting a bunch of InputBoxes on one form.
Update Here's a simple procedure that you can start with and enhance.
Public Sub GSeekA()
Dim rAdjust As Range
Dim rTarget As Range
Dim dGoal As Double
Dim i As Long
'Set these three lines to what you want
Set rAdjust = Sheet1.Range("I2:I322")
Set rTarget = Sheet1.Range("J2:J322")
dGoal = 12.1
For i = 1 To rAdjust.Count
rTarget.Cells(i).GoalSeek dGoal, rAdjust.Cells(i)
Next i
End Sub

Does there exist a VBA command which does not change the formula of a cell, but its value?

Suppose in a worksheet the formula of R4 cell is =B1+B2, and its current value is 10.
A VBA command Range("R4").Value = 5 will change both its formula and its value to 5.
Does anyone know if there exists a VBA command which changes the value of R4 to 5, but does not change its formula, such that its formula is still =B1+B2?
PS: we can also achieve the same state in another way: 1) do a Range("R4").Value = 5 2) change the formula of R4 to =B1+B2 but without evaluating it. In this case, does there exist a VBA command which change the formula of a cell without evaluating it?
Edit: What I want to do is...
I would like to write a function, which takes a worksheet where some cells may be out of date (the formula does not match its value), and generates automatically a VBA Sub, this VBA Sub can reproduce this worksheet. The VBA Sub may look like:
Sub Initiate()
Cells(2,3).Value = 5
Cells(4,5).Value = 10
...
Cells(2,3).Formula = "=2+3"
Cells(4,5).Formula = "=C2+C2"
...
End Sub
Such that running Initiate() builds one worksheet with same values and formulas.
Without the VBA command I am asking, this Initiate() will be hard to generated.
You cannot change the value of a cell to something different than what the cell formula computes to.
Regarding your p.s.: You can probably change the formula of a cell without re-evaluation by changing the calculation mode to manual. But that would of course apply to the entire workbook, not just this one cell
EDIT: maybe a solution would be to temporarily save the formula of the cell in either a tag of that cell, or a hidden worksheet?
It is quite simple to change the result of a formula without changing the formula itself:
Change the value of of its argument(s). This is a Solver-type approach:
Sub ForceDesiredResult()
Dim r As Range
Set r = Range("B2")
With r
If r.HasFormula Then
.Formula = .Formula & "-5"
Else
.Value = .Value - 5
End If
End With
End Sub
Here is some very dirty code that will save all values of all formulas on the active sheet as custom properties of the sheet, and a 2nd sub that will mark red all cells where the value has changed from it's original value, while preserving all formulas. It will need some error-checking routines (property already exists, property doesn't exist,...) but should give you something to work with. Since I don't really understand your problem it's a bit hard to say ;)
Sub AddCustomProperty()
Dim mysheet As Worksheet
Dim mycell2 As Range
Dim myProperty As CustomProperty
Set mysheet = ActiveWorkbook.ActiveSheet
For Each objcell In mysheet.UsedRange.Cells
Debug.Print objcell.Address
If objcell.HasFormula Then Set myProperty = mysheet.CustomProperties.Add(objcell.Address, objcell.Value)
Next objcell
End Sub
Sub CompareTags()
Dim mysheet As Worksheet
Dim mycell2 As Range
Dim myProperty As CustomProperty
Set mysheet = ActiveWorkbook.ActiveSheet
For Each objcell In mysheet.UsedRange.Cells
Debug.Print objcell.Address
If objcell.HasFormula Then
On Error Resume Next
If mysheet.CustomProperties(objcell.Address).Value <> objcell.Value Then
objcell.Font.ColorIndex = 3
On Error GoTo 0
End If
End If
Next objcell
End Sub