Refer to Form Control Without Specific Naming - ms-access

I have a form with numerous images, each of which performs a series of actions when clicked. I can create a Private Sub with all of the actions for each button, however I think this is inefficient. Rather, I'd like to record all the actions in one Macro and then call this Macro when each image is clicked. To do so, I'd need the single Macro to refer to the current image selected and not refer to any image by name. Is this possible?
My current code includes the following:
Me.Image001.BorderColor = RGB(1, 1, 1)
Me.Image001.BorderWidth = 2
Me.Image001.BorderStyle = 1
I'd need to amend this so that it amends the border colour/width/style etc of whichever image is selected, and not a specific named image ('Image001').
Thanks!

You should use event sinking.
With event sinking you could bind to an event your own procedures.
You can see an example here http://p2p.wrox.com/access-vba/37472-event-triggered-when-any-control-changed.html
In simple words you create a module where you bind the event to your specific implementation . Then on the form you are interested you create a collection where you register the controls you want to "follow" the event sinking...
My sub sinking for checkboxes (i have alot)
1st a class module name SpecialEventHandler
Option Compare Database
Private WithEvents chkbx As CheckBox
Private m_Form As Form
Private Const mstrEventProcedure = "[Event Procedure]"
Public Function init(chkbox As CheckBox, frm As Form)
Set chkbx = chkbox
Set m_Form = frm
'Debug.Print frm.Name
chkbx.AfterUpdate = mstrEventProcedure
End Function
Private Sub chkbx_AfterUpdate()
'your Code here
End Sub
Private Sub Class_Terminate()
Set chkbx = Nothing
Set m_Form = Nothing
End Sub
Then on the form you want to use event sinking
Option Compare Database
Dim spEventHandler As SpecialEventHandler
Private colcheckBoxes As New Collection
Private Sub Form_Open(Cancel As Integer)
Dim ctl As Control
For Each ctl In Me.Detail.Controls
Select Case ctl.ControlType
Case acCheckBox
Set spEventHandler = New SpecialEventHandler
spEventHandler.init Controls(ctl.NAME), Me
colcheckBoxes.Add spEventHandler
End Select
Next ctl
End Sub

You could always create a Standard Sub, then call it on the click of the button. Something like
Public Sub changeColor(frm As Form, ctrl As Control)
frm.Controls(ctrl.Name).BorderColor = RGB(1, 1, 1)
frm.Controls(ctrl.Name).BorderWidth = 2
frm.Controls(ctrl.Name).BorderStyle = 1
End Sub
So when you click an image all you have to do is,
Private Sub Image001_Click()
changeColor Me, Image001
End Sub

Related

Linking form controls to class

