MS Access Change an 'OnChange' Event programmatically in VBA - ms-access

I am working on a project where I have to use MS Access and I have to make the database as loose as possible (Its weird and I dont think best practice but for now given my resources it is what I have).
Anyways I have 50 combo boxes but often have to make changes to one which would mean I have to manually change all of them. Id rather spend hour finding a programming solution then 5 minutes manually doing this.
I need to change the 'OnChange' event using VBA but my code produces an error.
Private Function RunChangePropagate()
Dim combo As ComboBox
RevealGrid
For Each combo In Me.Controls
combo.OnChange = "=ComboBox_Change()"
Next combo
ClearGrid
End Function
Error:
I have also tried making the text to change it to a Variant and then assigning the event to said Variant.
How do I go about doing this?
Thanks in advance.

This is some minimal example, that works:
Public Sub ChangeEvent()
Dim ctrl As Control
For Each ctrl In Me.Controls
If ctrl.Name = "Combo5" Then
Debug.Print ctrl.OnChange
ctrl.OnChange = "SomeProcedure"
End If
Next ctrl
End Sub
In your example you should only remove the = in the assigning. The parenthesis at the end of the assigned sub are not required.

Use WithEvents. This way, you form is completely "detached" from the class controlling (some controls of) the form.
I published an article with links and an example for anyone to study:
Create Windows Phone Colour Palette and Selector using WithEvents
The main code (code behind module and class module) is only:
Option Explicit
' Helper class for form Palette for event handling of textboxes.
' 2017-04-19. Gustav Brock, Cactus Data ApS, CPH.
' Version 1.0.0
' License: MIT.
' *
Private Const EventProcedure As String = "[Event Procedure]"
Private WithEvents ClassTextBox As Access.TextBox
Public Sub Initialize(ByRef TextBox As Access.TextBox)
Set ClassTextBox = TextBox
ClassTextBox.OnClick = EventProcedure
End Sub
Public Sub Terminate()
Set ClassTextBox = Nothing
End Sub
Private Sub ClassTextBox_Click()
' Select full content.
ClassTextBox.SelStart = 0
ClassTextBox.SelLength = Len(ClassTextBox.Value)
' Display the clicked value.
ClassTextBox.Parent!CopyClicked.Value = ClassTextBox.Value
' Copy the clicked value to the clipboard.
DoCmd.RunCommand acCmdCopy
End Sub
and:
Option Explicit
' Form to display the Windows Phone 7.5/8.0 colour theme.
' Also works as a basic example of implementing WithEvents for a form.
' 2017-04-19. Gustav Brock, Cactus Data ApS, CPH.
' Version 1.0.0
' License: MIT.
' *
Private ControlCollection As Collection
Private Sub Form_Load()
' Load events for all colour value textboxes.
Dim EventProcedure As ClassTextboxSelect
Dim Control As Access.Control
Set ControlCollection = New Collection
For Each Control In Me.Controls
If Control.ControlType = acTextBox Then
Set EventProcedure = New ClassTextboxSelect
EventProcedure.Initialize Control
ControlCollection.Add EventProcedure, Control.Name
End If
Next
Set EventProcedure = Nothing
Set Control = Nothing
End Sub
Private Sub Form_Unload(Cancel As Integer)
' Unload events for all colour value textboxes.
Dim EventProcedure As ClassTextboxSelect
For Each EventProcedure In ControlCollection
EventProcedure.Terminate
Next
Set EventProcedure = Nothing
Set ControlCollection = Nothing
End Sub
Full code is also on GitHub: VBA.ModernTheme

Related

How to pass reference to combobox?

