I've been working on some Excel VBA code with the hope of making it expandable, maintainable and easy to read. Some variables in the code require unique hardcoded ranges while others store common/repeated information. I moved the common/repeat code to a global module to reduce the number of times it needs to be defined with the same information, but I'm concerned it will make it too difficult to track the code or leave the code open to errors later by having public variables. Is this a good way to go about coding this or is there a more efficient/user friendly way to make it OOP?
Snippet below.
Module: mCommon
Option Explicit
Public wrkshtInput As Object
Public rngPartSize As Range
Public rngPart2Size As Range
Sub CommonDefinitions()
Set wrkshtInput = Worksheets("INPUT (BOM)")
Set rngPartSize = Range("C5")
Set rngPartSize2 = Range("C6")
End Sub
Module: mUI
Option Explicit
Sub PartToggle()
'OBJECT REF(S): Sheet2 (INPUT (BOM))
'METHOD REF(S): mCommon.CommonDefinitions
'VARIABLE REF(S): mCommon.wrkshtInput, mCommon.rngPartSize
'COMMON VARIABLE DEFINITIONS:
Call mCommon.CommonDefinitions
'DEFINE VARIABLES:
Set rngBlueACM = Range("H129:H134, H136:H141, H144:H147")
Set rngRedACM = Range("H149:H154, H156:H161, H164:H167")
'PART TOGGLE: ON
If mCommon.wrkshtInput.tglbtnPartToggle.Value = True Then
mCommon.rngPartSize.Value = ""
rngBlueACM.Value = "MANUAL"
rngRedACM.Value = "MANUAL"
End If
'PART TOGGLE: OFF
If mCommon.wrkshtInput.tglbtnPartToggle.Value = False Then
mCommon.rngPartSize.Value = "--"
rngBlueACM.Value = "--"
rngRedACM.Value = "--"
End If
End Sub
Sub Part2Toggle()
'OBJECT REF(S): Sheet2 (INPUT (BOM))
'METHOD REF(S): mCommon.CommonDefinitions
'VARIABLE REF(S): mCommon.wrkshtInput, mCommon.rngPart2Size
'COMMON VARIABLE DEFINITIONS:
Call mCommon.CommonDefinitions
'DEFINE VARIABLES
Set rngWhiteACM = Range("H107:H108")
'PART2 TOGGLE: ON
If mCommon.wrkshtInput.tglbtnPart2Toggle.Value = True Then
mCommon.rngPart2Size.Value = ""
rngWhiteACM.Value = "MANUAL"
End If
'PART2 TOGGLE: OFF
If mCommon.wrkshtInput.tglbtnPart2Toggle.Value = False Then
mCommon.rngPart2Size.Value = "--"
rngWhiteACM.Value = "--"
End If
End Sub
This
Set rngPartSize = Range("C5")
has a flaw in that it doesn't have a worksheet qualifier, so will return a range from whatever happens to be the active sheet.
Your "common" code should also not need to be initialized any time you want to use a part of it - that's a little awkward to have those calls spread over your code.
If you want to be "object-oriented" then a better approach (IMO) is to:
First: Change the codename(s) of your worksheet(s) instead of constantly needing to run things like:
Set wsData = ThisWorkbook.Worksheets("Data")
you can just refer directly to Data, Lists etc in your code.
Next: use the worksheet code modules to define methods which "belong" to those sheets. A good example of this would be an "Inputs" sheet where various parameters are entered. So say you have "Account Number" you can put this in your worksheet module for Inputs:
Const RNG_ACCT_NUM As string = "D10"
Property Get Accountnumber()
Accountnumber = Me.Range(RNG_ACCT_NUM).Value
End Property
Property Let Accountnumber(v)
Me.Range(RNG_ACCT_NUM).Value = v
End Property
Now you can use this in your code:
Dim acct
acct = Inputs.AccountNumber
'or
Inputs.AccountNumber = "AB3456"
instead of
Dim wsInputs As Worksheet, acct
Set wsInputs = ThisWorkbook.Worksheets("Inputs")
acct = wsInputs.Range("D10")
'....
wsInputs.Range("D10") = "AB3456"
Related
I have a table with the following values:
Now, I would like to call the Userform in column H based on the value in column G, but I can't work out how to call the Userform based on the cell value. The error occurs in line
form.Name = wsControls.Cells(loop2, 8).Value
Here is my code:
Sub Check_Scenarios()
Dim wsAbsatz As Worksheet
Dim wsControls As Worksheet
Dim wsData As Worksheet
Dim loop1 As Long
Dim loop2 As Long
Dim lngKW As Long
Dim form As UserForm
Set wsAbsatz = ThisWorkbook.Worksheets("Production")
Set wsData = ThisWorkbook.Worksheets("Data")
Set wsControls = ThisWorkbook.Worksheets("Controls")
lngKW = wsControls.Cells(1, 2).Value + 2
If lngKW = 3 Then
Exit Sub
End If
For loop1 = wsControls.Cells(10, 2).Value To wsControls.Cells(19, 2).Value Step 10
If wsData.Cells(loop1 + 3, lngKW).Value <> "" Then
MsgBox (wsData.Cells(loop1 + 3, lngKW).Value)
For loop2 = 2 To 16
If wsData.Cells(loop1 + 3, lngKW).Value = wsControls.Cells(loop2, 7).Value Then
form.Name = wsControls.Cells(loop2, 8).Value 'error occurs here
form.Show
End If
Next loop2
End If
Next loop1
End Sub
Project:
Many thanks for your help!
You are trying to assign a Name to a blueprint. These are two errors.
You have to initialize your blueprint as something. Like this:
Dim form As New UserForm
Then, most probably your UserForm does not have a property called Name. It is called Caption. Thus it is like this:
Sub TestMe()
Dim uf As New UserForm1 'judging from your screenshot
uf.Caption = "Testing"
uf.Show
End Sub
Disclaimer:
There is a better way to work with UserForms, not abusing the blueprint, although almost every VBA book shows this UserForm.Show method (in fact every single one I have read so far).
If you have the time and the OOP knowledge implement the ideas from here - or from my interpretation of the ideas. There was also a documentation article about it in StackOverflow, but it was deleted with the whole documentation idea.
You don't "call" a userform. You instantiate it, and then you Show it.
UserForm is the "base class" from which all userforms are derived. See there is inheritance in VBA, only not with custom classes.
So you have a UserForm2 class, a UserForm3 class, a UserForm4 class, and so on.
These classes need to be instantiated before they can be used.
Dim theForm As UserForm
Set theForm = New UserForm2
theForm.Show
Set theForm = New UserForm3
theForm.Show
'...
So what you need is a way to parameterize this Set theForm = New ????? part.
And you can't. Because whatever you're going to do, the contents of a cell is going to be a string, and there's no way you can get an instance of a UserForm3 out of a String that says "UserForm3".
Make a factory function that does the translation:
Public Function CreateForm(ByVal formName As String) As UserForm
Select Case formName
Case "UserForm1"
Set CreateForm = New UserForm1
Case "UserForm2"
Set CreateForm = New UserForm2
Case "UserForm3"
Set CreateForm = New UserForm3
'...
End Select
End Function
And then call that function to get your form object:
Set form = CreateForm(wsControls.Cells(loop2, 8).Value)
If Not form Is Nothing Then form.Show
This is the beginning of my code:
Private FilesPath As String
Private CostCentersPath As String
Private FinalPath As String
Private CurrentName As String
Private CostCenters As Worksheet
Private Final As Workbook
Private Template As Worksheet
Sub ReadySetGo()
FilesPath = "O:\MAP\04_Operational Finance\Accruals\Accruals_Swiss_booked\2017\Month End\10_2017\Advertising\automation\" 'path change ("automation")
CostCentersPath = FilesPath & "CostCenters.xlsx"
CurrentName = InputBox("Please adjust the final file name:", , "ABGR_2017-10_FINAL.xls.xlsx")
FinalPath = FilesPath & CurrentName
Set CostCenters = Workbooks("CostCenters.xlsx").Worksheets("Cost Center Overview")
Set Final = Workbooks(CurrentName)
Set Template = Workbooks("Template.xlsm").Worksheets("Template")
End Sub
Sub ReadySetGo is used only to assign values to variables and is called from within other subs in the module. But obviously with this method I get input box popping up every time the sub is called.
Is there any other way, apart from Workbook.Open event, of passing variable's CurrentName value to other subs in the module, to avoid multiple InputBoxes?
Thanks,
Bartek
In general, there are plenty of good ways to do what you want, depending on the way your application is settled.
Probably the easiest is simply to write:
If CurrentName <> vbNullString Then
InputBox("Please adjust the final file name:", , "ABGR_2017-10_FINAL.xls.xlsx")
End if
But be careful, because it may break your code somewhere. However, in the best case you will only run it once.
Another way is to save the CurrentName value in a given range and to read it from there every time:
If Len(Range("A1")) = 0 Then
Range("A1") = InputBox("Please ...")
End if
CurrentName = Range("A1")
In general, you can go a few steps further and introduce the Singleton pattern to your code https://en.wikipedia.org/wiki/Singleton_pattern, thus making sure that CurrentName is only assigned once. However, it may be a bit overkill in this case - How to create common/shared instance in vba
Great answer by Vityata , I would declare the CurrentName as public variable however If I had to do it in the same setup then
Static CurrentName As String
Static HasName As Boolean
If Not HasName Then
CurrentName = InputBox("Please adjust the final file name:", , "ABGR_2017-10_FINAL.xls.xlsx")
HasName = True
End If
I have function testFunc that takes Range as an argument, loops thorugh its cells and edits them. I have sub testSub that tests two cases: first is when I pass the current workbook's range to testFunc through: 1. variable Range 2. just as an argument testFunct(...Range("A1:A16")).
The second case is when I open another workbook and do the same - pass one of its worksheets range to the testFunc directly or via variable.
Function testFunc(aCol As Range)
Dim i As Integer
i = 0
For Each cell In aCol
MsgBox (cell.Value)
cell.Value = i
i = i + 1
Next
testFunc = i
End Function
Sub testSub()
Dim origWorkbook As Workbook
Set origWorkbook = ActiveWorkbook
Dim aRange As Range
Set aRange = ThisWorkbook.Sheets("sheet 1").Range("A1:A16")
Dim dataWorkbook As Workbook
Set dataWorkbook = Application.Workbooks.Open("Y:\vba\test_reserves\test_data\0503317-3_FO_001-2582480.XLS")
Dim incomesNamesRange As Range
Set incomesNamesRange = dataWorkbook.Worksheets("sheet 1").Range("A1:A20")
' origWorkbook.Worksheets("sheet 1").Cells(1, 5) = testFunc(dataWorkbook.Sheets("1").Range("A1:A20"))
origWorkbook.Worksheets("Ëèñò2").Cells(1, 50) = testFunc(incomesNamesRange)
' testFunc aRange
'origWorkbook.Worksheets("sheet 1").Cells(1, 5) = testFunc(aRange) '<---good
' origWorkbook.Worksheets("sheet 1").Cells(1, 5) = testFunc(ThisWorkbook.Sheets("sheet 1").Range("A1:A16"))
End Sub
The problem is, as I indicated with comments, when I open a foreign workbook and pass its range through a variable it gives an error variable not definedFor Each cell In aCol`, while all other cases (including passing range variable of the current workbook) work fine.
I need testFunc to stay a Function, because it's a simplfied example of some code from bigger program which needs to take a returned value from a function. And I need to store that Range in a variable too, to minimize time of the execution.
EDIT: As pointed out in the comments, I replaced Cells(1,5) with origWorkbook.Worksheet("shee t1").Cells(1,5) and fixed variable name, but the original mistake changed to variable not defined. I edited the title and body of the question.
The default name for the worksheet shouldn't have a space in it.
Set incomesNamesRange = dataWorkbook.Worksheets("sheet 1").Range("A1:A20")
Change this to:
Set incomesNamesRange = dataWorkbook.Worksheets("Sheet1").Range("A1:A20")
And see if that fixes it.
The following code allows me to go through the workbook and worksheets that have macros:
For Each VBCmp In ActiveWorkbook.VBProject.VBComponents
Msgbox VBCmp.Name
Msgbox VBcmp.Type
Next VBCmp
As this page shows, for a workbook and a sheet, their type are both 100, ie, vbext_ct_Document. But I still want to distinguish them: I want to know which VBCmp is about a workbook, which one is about a worksheet.
Note that VBCmp.Name can be changed, they are not necessarily always ThisWorkbook or Sheet1, so it is not a reliable information for what I am after.
Does anyone know if there exists a property about that?
Worksheet objects and Workbook objects both have a CodeName property which will match the VBCmp.Name property, so you can compare the two for a match.
Sub Tester()
Dim vbcmp
For Each vbcmp In ActiveWorkbook.VBProject.VBComponents
Debug.Print vbcmp.Name, vbcmp.Type, _
IIf(vbcmp.Name = ActiveWorkbook.CodeName, "Workbook", "")
Next vbcmp
End Sub
This is the Function I'm using to deal with exported code (VBComponent's method) where I add a preffix to the name of the resulting file. I'm working on an application that will rewrite, among other statements, API Declares, from 32 to 64 bits. I'm planning to abandon XL 32 bits definitely. After exportation I know from where did the codes came from, so I'll rewrite them and put back on the Workbook.
Function fnGetDocumentTypePreffix(ByRef oVBComp As VBIDE.VBComponent) As String
'ALeXceL#Gmail.com
Dim strWB_Date1904 As String
Dim strWS_EnableCalculation As String
Dim strChrt_PlotBy As String
Dim strFRM_Cycle As String
On Error Resume Next
strWB_Date1904 = oVBComp.Properties("Date1904")
strWS_EnableCalculation = oVBComp.Properties("EnableCalculation")
strChrt_PlotBy = oVBComp.Properties("PlotBy")
strFRM_Cycle = oVBComp.Properties("Cycle")
If strWB_Date1904 <> "" Then
fnGetDocumentTypePreffix = "WB_"
ElseIf strWS_EnableCalculation <> "" Then
fnGetDocumentTypePreffix = "WS_"
ElseIf strChrt_PlotBy <> "" Then
fnGetDocumentTypePreffix = "CH_"
ElseIf strFRM_Cycle <> "" Then
fnGetDocumentTypePreffix = "FR_"
Else
Stop 'This isn't expected to happen...
End If
End Function
I have this sub/macro that works if I run it as BeforeRightClick. However, I would like to change it so I can actually use my rightclick and put the macro on a button instead.
So I have tried to change the name from BeforeRightClick.
I have tried with both a normal form button and an ActiveX.
All this + some more code is posted under Sheet1 and not modules
Dim tabA As Variant, tabM As Variant
Dim adrA As String, adrM As String
' Set columns (MDS tabel) where data should be copied to (APFtabel)
'Post to
'P1-6 divisions ' Name adress, etc
Const APFtabel = "P1;P2;P3;P4;P5;P6;E9;E10;E13;E14;E23;N9;N10;N11;N12;N20"
'Load data from
Const MDStabel = "N;O;P;Q;R;S;H;Y;Z;AB;W;AF;T;D;AA;V;"
Dim APF As Workbook
' APFilNavn is the name of the AP form
Const APFilNavn = "APForm_macro_pdf - test.xlsm"
' Const APFsti As String = ActiveWorkbook.Path
Const APFarkNavn = "Disposition of new supplier"
' APsti is the path of the folder
Dim sysXls As Object, APFSti As String
Dim ræk As Integer
Private Sub CommandButton1_Click()
APFormRun
End Sub
' Here I changed it from BeforeRightClick
Private Sub APFormRun(ByVal Target As Range, Cancel As Boolean)
Dim cc As Object
If Target.Column = 8 Then
APFSti = ActiveWorkbook.Path & "\"
If Target.Address <> "" Then
For Each cc In Selection.Rows
Cancel = True
ræk = cc.Row
Set sysXls = ActiveWorkbook
åbnAPF
overførData
opretFiler
APF.Save
APF.Close
Set APF = Nothing
Set sysXls = Nothing
Next cc
End If
End If
End Sub
Private Sub overførData()
Dim ix As Integer
tabA = Split(APFtabel, ";")
tabM = Split(MDStabel, ";")
Application.ScreenUpdating = False
For ix = 0 To UBound(tabM) - 1
If Trim(tabM(ix)) <> "" Then
adrM = tabM(ix) & ræk
If tabA(ix) <> "" Then
adrA = tabA(ix)
End If
With APF.ActiveSheet
.Range(adrA).Value = sysXls.Sheets(1).Range(adrM).Value
End With
End If
Next ix
End Sub
Private Sub opretFiler()
' Here I run some other macro exporting the files to Excel and PDF
btnExcel
btnExportPDF
End Sub
if you put this code in Sheet1, then to access it from a button you need to define its name (in the button) as Sheet1.APFormRun (and I think you need to make it Public).
If you move the sub and everything it calls to a Module (after doing an Insert->Module), then you do not need the Excel Object Name prefix.
A very detailed write-up about scoping is at the link below. Scroll down to the "Placement of Macros/ Sub procedures in appropriate Modules" section: http://www.globaliconnect.com/excel/index.php?option=com_content&view=article&id=162:excel-vba-calling-sub-procedures-a-functions-placement-in-modules&catid=79&Itemid=475
In your code above, I had to comment out all the subs you didn't include just to get it to compile for debugging.
To make a sub accessible to the Macros button or to "Assign Macro..." you have to make it Public
Also to make a sub accessible, it cannot have any passed parameters.
So you will have to remove the passed parameters from the Public Sub APFormRun() definition
Therefore you will have to re-write the initial portion of APFormRun ... currently your APFormRun relies upon getting a passed parameter (Target) of the selected cell that you right-clicked upon. When you press a button, there is no cell that you are right-clicking upon. It is not a cell-identifying Excel event. You will have to obtain the selected cell via the Selection excel object. There are a lot of StackOverflow answers on how to do that.