First time postings and I'm far from a VBA expert, but I've managed to stumble most of the way to my desired outcome thanks to boards like this one. I'm hoping to automate one last step and am looking for some assistance.
Background:
I run project reports for our Project Managers every Monday morning. The report template queries several tables in our database and populates all of the appropriate fields. It then creates a copy of itself, saves the formulas as values and saves the report using a naming mechanism capturing data from various fields in the report. All of this works great!
The Issue:
At any given time, I have 80-100 active projects. As it stands, I copy the list of projects to a table on the "Parameters" tab. Then, using data validation, I created a dropdown list on the "Report" tab. I then manually go 1 by 1 through the list to generate the report. Each time I change project number in cell B1, the data refreshes and runs the report for the project. I'm using this code to accomplish that:
Private Sub Worksheet_Change(ByVal Target As Range)
'MsgBox Target.Address
If Not Application.Intersect(Range("b1"), Range(Target.Address)) Is Nothing Then
Call AA_RunAll
End If
End Sub
What I'd like to do is create a macro that will run through each one of the projects on my list and run the report. I'm assuming it's a loop function, but I can't seem to get it to work as I want.
One other consideration to note: it takes 3-5 minutes per report to refresh all the data, generate the report and save it. I'd like to set this to run before I leave at night and have it done in the morning.
Thanks in advance.
Aaron
I think this may be what you are looking for.
Dim DataValidationRange As Range
Dim Str As String
Str = Replace(Range("B1").Validation.Formula1, "=", "")
Set DataValidationRange = Range(Str)
Dim i
For Each i In DataValidationRange
Range("B1").Value = i
Call AA_RunAll
Next
Also if you need to wait for the data to update before calling AA_RunAll, you could use this:
Public Function MyTimer(MyDelay As Double)
Dim MyTimerTimer As Double
MyTimerTimer = Timer
MyDelay = MyDelay + Timer
Do While MyTimerTimer <= MyDelay
MyTimerTimer = Timer
DoEvents
Loop
End Function
Private Sub GoThrough_Dropdown()
Dim DataValidationRange As Range
Dim Str As String
Str = Replace(Range("B1").Validation.Formula1, "=", "")
Set DataValidationRange = Range(Str)
Dim i
For Each i In DataValidationRange
Range("B1").Value = i
WaitForDataToUpdate
Call AA_RunAll
Next
End Sub
Private Sub WaitForDataToUpdate()
Dim RangeToWaitFor As Range
Set RangeToWaitFor = Range("H5")
Dim Str As String
Str = RangeToWaitFor.Value
Do While Str = RangeToWaitFor.Value
MyTimer 1
Loop
End Sub
Related
I want to get a list of routines from a VBA project, then run the macros selected by the user.
The image below shows the native "Macros" box. I want to extend this functionality to multiple macros across multiple documents.
I found this link which solves the first part of the problem. Now that I have my list, how do I run a selected routine by name?
Hello and welcome to SO
Below is code sample how to execute VBA macro using code. You need to add some form to select documents and macros for execute. This depends on your implementation.
Sub RunMacroUsingCode()
Dim vbaProjectName As String
vbaProjectName = "InventorVBA"
Dim vbaModuleName As String
vbaModuleName = "m_Tests"
Dim vbaMacroName As String
vbaMacroName = "RunMultipleMacrosTestCall"
Dim vbaProject As InventorVBAProject
For Each vbaProject In ThisApplication.VBAProjects
If vbaProject.name = vbaProjectName Then Exit For
Next
Dim vbaModule As InventorVBAComponent
For Each vbaModule In vbaProject.InventorVBAComponents
If vbaModule.name = vbaModuleName Then Exit For
Next
'Using result is optional
Dim result As Variant
Call vbaModule.InventorVBAMembers(vbaMacroName).Execute(result)
End Sub
Function RunMultipleMacrosTestCall()
Call MsgBox("TEST")
RunMultipleMacrosTestCall = True
End Function
I'm trying to build a dynamic progress bar in Excel, and the way I'm currently doing this is by hard-coding a number of steps into my script as the "total number of steps"... the Application.StatusBar code gets updated with a counter that is divided by this total.
What I'd like to do is to have "TotalSteps" pre-populate with 3 (using the example below) by searching ahead through the code to identify the number of times "Call fnProgress" appears in the subroutine.
Public Sub Example()
TotalSteps = 3 'Enter a value here equal to the number of times "Call fnProgress" appears below
Call ABC
Call fnProgress
Call 123
Call fnProgress
Call DoReMi
Call fnProgress
End Sub
I don't get the impression this is possible in VBA, but I figured someone here would know how to do it if it was! Or perhaps someone could offer a better solution to my simple progress bar... Being able to search ahead in the actual code is also an interesting problem, to me, and I'd imagine it's got further application beyond my status bar.
Thanks so much!
-Julia :)
It is possible, but it's not pretty.
I would consider the solution below bad practice in most cases. Anyway - here it goes:
Sub Test()
Dim StepCount As Integer
Dim TotalSteps As Integer
Steps = Array("Abc", "Def", "Ghi")
TotalSteps = UBound(Steps) + 1
For Each Step In Steps
StepCount = StepCount + 1
ActiveSheet.Evaluate Step & "()+0" '+0 is workaround to handle a bug in VBA. Without it, the method is called twice
Progress StepCount, TotalSteps
Next
End Sub
Sub Progress(StepCount As Integer, TotalSteps As Integer)
Debug.Print StepCount & "(" & TotalSteps & ")"
End Sub
Sub Abc()
Debug.Print "Abc"
End Sub
Sub Def()
Debug.Print "def"
End Sub
Sub Ghi()
Debug.Print "ghi"
End Sub
The base for this is the Evaluate function, that allows you to evaluate expressions in strings. The Test method calls the methods in the Steps array, using Evaluate.
Here's a different approach. Not a good way to write code, but again, there are situations where it is warranted.
Start by adding a reference to Microsoft Visual Basic for Applications Extensibility, then add this code:
Sub Test()
Dim CodeMod As VBIDE.CodeModule
Set CodeMod = ActiveWorkbook.VBProject.VBComponents("Module1").CodeModule
Debug.Print CountOccurrences("Call fnProgress", CodeMod.Lines(1, CodeMod.CountOfLines)) - 1 'Remove 1 to ignore this line
End Sub
Function CountOccurrences(SoughtString As String, InString As String) As Long
CountOccurrences = (Len(InString) - (Len(Replace(InString, SoughtString, "")))) / Len(SoughtString)
End Function
The module name is hardcoded. It reads the code, counts the number of occurences of Call fnProgress and subtracts one (so that the count it self isn't counted). Adjust as needed.
In the aim to allow users from different countries to use my application, I would like to initialize a translation of each object in each existing userform (labels,commandbuttons,msgbox,frames, etc...) at the start of the application.
I'll write all the translation in my Languages sheet:
I've already made a first userform where the user types his login, password and selects his language.
After this step, the main userform called "Menu" will be launched.
I've already tried to type a piece of code (here below) to find the line of code, in a msgbox that I want to run (example : menu.commandbutton1.caption="Envoyer email")
Private Sub UserForm_Initialize()
' Definition of language selected during login
Set langue = Sheets("Languages").Cells.Find("chosen",
lookat:=xlWhole).Offset(-1, 0)
' Initialisation of the texts in the selected language
Dim cel As Range
Dim action As String
For Each cel In Sheets("Languages").Range("d3:d999")
If cel <> "" Then
action = cel & "=" & """" & cel.Offset(0, -2) & """"
MsgBox (action)
End If
Next cel
End Sub
I've already read some topics about this subject but those does not correspond exactly to what i would like to do.
If you have a solution, or a work around, it would be very helpful.
If you simply want different MsgBox, based on a coutry, this is probably the easiest way to achieve it. Imagine your file is like this:
Then something as easy as this would allow you to use different strings, based on the country:
Public Sub TestMe()
Dim country As String
Dim language As Long
country = "Bulgaria" 'or write "England" to see the difference
language = WorksheetFunction.Match(country, Range("A1:B1"), 0)
MsgBox (Cells(2, language))
MsgBox "The capital of " & country & " is " & (Cells(3, language))
End Sub
The idea of the whole trick is simply to pass the correct column, which is done through WorksheetFunction.Match.
Taken from an old CR post I have here, this solution pretty much mimicks .NET .resx resource files, and you can easily see how to extend it to other languages, and if I were to write it today I'd probably use Index+Match lookups instead of that rather inefficient loop - but anyway it works nicely:
Resources standard module
Option Explicit
Public Enum Culture
EN_US = 1033
EN_UK = 2057
EN_CA = 4105
FR_FR = 1036
FR_CA = 3084
End Enum
Private resourceSheet As Worksheet
Public Sub Initialize()
Dim languageCode As String
Select Case Application.LanguageSettings.LanguageID(msoLanguageIDUI)
Case Culture.EN_CA, Culture.EN_UK, Culture.EN_US:
languageCode = "EN"
Case Culture.FR_CA, Culture.FR_FR:
languageCode = "FR"
Case Else:
languageCode = "EN"
End Select
Set resourceSheet = Worksheets("Resources." & languageCode)
End Sub
Public Function GetResourceString(ByVal resourceName As String) As String
Dim resxTable As ListObject
If resourceSheet Is Nothing Then Initialize
Set resxTable = resourceSheet.ListObjects(1)
Dim i As Long
For i = 1 To resxTable.ListRows.Count
Dim lookup As String
lookup = resxTable.Range(i + 1, 1)
If lookup = resourceName Then
GetResourceString = resxTable.Range(i + 1, 2)
Exit Function
End If
Next
End Function
The idea is, similar to .NET .resx files, to have one worksheet per language, named e.g. Resources.EN and Resources.FR.
Each sheet contains a single ListObject / "table", and can (should) be hidden. The columns are basically Key and Value, so your data would look like this on sheet Resources.EN:
Key Value
menu.caption Menu
menu.commandbutton1.caption Send email
menu.commandbutton1.controltiptext Click to send the document
And the Resources.FR sheet would have a similar table, with identical keys and language-specific values.
I'd warmly recommend to use more descriptive names though; e.g. instead of menu.commandbutton1.caption, I'd call it SendMailButtonText, and instead of menu.commandbutton1.controltiptext, I'd call it SendMailButtonTooltip. And if your button is actually named CommandButton1, go ahead and name it SendMailButton - and thank yourself later.
Your code can then "localize" your UI like this:
SendMailButton.Caption = GetResourceString("SendMailButtonText")
The Resources.Initialize procedure takes care of knowing which resource sheet to use, based on Application.LanguageSettings.LanguageID(msoLanguageIDUI) - and falls back to EN, so if a user has an unsupported language, you're still showing something.
I created code to set the pagebreaks in an excel report to deal with the orphan issue (i.e. one line of text spills over onto the next page, etc.). The code works fine when I run it with the report open / visible.
It is part of a larger application which is opened and the code executed from MS Access. Excel is not visible during the process to improve performance.
When I run my code from MS Access it no longer works... it doesn't produce an error, but simply ignores the actual pagebreak setting command.
I read on various forums that in order to avoid this problem, excel needs to be first switched over to ActiveWindow.View = xlPageBreakPreview, but that doesn't work either (I suspect since Excel isn't visible).
I have tested for the following:
Code works when it is started manually or stepped through with F8
Code is executed when called upon from Access (I set breakpoints)
Switching the window view doesn't do anything either
How can I get Excel to change the pagebreaks via code when Excel is run in the background?
This is my code:
Sub TheOrphanProblem()
Dim iPageBrkRow
'Determine if there are page breaks and if so on which row of the document
If FindNthAutoPageBreak(wsRptHolding, 1) Is Nothing Then
'No pagebreak found so we exit the sub
Exit Sub
Else
iPageBrkRow = FindNthAutoPageBreak(wsRptHolding, 1).Row 'Get row
End If
Debug.Print iPageBrkRow
Dim x As Integer
Dim sCase As String
Dim rNewposition As Range
With wsRptHolding
'Code edited out for brevity. This part checks if there is an orphan problem and finds the new position for a pagebreak if needed.
It then provides that position as a range called "rNewposition".
'Moves page break to calculated position
ActiveWindow.View = xlPageBreakPreview
.HPageBreaks.Add rNewposition
ActiveWindow.View = xlNormalView
End With
End Sub
This is the code I use to find the pagebreak positions...
Private Function FindNthAutoPageBreak(Sht As Worksheet, Nth As Long) As Range
'Set page break of the last page so that sub asset groups are kept together
Dim HP As HPageBreak
Dim Ctr As Long
For Each HP In Sht.HPageBreaks
If HP.Type = xlPageBreakAutomatic Then
Ctr = Ctr + 1
If Ctr = Nth Then
Set FindNthAutoPageBreak = HP.Location
End If
End If
Next HP
End Function
Try this
ActiveSheet.DisplayPageBreaks = True
I have the following code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim RR As Range
Dim TestArea As Range
Dim foremenList As Range
Dim workerList As Range
Dim workers As Range
Dim Foremen As Range
Dim i As Integer
Dim R As Range
Dim EmplList() As Variant
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
Set foremenList = Sheet90.Range("V24:V30")
Set RR = Sheet90.Range("AA25:AA46")
i = 0
For Each R In RR.Cells
If Len(R.Value) > 0 Then
EmplList(i) = R.Value
i = i + 1
End If
Next R
Dim ValidStr As String
Set ValidStr = Join(EmplList, ",")
With Sheet90.Range("b26").Validation
.Delete
.Add xlValidateList, xlValidAlertStop, _
xlBetween, "1,2,3"
End With
Sheet90.Range("b40").Value = "Test"
End Sub
But when I press run to test it, it prompts me for a macro name.
Additionally, it does not trigger on Worksheet_Changeany more.
Is this an error (i.e. I forgot a semicolon or something) that consistently triggers Excel VBA to behave like this? If so, what should I look for in the future?
The reason you can't run this one with the Run Sub button is because it requires a parameter. If you want to run this standalone, one possibility is to run it in the Immediate Window so you can manually pass in the parameter. Since this one is expecting a more complex data type (range) you may want to create a small sub to call it so that you can properly create your range and pass that in. Then you can use the Run Sub on this sub which will call your other one.
As far is it not triggering on Worksheet_Change, I am not able to tell what is causing it just from what you posted. However, you do need to make sure that it is located on the code page for the worksheet you are trying to run it from. If you need the same one to run from multiple sheets, you should put it into a module and call it from each sheet's Worksheet_Change method.
You can't press F5 or the run button to run triggered code. You would have to make a change in the sheet where this code is located in order for the code to run. Also, if this code is not located in Sheet90, then you won't see anything happen because this code only makes changes to Sheet90. Lastly, to make sure events are enabled, you can run this bit of code:
Sub ReEnable_Events()
Application.EnableEvents = True
End Sub
Note that you will still have to enable macros.
The problem stems from two lines:
Set ValidStr = Join(EmplList, ",")
was not a valid use of the Set keyword (It's a string and not an object), and
Set TestArea = Sheet90.Range("b4:q8", "b15:q19", "b26:q30")
apparently has too many arguments.
According to Microsoft, it should be a single string argument like:
Set TestArea = Sheet90.Range("b4:q8, b15:q19, b26:q30")
Commenting both of these out made the code run fine both with the run sub button, and on the event.
The "Name Macro" dialog is some kind of error indicator, but I still don't know what it means, other than Code Borked