I managed to write a code from different threads and code examples from around the web. It is trial and error and lots of copy-pasting.
I have several ranges defined within my subs:
Define range names:
X.Sheets("Sheet1").Range("B4").Name = "Type1"
X.Sheets("Sheet1").Range("B9").Name = "SubTotal1"
X.Sheets("Sheet1").Range("A6:F8").Name = "Data1"
X.Sheets("Sheet1").Range("B11").Name = "Type2"
X.Sheets("Sheet1").Range("B16").Name = "SubTotal2"
X.Sheets("Sheet1").Range("A13:F15").Name = "Data2"
X.Sheets("Sheet1").Range("B18").Name = "Type3"
X.Sheets("Sheet1").Range("B23").Name = "SubTotal3"
X.Sheets("Sheet1").Range("A20:F22").Name = "Data3"
Y.Sheets("Sheet1").Range("A4:A6").Name = "Period"
Y.Sheets("Sheet1").Range("B4:B6").Name = "Name"
Y.Sheets("Sheet1").Range("D4:D6").Name = "Code"
Y.Sheets("Sheet1").Range("E4:E6").Name = "Type"
Y.Sheets("Sheet1").Range("F4:K4").Name = "Data"
This name range is used in every sub (I have around 15, with around 165 more needed) for copying and inserting information from Workbook X to Workbook Y.
Since it is redundant to reuse the code, I would like to put these Ranges in a separate Sub and call on it in each new Sub.
I would also like to do the same with the following code, which refers to the ranges defined above:
'Insert Type1 Data from X:
If X.Sheets("Sheet").Range("SubTotal1").Value > 0 Then
Range("Type1").Copy
Y.Sheets("Sheet1").Range("Type").Insert xlShiftDown
Range("Data1").Copy
Y.Sheets("Sheet1").Range("Data").Insert xlShiftDown
'Insert Period:
X.Sheets("Sheet1").Range("C3").Copy
Y.Sheets("Sheet1").Range("Period").Insert xlShiftDown
'Insert Name:
X.Sheets("Sheet1").Range("C12").Copy
Y.Sheets("Sheet1").Range("Name").Insert xlShiftDown
'Insert Code Type:
X.Sheets("Sheet1").Range("C10").Copy
Y.Sheets("Sheet1").Range("Code").Insert xlShiftDown
End If
This code, and 6 more like it (Type 1-6) are also redundant in other Subs, so ideally, I would put it in a separate sub and call on it when necessary too. I use this at the beginning of my subs to define X and Y sheets:
Dim X As Workbook
Dim Y As Workbook
'Define workbooks:
Set X = Workbooks.Open("C:\Users\user\Folder\File.xlsx")
Set Y = ThisWorkbook
EDIT: To give a better example of what I mean, I imagine Subs going something like this:
Sub Sub1
Call Sub "RangeNames"
Call Sub "Insert Type1 Data while referring to RangeNames"
Call Sub "Insert Type2 Data while referring to RangeNames"
End Sub
And/Or
Sub Sub2
Call Sub "RangeNames"
Call Sub "If RangeName 'SubTotal 3' > 0 then Insert Type3 Data while referring to RangeNames"
End Sub
EDIT 2:
For #SJR:
Sub Sub1
Dim X As Workbook
Dim Y As Workbook
Set X = Workbooks.Open("C:\Users\user\Folder\File.xlsx")
Set Y = ThisWorkbook
X.Sheets("Sheet1").Range("B4").Name = "Type1"
X.Sheets("Sheet1").Range("B9").Name = "SubTotal1"
Y.Sheets("Sheet1").Range("E4:E6").Name = "Type"
Sub2
End Sub
Sub 2 is:
Sub Sub2
If X.Sheets("Sheet").Range("SubTotal1").Value > 0 Then <- ERROR HAPPENS HERE
Range("Type1").Copy
Y.Sheets("Sheet1").Range("Type").Insert xlShiftDown
End If
End Sub
What you need are arguments (aka parameters).
e.g.
Sub CopyAndInsertStuff(sourceLocation as String, destinationLocation as String)
Set wbSrc = Workbooks(sourceLocation)
Set wbDst = Workbooks(destinationLocation)
'Do your copying and inserting logic here...
End Sub
Then call that function by:
Call CopyAndInsertStuff("C:\path\to\source\File.xlsx", "C:\path\to\destination\File.xlsx")
If you are looking at adding another 165 subs, may I suggest to have a look at loops and/or arrays?
It might take you over all about the same time to develop it (considering the learning curve), but the code will be about 150 times shorter (do everything in 1-2-3 subs), and much easier to maintain. This, and in conjunction with the suggested parameters to call similar functionality from other subs or functions, would be a lot more efficient.
Here are the first results from Google when it comes to loops and arrays, and after a quick look, they do cover the basic needs:
Loops: https://www.excel-easy.com/vba/loop.html
Arrays: https://www.excel-easy.com/vba/array.html
Final advice, keep in mind that the less you interact with the workbooks from VBA, the faster your macros will run. ie: load your full range in an array, perform the transforming you want, then put it back in the workbook - you are only accessing the workbook 2 times as needed. If on the other hand, you use vba to copy cell A to cell B, few tens/hundreds of thousands times... it will be slower.
Related
I have a main macro (macro1) running smaller macros (macroA and macroB) in other *.xlam or *.xslm files .
I need, besides the little macros (A+B) doing their job, specific variables and their values in the "outer" macro (macro1) for further use in its code.
Is there a way of passing variable values from within the macros A+B to macro1?
Background:
I know that declaring a Public Variable can be used in different macros. I also know how to pass variables or their values from one macro to a following one. But (given my example below) how can I give a variable "back" to macro1?
Example (1) of passing variables in between Subs without calling other workbooks/files (working):
Public wbA as Workbook
Public wbB as Workbook
Sub MySubRoutine()
Set wbA = Workbooks.Open("C:\file.xlsx")
Set wbB = Workbooks.Open("C:\file2.xlsx")
OtherSubRoutine
End Sub
Sub OtherSubRoutine()
Debug.Print wbA.Name
End Sub
OUTPUT: file.xlsx
Example (2) of passing variables from called Sub to another from other workbook/file (non-working):
Public count As Integer
Sub macro1()
Run file.xlam! & macroB, argument1, argument2
Debug.Print count 'second print
End Sub
Sub macroB(argument1, argument2)
'code that does something
count = 5
Debug.Print count 'first print
End Sub
OUTPUT first print: 5
OUTPUT second print: 0
Code finishes without errors or debug messages. Where is my mistake?
You should transform the Sub in a function (able to return something) and call it in a little different way:
Put the next function in workbook 1 (for exemplification, I used 'PERSONAL.XLSB'):
Function GiveMeFive(x As Long, y As Long) As Long
Debug.Print x + y 'not important, ONLY TO SEE IT WORKING IN immediate Window
GiveMeFive = 5
End Function
Call it from another workbook in this way:
Sub testFunctionWithArgsOtherWb()
Dim x As Long
x = Run("PERSONAL.XLSB!" & "GiveMeFive", 4, 3)
Debug.Print x
End Sub
It will return in Immediate Window: 7 (from the function itself) and 5, what the function returned when it was called...
Second version:
If you don't like functions, you can return a Public variable value using a Sub, in this way:
Create a Public variable in another workbook (also used Personal.xlsb), but in ThisWorkbook code module:
Public MyVar As Long
Create such a Sub in a standard module:
Sub MyMacro(x As Long, y As Long)
ThisWorkbook.MyVar = x * y
End Sub
Call the Sub from a different workbook and read the global variable, modified by the called Sub:
Sub testValFromOtherWB()
Dim x As Long
Run "PERSONAL.XLSB!" & "MyMacro", 4, 3
x = Workbooks("PERSONAL.XLSB").MyVar
Debug.Print x
End Sub
Please, test them and send some feedback.
I am a complete novice, this is my first VBA code (necessity is mother of . . . inept coding by novice).
Problem: Why is my code not updating in real-time? Or in any time at all? Can it be fixed? Do I need to somehow put all 16 sheets worth of VBA code into a "module" or do some other trick to fix it?
Background:
I have VBA code "behind" multiple "client" spreadsheets in a workbook. The code allows cell colors to transfer to a master "all clients" spreadsheet. The reason I needed the VBA code was that there was a function (and INDEX function) already in the color-filled cells.
The code was not working properly, so I figured out that the references were wrong and edited one of the sheets' VBA code to ensure I had the references right. They were correct. But even getting those edited references in that one sheet's code to work correctly took a bunch of clicking around and saving and reopening the document.
I then needed to fix the code in all the other sheets, starting with one of them. I can't for the life of me get anything to happen even though I made the correct edit. I should have seen colors change, but nothing happened.
Google search led me to the news that just putting code "behind" spreadsheets often doesn't work. One reference said I should place it in a module. But I have no idea how to do that across all of my 16 client sheets.
I'm also working over Remote Desktop which is probably not helping. I could probably send myself the workbook if needed.
Below is my code (one sheet's worth). The references are different across sheets so that the various client's data (in vertical columns) populates on the correct horizontal rows of the master sheet. Along with that data are the colors that this VBA code is supposed to help render onto the master sheet.
This is the "Glen" spreadsheet's VBA code, Glen's data that needs to be color coded identically on the "WeeklyRatingsAllClients" sheet (ending up in the BD6:CH6 range and BD7:CH7 range) is in the Q4:Q38 range and the U4:U38 range. The other sheets are the exact same except that in the next person's sheet the BD6:CH6 range and BD7:CH7 ranges will update to become BD8:CH8 range and BD9:CH9 and so on sequentially (next client is 10, 11; next is 12, 13 etc.).
If it matters to anyone, I got the original code here and modified it for my needs: https://www.extendoffice.com/documents/excel/4071-excel-link-cell-color-to-another-cell.html
Also, I make a long comment on above page under "Sara" dated 3 months ago that describes more about the code/purpose and shows how I modified the example code for my purpose and it worked--it's just not working now (probably not useful if you already know this stuff well, like I don't).
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim xRg As Range
Dim xCRg As Range
Dim xStrAddress As String
xStrAddress = "WeeklyRatingsAllClients!$BD$6:$CH$6"
Set xRg = Application.Range(xStrAddress)
Set xCRg = Me.Range("$Q$4:$Q$38")
On Error Resume Next
For xFNum = 1 To xRg.Count
xRg.Item(xFNum).Interior.Color = xCRg.Item(xFNum).Interior.Color
Next
xStrAddress = "WeeklyRatingsAllClients!$BD$7:$CH$7"
Set xRg = Application.Range(xStrAddress)
Set xCRg = Me.Range("$U$4:$U$38")
On Error Resume Next
For xFNum = 1 To xRg.Count
xRg.Item(xFNum).Interior.Color = xCRg.Item(xFNum).Interior.Color
Next
End Sub
Perhaps use the Workbook.SheetSelectionChange event, something like the following. Note that this can definitely be refactored.
Make sure to add this code in the ThisWorkbook module.
Change "Bob", "Fred", "Joe" to the sheet names in question (in order), and add more Cases as needed, always increasing the offsetNum by 2 from the previous Case.
There's a mismatch in the number of cells on the main sheet vs the client sheet. U4:U38 would be 35 cells, but BD6:CH6 is only 31... more an FYI.
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
Dim offsetNum As Long
Select Case Sh.Name
Case "Glen"
offsetNum = 0
Case "Bob"
offsetNum = 2
Case "Fred"
offsetNum = 4
Case "Joe"
offsetNum = 6
Case Else
Exit Sub
End Select
Dim allClientsSheet As Worksheet
Set allClientsSheet = Me.Worksheets("WeeklyRatingsAllClients")
Dim mainColorRange As Range
Set mainColorRange = allClientsSheet.Range("BD6:CH6").offset(offsetNum)
Dim sourceColorRange As Range
Set sourceColorRange = Sh.Range("Q4:Q38")
Dim i As Long
For i = 1 To mainColorRange.Rows(1).Cells.Count
mainColorRange.Rows(1).Cells(i).Interior.Color = sourceColorRange.Cells(i).Interior.Color
Next
Set sourceColorRange = Sh.Range("U4:U38")
For i = 1 To mainColorRange.Rows(2).Cells.Count
mainColorRange.Rows(2).Cells(i).Interior.Color = sourceColorRange.Cells(i).Interior.Color
Next
End Sub
I encounter a weird problem that I believe is related to Excel behavior, rather than to my code.
I have a global variable named "numEtape", which is an integer. My code consists in several steps where the user has to type data on a sheet, then press a button which saves the data in an array and increments the "numEtape", before going to the next step.
The code (simplified) looks like this :
Dim numEtape As Integer
Sub AjoutEssai()
numEtape = 2
UFPreAjoutInfos.Show 'Unrelated Userform that asks user for more informations, but doesn't modify "numEtape" or call any other macro
Call InterfaceFiller
End Sub
Sub InterfaceFiller()
Dim rangedubtn As Range
Dim btnPrecedent As Button
Select Case numEtape
Case 2
'Change value of some cells
Case 3
'Change value of some cells
Case 4
'Change value of some cells
Case Is >= 5
'Change value of some cells
Case Else
Debug.Print "Error"
End Select
Set rangedubtn = Sheets("Interface").Range("M3")
Set btnPrecedent = Sheets("Interface").Buttons.Add(rangedubtn.Left, rangedubtn.Top,rangedubtn.Width, rangedubtn.Height)
With btnPrecedent
.OnAction = "mSuivant"
.Caption = "Suivant"
.Name = "btnSuivant"
End With
End Sub
Sub mSuivant()
numEtape = numEtape + 1
Call InterfaceFiller
End Sub
I don't think the code itself is important, what I can expect from it, since I first call AjoutEssai(), is for numEtape to always be greater than 2.
However, when during the steps the user opens and close other excel/office files (that don't have any vba code/macros in it), excel seems to empty numEtape, which makes the Select Case go to the Case Else.
When does excel remove global variables from memory, and is there a way to prevent this behavior from happening?
Public numEtape As Long
A viable option is to use the word public like public numEtape As Long.
Then the variable will be saving its value for as long as the Excel workbook is opened. In older versions of VBA the word was Global (What is the difference between Dim, Global, Public, and Private as Modular Field Access Modifiers?)
Dim numEtape As Long
For using Dim outside of Sub or Function, the variable will be emptied, after the code is over. Take this snippet only:
Dim numEtape As Long
Sub MainTest()
numEtape = 23
End Sub
Once you run it and you hit End Sub the variable would be emptied as well. Check after running the MainTest():
Sub PrintingVariable()
Debug.Print numEtape
End Sub
If you want to save the value, there are 2 basic ways that work:
Write the value in an excel cell
Write the value in a database
Here is the vba I currently have. I am needing it to delete all data in any worksheet that starts with "Demand". The code is running but no data is being deleted. Any help is appreciated!!
Option Explicit
Sub ClearExcelContent()
Dim DemandData As Worksheet
For Each DemandData In ActiveWorkbook.Worksheets
If LCase(Left(DemandData.Name, 6)) = "Demand" Then
DemandData.Rows("2:" & Rows.Count).ClearContents
End If
Next DemandData
MsgBox "All Demand Data has been Deleted from Consolidation Tab"
End Sub
If LCase(Left(DemandData.Name, 6)) = "Demand" Then
The above statement will not match any worksheet, because the LHS is all lowercase and the RHS has an uppercase D. Replace Demand with lowercase demand.
If LCase(Left(DemandData.Name, 6)) = "demand" Then
These are 3 basic functions for edition of strings:
LCase, UCase, and WorksheetFunction.Proper
This is what they would return:
Demand Is Diamand
demand is diamand
DEMAND IS DIAMAND
if you run them like this:
Public Sub TestMe()
Dim strText As String
strText = "demaNd is dIamand"
Debug.Print WorksheetFunction.Proper(strText)
Debug.Print LCase(strText)
Debug.Print UCase(strText)
End Sub
In order to compare the result of the functions, you should be comparing with similar string. Probably the most sure thing is to put the same string functions on both parts of the comparison. Like this:
LCase(Left(DemandData.name, 6)) = LCase("demand") Then
In general, if you want to make sure that Vit=VIT then consider adding Option Compare Text on top of the module. Then this would be true:
Option Compare Text
Sub TestMe()
Debug.Print "Vit" = "VIT"
End Sub
I am creating a calendar where it pulls events from a user inputsheet and places a text box object on another worksheet (Dates across the top and different departments down the left). It currently seperates the events on the top row of each section (i.e. all HR events on top row of HR section). I then run a MACRO to check for overlapping objects and move them down to the next row.
The code I use to move objects is below:
Sub MoveShapes()
'This Macro moves overlapping shapes down to the next row
Dim wb As Workbook
Set wb = ActiveWorkbook
Dim sh As Worksheet
Set sh = wb.ActiveSheet
Dim s1 As Shape
Dim s2 As Shape
Dim CheckOverlap As Boolean
Worksheets("SRTC").Activate
For i = 1 To sh.Shapes.count
If i <= sh.Shapes.count Then
Set s1 = sh.Shapes(i)
Search:
CheckOverlap = False
For Each s2 In Worksheets("SRTC").Shapes
If s2.ID = s1.ID Then GoTo Suit
If s2.Left <= (s1.Left + s1.Width) And s2.Left >= s1.Left _
And s2.Top <= (s1.Top + s1.Height) And s2.Top >= s1.Top Then
s1.Top = s1.Top + 18 ' 32
CheckOverlap = True
Exit For
End If
Suit:
Next
If CheckOverlap = True Then GoTo Search
End If
Next
End Sub
(I found this code in a different forum) This code works but is extremely slow. It is comparing each textbox with all the text boxes on the worksheet. My worksheet has over 3000 shapes and the MACRO takes over 4 hours to run.
Is there a way to write this code to only move objects within certain ranges? (ie only HR section)
Thanks
The first thing would be to use application.screenupdating.
Also some variable declarations are not or badly done (i, Sh).
Don't use Goto. (i may use a do while or do until loop)
Why test on each loop If i <= sh.Shapes.count Thenwich obviously is the case?
You can avoid If s2.ID = s1.ID Then GoTo Suit by not using a for each (wich tests shapes already corrected also), but for j=i+ 1 to sh.shapes.count : set S2=sh.shapes(j) ....`
little reminder : on long IF tests , with several conditions, VBA will test all the conditions before continuing, so instead of testing 4 conditions, test only the 2 more important and then test the two others, for example.
Beware, comments, buttons, and many other stuff is also a shape, so you might need a test of the shape's type (on s1). Avoid unessaceray looping.
On a personal note, i'd use a dictionary and a class type, the whole thing would take max 5 secondes to loop, and no i won't write that code for you.
Your approach is more at your level and is good enough with a bit of code optimization using the hints i gave.