I am trying to link controls on a form that I have created in a Class Module and am having trouble getting it to work properly.
'Class Name is CustForm
Option Explicit
Private WithEvents btnTest as CommandButton
Public Function showForm()
Dim tempForm as Form
Dim formName as String
Set tempForm = CreateForm
formName = tempForm.Name
Set btnTest = CreateControl(formName,
acCommandButton,acDetail,,,300,300,1000,500)
Dim btnName As String
btnName = btnTest.Name
Docmd.RunCommand acCmdFormView
End Function
Private Sub btnTest_Click()
MsgBox "Test"
End Sub
In a separate form I create the object and call the showForm in a click event
Private Sub Command0_Click()
Dim tstForm as CustForm
set tstForm= New CustForm
tstForm.showForm
End Sub
But nothing happens when I click the button created in the CustForm
I have tried using a temporary CommandButton in the showForm and after the docmd.runcommand acCmdFormView
set btnTest = Forms(formName).Controls(btnName)
under the assumption that maybe the instance of the command button changes when the form goes to Form View. Again no joy.
However if I add this to the CustForm Class
Public Function init(lclBtn as CommandButton)
set btnTest = lclBtn
btnTest.OnClick = "[Event procedure]"
End Function
And then I delete the OnClick and add this code to the form's module
Option Explicit
Dim tester as CustForm
Private Sub Form_Open(Cancel as Integer)
Set tester = new CustForm
tester.init Me.Command0
End Sub
Then it fires the MsgBox when I click the button.
But I need to be able to eventually build a form factory class that allows me to build forms for class objects dynamically and handle the events in the object class. I would rather not make a bunch of purpose built forms for each class and have the form instantiate the class. I want to do it the other way around. Class builds the form.
Can this be done?
As can be read in the documentation of the Form.HasModule property, there are two kinds of forms and reports: lightweight forms and reports that don't have a class module and don't support events, and full forms and reports that do have a class module and support events.
This means you still have to toggle this property for it to support events:
Private WithEvents btnTest as CommandButton
Public Function showForm()
Dim tempForm as Form
Dim formName as String
Set tempForm = CreateForm
tempForm.HasModule = True
formName = tempForm.Name
Set btnTest = CreateControl(formName, acCommandButton,acDetail,,,300,300,1000,500)
btnTest.OnClick = "[Event Procedure]"
Dim btnName As String
btnName = btnTest.Name
DoCmd.RunCommand acCmdFormView
End Function
Private Sub btnTest_Click()
MsgBox "Test"
End Sub
Do note that setting Form.HasModule modifies the VB Project part of the Access database (it adds a class module), and thus every time you do this, your database will need to be recompiled. Generally, you want to avoid that, as it might lead to problems.
Instead, I recommend having a single form with a module and all the controls you might want. Then, you can move around the controls, change their caption, and resize them, bind them to table fields, and set up event handlers, all without modifying the VB Project behind the database (note that you can't add controls to a full form or change the names of controls without modifying the VB Project).
Note that a second issue is the persistence of your class object. Currently, there are no references to the class so it's destroyed. You can easily make your class persist indefinitely by using code like this:
Private WithEvents btnTest as CommandButton
Private Myself As Object
Public Function showForm()
Set Myself = Me 'Circular reference, object won't get destroyed until myself is set to nothing
For more information about gracefully handling classes with references to forms, you can see this answer by me. You should probably listen to the Form_Unload event and clean up when that occurs.

New to classes VBA Access

I have been working on a project and have multiple tick boxes (25) and multiple labels in a form that are names SC1, SC2...SCN and Lbl1, Lbl2...LblN depending on a recordset. When I click the tickbox I want the label beside it to display some information, see below -
Private Sub SC1_Click()
If (Me!SC1) = True Then
Form.Controls("Lbl1").Caption = ("Completed by " & (Environ$("Username")))
Form.Controls("Lbl1").ForeColor = vbGreen
Else
Form.Controls("Lbl1").Caption = ("Please tick if Complete")
Form.Controls("Lbl1").ForeColor = vbBlack
End If
End Sub
My issue is I can't change the number in the Sub name so I would have to create multiple sub procedures. I think if I created a class for the tick box this would change but I am not sure how I can set up the class. I have tried the below class template but am not sure where I can change the property values in order to reach my goal. I am not sure why you would have both get and set properties in one class. Any help on this is greatly appreciated.
Option Compare Database
Option Explicit
Private pName As String
Private pCaption As String
Private pVisiblity As Boolean
Private pValue As Boolean
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
Public Property Get Caption() As String
Caption = pCaption
End Property
Public Property Let Caption(Value As String)
pCaption = "Please Tick Box if complete"
End Property
Public Property Get Visibility() As Boolean
Visibility = pVisibility
End Property
Public Property Let Visibility(Value As Boolean)
pVisibility = True
End Property
Public Property Get Value() As Boolean
Value = pValue
End Property
Public Property Let Value(Value As Boolean)
pValue = True
End Property
There are two parts to creating and linking form controls to custom support objects (classes). In your case
Class Module: clsMyCheckbox
Option Explicit
Option Compare Database
Public WithEvents chkBox As CheckBox
Public chkLabel As Label
Private currentUser As String
Private Sub chkBox_Click()
If chkBox.Value = True Then
chkLabel.Caption = "Completed by " & currentUser
chkLabel.ForeColor = vbGreen
Else
chkLabel.Caption = "Please tick if Complete"
chkLabel.ForeColor = vbBlack
End If
End Sub
Private Sub Class_Initialize()
currentUser = Environ$("Username")
End Sub
And in your form module:
Option Explicit
Option Compare Database
Private localCheckboxes As New Collection
Private Sub Form_Load()
'--- find all the checkboxes on the form and create a
' handler object for each one
Dim ctl As Control
Dim chknum As String
Dim cbObj As clsMyCheckbox
Dim chkLbl As Label
For Each ctl In Me.Controls
If ctl.ControlType = acCheckBox Then
'--- you can filter by name if needed
If ctl.Name Like "SC*" Then
chknum = Right(ctl.Name, Len(ctl.Name) - 2)
Set chkLbl = Me.Controls.Item("Lbl" & chknum)
chkLbl.Caption = "initialized" 'optional during form load
Set cbObj = New clsMyCheckbox 'class object for this checkbox
Set cbObj.chkBox = ctl 'link the control to the object
Set cbObj.chkLabel = chkLbl 'link the label too
'--- add it to a local store so the object persists
' as long as the form is open
localCheckboxes.Add cbObj
End If
End If
Next ctl
End Sub
Private Sub Form_Unload(Cancel As Integer)
'--- guarantee the objects are destroyed with the form
Set localCheckboxes = Nothing
End Sub
I think you are going the wrong way. In Access you can't really derive your own classes for GUI control and use them on the form. For your problem, you basically have three options:
Use the default event handlers and call one custom function from each. This will improve your situation a little.
Use one custom event handler for all checkboxes, instead of the default event-handlers.
Use a class and attach an instance to each of the checkboxes you use. The class can then recieve any event from the checkbox. This is powerful but you will still need to register your class with each control and hold all you instances somewhere for this to work.
For your problem, I'd go with solution 2:
First, write a custom event handler like this in your Form-module:
Private Function chkClick(sName As String)
Debug.Print "Clicked: " & sName
Me.Controls(sName).Controls(0).Caption = "x"
End Function
Next, enter design mode of you form and go to all checkboxes. In Checkbox "SC1", you go to the "OnClick" event and enter =chkClick("SC1") as event handler instead of [Eventprocedure]. Make sure you use the correct name of the control as the parameter of the function.
Congratulations! From now on, all your checkboxes will call the same event-handler and pass their name. Since the label of a checkbox is its associated control, you get to that label from the checkbox via .Controls(0), meaning the first "sub"-control of the checkbox. This way, you don't need to know the name of the associated label at all!

Clear Textbox on key press

Is there any way to clear the textbox on keypress like in excel.
I tried the following code but it clears the text when clicking on the textbox. I want it to clear it when a certain key is pressed.
Private Sub Text10_GotFocus()
Text10.Value = ""
End Sub
You could select the control's entire text content whenever that control gets focus. Then your keypress would replace the selected text.
If you want that to happen for every text box on every form, you can set "Behavior entering field" setting to "Select entire field". (In Access 2007, find that setting from Office Button -> Access Options -> Advanced, then look under the Editing heading of that dialog. For Access 2003, see this page.)
Not only will that setting be applied to form controls, but also to tables and queries in datasheet view. If that's not what you want, you can use VBA in your form's module to select the text for only specific controls:
Private Sub MyTextBox_GotFocus()
Me.MyTextBox.SelStart = 0
Me.MyTextBox.SelLength = Len(Me.MyTextBox)
End Sub
If you want to do that for multiple controls, you could create a general procedure:
Private Sub SelectWholeField()
Me.ActiveControl.SelStart = 0
Me.ActiveControl.SelLength = Len(Me.ActiveControl)
End Sub
Then call that procedure from the got focus event of an individual control like this:
Private Sub MyTextBox_GotFocus()
SelectWholeField
End Sub
Private Sub Field1_KeyPress(KeyAscii As Integer)
If KeyAscii = 65 Then Me.Field1 = ""
'Or: If Chr(KeyAscii) = "A" Then Me.Field1 = ""
End Sub
change the number to the ascii value of whatever key you are wanting to use to clear the field
Declare a Form Level variable
Private CanDelete as Boolean
When the TextBox receives focus, set CanDelete to True
Private Sub txtTest_GotFocus()
CanDelete = True
End Sub
On the KeyPress event, clear the text if CanDelete is True
Private Sub txtTest_KeyPress(KeyAscii As Integer)
If CanDelete Then
Me.txtTest = ""
CanDelete = False
End If
End Sub

global click event handler (WithEvents)

I am trying to create a class module that will act as a global handler for when ever someone clicks one of the sixty textboxes I have in my form. Th textboxes represent a timecard for the week displaying information as clock in, clock out, lunch start,end,duration, total daily hours under each fo the seven days of the week. When someone clicks anyone of the boxes under a day all the boxes will unlock and enable so that the user can edit the information in them.
After scouring the web for a solutio of a global click event I found that I could create a class module that would handle the event without create a click event for every single text box that calls a seperate function to handle the event. The problem I am having is that my class module doesn't seem to be handling my event and was wondering if someone could suggest a solution to my problem. FYI, All my textboxes and locked and disabled to prevent data corruption. Below is my code:
''# Class module
Option Compare Database
Option Explicit
Public WithEvents TC_txtbox As TextBox
''# Set the textbox so that its events will be handled
Public Property Set TextBox(ByVal m_tcTxtBox As TextBox)
TC_txtbox = m_tcTxtBox
End Property
''# Handle and onClick event of the
Private Sub TC_txtbox_Click()
''# Find out the controls that where clikck
Debug.Print Form_TimeCard.ActiveControl.Name
Dim ctl As Control
For Each ctl In access.Forms.Controls
Debug.Print ctl.Name
Next ctl
End Sub
Form Code
Option Compare Database
Option Explicit
''# Global Variables
Public clk_inout As Boolean
Public settings
Public weekDict
Public weekOf As Variant
Public curDay As Variant
Public txtBxCollection As Collection
''# Event Handler for when the form opens
Private Sub Form_Open(Cancel As Integer)
''# Configure varaibles
Me.TimerInterval = 60000 ''# 10 sec Interval
weekOf = getFirstDayofWeek(Date)
curDay = Date
Set weekDict = CreateObject("Scripting.Dictionary")
Set settings = CreateObject("Scripting.Dictionary")
Set txtBxCollection = New Collection
''# Load Time Card Data
Call initSettings
''# Debug.Print "Work Day Goal " & settings.Item("Work_day_goal_hrs")
Call initDict
Call initTextBoxEventHandler
Debug.Print "Collection count " & txtBxCollection.Count
Call loadDates(Date)
Call clearDay
Call selectDay(Date)
Call loadWeeksData(weekOf)
Dim ctl As Control
Set ctl = weekDict.Item(Weekday(curDay)).Item("In")
If IsDate(ctl.Value) And (Not ctl.Value = "") Then
Me.but_clk_inout.Caption = "Clock Out"
Me.but_lunch.Visible = True
clk_inout = False
Else
Me.but_clk_inout.Caption = "Clock In"
Me.but_lunch.Visible = False
clk_inout = True
End If
''# Debug.Print "Work Day Goal " & settings.Item("Salary")
End Sub
Public Sub initTextBoxEventHandler()
Dim eventHandler As TextBoxEventHandler
Set eventHandler = New TextBoxEventHandler
Debug.Print "Collection count " & txtBxCollection.Count
Set eventHandler.TextBox = Me.txt_F_in
txtBxCollection.Add eventHandler
Debug.Print "Collection count " & txtBxCollection.Count
End Sub
Are you missing a Set? The public property set should be
Public Property Set TextBox(ByVal m_tcTxtBox As TextBox)
Set TC_txtbox = m_tcTxtBox ' dont forget the Set! '
End Property
I figure out My problem in the class module where I am setting the text box I forgot to add "TC_txtbox.OnClick = "[Event Procedure]"" VBA will not fire the custom even handler in my extended textbox if [Event Procedure] is not declared in the property of the event you would like to handle

How do I access the selected rows in Access?

I have a form which includes a data sheet. I would like to make it possible for a user to select multiple rows, click on a button and have some sql query run and perform some work on those rows.
Looking through my VBA code, I see how I can access the last selected record using the CurrentRecord property. Yet I don't see how I can know which rows were selected in a multiple selection. (I hope I'm clear...)
What's the standard way of doing this? Access VBA documentation is somewhat obscure on the net...
Thanks!
I used the technique similar to JohnFx
To trap the Selection height before it disappears I used the Exit event of the subform control in the Main form.
So in the Main form:
Private Sub MySubForm_Exit(Cancel As Integer)
With MySubForm.Form
m_SelNumRecs = .SelHeight
m_SelTopRec = .SelTop
m_CurrentRec = .CurrentRecord
End With
End Sub
Here is the code to do it, but there is a catch.
Private Sub Command1_Click()
Dim i As Long
Dim RS As Recordset
Dim F As Form
Set F = Me.sf.Form
Set RS = F.RecordsetClone
If F.SelHeight = 0 Then Exit Sub
' Move to the first selected record.
RS.Move F.SelTop - 1
For i = 1 To F.SelHeight
MsgBox RS![myfield]
RS.MoveNext
Next i
End Sub
Here's the catch:
If the code is added to a button, as soon as the user clicks that button, the selection is lost in the grid (selheight will be zero). So you need to capture that info and save it to a module level variable either with a timer or other events on the form.
Here is an article describing how to work around the catch in some detail.
http://www.mvps.org/access/forms/frm0033.htm
Catch 2: This only works with contiguous selections. They can't select mutliple non-sequential rows in the grid.
Update:
There might be a better event to trap this, but here is a working implementation using the form.timerinterval property that i have tested (at least in Access 2k3, but 2k7 should work just fine)
This code goes in the SUBFORM, use the property to get the selheight value in the master form.
Public m_save_selheight As Integer
Public Property Get save_selheight() As Integer
save_selheight = m_save_selheight
End Property
Private Sub Form_Open(Cancel As Integer)
Me.TimerInterval = 500
End Sub
Private Sub Form_Timer()
m_save_selheight = Me.selheight
End Sub
I've tried doing something like that before, but I never had any success with using a method that required the user to select multiple rows in the same style as a Windows File Dialog box (pressing Ctrl, Shift, etc.).
One method I've used is to use two list boxes. The user can double click on an item in the left list box or click a button when an item is selected, and it will move to the right list box.
Another option is to use a local table that is populated with your source data plus boolean values represented as checkboxes in a subform. After the user selects which data they want by clicking on checkboxes, the user presses a button (or some other event), at which time you go directly to the underlying table of data and query only those rows that were checked. I think this option is the best, though it requires a little bit of code to work properly.
Even in Access, I find sometimes it's easier to work with the tables and queries directly rather than trying to use the built-in tools in Access forms. Sometimes the built-in tools don't do exactly what you want.
A workaround to the selection loss when the sub form loses the focus is to save the selection in the Exit event (as already mentioned by others).
A nice addition is to restore it immediately, using timer, so that the user is still able to see the selection he made.
Note: If you want to use the selection in a button handler, the selection may not be restored already when it executes. Make sure to use the saved values from the variables or add a DoEvents at the beginning of the button handler to let the timer handler execute first.
Dim m_iOperSelLeft As Integer
Dim m_iSelTop As Integer
Dim m_iSelWidth As Integer
Dim m_iSelHeight As Integer
Private Sub MySubForm_Exit(Cancel As Integer)
m_iSelLeft = MySubForm.Form.SelLeft
m_iSelTop = MySubForm.Form.SelTop
m_iSelWidth = MySubForm.Form.SelWidth
m_iSelHeight = MySubForm.Form.SelHeight
TimerInterval = 1
End Sub
Private Sub Form_Timer()
TimerInterval = 0
MySubForm.Form.SelLeft = m_iSelLeft - 1
MySubForm.Form.SelTop = m_iSelTop
MySubForm.Form.SelWidth = m_iSelWidth
MySubForm.Form.SelHeight = m_iSelHeight
End Sub
There is another solution.
The code below will show the number of selected rows as soon as you release the mouse button.
Saving this value will do the trick.
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
MsgBox Me.SelHeight
End Sub
Use a Global variable in the form, then refer to that in the button code.
Dim g_numSelectedRecords as long
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
g_numSelectedRecords = Me.SelHeight
End Sub
Dim formRecords As DAO.Recordset
Dim i As Long
Set formRecords = Me.RecordsetClone
' Move to the first record in the recordset.
formRecords.MoveFirst
' Move to the first selected record.
formRecords.Move Me.SelTop - 1
For i = 1 To numSelectedRecords
formRecords.Edit
formRecords.Fields("Archived") = True
formRecords.Update
formRecords.MoveNext
Next i
Why not use an array or recordset and then every time the user clicks on a row (either contiguous or not, save that row or some identifier into the recordset. Then when they click the button on the parent form, simply iterate the recordset that was saved to do what you want. Just don't forget to clear the array or recordset after the button is clicked.?
Another workaround to keeping the selection while attempting to execute a procedure - Instead of leaving the datasheet to activate a button, just use the OnKeyDown event and define a specific keycode and shift combination to execute your code.
The code provided by JohnFx works well. I implemented it without a timer this way (MS-Access 2003):
1- Set the Form's Key Preview to Yes
2- put the code in a function
3- set the event OnKeyUp and OnMouseUp to call the function.
Option Compare Database
Option Explicit
Dim rowSelected() As String
Private Sub Form_Load()
'initialize array
ReDim rowSelected(0, 2)
End Sub
Private Sub Form_Current()
' if cursor place on a different record after a selection was made
' the selection is no longer valid
If "" <> rowSelected(0, 2) Then
If Me.Recordset.AbsolutePosition <> rowSelected(0, 2) Then
rowSelected(0, 0) = ""
rowSelected(0, 1) = ""
rowSelected(0, 2) = ""
End If
End If
End Sub
Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer)
rowsSelected
If KeyCode = vbKeyDelete And Me.SelHeight > 0 Then
removeRows
End If
End Sub
Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
rowsSelected
End Sub
Sub rowsSelected()
Dim i As Long, rs As DAO.Recordset, selH As Long, selT As Long
selH = Me.SelHeight
selT = Me.SelTop - 1
If selH = 0 Then
ReDim rowSelected(0, 2)
Exit Sub
Else
ReDim rowSelected(selH, 2)
rowSelected(0, 0) = selT
rowSelected(0, 1) = selH
rowSelected(0, 2) = Me.Recordset.AbsolutePosition ' for repositioning
Set rs = Me.RecordsetClone
rs.MoveFirst ' other key touched caused the pointer to shift
rs.Move selT
For i = 1 To selH
rowSelected(i, 0) = rs!PositionNumber
rowSelected(i, 1) = Nz(rs!CurrentMbr)
rowSelected(i, 2) = Nz(rs!FutureMbr)
rs.MoveNext
Next
Set rs = Nothing
Debug.Print selH & " rows selected starting at " & selT
End If
End Sub
Sub removeRows()
' remove rows in underlying table using collected criteria in rowSelected()
Me.Requery
' reposition cursor
End Sub
Private Sub cmdRemRows_Click()
If Val(rowSelected(0, 1)) > 0 Then
removeRows
Else
MsgBox "To remove row(s) select one or more sequential records using the record selector on the left side."
End If
End Sub