I am trying to figure out how scope works.
For instance: I'm working on a userform in which a check procedure validates that new entries are not already present in the list. Because I'm probably going to use the full name (concatenation of first and last name) in different userforms within the same worksheet, I entered it as a Global variable in the general module:
Public Sub PublicVars()
Public FullName As String
End Sub
When I add the definition of the variable to the public sub, the check box does not work, but when I define it in the object in the userform, it does.
Private Sub cmbLastName_AfterUpdate()
FullName = cmbFirstName & " " & cmbLastName
If Application.WorksheetFunction.CountIf(Range("List_Full_Name"), FullName) Then
FullNameError = MsgBox("Name already included in contact list. Edit contact?", vbYesNo, "Error")
Else
End If
End Sub
This seems sensitive to mistakes, as when I want to use FullName for another object in the same userform or in a different userform in the same workbook, I would have to redefine it locally. I am probably overlooking something, as I cannot believe this is the right/most effective way.
Are there any other ways to tackle this?
Related
I am new to access so this might be an easy task or I am just trying to tackle it wrongly. I have a report that has various columns, sample id, sample time, sample type, dry matter, moisture. I am trying to create a button that has an input box for the user to chose what column to sort the report by. So far I thought of creating various reports that have been sorted by each column, named the reports by the column that sorts them then I am trying to have the open report action have a parameter that opens the report linked to the column entered at the input box. Is this even possible or is there a workaround for this.
PS. I am avoiding creating various buttons since it will fill up the screen.
Okay, this is pretty generic and will require some tweaking but it shows the core of how to do this.
To start with you need a module (so not form/report code). This is where the globals will be assigned values:
Option Compare Database
Option Explicit
Global rptname As String
Global fldname As String
Sub setRptName(name As String)
rptname = "Report Sorted by: " & name
fldname = name
End Sub
You will call that code inside the Click() event of your command button on your form and then open the report after that. This will take a combo box value and pass that value to the module code, creating the two global variables.:
Private Sub cmd_report_Click()
mdl_Globals.setRptName Me.cmb_fields.Value
DoCmd.OpenReport "Report1", acViewPreview
End Sub
I'm unsure if this will work with non-preview views, but you probably want preview anyway.
Lastly in the report code behind, you need the Load() and open() events, you may be able to get it to work with both inside one or other but I know this one works.
So to set the caption:
Private Sub Report_Load()
Me.lbl_header.Caption = rptname
End Sub
And then to sort:
Private Sub report_open(Cancel As Integer)
Me.Report.OrderBy = "[" & fldname & "]"
Me.Report.OrderByOn = True
End Sub
If your entry box on the form does not have entries that exactly match the name(s) of the fields in the table you will get a parameter popup.
I have a bunch of TextBox-Button pairs on a form. When the button is clicked I want to insert the value of the text box into a database. The name TextBoxes and Buttons follow a naming standard, for example Value1Tb - Value1Cmd and Value2Tb - Value2Cmd.
My problem is that since I want to do the same for every button I would like the possibility to write a Sub like:
Private Sub AnyButton_Click(sender As CommandButton)
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
End Sub
But I cannot find a way to point the Click-event of a Button to a different sub than the standard Name_Click().
Anybody know a way around this, that doesn't involve me writing 50 or so different Name_Click() subs?
If you are OK to use Form Controls rather that ActiveX, as it looks as though you may be at the moment, then Chris' solution seems good.
However if you need ActiveX CommandButtons then you are unable (as the VBA compiler will tell you, "Procedure declaration does not match...") to have parameters in the callback for the click event, and you are unable to raise the event from multiple objects, although you do of course know which button raised the event (since the relationship is 1 CommandButton = 1 Sub).
So... I would go with something like:
Private Sub Value1Cmd_Click()
Call TheMethod(Value1Cmd)
End Sub
Private Sub Value2Cmd_Click()
Call TheMethod(Value2Cmd)
End Sub
Private Sub TheRealMethod(sender As CommandButton)
' Do your thing '
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
' Etcetera... '
End Sub
Requires a stub for each button, so some copying and pasting to begin with, but then easy to maintain etcetera as all _Click event callbacks are pointing at the same method...
Edit:
E.g.
Sub AutoWriteTheStubs()
Dim theStubs As String
Dim i As Long
For i = 1 To 10
theStubs = theStubs & "Private Sub Value" & CStr(i) & "Cmd_Click()" & vbCrLf _
& " Call TheMethod(Value" & CStr(i) & "Cmd)" & vbCrLf _
& "End Sub" & vbCrLf & vbCrLf
Next i
Debug.Print theStubs
End Sub
It seems that what you want is to get the name of the clicked button. If you are creating buttons like this:
(where 'i' increments in a loop)
Set btn = Sheet1.Buttons.Add( , , , ,)
With btn
.OnAction = "btnSub"
.Caption = "Upadate"
.Name = "btn" & CStr(i) & "Cmd"
End With
and then defining a generic "private sub btnSub()" for all the buttons, you could at least get the name of the button that was clicked using Application.Caller. Something like:
Private Sub btnSub()
Dim ButtonName As String
ButtonName = Application.Caller
MsgBox ("Hello:" & ButtonName)
End Sub
Hope it helps!
I decided to make this an answer because I am doing something similar and I confirmed that it works.
You can store the OLEobjects in a Collection, of arbitrary size, containing Custom Class Objects that include the OLEobjects and associations and the events that you need. Thus you can completely avoid any code stubs.
Create a Custom Class to bind the Button and TextBox pairs.
Declare the Button object WithEvents.
Include your call-back in the exposed button event handler in the Class Module.
Put a Public routine in a Standard Module to initialise a Collection of these Custom Class objects by spanning the Form Controls. You can also use this to Add the controls programmatically as a 'reBuild' option. The Collection can be inside another Class Module with all of the management routines, but it needs to be Instantiated and loaded in a Standard Module.
Put a public routine in a standard module to receive the call-backs with whatever context you need. This can also be in a Worksheet Module if it makes for better encapsulation. You can use late binding to reference the callback or CallByName.
You need to bear in mind that the Module of the Form will recompile every time you add a control, so you have to be careful where you put your code.
My application has the controls directly on the Worksheet Surface, so I can't put the the Collection Class in, or source any initialisation of the Collection from the Worksheet module. This would amount to self modifying code and it grinds excel to a halt.
I dreamed this idea up through bloody-minded idealism (not necessarily a good thing) but, of course, I was not the first one to think of it as you can see here. #Tim Williams explains it in his answer. You can also google VBA Control Array Events to see plenty of similar examples including an excellent article by #SiddharthRout. In line with the VB6 analogy, he uses an Array instead of a Collection to achieve the same result.
I'll try to post some code later. My application is a bit different so it will take a lot of work to trim it down, but the principle is the same.
The other thing to bear in mind is that VBE really struggles with this type of thing so don't worry if it is loading up you processors. After you re-start with VBE off, all will be fine.
I have this same situation, and I just have a click event for every button that is a wrapper to the function I want to call. This also allows you to pass sheet-specific parameters if you need to.
Example:
Public Sub StoreButton_Click()
' Store values for transaction sheet 3/27/09 ljr
Call StoreTransValues(ActiveSheet)
End Sub
I just published (Open Source) the Event Centralizer for MSForms.
Citation: "The Event Centralizer for MSForms is a VBA programming tool that allows all sorts of custom grouping when writing handlers for the events occurring in UserForms.
With the Event Centralizer for MSForms, it is easy for example to have all TextBoxes react the same way when the Enter event occurs, or all except one, or only those with a particular Tag value.
Thanks to its events logs system, the Event Centralizer for MSForms is a powerful learning and debugging help."
I can't explain here how it works. I tried to do it on the site.
Set the event to =FunctionName(parameter).
A bit late but this may help someone else:
If you have a function called OpenDocumentById(docID as integer), then each control calling the function would have in the event the following:
cmd1's On Click event:
=OpenDocumentById([DocID])
cmd2's On Click event:
=OpenDocumentById([DocID])
etc...
I am using Excel VBA. I have two user forms:
ClientInfo and ClientSearch
In ClientSearch, I search through an excel worksheet to list all clients with the same last name. From there, I pick the client I want (by highlighting the name in the list and clicking on command button cmdOpenClientInfo) and determine their ClientID (which is also in the worksheet).
I then want to pass this ClientID to the form ClientInfo in order to populate all text boxes on this form with the relevant data from the worksheet:
Coded in the ClientSearch form:
Private Sub cmdOpenClientInfo_Click()
Dim ClientID As Integer
ClientID = textSrchClientID.value
'user msgbox to check to make sure I get the correct ClientID ... and I do
msgbox(ClientID)
Me.Hide
frmClientInfo.show
Call frmClientInfo.PopulateClientInfo(ClientID) 'this is where it fails
End Sub
Coded in the ClientInfo form:
Sub PopulateClientInfo(ClientID As Integer)
'this is where I would populate the text field
End Sub
The routine always gets stuck at the
CALL frmClientInfo.PopulateClientInfo(ClientID)
In the case above, I get Run-time error '424' Object Required.
I have tried various solutions presented in this forum, but have not found a solution.
Your call to frmClientInfo.show is in Modal mode, so the next statement wont execute until the new form closes. You can try to make the call non-modal:
frmClientInfo.show Modal:=False
But this may be source of other problems. Better keep working in modal mode but pass the ClientID parameter to the form before it shows up.
1- Modify your PopulateClientInfo method of frmClientInfo like this:
Public Sub PopulateClientInfo(ClientID As Integer)
'....
'populate The fields, then:
'....
Me.Show ' <-- form shows itself after populating its fields
End Sub
2- Remove the call to frmClientInfo.show in the ClientSearch form.
You can't call a procedure in a form module from outside that module. Try this code (sorry, I didn't).
Private Sub cmdOpenClientInfo_Click()
Dim ClientID As Integer
Dim FrmInfo As frmClientInfo
ClientID = textSrchClientID.Value
'user msgbox to check to make sure I get the correct ClientID ... and I do
MsgBox (ClientID)
Me.Hide
Set FrmInfo = New frmClientInfo
With FrmInfo
.Tag = ClientID
.Show
' make this call in the FrmInfo Activate event procedure
' PopulateClientInfo(cint(Me.Tag)
End With
Unload FrmInfo
Set FrmInfo = Nothing
End Sub
I presume that you have a form which is named frmClientInfo. You can create an instance of that form with the command Set FrmInfo = New frmClientInfo. This object will not show until the Show method is invoked but you gain access to all its controls. To pass a variable to that form you can address any of them. Perhaps you have a Tbx which should show the ClientID. You can access that Tbx and set its value. The above code assigns the ClientID to the Tag property of the form itself.
The form's Activate event will occur when the Show method is invoked. That would be the moment to run the PopulateClientInfo procedure (from within the frmClientInfo module, of course), retrieving the ClientId from the Tag property.
Bear in mind that the code will continue running in the cmdOpenClientInfo_Click procedure when the ClientInfo form is closed. So, that is the time to remove that form from memory, on the one hand. On the other, the 'FrmInfo' object still exists and you could pick any information from it that you might want to use in the form which made the call. The syntax is very simple, like, FrmInfo.Textbox1.Value.
I want to pass objects between form modules.
I have made a simple database to explain the problem I am having.
I have two forms --> form1, form2 and one module --> Module1
Based on the threads I have read, I believe if I want to define a global object, it should be done in the module so I placed the declaration there;
Option Compare Database
Public Type Name
First As String
Last As String
Phone As String
End Type
On form1 I have a cmd button with the following code behind it;
Option Compare Database
Dim My_Name As Name
Private Sub cmd_Button_Click()
My_Name.First = "MyFirstName"
My_Name.Last = "MyLastName"
MsgBox ("In Form1 " & My_Name.Last)
DoCmd.OpenForm "Form2"
End Sub
on the second form I have;
Option Compare Database
Dim my_Name As Name
Private Sub Form_Load()
MsgBox (my_Name.First)
End Sub
In the first form, I see the my_Name object variable fine, when I pass control to the second form, I lose the variable.
I have tried a number of variations of Public, Static declarations all over the place on both the object variable and the sub's but I cannot figure out how to preserve the object across modules.
You are re-declaring my_name. So it is in -effect two different things. Like george forman and george forman.
I am sorry I can't be more help it's been like a a million years since I have had to deal with this. However, it seems that your 'froms' are actually declared in an 'module' like block -name from1. That from global scope you can reference a variable in your form1.
In this case form1.my_name. However, this can lead to issues if form1.my_name has not yet been instanciated.
My recommendation is to find the module with global scope ( I dont remember exactly what it is, if you have to create it or now, and when it gets life). Declare and initicialize your variables in it. Then access those from within you event handlers ("cmd_Button_Click" for example.)
I want to do this but it won't compile:
Public MyVariable as Integer = 123
What's the best way of achieving this?
.NET has spoiled us :)
Your declaration is not valid for VBA.
Only constants can be given a value upon application load. You declare them like so:
Public Const APOSTROPHE_KEYCODE = 222
Here's a sample declaration from one of my vba projects:
If you're looking for something where you declare a public variable and then want to initialize its value, you need to create a Workbook_Open sub and do your initialization there.
Example:
Private Sub Workbook_Open()
Dim iAnswer As Integer
InitializeListSheetDataColumns_S
HideAllMonths_S
If sheetSetupInfo.Range("D6").Value = "Enter Facility Name" Then
iAnswer = MsgBox("It appears you have not yet set up this workbook. Would you like to do so now?", vbYesNo)
If iAnswer = vbYes Then
sheetSetupInfo.Activate
sheetSetupInfo.Range("D6").Select
Exit Sub
End If
End If
Application.Calculation = xlCalculationAutomatic
sheetGeneralInfo.Activate
Load frmInfoSheet
frmInfoSheet.Show
End Sub
Make sure you declare the sub in the Workbook Object itself:
Just to offer you a different angle -
I find it's not a good idea to maintain public variables between function calls. Any variables you need to use should be stored in Subs and Functions and passed as parameters. Once the code is done running, you shouldn't expect the VBA Project to maintain the values of any variables.
The reason for this is that there is just a huge slew of things that can inadvertently reset the VBA Project while using the workbook. When this happens, any public variables get reset to 0.
If you need a value to be stored outside of your subs and functions, I highly recommend using a hidden worksheet with named ranges for any information that needs to persist.
Sure you know, but if its a constant then const MyVariable as Integer = 123 otherwise your out of luck; the variable must be assigned an initial value elsewhere.
You could:
public property get myIntegerThing() as integer
myIntegerThing= 123
end property
In a Class module then globally create it;
public cMyStuff as new MyStuffClass
So cMyStuff.myIntegerThing is available immediately.
Little-Known Fact: A named range can refer to a value instead of specific cells.
This could be leveraged to act like a "global variable", plus you can refer to the value from VBA and in a worksheet cell, and the assigned value will even persist after closing & re-opening the workbook!
To "declare" the name myVariable and assign it a value of 123:
ThisWorkbook.Names.Add "myVariable", 123
To retrieve the value (for example to display the value in a MsgBox):
MsgBox [myVariable]
Alternatively, you could refer to the name with a string: (identical result as square brackets)
MsgBox Evaluate("myVariable")
To use the value on a worksheet just use it's name in your formula as-is:
=myVariable
In fact, you could even store function expressions: (sort of like in JavaScript)
(Admittedly, I can't actually think of a situation where this would be beneficial - but I don't use them in JS either.)
ThisWorkbook.Names.Add "myDay", "=if(isodd(day(today())),""on day"",""off day"")"
Square brackets are just a shortcut for the Evaluate method. I've heard that using them is considered messy or "hacky", but I've had no issues and their use in Excel is supported by Microsoft.
There is probably also a way use the Range function to refer to these names, but I don't see any advantage so I didn't look very deeply into it.
More info:
Microsoft Office Dev Center: Names.Add method (Excel)
Microsoft Office Dev Center: Application.Evaluate method (Excel)
As told above, To declare global accessible variables you can do it outside functions preceded with the public keyword.
And, since the affectation is NOT PERMITTED outside the procedures, you can, for example, create a sub called InitGlobals that initializes your public variables, then you just call this subroutine at the beginning of your statements
Here is an example of it:
Public Coordinates(3) as Double
Public Heat as double
Public Weight as double
Sub InitGlobals()
Coordinates(1)=10.5
Coordinates(2)=22.54
Coordinates(3)=-100.5
Heat=25.5
Weight=70
End Sub
Sub MyWorkSGoesHere()
Call InitGlobals
'Now you can do your work using your global variables initialized as you wanted them to be.
End Sub
You can define the variable in General Declarations and then initialise it in the first event that fires in your environment.
Alternatively, you could create yourself a class with the relevant properties and initialise them in the Initialise method
This is what I do when I need Initialized Global Constants:
1. Add a module called Globals
2. Add Properties like this into the Globals module:
Property Get PSIStartRow() As Integer
PSIStartRow = Sheets("FOB Prices").Range("F1").Value
End Property
Property Get PSIStartCell() As String
PSIStartCell = "B" & PSIStartRow
End Property
there is one way to properly solve your question. i have the same concern with you for a long time. after searching and learning for a long time, finally i get a solution for this kind of question.
The solution is that no need to declare the variable and no need to set value to the variable, and even no need VBA code. Just need the "named range" in excel itself.
For example, the "A1" cell content is "hello, world". and we define the "A1" cell a name as "hello", that is, the "A1" cell have a name now, it's called "hello".
In VBA code, we just need use this method [hello], then we can get the "A1" value.
Sub test()
msgbox [hello]
end sub
the msgbox will show "Hello, word".
this way, we get a global variable without any declaration or assignment. it can be used in any Sub or Function.
we can define many named range in excel, and in VBA code we just use [] method to get the range value.
in fact, the [hello] is a abbreviation of the function Evaluate["Hell"], but it's more shorter.
It's been quite a while, but this may satisfy you :
Public MyVariable as Integer: MyVariable = 123
It's a bit ugly since you have to retype the variable name, but it's on one line.