Excel VBA Worksheet Change Event assigned - vba

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

Related

Excel Add .xlam module inject code into new sheet

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.

How can I create/copy a worksheet while naming it based on the value of a cell in a specific column but variable row?

Essentially I'm creating a tracking sheet which will have a cell on it that, when clicked, will create a new excel sheet in the same workbook. For testing purposes I'm currently just having it create a new sheet, but eventually I'll have a sheet that it'll copy. What I need help with is, how do I get VB to pull a cell value to use as the name of the new/copied sheet? Here's the scenario:
Each row will have a Client column (which is Column C) which I want to use for the names of the workbooks that will be created. I'm trying to have a cell (say column R in that row) that when clicked creates a new worksheet and pulls in the value of column C in that row as the worksheet's name.
So, say Row 5 has "Test Client" in C5. When R5 is clicked, I want it to create a sheet that is named "Test Client". I've seen solutions that use loops to go through the column and create a sheet for each, but that wouldn't really work for my scenario as I'd need them to be created on the fly and not always for each row.
I know how to create the sheets in vb but my issue is getting the name. Is there a way to get vba to pull the name from column C for the row in which it was activated? So if it was activated for Row 5, it pulls C5, if it was Row 10, it pulls C10 etc.
Any suggestions would be greatly appreciated, I'm currently using this to create the sheets:
Sub CreateSheet()
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets.Add(After:= _
ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
End Sub
and this to call:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Row > 5 And Target.Column = 18 And Target.Count = 1 Then Call CreateSheet
End Sub
The code below reads the value in Column C for the relevant row, and then passes it as a String to your Function:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Row > 5 And Target.Column = 18 And Target.Count = 1 Then
Dim ShtName As String
ShtName = Cells(Target.Row, "C").Value
Call CreateSheet(ShtName)
End If
End Sub
This is your function, I've added a String that is passed representing the worksheet name:
Public Sub CreateSheet(ws_Name As String)
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets.Add(After:= _
ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
ws.Name = ws_Name
End Sub
Update: As Shai Rado pointed out I was missing an error handler.
You should test to see if the worksheet exists first. This pattern will make it easier to debug and add functionality to your code.
Worksheet Module
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim ws As Worksheet
Dim WorksheetName As String
If Target.Row > 5 And Target.Column = 18 And Target.Count = 1 Then
WorksheetName = Cells(Target.Row, "C").Value
Set ws = getWorkSheet(WorksheetName)
If Not ws Is Nothing Then Set ws = getNewWorkSheet(WorksheetName)
End If
End Sub
Standard Module
Function getWorkSheet(WorksheetName As String, Optional WorkbookName As String) As Worksheet
If Len(WorkbookName) = 0 Then WorkbookName = ThisWorkbook.Name
With Workbooks(WorkbookName)
On Error Resume Next
Set getWorkSheet = .Worksheets(WorksheetName)
On Error GoTo 0
End With
End Function
Function getNewWorkSheet(WorksheetName As String, Optional WorkbookName As String) As Worksheet
Dim ws As Worksheet
If Len(WorkbookName) = 0 Then WorkbookName = ThisWorkbook.Name
With Workbooks(WorkbookName)
Set ws = .Worksheets.Add(After:=.Worksheets(.Worksheets.Count))
On Error Resume Next
ws.Name = WorksheetName
If Err.Number = 0 Then
Set getNewWorkSheet = ws
Else
ws.Delete
End If
On Error GoTo 0
End With
End Function

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

Dynamic Combo box values

Problem:
I have a user-form with a comboBox, textBox and button, the items of comboBox are the cells value in range ((A1:A10) for example).
If I enter a new text in comboBox which isn't in the range, I need to add this value to the range, and write it in the textBox, and if it is already exist I want to write it in textBox directly.
I tried to do it but I didn't succeed.
Can anyone help?
Code:
Private Sub UserForm_Initialize()
'cmbx.RowSource = "d2:d100"
Dim cLoc As Range
Dim ws As Worksheet
Set ws = Worksheets("LookupLists")
For Each cLoc In ws.Range("LocationList")
cmbx.AddItem cLoc.Value
Next cLoc
End Sub
If I have understood you correctly then I guess this is what you are tying to do?
For this, please ensure that in design mode, you set the ComboBoxes's .Style property to 0-fmStyleDropDownCombo. This will ensure that you can type in the combobox. :) I have also commented the code so that you will not have a problem understanding the code. But if you still do then simply post back.
My Assumptions: There is nothing below Cell A10
Code:
Dim ws As Worksheet
Dim cLoc As Range
'~~> Prepare your form
Private Sub UserForm_Initialize()
Set ws = ThisWorkbook.Sheets("LookupLists")
For Each cLoc In ws.Range("LocationList")
cmbx.AddItem cLoc.Value
Next cLoc
End Sub
'~~> This will do what you want
Private Sub cmbx_AfterUpdate()
Dim lRow As Long
'~~> Check if the value is in the range
'~~> If not then add it to the range and textbox as well
If Not IFEXISTS(cmbx.Value) Then
lRow = ws.Range("A" & ws.Rows.Count).End(xlUp).Row + 1
ws.Range("A" & lRow).Value = cmbx.Value
'~~> Delete the Named range so that we can re-create
'~~> it to include the new value
ThisWorkbook.Names("LocationList").Delete
ThisWorkbook.Names.Add Name:="LocationList", RefersToR1C1:= _
"=LookupLists!R1C1:R" & lRow & "C1"
End If
'~~> Add to textbox
TextBox1.Text = cmbx.Value
End Sub
'~~> function to check if the value is in the textbox or not
Function IFEXISTS(cmbVal As String) As Boolean
For Each cLoc In ws.Range("LocationList")
If UCase(Trim(cLoc.Value)) = UCase(Trim(cmbVal)) Then
IFEXISTS = True
Exit For
End If
Next cLoc
End Function

macro that auto-executes when sheet is opened

Is it possible that my macro (update () ) auto-executes everytime the excel file is opened. The code below doesn't work well. Thanks
Private Sub Workbook_Open()
Run "update"
End Sub
Option Explicit
Sub update()
Dim rng As Range
Dim Sh As String, Cl As String
Dim ws As Worksheet
Dim i As Integer, ncol As Integer
Dim Row1 As String
ncol = Range("B1:O1").Columns.Count
For i = 1 To ncol
Set ws = ThisWorkbook.Sheets("sheet1")
With ws
Row1 = .Cells(1, i).Value
If Len(Row1) > 0 Then
Sh = Split(Row1, "'!")(0)
Cl = Split(Row1, "'!")(1)
Set rng = ThisWorkbook.Sheets(Sh).Range(Cl)
'Here you were always refering to cell A2 not moving through the values which was the main problem.
rng.Value = .Cells(2, i).Value
End If
End With
Next i
End Sub
As mentioned in the comments. Move the following:
Private Sub Workbook_Open()
Run "update"
End Sub
To here:
As mentioned by Siddharth there is another way to get a macro to run on the file open event and that is to simply to give it the following signature:
Sub Auto_Open
Also, personally I'd probably not call a sub-routine just "update" as it is quite close to lots of reserved words - I'd go for something like "updateSomething". This is just personal choice.