This is a continuation from a previous post of mine: How to select a printer for entire job?, where I basically want to print a series of reports from a form in my Access database.
Which refers to this help doc: https://msdn.microsoft.com/en-us/library/ee336132(v=office.12).aspx
I'm trying to create the combobox with active printers in it in order to temporarily change the default printer for a series of documents. I'm tripped up on where it says "pass a reference to a ComboBox control"... How does one implement this?
Here's the code I have so far, where cboPrinterSelect is the combobox name:
Private Sub cboPrinterSelect_Load(Cancel As Integer)
Call GetPrinterList
' I'm not sure about this next part either'
cboPrinterSelect.Value = GetPrinterList.value
End Sub
'***************************************************
Private Sub cboPrinterSelect_AfterUpdate(Cancel As Integer)
Set Application.Printer = Application.Printers(cboPrinterSelect.ListIndex)
End Sub
'***************************************************
Private Sub GetPrinterList(ctl As Control)
Dim prt As Printer
For Each prt In Printers
ctl.AddItem prt.DeviceName
Next prt
ctl = Application.Printer.DeviceName
End Sub
Any help/advice would be appreciated.
EDIT:
Here is my updated code, which is still throwing an error (described in Andre's comments):
Private Sub Form_Load()
Call GetPrinterList(Me.cboPrinterSelect)
End Sub
'*********************************************************
Private Sub cboPrinterSelect_AfterUpdate(Cancel As Integer)
Set Application.Printer = Application.Printers(cboPrinterSelect.ListIndex)
End Sub
'***************************************************************
Private Sub GetPrinterList(ctl As Control)
Dim prt As Printer
For Each prt In Printers
ctl.AddItem prt.DeviceName
Next prt
ctl = Application.Printer.DeviceName
End Sub
The first part, the call of GetPrinterList(), must go into the On Load event of the form.
Private Sub Form_Load()
Call GetPrinterList(Me.cboPrinterSelect)
End Sub
That should be all that's missing.
The default printer is pre-selected by this line:
ctl = Application.Printer.DeviceName
And make sure that cboPrinterSelect has a RowSourceType of Value List.
Try the following code, it will display all connected Printers in the combo_box in your User_Form.
Public Sub GetPrinters()
' Use a large array (supports up to 100 printers).
ReDim result(100) As String
Dim wshNetwork, allPrinters As Object
' Get the network object
Set wshNetwork = CreateObject("WScript.Network")
Set allPrinters = wshNetwork.EnumPrinterConnections
' Printers collection has two elements for each printer.
For i = 0 To allPrinters.Count - 1 Step 2
Print_Series.cboPrinterSelect.AddItem allPrinters.Item(i + 1)
Next
' call your user form and combo box will have all active printers
Print_Series.Show
End Sub

Refer to Form Control Without Specific Naming

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

Access VBA - TreeView Control - AfterLabelEdit Event

I've got a treeview control in my form. I would like to be able to edit the node in the tree and push the change to Access DB. However, I'm having trouble finding an appropriate event. MSDN treeview events reference page advices AfterLabelEdit, but I am not able to get it to work. Is anyone aware of any workaround/solution?
Snippet of the code I use (I've added onClick and onDblClick for comparison, as they work):
Private Sub xMyTreeview_Click() 'OK
testit
End Sub
Private Sub xMyTreeview_DblClick() 'OK
EditLabel
End Sub
Private Sub xMyTreeview_AfterLabelEdit() 'Problem
AfterLabel
End Sub
Sub EditLabel()
Me.xMyTreeview.StartLabelEdit
End Sub
Sub AfterLabel()
MsgBox prompt, vbOKOnly, "afterlabel"
End Sub
Sub testit()
Dim nodSelected As MSComctlLib.Node ' a variable for the currently selected node
Set nodSelected = Me.xMyTreeview.SelectedItem ' get the currently selected node
NodesStrLength = Len(nodSelected.Key)
Dim NodeStr As String
Dim StrToChange As String
StrToChange = nodSelected.Key
NodeStr = Mid(StrToChange, 2, NodesStrLength - 1)
Me.txtNodeID = NodeStr
Me.sfrmDOCNODE.Visible = True
End Sub
Your link is to the .net Windows Form Treeview but MSComctlLib is the older Visual Basic Common Controls: http://msdn.microsoft.com/en-us/library/aa443023(v=vs.60).aspx
Note the event prototype is different; if you don't declare it with the correct arguments it will never get raised;
Private Sub object_AfterLabelEdit(cancel As Integer, newstring As String)

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