Find open subform - vba

I have a subform with a button that opens another form.
On the secondary form, the user can select an address.
The selected address should be applied to the calling form.
I pass the window handle when opening the child form.
But when it tries to find the calling form in the Forms collection, it isn't there.
I suspect that is because the calling form is actually a subform.
I don't know where to go from here.
Calling the form, passing the windows handle
OpenCCCustAddr [CustFID], "CCInt", Me.hWnd
In the Form Close event, I try to set the address values on the calling form,
but GetFormByHWND returns null.
Set frm = GetFormByHWND(Me!txtCallingHWND)
// Me!txtCallingHWND here is populated and looks reasonable
frm!BillStreet = strAddr // This blows up since frm is null
frm!HolderZipCode = strZip
frm!AddressUpdated = -1
Set frm = Nothing
Public Function GetFormByHWND(lngHWND As Long) As Form
Dim frm As Form
Dim nm As String
Select Case lngHWND
Case 0
Case Else
For Each frm In Forms
nm = frm.NAME // the name of the parent form shows, but not my calling subform
If frm.hWnd = lngHWND Then
Set GetFormByHWND = frm
Exit For
End If
Next
End Select
End Function
For Each and For I=0 to Count-1 both give the same results. The form just isn't in Forms. It's possible that it is because it is a subform.
I tried searching the subforms, but this blows up when I check ctl.hWnd with "Object doesn't support this property"
Public Function GetFormByHWND(lngHWND As Long) As Form
Dim frm As Form
Dim ctl As Access.Control
Dim nm As String
Select Case lngHWND
Case 0
Case Else
For Each frm In Forms
nm = frm.NAME
If frm.hWnd = lngHWND Then
Set GetFormByHWND = frm
Exit For
End If
Next
Rem If we didn't find the form, check for a subform
If GetFormByHWND Is Nothing Then
For Each frm In Forms
nm = frm.NAME
For Each ctl In frm.Controls
If ctl.Properties("ControlType") = acSubform Then
nm = ctl.NAME
If ctl.hWnd = lngHWND Then // Error: "Object doesn't support this property"
Set GetFormByHWND = ctl
Exit For
End If
End If
Next
Next
End If
End Select
End Function

As #June7 pointed out, my mistake was assuming that the control was the form. Instead is has a form.
So the proper solution is
Rem If we didn't find the form, check for a subform
If GetFormByHWND Is Nothing Then
For Each frm In Forms
For Each ctl In frm.Controls
If ctl.Properties("ControlType") = acSubform Then
If ctl.Form.hWnd = lngHWND Then // note the change here
Set GetFormByHWND = ctl.Form
Exit For
End If
End If
Next
Next
End If

