Excel Add .xlam module inject code into new sheet - vba

I have a module which is creating a sheet. It builds a worksheet with a series of tables.
I'd like to add a feature that uses the OnChange event for a cell to validate that the user entered a decimal. The following code does this If I can just inject into the new worksheet. That's the only thing I can't figure out.
Given 's' is the current Worksheet we've just created is there any way to inject the following code into the sheet code module of 's'?
Private Sub Worksheet_Change(ByVal Target As Range)
Const CELL_ADDRESS = "$R$4:$AQ$500"
If Not Application.Intersect(Target, Range(CELL_ADDRESS)) Is Nothing Then
If Not IsNumeric(Target.Value) Then
MsgBox "Please enter numbers only", vbCritical, "Invalid Entry"
Target.Value = vbNullString
End If
End If
End Sub
EDIT: Showing the chosen solution to the problem (chose Wedge's solution).
(Answer:) We will add a public function to the Addin which we will call from the Template and therefore all sheets created from the template.
Using a template and copying it will allow us to have custom code built into new sheets without having to change security settings.
Calling a public function allows us to make modifications to the sheet without putting the protected password in the sheet's code.
(Public Function Call inside the sheet)
Private Sub Worksheet_Change(ByVal Target As Range)
Dim wb As Workbook
Set wb = ActiveWorkbook
Dim ws As Worksheet
Set ws = wb.ActiveSheet
Application.Run "numberaddin.Validate_Input", wb, ws, Target
End Sub
(Public Function built into Addin which will be called by the sheet when a user modifies data.)
-- All this function does is make sure our cells store only numbers AS NUMBERS and with formatting. Any non-value text becomes a 0 in the cell. This works even if the user copy-pastes the data.
Public Function Validate_Input(wb As Workbook, ws As Worksheet, r As Range)
CELL_ADDRESS = Cells(1, 2).Value ''''we'll use the locked Cell B1 to specify the Validation Range
Dim rCell As Range
Dim eCell As Range
Dim numErr As Boolean
numErr = False
Set rCell = Range(CELL_ADDRESS)
If Not Application.Intersect(rCell, r) Is Nothing Then
ActiveSheet.Protect Password:="pw", UserInterfaceOnly:=True
Application.EnableEvents = False
For Each eCell In rCell.Cells
If Not eCell Is Nothing And eCell.Locked = False And Not Application.Intersect(eCell, r) Is Nothing Then
If IsNumeric(eCell.Value) = False Or IsEmpty(eCell.Value) = True Or eCell.Value <> eCell.Value + "0" Then
If Not IsNumeric(eCell.Value) Then
numErr = True
End If
eCell.Value = Val(eCell.Value)
End If
eCell.Interior.Color = RGB(255, 255, 153)
eCell.NumberFormat = "_(* #,##0_);_(* (#,##0);_(* "" - ""??_);_(#_)"
If eCell.Value > 1000000 Then
eCell.Columns.AutoFit
eCell.ColumnWidth = eCell.ColumnWidth * 1.2
End If
End If
Next eCell
Application.EnableEvents = True
ActiveSheet.Protect Password:="pw", UserInterfaceOnly:=False
End If
If numErr = True Then
MsgBox "Only numbers are allowed here.", vbCritical, "Invalid Entry"
End If
End Function

First of all you must enable the "Trust access to the VBA project object model" setting in the Trust Center.
After that you'll have to write something like this:
Sub AddModule()
Dim Module As VBComponent
Dim ModuleString As String
ModuleString = "Sub Test()" & vbCrLf & _
" Msgbox(""Test"")" & vbCrLf & _
"End Sub"
Set Module = Workbooks(2).VBProject.VBComponents.Add(vbext_ct_StdModule)
Module.CodeModule.AddFromString ModuleString
End Sub
Obviously, you will have change the workbook reference and the ModuleString. Also be careful with the trust change. It is there for a reason.

It's not exactly what you were asking, but I would think that you could just create a hidden "template" sheet with the code you want in it (there is an xlVeryHidden option you can use to keep the template sheet from being unhidden from the UI even). Then instead of creating a new worksheet, you create a copy of that "template sheet", which should copy over the sheet VBA code with it.

Related

Excel VBA Worksheet Change Event assigned

Using Excel 2007, I understand that I can create worksheet_change event on the worksheet it's created.
But how do I assign a global sub change events to a newly created worksheet?
e.g.
Public Sub DataChange(ByVal Target As Range)
' this will check and see if the user or operator has change the column field
' if they fill in "X", mark the whole row to red color
' otherwise leave it black
Dim KeyCells As Range
Dim LastRow As Long
LastRow = Cells(Rows.Count, 1).END(xlUp).Row
Set KeyCells = Range("L2:L" & LastRow)
If Not Application.Intersect(KeyCells, Range(Target.Address)) _
Is Nothing Then
If Target.Value = "X" Or Target.Value = "x" Then
Target.EntireRow.Font.color = vbRed
Else
Target.EntireRow.Font.color = vbBlack
End If
End If
End Sub
Then in a separate sub procedure in Module1...
Public Sub CreateWorkSheet()
Dim ws As Worksheet
Set ws = Sheets.Add
ws.Name = "Test1"
' Here where I want to set the event but I do not know the syntax
' ws.OnChange = DataChange
Debug.Print "Done"
End Sub
I'm used to assign events on the fly when creating controls (C#/WPF/Pascal), so I figured there would be one in Excel world. Any advice or help would be greatly appreciated.
As mentioned by Jeeped, probably the easiest way would be to copy the sheet that already had the Private Sub Worksheet_Change code behind it, but there is also another way, if you place the following code under ThisWorkbook, whenever a new sheet is created it will add the desired code behind it:
Private Sub Workbook_NewSheet(ByVal Sh As Object)
Dim NewSheet As Worksheet
Set NewSheet = Sheets(ActiveSheet.Name)
Code = "Private Sub Worksheet_Change(ByVal Target As Range)" & vbCrLf
Code = Code & "MsgBox ""your code here""" & vbCrLf
Code = Code & "End Sub"
With ThisWorkbook.VBProject.VBComponents(NewSheet.Name).CodeModule
NextLine = .CountOfLines + 1
.InsertLines NextLine, Code
End With
End Sub
The drawback here is that the Trust Settings for Macros would need to be changed by clicking on the Trust access to the VBA project object model:
EDIT:
You could also copy code from one worksheet to another using a similar method:
Sub test()
Dim CodeCopy As VBIDE.CodeModule
Dim CodePaste As VBIDE.CodeModule
Dim numLines As Long
Set CodeCopy = ActiveWorkbook.VBProject.VBComponents("Sheet1").CodeModule
Set CodePaste = ActiveWorkbook.VBProject.VBComponents("Sheet2").CodeModule
numLines = CodeCopy.CountOfLines
'Use this line to erase all code that might already be in sheet2
'If CodePaste.CountOfLines > 1 Then CodePaste.DeleteLines 1, CodePaste.CountOfLines
CodePaste.AddFromString CodeCopy.Lines(1, numLines)
End Sub
I'd go for the last #Jeeped's suggestion
place this code in ThisWorkbook code pane
Option Explicit
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
DataChange Target ' this sub will be called at any change of any worksheet passing the chenged range
End Sub
then place this in the same code pane or in any other Module
Public Sub DataChange(ByVal Target As Range)
' this will check and see if the user or operator has change the column field
' if they fill in "X", mark the whole row to red color
' otherwise leave it black
Dim KeyCells As Range
Set KeyCells = Range("L2:L" & Cells(Rows.Count, 1).End(xlUp).Row)
If Not Application.Intersect(KeyCells, Target) Is Nothing Then Target.EntireRow.Font.color = IIf(UCase(Target.Value2) = "X", vbRed, vbBlack)
End Sub

Subscript out of range - Most computers work fine (mine included)

I am a new user of vba.
There is recently a vba problem that has left me rather clueless and helpless - subscript out of range - on a particular user's computer when every other user seems to have no issue with using the macro (myself included) hence I can't simply trial and error to troubleshoot.
Hence really need expert help from all of you! Really really appreciated!!
I have used a series of vba, which will run one after another and have pasted them in chronological order as follows.
VBA 1
Sub VBA_1()
Dim ws As Worksheet
For Each ws In ActiveWorkbook.Worksheets
ws.Outline.ShowLevels 1, 1
Next ws
End Sub
VBA 2
Sub VBA_2()
Dim ws As Worksheet
For Each ws In ActiveWorkbook.Worksheets
ws.Protect ("Password")
Next ws
End Sub
VBA 3
Sub VBA_3()
Dim iRet As Integer
Dim strPrompt As String
'Prompt
strPrompt = "This will take about 2 minutes. Click 'OK' to proceed."
'Show msgbox
iRet = MsgBox(strPrompt, vbOKCancel)
'When user clicked 'OK'..
If iRet = vbOK Then
'SaveAs Function
Dim fName As String
fName = Application.GetSaveAsFilename(, "Excel Binary Workbook (*.xlsb), *.xlsb")
If fName = "False" Then
MsgBox "File not saved.", vbOKOnly
Cancel = True
End If
Application.EnableEvents = False
ThisWorkbook.SaveAs Filename:=fName, FileFormat:=xlExcel12
Application.EnableEvents = True
' Calculate
Application.Calculate
Application.ScreenUpdating = True
' Outlet
Worksheets("Total Outlets").Activate
'Copy and Paste this portion to each worksheet
For Each cell In Range("A1")
If cell.Value = "Not Applicable" Then
ActiveSheet.Visible = xlSheetHidden
Else
Call HypMenuVRefresh
End If
Next
'End Outlet & Copy and Paste
Worksheets("D11101").Activate
For Each cell In Range("A1")
If cell.Value = "Not Applicable" Then
ActiveSheet.Visible = xlSheetHidden
Else
Call HypMenuVRefresh
End If
Next
Worksheets("D11102").Activate
For Each cell In Range("A1")
If cell.Value = "Not Applicable" Then
ActiveSheet.Visible = xlSheetHidden
Else
Call HypMenuVRefresh
End If
Next
'Hide sheets accordingly
Worksheets("Restaurant List").Visible = xlSheetVeryHidden
Worksheets("Hotel List").Visible = xlSheetVeryHidden
'Recalculate
Application.Calculate
Application.ScreenUpdating = True
'Renaming of tabs
Dim ws As Worksheet
For Each ws In ActiveWorkbook.Worksheets
If ws.Range("A2").Value = 1 Then
If ws.Visible = xlSheetVisible Then
On Error Resume Next
ws.Name = ws.Range("A10").Value
End If
End If
Next ws
'Save Workbook
ActiveWorkbook.Save
'Enable finishing screen to be 'Input'
Sheets("Input").Select
'Show msgbox
MsgBox ("Retrieval Completed!")
Else
MsgBox ("Retrieval of Data Cancelled")
End If
End Sub
I can think of the following possible causes but do not say any of them is the actual cause:
"...on a particular user's computer..."
Then:
the version of Excel/VBA is different;
somehwere a global Option Base is set (but I believe this cannot be set global, i.e. applicable to all workbooks loaded);
somewhere a separator is "hard coded" that does not conform to the Windows global setings (Control Panel --> Region and Language --> Formats --> Additional Settings);
the language differs with a reflection in VBA (e.g. a keyword/function name in the native language or identifier names with non-US ASCII 7 bit characters).
To find in where the program encounters the error (and stops), make a function that writes a status message to a file after every major step. Make sure to close the file after every message so the message is actually written.

How to call a Sub from another Sub dynamically

I have never come across this idea until now, but is it possible to set another macro to a variable within my current macro to call it from the VBA code? I have made the attempt to do so in my limited knowledge and it ain't working.
What I have is a user form that contains a combo box that is a list of reports. Then I have the selected value from that combo box matched to a specific Sub (by title). Each report has its own macro for updating each month.
I have coded the VBA of my first macro (based on a 'Run' button on the user form) to set a variable (iVal) to equal the value within the cell of the worksheet containing the matching Sub title. I was hoping I could use that variable to call the appropriate Sub title based on that value. It doesn't like the variable.
See a screenshot of the error here: https://i.imgsafe.org/a2a199edb8.png
Otherwise, I'm thinking my best option is to use an array loop. I was hoping to avoid that since this list of possible selections from this combo box is almost 50 different possibilities that could expand or diminish over time. Obviously, this would be time consuming and a challenge to manage as the list of reports and matching macros changed.
I don't even know if this is possible. This is a new VBA challenge I have never tackled before so it falls into that 'I don't know what I don't know' territory. Thanks in advance for any constructive feedback.
Private Sub Run_Click()
'Runs the Analysis
Dim ProjectWB As Workbook
Set ProjectWB = ActiveWorkbook
Dim iWS As Worksheet
Dim sName As String
Dim tName As String
Dim iName As String
Set iWS = Worksheets("Streetwise Ideas")
'Error handling for empty file fields
Application.ScreenUpdating = False
Unload Me
If TextBox1.Value = "" Then
MsgBox "Please select a Source file.", vbCritical, "Error No Source file"
If vbOK Then
UserForm1.Show
End If
Else
If TextBox2.Value = "" Then
MsgBox "Please select a Target file.", vbCritical, "Error No Target file"
If vbOK Then
UserForm1.Show
End If
Else
End If
End If
'place value of the selection from the combobox in cell D2 on "Streetwise Ideas" sheet for referencing later in the macro
iWS.Activate
Range("D2").Select
Selection.Value = cbSWIdeas
sName = TextBox1.Value
tName = TextBox2.Value
'Opens Source workbook file and sets it to variable sWB
Dim sWB As Workbook
Set sWB = Workbooks.Open(sName)
'Opens Target workbook file and sets it to variable tWB
Dim tWB As Workbook
Set tWB = Workbooks.Open(tName)
'Calls the correct macro for the combobox selection
Dim iVal As String
iVal = iWS.Range("E2").Value
If iVal <> "" Then
Call iVal
Else
'do nothing
MsgBox ("No Idea Selected.")
Exit Sub
End If
Application.ScreenUpdating = True
End Sub
You can use Application.Run to call a Macro by name
Sub Example()
Dim MacroName As String
MacroName = "HelloWorld"
Application.Run MacroName
End Sub
Sub HelloWorld()
MsgBox "Hello World!"
End Sub

Referencing Most Recently Added Worksheet

I have a userform that fields the user's input to take certain actions within a workbook, one of the actions is inserting a new tab in the workbook and having the user input the new sheet's name within an input box. I want to be able to then reference this new sheet (but I won't know what someone else might name it) and to paste a chart object within the newly created sheet.
So far the adding sheet code is working fine, but any of my attempts to paste the chart range are not working. My current code for adding the worksheet is:
Private Sub MyChart_Click()
Dim Answer As String
Dim sht_name As Variant
On Error Resume Next
If Me.OptionButton2.Value = True Then
Unload Me
sht_name = InputBox("Please enter value")
If sht_name <> "" Then
Worksheets.Add(After:=Worksheets(Worksheets.Count)).Name = sht_name
Else
Exit Sub
End Sub
My chart lives in another worksheet ("Sheet2") and I am trying to just copy it into the newly created sheet whenever the user selects this OptionButton2 in the Userform... Any help is appreciated.
When you use the Worksheets.Add method, that sheet automatically is activated. To test this you can run this small portion of code:
Option Explicit
Private Sub SheetReference()
Worksheets.Add(After:=Worksheets(Worksheets.Count)).Name = "Test"
Debug.Print ActiveSheet.Name
End Sub
And the output you would see is
Test
So in your case, you could declare a worksheet variable and then set the reference after you call the add method. Something like this:
Option Explicit
Private Sub MyChart_Click()
Dim Answer As String
Dim sht_name As Variant
Dim ws As Worksheet
On Error Resume Next
If Me.OptionButton2.Value = True Then
Unload Me
sht_name = InputBox("Please enter value")
If sht_name <> "" Then
Worksheets.Add(After:=Worksheets(Worksheets.Count)).Name = sht_name
Set ws = ActiveSheet
With ws
'Do whatever you need to do on the worksheet
End With
Else
Exit Sub
End If
End Sub

how to make visual basic work on protected worksheets (no password on protection)

I have a work book with several worksheets that I would like to protect. I am not using a password on the protection. I have some visual basic code associated with this sheet to expand the row width on merged cells. The code will not work when the sheets are protected.
I did find some guidance on adding unprotect code to my code, but can't figure out where to put it and how to address the fact that there is no passord. Further guidance woudl be greatly appreciated!
Here is my code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim NewRwHt As Single
Dim cWdth As Single, MrgeWdth As Single
Dim c As Range, cc As Range
Dim ma As Range
With Target
If .MergeCells And .WrapText Then
Set c = Target.Cells(1, 1)
cWdth = c.ColumnWidth
Set ma = c.MergeArea
For Each cc In ma.Cells
MrgeWdth = MrgeWdth + cc.ColumnWidth
Next
Application.ScreenUpdating = False
ma.MergeCells = False
c.ColumnWidth = MrgeWdth
c.EntireRow.AutoFit
NewRwHt = c.RowHeight
c.ColumnWidth = cWdth
ma.MergeCells = True
ma.RowHeight = NewRwHt
cWdth = 0: MrgeWdth = 0
Application.ScreenUpdating = True
End If
End With
End Sub
You could probably do something like this:
Surround your code with .Unprotect and .Protect
Sub protectSheet()
Dim ws As Worksheet
Set ws = Sheets(1)
With ws
.Unprotect "password"
'Insert Code Here
.Protect "password"
End With
End Sub
try this:
Private Sub Workbook_Open()
Dim wSheet As Worksheet
For Each wSheet In Worksheets
wSheet.Protect Password:="Password_here", _
UserInterFaceOnly:=True
Next wSheet
End Sub
Put this code in 'ThisWorkbook' then use the Workbook_Open Event.
This code protects all the WS everytime you open the WB
but allows macro to run due to UserInterfaceOnly set to true
You need to protect the sheet with password.
If you want a user to edit some cells even if the worksheet is protected then set the locked property of those cells to false before protecting the sheet.
Now when Worksheet_Change is triggered or any procedure is called which is trying to make some changes to excel range (locked cells = true) then you need to Unprotect the Sheet at beginning of the code and protect it at the end again. You may refer #sobin answer for syntax.
Also you may use error handlers and explicitly protect the sheet. This is done to avoid situation wherein the sheet is unprotected and then there is error which comes up for any reason then that would leave the sheets unprotected.