First, it not at all clear why all that code and hwn stuff is required?
We assume that you have a form.
On that form, you have a button, and it launches the 2nd form.
so, in first form, we have this:
' write data to table before launching form
If Me.Dirty = True Then Me.Dirty = False
DoCmd.OpenForm "formB"
Ok, now in formB on-load event, we have this:
Option Compare Database
Option Explicit
Dim frmPrevious As Form
Dim frmPreviousSub As Form
Private Sub Form_Load()
Set frmPrevious = Screen.ActiveForm
Set frmPreviousSub = frmPrevious.MySubFormControl.Form
' do whatever
End Sub
So now we have both a reference to the previous form, and also the sub form.
Say the user selects some address and hits the ok button.
The code then does this:
frmPreviousSub!AddressID = me!ID ' get/set the PK address ID
docmd.Close acForm, me.name
So no need for all that world poverty, grabbing and looping hwnd or any such hand stands.
Just a few nice clean lines of code.
Now, I DO HAVE a recursive loop that will return the form handle ALWAYS as a object reference, and you thus don't even have to hard code the form(s) name.
So, say that main form had 2 sub forms, and on those to sub forms, you have
A Company address, and a ship to address. So, you want to launch form B from EITHER of these two sub forms, and when you select a address, you return that value, and thus two or even potential 3 sub forms could in fact call this way call pop up address selector form.
The way you do this is similar to the above code, but we do NOT need to hard-code the sub form.
The code will now look like this:
Private Sub Form_Load()
Dim f As Form
Set f = Screen.ActiveForm ' pick this up RIGHT away -
' previous active form only valid
' in open/load event
' we have the previous active form, get the sub form.
Set frmPrevous = GetSubForm(f)
End Sub
Note that the sub form can be 3 or even 5 levels deep. This routine is "recursive". It grabs the previous form, then checks if a sub form has focus. If the sub form has focus, then it gets that control, and if that control is a sub form, then it just keeps on going until such time we drill down this rabbit hole and NO MORE drilling down can occur.
This routine should be placed outside of the form and placed in your standard "global" module of routines.
Public Function GetSubForm(f As Form) As Form
Static fs As Form
If f.ActiveControl.Controltype = acSubform Then
GetSubForm f.ActiveControl.Form
Else
Set fs = f
End If
Set GetSubForm = fs
End Function
So note how if it find that a sub form has the focus? Well then it just calls itself again with that form and keeps on drilling down. As a result, it don't matter if the form is 1 or 5 levels deep. The resulting "frmPrevous" will be a valid reference to that sub form, and thus after you select or do something in the supposed popup form? You can set the value of some PK or whatever and then close the form.
There is no hwnd, very clean code, and the recursion trick means that even for nested sub forms more then one deep, your frmPrevious is in fact a reference to the form that launch the form we pop up for the user to select whatever.
If you don't have a common Address ID column? Then our popup form should ASSUME that you always have a public variable defined in the calling form.
Say ReturnAddressID as long
Make sure you dim the value as public, say like this:
Public ReturnAddressID as long
So, now in our popup form, we can do this:
frmPrevious.ReturnAddressID = me!PD
frmPrevous.MyUpdate
(we assume that all forms that call the popup also have a public function names MyUpdate.
Thus, now we have a generalized approach, and 2 or 10 different address forms, even as sub forms can now call the one address picker. As long as any of those forms adopts that public ReturnAddressID and a public function MyUpdate, then we can pass back the values, and MyUpdate will shove/take/set the ReturnAddressID into whatever column and value you use for the Address ID in that sub form.
And of course if there are no sub forms, the routine will just return the top most form that called the pop up form.

Related

Textbox Exit Event for unknown Textbox

I am working on a userform which contains numerous (an unknown number) of textbox controls. The number of textboxes on the form is based on certain parameters being met elsewhere within the project.
The number of textboxes on the form could get up to 100 or so and my users are expected to fill in each of them. However, in many cases the value for one textbox will be the same as the textbox positioned directly above it.
I therefore wish to implement functionality to allow the user to enter a "." followed by tabbing out of the field in order to copy the value from the textbox above it.
There are a couple of ways I could go about this initial requirement:
Private Sub TextBox2_Exit(ByVal Cancel As MSForms.ReturnBoolean)
If TextBox2.Value = "." Then
TextBox2.Value = TextBox1.Value
End If
End Sub
This works absolutely fine.. however given that there is no indication here that the textbox2 is below textbox1 (Other than me knowing where I put it) we could do something like:
Private Sub TextBox2_Exit(ByVal Cancel As MSForms.ReturnBoolean)
If TextBox2.Left = TextBox1.Left And TextBox2.Value = "." Then
TextBox2.Value = TextBox1.Value
End If
End Sub
And again this works absolutely fine.
My problem is this, with up to 100 textbox controls I do not particularly want to write and maintain an Exit event for each possible textbox control.
I will probably phrase this incorrectly, but is there a way to trigger the exit event dynamically by passing the name of the textbox the user has tabbed out of?
Or am I lumbered within dozens of identical subs handling the exit of each textbox specifically?
EDIT:
Reading the post linked from Word Nerd, I have the following included with the userform initialize event
Dim tbCollection As Collection
Private Sub UserForm_Initialize()
Dim Ctrl As MSForms.Control
Dim obj As clsTextBox
Set tbCollection = New Collection
For Each Ctrl In usfEnterTime.Controls
If TypeOf Ctrl Is MSForms.TextBox Then
Set obj = New clsTextBox
Set obj.Control = Ctrl
tbCollection.Add obj
End If
Next Ctrl
Set obj = Nothing
Then we have the class module clsTextBox
Private WithEvents MyTextBox As MSForms.TextBox
Public Property Set Control(tb As MSForms.TextBox)
Set MyTextBox = tb
End Property
Private Sub MyTextBox_Change()
If MyTextBox.Value = ". " Then
If MyTextBox.Top = 24 Then
MsgBox "Nothing to copy from"
MyTextBox.Value = ""
Exit Sub
Else
For Each Ctrl In usfEnterTime.Controls
If Ctrl.Left = MyTextBox.Left And Ctrl.Top = MyTextBox.Top - 18 Then
MyTextBox.Value = Ctrl.Value
Application.SendKeys "{TAB}"
Exit Sub
End If
Next Ctrl
End If
Else
Exit Sub
End If
End Sub
My initial requirement was to use the EXIT event to implement a "." + TAB out of textbox to copy from above, however it appears the TextBox control does not have the EXIT event available. So I am using the change event, application.sendkeys and expecting my users to use ". " (Note blank space) to mimic the same functionality and tab to the next textbox in the tab order index using the spacebar instead. As you can see I am simply looping through all controls and checking the relative TOP and LEFT positions to determine which textbox to copy from. 24 is the position of the top most textbox control and so I am capturing this and displaying a msgbox to advise the user there will be nothing to copy from. -18 represents the gap between the tops of two textbox controls.
Working perfectly, thanks to Word Nerd for linking through to the other post.

How do I prevent control passing to the next Userform textbox?

I have a Userform with multiple Textboxes. All the Textboxes are in tab-order.
Each subsequent Textbox is back colored yellow to indicate to the user which is the next Textbox to complete.
If the Textbox Value is determined to be invalid I want the control to SetFocus back on that particular Textbox. However, control is automatically handed over to the next Textbox in the tab-order.
When I try to focus back on the required Textbox with the mouse, this fires an event on the next Textbox, which follows the rules of my program and requests the user to enter a valid value.
Below is a sample of two Textboxes, if the user fails to enter a first name I want the control to return to the tbxCustomerFirstName Textbox, however, the control is handed over to the tbxCustomerSurName Textbox, even though I've "tbxCustomerFirstName.SetFocus".
AstFlag = 2 means there has to be a valid value in the Textbox.
AstFlag = 1 means the Textbox value can be blank.
I stepped through the program and AstFlag does indeed = 2, and the set focus code is executed.
'====================================================================================
'
' Customer First Name
'
Private Sub tbxCustomerFirstName_Exit(ByVal Cancel As MSForms.ReturnBoolean)
Call tbxValues(3)
If tbxCancel = True Then
Cancel = True
End If
If AstFlag = 2 Then
tbxCustomerFirstName.SetFocus
End If
End Sub
'====================================================================================
'
' Customer Surname
'
Private Sub tbxCustomerSurName_Exit(ByVal Cancel As MSForms.ReturnBoolean)
Call tbxValues(4)
If tbxCancel = True Then
Cancel = True
End If
If AstFlag = 2 Then
tbxCustomerSurName.SetFocus
End If
End Sub
You want to set the Cancel parameter to True when you mean to cancel exiting the control.
I don't know what your tbxCancel flag logic is, but...
If AstFlag = 2 Then
theControlYouAreExiting.SetFocus
End If
Should be
If AstFlag = 2 Then
Cancel.Value = True
End If
That will prevent exiting the control, so there's no need to SetFocus on anything. Now again I don't know what tbxCancel is supposed to be doing, but it doesn't look right.
In fact I would scrap all this painfully duplicated conditional logic, and implement some Validate method that returns True if the field is valid (and thus can be exited) and False if the field is invalid (and thus Cancel.Value must be True and focus remains on the control).
Cancel.Value = Not Validate(args)
Where args could be theControlYouAreExiting.Tag, and then you could use the controls' Tag property to hold the metadata you're likely currently hard-coding in tbxValues.
Ultimately the problem you're solving is going to create a mess no matter how you put it, because everything is happening in the form's code-behind: you want to separate the View (the form/UI) from the Model (the data it's manipulating).
The solution is to create a class module that represents your model. This model exposes a property that each control on your form manipulates, and then the model itself knows how to validate each property.
Option Explicit
Private model As CustomerModel
Private Sub UserForm_Initialize()
Set model = New CustomerModel
End Sub
Public Property Get CustomerModel() As CustomerModel
Set CustomerModel = model
End Property
Public Property Set CustomerModel(ByVal value As CustomerModel)
Set model = value
End Property
Private Sub tbxCustomerSurName_Change()
model.Surname = tbxCustomerSurName.Text
End Sub
Private Sub tbxCustomerSurName_Exit(ByVal Cancel As MSForms.ReturnBoolean)
Cancel.Value = Not model.IsValidSurname
End Sub
And with that you don't need to maintain flags and state switches to keep track of what the metadata is for the control you're in.

Is it possible to raise events on a subform's form, when that subform is bound to a table?

I've got a form with a subform which is bound to a temporary table. The temporary table contains the result of a pivot, but I'd like users to be able to edit that pivot and then push the changes to the source table.
To do this, I'd like to fire events on AfterInsert, AfterUpdate and Delete so I can act on changes. As I understand it, the subform's form property refers to a temporary datasheet form when the subform is bound to a table. However, I can't get this temporary form to raise any events.
MCVE:
I have a database with a single table, Table1, a single form, Form1, and that form has a single subform control which is unbound.
I have a class, Class1, with the following code:
Private WithEvents subformForm As Access.Form
Public Sub Init(subformControl As Access.SubForm)
Set subformForm = subformControl.Form
subformForm.OnCurrent = "[Event Procedure]"
End Sub
Private Sub subformForm_Current()
MsgBox "Current!"
End Sub
The form, Form1, has the following code:
Private c1 As Class1
Private Sub Form_Load()
sub1.SourceObject = "Table.Table1"
Set c1 = New Class1
c1.Init sub1
End Sub
However, when I move about records, add records, and do whatever in that subform, the subformForm_Current event never fires.
It's because the subform object isn't a form having a code module. Thus, nowhere is the event procedure to run.
So, create a form in datasheetview using the table as source, having no code module, and use that as the subform:
Your code is ignored.
Now, adjust the form to have a code module:
Your code runs as expected.
Anyway, that's how it works for me in Access 2016.
As Gustav pointed out to me, a form needs a module to raise events.
This means you can't use the auto-created temporary datasheet form. But you can create your own form to take its place.
To work around the limitation, I created a form named frmDynDS, and set it's default view to datasheet view. Then, I opened the form in design view and added 255 text boxes to the form using the following code:
Public Sub DynDsPopulateControls()
Dim i As Long
Dim myCtl As Control
For i = 0 To 254
Set myCtl = Application.CreateControl("frmDynDS", acTextBox, acDetail)
myCtl.Name = "Text" & i
Next i
End Sub
I added a module, and added the following code to dynamically load a table into the form:
Public Sub LoadTable(TableName As String)
Dim fld As DAO.Field
Dim l As Long
Me.RecordSource = TableName
For Each fld In Me.Recordset.Fields
With Me.Controls("Text" & l)
.Properties("DatasheetCaption").Value = fld.Name
.ControlSource = fld.Name
.ColumnHidden = False
.columnWidth = -2
End With
l = l + 1
Next
For l = l To 254
Me.Controls("Text" & l).ColumnHidden = True
Next
End Sub
Then, I could adjust Class1 to the following:
Private WithEvents subformForm As Access.Form
Public Sub Init(subformControl As Access.SubForm, TableName As String)
subformControl.SourceObject = "Form.frmDynDS"
Set subformForm = subformControl.Form
subformForm.LoadTable TableName
subformForm.OnCurrent = "[Event Procedure]"
End Sub
Private Sub subformForm_Current()
MsgBox "Current!"
End Sub
And Form1 to the following:
Private c1 As Class1
Private Sub Form_Load()
Set c1 = New Class1
c1.Init sub1, "Table1"
End Sub
Using this approach, you can have a subform that can display tables created on the fly in datasheet view, and handle events for that subform.
You can have multiple subforms bound to frmDynDS displaying different tables, and handle events in different event handlers, on a single form.

Treat Userform as a Function

I have created a VBA UserForm which is essentially a glorified input box
Just like an input box can be employed like this
Dim returnVal As String
returnVal = InputBox("Write some string")
I would like my userform to run like this
Dim returnVal As customClass
Set returnVal = MyUserForm([some arguments])
ie. the MyUserForm() code passes some arguments to the userform, and when the userform is closed, it gets some arguments back (in the form of a custom class rather than a plain string)
What's the best way of structuring my userform to allow this functionality?
Currently, I'm just declaring some variables and the custom class publicly. I'm catching command button clicks and Query_close() events to hide the form, then I read the outputVal and close the form completely. I don't like this because I'd like my form to be totally self contained, and I think the capturing of events is messy.
In simplified code (read/return a string):
Function myUf(inVal As String) As String
Dim frm As New frmTest
frm.inputval = inVal
frm.Init 'sets caption. We cannot rely on userform initialize as this runs before inputval is set
'We could pass a variable here to save writing to the public variable
frm.Show
myUf = frm.outputVal
Set frm = Nothing
End Function
And in my Userform called frmTest with a textbox called tb1
Public inputval As String
Public outputVal As String
Public Sub Init()
Me.Caption = inputval 'setting caption, but could pass this anywhere
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode <> 1 Then Cancel = 1
outputVal = tb1 'reading value from textbox, but could return anything here
Me.Hide
End Sub
You need to find a way to initiate the UserForm from a ClassObject. Then, you can use a simple factory pattern to create the UserForm exactly the way you want.
In general, I have copied a bit of the code of Mat's Mug somewhere in StackOverflow and I wrote an article about the User Forms. If you take a look here (http://www.vitoshacademy.com/vba-the-perfect-userform-in-vba/) you will find a way to initialize the form with Public Sub ShowMainForm() It's possible to add a parameter to the ShowMainForm, then pass it to the initializer of the class.
In general, take the code from the article, make sure it works, and change the ShowMainForm initializer to the following:
Public Sub ShowMainForm(strText As String, strText2 As String)
If (objPresenter Is Nothing) Then
Set objPresenter = New clsSummaryPresenter
End If
objPresenter.Show
Call objPresenter.ChangeLabelAndCaption(strText, strText2)
End Sub
Then, if you call like this in the immediate window:
call ShowMainForm("Just","testing")
You will get this:
Which is quite what you need. :)
The basic idea is:
Create a Function in witch you combine your arguments to a string like:
strOpenArg = "param1:=value1;param2:=value2;"
than open the form with the OpenArgs
DoCmd.OpenForm "UserForm", acNormal, , , , acDialog, strOpenArgs
get your value and close the Form
Value= Form_UserForm.Value
DoCmd.Close acForm, "UserForm", acSaveNo
in the UserForm set Form_open. Here you can get your parameters.you can devide this by string splitting.
Set also an OK Button, where you make the form just invisible and set the return value
Private Sub Form_Open(Cancel As Integer)
Dim strParameter as String
strParameter = Me.OpenArgs 'Here are your parmeters
End Sub
Private Sub ok_Click()
m_Value = "Your ReturnValue"
Me.Visible = False
End Sub
Private m_Value As String
Public Property Get Value() As String
Value = m_msgBoxResult
End Property
There's no way to one-liner the code like you want to, unfortunately. If all your userform code is self-contained then the only way for it to pass values out is to change the values of public variables. Mat's Mug's answer here is the layout I usually use when trying to simulate functions like 'InputBox' but you still can't get it in one line without writing a separate function. Using userform properties allows you to contain more of your code within the form itself.

VBA: how to use class module to change value of checkbox in userform

I have a userform with a lot of emailaddresses. I want the user to be able to select who to send an email to. I do so with checkboxes which are created on run time. To make it easier to use, I have also added a checkbox which allows the user to select of deselect all checkboxes.
This works perfectly as I want it to, but there is one problem I'm breaking my head over. If all checkboxes are checked and one gets unchecked, I want the "Select all" checkbox to be unchecked as well - and vice versa, if not all checkboxes were checked and the final checkbox is being checked by the user I want the "Select all" checkbox to be checked as well.
I try to do this using a class module. My overall knowledge of vba is pretty okay, but class modules are new territory for me, so excuse me if my language gets a bit fussy now.
In the initialize event of the userform I create a new collection and I assign clicks to these specific checkboxes. That works perfectly, as it doesn't give any errors in the initialize event of the userform and an event is triggered once I click one of these checkboxes. The problem I'm having is that I can't get a grip on the "Select all" checkbox (chkSelAll) in the userform. I've tried creating a public object for this checkbox in the userform (Public objSelAll As MSForms.CheckBox), but still it gives me the "Variable not defined" error once I click one of the checkboxes.
Here's the code for the class module (cls_RIRI):
Private WithEvents chkBox As MSForms.CheckBox
Public Sub AssignClicks(ctrl As Control)
Set chkBox = ctrl
End Sub
Private Sub chkBox_Click()
If chkBox.Value = False Then objSelAll.Value = False
'^This is where the error occurs: variable not defined
End Sub
And here's the relevant part of the Userform_Initialize event:
Private colTickBoxes As Collection
Public objSelAll As MSForms.CheckBox
Private Sub UserForm_Initialize()
Dim ChkBoxes As cls_RIRI
Dim ctrl As Control
Set objSelAll = Me.Controls.Item("chkSelAll")
Set colTickBoxes = New Collection
For Each ctrl In Me.Controls
If TypeName(ctrl) = "CheckBox" And Left(ctrl.Name, 1) = "M" Then
Set ChkBoxes = New cls_RIRI
ChkBoxes.AssignClicks ctrl
colTickBoxes.Add ChkBoxes
End If
Next ctrl
Set ChkBoxes = Nothing
Set ctrl = Nothing
End Sub
As you can see I didn't get to the point yet where I let the code check if all checkboxes are checked so that the select all checkbox can be checked as well. I'm not really looking for this code, I'll probably manage it once I get grip on the select all checkbox from the class module, so please don't worry about this part! :)
Private WithEvents chkBox As MSForms.CheckBox
private strParentFormName as string
Public Sub AssignClicks(ctrl As Control,strFormName as string)
strParentFormName=strFormName
.....
end sub
Private Sub chkBox_Click()
dim f as userform
set f=userforms(0) <--- or loop the userforms to get form name
If chkBox.Value = False Then f.controls("objSelAll").Value = False
'^This is where the error occurs: variable not defined
End Sub
and something like this
Public Function GET_USERFORM(strUserform As String) As UserForm
Dim i As Integer
For i = 0 To UserForms.Count - 1
If UserForms(i).Name = strUserform Then
Set GET_USERFORM = UserForms(i)
Exit For
End If
Next i
End Function
You'll need to pass in the form also, as the variable doesnt exist in the class. Add a property for the formname or if you'll only have 1 form open, then use the open form or reference the form if multiples are open. Your class exists on it's own as a check box. I am not sure, but you may be able to get the parent object from the checkbox. Hope this helps.
private strFormName as string
Public Property Let ParentForm(value as string)
strFormname=value
End Property
then...
userforms(strFormname).controls("objSelectAll").value=true