Microsoft Access VBA Tutorials for self-paced learning, Class Modules, SQL Techniques, and AI Integration Guides.

WithEvents and Report Line Highlighting

Draw a Circle Around Marks within a TextBox.

    This article is essentially a revisit of an earlier post titled “Highlighting Reports” (August 2007). In that earlier version, the entire code resided within the Report’s Class Module.

    The key change here is that we are moving all the code from the Report’s Class Module into a standalone Class Module, thereby freeing up the Report’s own module. The Print event of the Report’s Detail section is now captured within the Class Module object, which executes the code to highlight the desired report line item.

    If you have experimented with the sample reports included in the demo databases from the previous two posts. You will find that much of the code used here (in the Class Module) is already familiar to you.

    If you have not yet seen those articles, their links are provided below:

  • WithEvents and Access Report Event Sink.
  • WithEvents and Report Lines Hiding.

The Highlight of this Project.

The OnPrint() event procedure of the Report’s Detail section draws an oval-shaped outline around the Text Box that displays the exam marks retrieved from the Students table.

If a student fails to achieve the minimum passing mark of 60%, the corresponding marks are visually highlighted by enclosing them within this oval-shaped outline.

A sample image from the Report’s Print Preview demonstrating this feature is shown below:


Even though the report design is quite simple, I would like to draw your attention specifically to the Text Box that displays the marks, where we dynamically draw the oval shape to highlight the values.  

The oval shape is drawn within the boundaries of the Total Text Box. However, the size of this Text Box should be chosen carefully—it should neither be too wide (like the Remarks label) nor too narrow. Although the shape can still be drawn within any-sized Text Box, if the size is inappropriate, parts of the oval may overlap the text or fail to align properly around the marks.

The Class Module.

More details on this when we review the Class Module Code.

The Class Module: ClsStudentHighlight Code is given below:

Option Compare Database
Option Explicit

Private WithEvents Rpt As Access.Report
Private WithEvents secRpt As Access.[_SectionInReport]
Private WithEvents secFutr As Access.[_SectionInReport]

Private txt As Access.TextBox
Private max As Access.TextBox
Private pct As Access.TextBox
Private lgnd As Access.Label

Public Property Get mRpt() As Access.Report
   Set mRpt = Rpt
End Property

Public Property Set mRpt(RptNewVal As Access.Report)
Const strEvent = "[Event Procedure]"

On Error GoTo mRpt_Err

  Set Rpt = RptNewVal
  
  With Rpt
     Set secRpt = .Section(acDetail)
     Set secFutr = .Section(acFooter)
     secRpt.OnPrint = strEvent
     secFutr.OnPrint = strEvent
  End With

  Set txt = Rpt.Controls("Total")
  Set max = Rpt.Controls("Maxmarks")
  Set pct = Rpt.Controls("Percentage")
  Set lgnd = Rpt.Controls("Legend")

mRpt_Exit:
Exit Property

mRpt_Err:
MsgBox Err.Description, , "mRpt()"
Resume mRpt_Exit

End Property

Private Sub secRpt_Print(cancel As Integer, PrintCount As Integer)
'  Draw ellipse around controls that meet specified criteria.

Dim m_max As Double
Dim m_pct As Double
Dim curval As Double
Dim pf As Double
Dim pp As Double
Dim yn As Boolean

On Error GoTo secRpt_Print_Err

m_max = max.Value 'read Maxmarks TextBox Value
pp = pct.Value 'read Pass Percentage TextBox value

curval = Nz(txt.Value, 0) 'read obtained marks from Total TextBox
pf = Int(curval / m_max * 100 ^ 2) / 100 'calculate obtained marks percentage
yn = (pf >= pp) 'Passed or Not (TRUE/FALSE)

'call the DrawCircle Subroutine with Pass/Fail flag
'and the Control as parameters
Call DrawCircle(yn, txt)

secRpt_Print_Exit:
Exit Sub

secRpt_Print_Err:
MsgBox Err.Description, , "secRpt_Print"
Resume secRpt_Print_Exit

End Sub

Private Sub secFutr_Print(cancel As Integer, PrintCount As Integer)
Dim y As Boolean, lbl As Control

On Error GoTo secFutr_Print_Err

y = False 'set the flag false to draw oval shape
Set lbl = lgnd 'pass label control in Page Footer
Call DrawCircle(y, lbl) 'draw circle in legend label

secFutr_Print_Exit:
Exit Sub

secFutr_Print_Err:
MsgBox Err.Description, , "secFutr_Print"
Resume secFutr_Print_Exit

End Sub

Private Sub DrawCircle(ByVal bool As Boolean, ovlCtl As Control)
Dim ctl As Control
Dim bolPrintCircle As Boolean
Dim sngAspect As Single
Dim intShapeHeight As Integer
Dim intShapeWidth As Integer
Dim sngXCoord As Single
Dim sngYCoord As Single

On Error GoTo DrawCircle_Err

If bool Then 'if pass no highlighting, change logic for pass cases
    bolPrintCircle = False
Else 'highlight failed cases
    bolPrintCircle = True
End If

Set ctl = ovlCtl
        
    If Not IsNull(ctl) Then
        If bolPrintCircle Then
           ' change this value to adjust the oval shape of the circle.
            sngAspect = 0.25
   
            ' Determine coordinates of ctl and to draw ellipse.
            ' Determine height and width of ellipse.
            intShapeHeight = ctl.Height
            intShapeWidth = ctl.Width
    
            'calculate circle vertical Y coordinate
            sngYCoord = ctl.Top + (intShapeHeight \ 2)

            'calculate horizontal X coordinate of circile
            sngXCoord = ctl.Left + (intShapeWidth \ 2)
            
            'draw an ellipse around the Total TextBox
            Rpt.Circle (sngXCoord, sngYCoord), intShapeWidth \ 2, RGB(255, 0, 0), , , sngAspect
          bolPrintCircle = False
        End If
    End If


DrawCircle_Exit:
Exit Sub

DrawCircle_Err:
MsgBox Err.Description, , "DrawCircle()"
Resume DrawCircle_Exit

End Sub
 

In the Class Module’s property declarations, the first line defines the Report object in the Rpt variable.

The next two lines declare the Detail and Footer sections of the Report as secRpt and secFutr objects, respectively.

Following that, three TextBox objects—txt, max, and pct—are declared to access values from their corresponding controls on the Report.

Finally, a Label control is declared to function as a legend symbol. This label, together with another label caption: “Not Successful,” will serve as a visual indicator explaining the meaning of the oval symbol drawn around a student’s marks.

The Property Get procedure is not actually required in this module and is included only for completeness, as the Rpt object is never accessed from outside the module.

The Property Set procedure receives the current Report object as a parameter from the Report’s Open (or Load) event procedure and assigns it to the Rpt object.

After this assignment, the Detail and Footer sections of the report are assigned to the secRpt and secFutr properties, respectively. These assignments also enable their Print events to be captured when they are triggered on the report.

Finally, the next three lines in the code assign the report’s TextBox controls to the txt, max, and pct properties declared in the module’s global area.

There is an empty Label control named Legend placed in the Report Footer section. A Label property named lbl is declared in the global area of the Class Module. Within the Set property procedure, the Legend label is assigned to the lbl property using the statement:

Set lbl = Rpt.Controls("Legend").

The Class Module contains three subroutines:

  • secRpt_Print() — triggered by the Print event of the Detail section.

  • secFutr_Print() — triggered by the Print event of the Report Footer section.

  • DrawCircle() — a shared routine called from both of the above subroutines to draw an oval (ellipse) shape around certain Total text boxes in the Detail section, and around the Legend label in the Footer section.

The Report Detail Section Print Event

When the Report’s Detail section begins printing (in Print Preview mode, not in Report View), the Print event is triggered, and the secRpt_Print() subroutine captures this event and starts executing the code.

The values from the TextBox properties (max, pct, and txt) are read into the local variables m_max, pp, and curval, respectively. The student’s percentage of marks is then calculated with two decimal places and compared against the pass percentage (pp). Based on this comparison, the result—Passed (TRUE) or Not Successful (FALSE)—is stored in the Boolean variable yn.

Finally, the DrawCircle() subroutine is called, passing yn as the first parameter and the Total TextBox control as the second parameter.

The DrawCircle() Sub-Routine.

The DrawCircle() subroutine first checks whether the Boolean value received as its first parameter is TRUE or FALSE. Based on this, a local Boolean variable named bolPrintCircle is set accordingly. This variable is a flag to signal whether the circle-drawing code segment should execute or be skipped.

In this sample demo, the focus is on highlighting the marks of students not successful. When a student’s calculated percentage is below 60%, the yn flag is set to FALSE. Consequently, when yn is FALSE, bolPrintCircle is set to TRUE, instructing the routine to draw an oval shape around those marks.

The TextBox’s positional values—Left, Top, and its Width and Height—are then used to calculate the center point (horizontal and vertical coordinates) of the circle. The radius of the circle is determined as half the width of the TextBox.

If the TextBox is too wide, the circle drawn within it will appear distorted—the top and bottom parts may be cut off, while the left and right edges will look like two separate arcs. To fix this, you need to reduce the vertical radius of the circle relative to the horizontal radius calculated from the TextBox width.

This adjustment is done by setting the circle's aspect ratio. For example, by setting

sngAspect = 0.25, the vertical radius becomes one-fourth of the horizontal radius, producing a neat oval shape around the TextBox value instead of a distorted circle.

Aligning Text inside the Text Box

The TextBox value is horizontally centered within the control. However, vertically, the text usually appears near the top edge of the TextBox (and therefore close to the top edge of the circle as well). To visually center the value vertically inside the oval shape, the Top Margin is manually set to 0.1 cm in Design View. This property can only be adjusted at design time, not through code.

In the Report Footer Section, there is a label control named Legend. During the Report_Footer_Print() event, the DrawCircle() subroutine is called with this label control as a parameter to draw an oval shape inside it. Another label control with the caption “Not Successful” is placed alongside, serving as a legend to explain the meaning of the oval shape drawn around the marks of students who did not achieve passing scores.

Report Module Code

Option Compare Database
Option Explicit

Private R As New ClsStudentHighlight

Private Sub Report_Load()
  Set R.mRpt = Me
End Sub

The ClsStudentHighlight Class Module is instantiated in Object R.

On the Report_Load() Event Procedure, the current Report Object is assigned to the Property R.mRpt.

Download the Demo database from the link given below and try out the Report and Code.



Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types
Share:

WithEvents and Report Line Hiding

Report Event Handling.

If you have already tried last week’s Report OnPrint() event trapping in the Access Class Module, this next step will be easier to follow. In that earlier example, we captured the Report’s Detail Section OnPrint event inside a Class Module, validated the field values, and highlighted the TextBox and marks of students who passed their exams. We will do something similar here as well.

However, if our goal is only to highlight Text Boxes on the Report with colors or apply styles like Bold, Italic, or Underline, then we don’t need to capture the Print event in a Class Module. This can be done more simply using Conditional Formatting.

For example, in the Conditional Formatting dialog box, we can use an expression like:

Expression Is: [Total]/[MaxMarks]*100 >= [Percentage]

This will highlight the qualifying students’ marks automatically, based on the percentage threshold.

But Conditional Formatting has its limitations. It cannot perform tasks like changing the font type, adjusting the font size, or highlighting the border of the Text Box. These advanced styling changes can be done through event handling in the Class Module.

In the previous example, we captured the Detail Section OnPrint() event. This time, we will try out the OnFormat() built-in event to hide certain report lines, leaving only the required ones (either the passed or failed students) visible on the Report.

We will reuse the same Report from last week’s example to generate either a Passed-Students-List or a Failed-Students-List from the same report.

Sample Images of Report View, Print Previews

1. Full List of Students in Report View (not in Print Preview)

The complete list of students is shown below in Report View.

Keep in mind that while the formatting and printing processes do occur internally in Report View, the built-in Report events (such as OnFormat and OnPrint) are not triggered in this mode. These events are fired only when the report is opened in Print Preview mode.


2. The Passed Students' List in Print Preview is achieved by preventing the failed students' report lines from appearing on the Report.

3.  Failed Students' List in Print Preview and Passed Students report lines will not appear on the Report.

Report options 2 and 3 are prepared without applying any filtering condition directly on the source data,  but showing or hiding the report lines in the Detail Section's Format Event does the job.

Class Module: ClsStudentsList VBA Code

Private WithEvents Rpt As Access.Report
Private WithEvents secRpt As Access.[_SectionInReport]

Private txt As Access.TextBox
Private max As Access.TextBox
Private pct As Access.TextBox
Private i As Integer

Public Property Get mRpt() As Access.Report
   Set mRpt = Rpt
End Property

Public Property Set mRpt(RptNewVal As Access.Report)
Dim msg As String
Const strEvent = "[Event Procedure]"

  Set Rpt = RptNewVal

  With Rpt
     Set secRpt = .Section(acDetail)
     secRpt.OnFormat = strEvent
  End With
  
  msg = "1. Passed List" & vbCr & "2. Failed List"
  i = 0
  Do While i < 1 Or i > 2
    i = Val(InputBox(msg, "Report Options", 1))
  Loop

  Set txt = Rpt.Controls("Total")
  Set max = Rpt.Controls("MaxMarks")
  Set pct = Rpt.Controls("Percentage")
End Property

Private Sub secRpt_Format(Cancel As Integer, FormatCount As Integer)
Dim curval As Double
Dim m_Max As Double
Dim m_pass As Double
Dim mk As Double
Dim pf As Double
Dim yn As Boolean
Dim lbl As Access.Label

On Error GoTo secRpt_Print_Err

m_Max = max.Value
pp = pct.Value
curval = txt.Value

pf = curval / m_Max * 100

yn = (pf >= pp)

Set lbl = Rpt.Controls("lblpass")

secRpt.Visible = False
       
       If yn Then
            txt.FontBold = True
            txt.FontSize = 12
            txt.BorderStyle = 1
            lbl.Caption = "Passed"
            lbl.ForeColor = RGB(0, FF, 0)
            lbl.FontBold = True
                If i = 1 Then
                    secRpt.Visible = True
                End If
        Else
            txt.FontBold = False
            txt.FontSize = 9
            txt.BorderStyle = 0
            lbl.Caption = "Failed"
            lbl.FontBold = False
                If i = 2 Then
                    secRpt.Visible = True
                End If
        End If

secRpt_Print_Exit:
Exit Sub

secRpt_Print_Err:
MsgBox Err.Description, , "secRpt_Print()"
Resume secRpt_Print_Exit
End Sub

In the global declaration area of the Class Module, two key properties are declared: the Report property Rpt and the Report Section property secRpt.

Next, three Text Box control propertiestxt, max, and pct—are declared. These will later be assigned references to their corresponding Text Box controls on the Report. Through these references, we can read the student’s marks (txt), the maximum marks (max, representing the total marks for all subjects), and the pass percentage (pct). These values are used to calculate each student’s percentage score and categorize them as either Passed or Failed.

An Integer-type property i is also declared to hold the Report option chosen by the user at runtime:

  • 1 for Passed Students List

  • 2 for Failed Students List

 Set mRpt(): The Property procedure assigns the active Report object to the Rpt property when called from the Report_Open() event. It also passes the user-selected report option (RptNewVal) into the Rpt object.

Immediately after assigning the Rpt object, the Report Detail Section is assigned to the secRpt property, and the Detail Section’s OnFormat event is enabled within the same Set mRpt() procedure. This allows the Class Module to capture the OnFormat event and apply formatting logic line by line during the report’s generation.

With Rpt
     Set secRpt = .Section(acDetail)
     secRpt.OnFormat = strEvent
  End With 

We cannot use last week’s OnPrint() event to hide specific report lines because the Print event occurs after the Formatting phase has been completed. Once the report enters the printing stage, it is too late to selectively hide individual lines. If we attempt to execute secRpt.Visible = False at this point, it will hide the entire Detail Section altogether, rather than just the specific lines we want to remove.

Report Events before Viewing/Printing.

  1. During the first pass of report formatting, the Format event is triggered. At this stage, Access determines which data will appear on the report page, and our programmed logic and conditional actions are executed.

  2. In the second formatting pass, the selected report lines are prepared and laid out for printing or previewing.

  3. The Print event fires immediately after the second formatting pass, just before the formatted report lines are printed on the Detail Section.

Inside the Do While ... Loop, an InputBox() statement is used to obtain the user’s choice for the report option:

1 – Passed Students List
2 – Failed Students List

The user must enter either 1 or 2. Any other value is rejected, and the Do While...Loop continues prompting until a valid input is received and stored in the variable i.

After receiving a valid option, the next three statements assign the Report’s TextBox controls to their respective declared Properties in the Class Module.

When the report is opened in Print Preview mode, the Detail Section’s Format event is triggered and captured in the secRpt_Format() subroutine of the ClsStudentLine Class Module. A few local variables are declared at the start of this subroutine.

The Maximum Marks and Pass Percentage values are read from their respective TextBox controls and assigned to the max and pct properties.
The statement curval = txt.value retrieves each student’s total marks and assigns them to the curval variable.

Next, the statement pf = curval / m_Max * 100 calculates the percentage of marks obtained by the student.

The statement yn = (pf >= pp) compares the student’s obtained percentage (pf) with the Pass Percentage (pp).

  • If the obtained percentage is greater than or equal to the pass percentage, yn = TRUE (Student Passed).

  • Otherwise, yn = FALSE (student failed).

The lbl property is assigned to the Label control that appears to the right of the Total Marks TextBox on the Report.

Initially, the Report Detail Section is kept hidden. When a student is found to be in the Passed category (yn = TRUE), the Total Marks TextBox is formatted (highlighted), and the Label control’s Caption is set to "Passed".

If yn = FALSE, the formatting is reset to normal, and the Label Caption is set to "Failed", depending on which Report option the user has selected.

The user’s choice is obtained from the statement i = InputBox() within the Do While...Loop, which prompts for one of the two options:

1 – Passed Students List
2 – Failed Students List.

How it works.

Option 1:

If the student in the current line of the Detail Section is found to have passed the exam, the Detail Section is made visible, allowing that report line to appear on the Report. This check is performed for each line of the Report, and only the passed students’ lines are displayed.

Option 2:

If Option 2 is selected, the Detail Section is made visible only for failed students’ data lines, while all other lines are hidden from the Report.

Report Class Module Code.

The Report Class Module Code is given below:

Option Compare Database
Option Explicit

Private R As New ClsStudentsList

Private Sub Report_Open(Cancel As Integer)
  Set R.mRpt = Me
End Sub

In the Report’s Code Module, the ClsStudentList Class Module is instantiated as the object R. In the Report_Open() event procedure, the current Report object (Me) is passed to the class object through its R.mRpt() Set property procedure. These are the only lines of code required within the Report’s own Class Module.

All other operations are handled internally, behind the scenes, by the ClsStudentList Class Module.

Note: Always open the report in Print Preview mode (not in Report View) to ensure that the Format event in the Detail section is triggered.

Download the Demo Database from the Link given below and try out the Report and Code.


Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

WithEvents and Access Report Event Sink

Access Report and Print Format Events.

We have explored several WithEvents examples using Class Modules, capturing both built-in and user-defined events from form-based controls. We created Class Module object arrays for multiple TextBox controls on a form, as well as separate Class object instances for each TextBox on the form or its sub-forms, and added them as items of the Collection object.

We observed that both approaches—arrays and collection items—work effectively for capturing events raised from Form-based TextBoxes and in executing their respective event-handling procedures.

There are still other control types we can experiment with alongside TextBoxes when multiple controls are present on the same form, and we will definitely explore those later.

For now, after this lengthy trial run with TextBox controls on forms, it’s time for a change of scene—to move away from Access Forms for a while and try a few simple event-handling demo runs in Microsoft Access Reports.

Preparations.

Need the following Objects for the trial run:

  1. Table with Students' Names and Marks.

  2. Report designed with the Students Table.

  3. Class Module to handle the Report Print Event.

We need a sample table with a few student names and total exam marks. Our task is to highlight the marks of students who have passed and update a Label control positioned to the right of their marks, setting its Caption to the specified value.

Image of sample Table: Students

Sample Report designed using the above Table, and the sample image of the Report is given below, without running the Event Procedures.


Report Image Contents

The Report’s Detail section displays the table records, including employee names and their total marks. In the Report Header section, additional information explains how the pass percentage is calculated. The maximum marks for all subjects are 600, and students scoring 60% or above are considered to have passed.

A Text Box labeled Set Pass % is placed on the far right side of the Header section. The user can input a different percentage value here (as a whole number), which is currently set to 65. Based on this value, the Print event procedure calculates each student’s percentage and highlights the marks of students who meet or exceed the threshold. A label control with the caption Passed will also appear to the right of their marks.

An example report output, after executing the event procedure and highlighting the passed students’ marks, is shown below:



Class Module Code

The Class Module: ClsStudents VBA Code that handles the Report Detail Section Print Event is given below:

Option Explicit

Private txt As Access.TextBox
Private pct As Access.TextBox
Private max As Access.TextBox

Private WithEvents Rpt As Access.Report
Private WithEvents secRpt As Access.[_SectionInReport]

Public Property Get mRpt() As Access.Report
   Set mRpt = Rpt
End Property

Public Property Set mRpt(RptNewVal As Access.Report)
Const strEvent = "[Event Procedure]"

  Set Rpt = RptNewVal
  With Rpt
     Set secRpt = .Section(acDetail)
     secRpt.OnPrint = strEvent
  End With
  
  Set txt = Rpt.Controls("Total")
  Set max = Rpt.Controls("Maxmarks")
  Set pct = Rpt.Controls("Percentage")

End Property

Private Sub secRpt_print(Cancel As Integer, printCount As Integer)
Dim curval As Double
Dim m_Max As Double
Dim pf As Double
Dim pp As Double
Dim lbl As Access.Label

On Error GoTo secRpt_Print_Err

Set lbl = Rpt.Controls("lblPass") 'set reference to Label: lblpass

m_Max = max.Value 'retrieve Maximum Marks (600)
curval = txt.Value 'get current Report Line 'Total' TextBox value
pp = pct.Value 'get the percentage value (65)

pf = curval / m_Max * 100 'calculate obtained marks percentage

If pf >= pp Then 'if it is greater or equal to 65
    txt.FontBold = True
    txt.FontSize = 12
    txt.BorderStyle = 1
    lbl.Caption = "Passed" 'change label caption to 'passed'
Else 'reset to normal
    txt.FontBold = False
    txt.FontSize = 9
    txt.BorderStyle = 0
    lbl.Caption = ""
End If

secRpt_Print_Exit:
Exit Sub

secRpt_Print_Err:
MsgBox Err.Description, , "secRpt_Print()"
Resume secRpt_Print_Exit
End Sub

Let’s briefly review what happens within the above Class Module.

In the global declarations section of the Class Module, three Text Box control properties are defined. The first property, txt, will be assigned the marks of each student from the Detail section of the Report during its line-by-line printing phase.

The next two properties, max and pct, will be linked to the MaxMarks and Percentage Text Boxes located in the Report Header section. These will hold the maximum marks (e.g., 600) and the pass percentage (e.g., 65), respectively. These values are used to calculate each student’s percentage score.

Following these are two more declarations: the Rpt property, which references the Report object itself, and the secRpt property, which references the Report’s Detail section.

The only Get and Set property procedures in this Class Module are for the Report object. They receive the active Report object from the Report’s Class Module and assign it to the Rpt property. Once this reference is set, the secRpt property is assigned to the Report’s Detail section, and the Report_Detail_Section_OnPrint() event handler is enabled using the following statements:


Set Rpt = RptNewVal
  With Rpt
     Set secRpt = .Section(acDetail)
     secRpt.OnPrint = strEvent
  End With

The next three statements assign references of the Report’s Text Box controls to the txt, pct, and max properties declared at the top of the Class Module.

Before the Report is displayed in Print Preview or sent to the printer, Access performs several formatting passes to arrange the content on each page, line by line. Only after these formatting passes are complete does the Print action occur—the final phase in preparing and rendering each page of the Report.

The Report.Section(acDetail).OnPrint() Event.

We are specifically interested in the Print event of the Report’s Detail section, which we capture in the secRpt_Print() subroutine. During this event, the student’s total marks are retrieved into the curval variable using the expression curval = txt.value. The program then calculates the percentage of marks obtained out of 600 and compares it with the pass percentage specified in the Report Header.

If the student meets or exceeds the pass percentage, their total marks TextBox is visually highlighted—the border is emphasized, the font size is increased to 12 points, and the font style is set to Bold. Additionally, a label control appears to the right of the Text Box with the caption “Passed”.

The Report_Students Class Module Code is given below.

Option Compare Database
Option Explicit

Private R As New ClsStudents

Private Sub Report_Open(Cancel As Integer)
  Set R.mRpt = Me
End Sub

The Class Module ClsStudents is instantiated in Class Object R.

On the Report_Open Event, the current Report Object is passed to the Set Property Procedure Set R.mRpt().

Important Points to Note

Once the Report is fully designed and configured as described above, it’s time to view its contents and observe the Print event being captured by the Class Module object.

Microsoft Access provides several viewing modes for Reports besides Design View, such as:

  • Report View – Displays the report as a scrollable, interactive layout without pagination.

  • Print Preview – Shows how the report will appear when printed, with page breaks and formatting applied.

  • Layout View – Allows you to adjust the layout while viewing live data.

For our demonstration, Print Preview is the most suitable option because it triggers the Print event for each detail line, allowing our event-handling code in the Class Module to execute as intended.

The Report or Report Section onPrint or Format Event will not fire on the first two Report Views. 

You can find the Report with Data and the way you designed it.  But you will not find the result of your Event Procedure running in the Class Module if you use the first two methods.

In that case, use the following methods:

  1. Right-click on the Report in the navigation pane and select Print Preview from the displayed menu.
  2. If you double-clicked on the Report in the navigation pane and you ended up in the Report view mode, then right-click on an empty area in the Report View and select Print Preview from the displayed menu.

If you are not using Access 2007, always ensure to open the report in Print Preview mode—using whichever option is available in your version—to ensure that the Report_Print or Report_Format events are triggered.

Summary

The active Report Detail Section OnPrint event is enabled from within the Set mRpt() Property Procedure of the ClsStudents Class Module. When this event is raised on the Report, it is captured within the Class Module itself through the Private Sub secRpt_Print() procedure. Each data line in the Report’s Detail Section is validated, and if the student is found qualified, their Marks Text Box is highlighted in the Report’s Print Preview.

All these actions are handled entirely within the Class Module, keeping the Report’s own Class Module almost empty—containing only four lines of code.

A demo database is attached. You may download it to try out the example and study the code. Experiment with something similar on your own as a self-test, using the demo as a reference point whenever you are unsure about syntax or other details.

In the next session, we will explore how to print only the passed students on the Report without using a Query to filter the data. As a hint, we will achieve this by hiding the failed students’ lines in the Report’s Detail Section.

Downloads.




Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types
Share:

WithEvents in Class Module and Data Entry Form

1. Class Module and Data Entry Form.

So far, we have used unbound text boxes on the form for demo runs and for capturing built-in events within the Class Module object. Since text boxes are the primary controls used on forms for data entry and viewing of information, they were chosen first to demonstrate the most commonly used built-in events: AfterUpdate, OnGotFocus, and OnLostFocus.

However, this does not mean that we have ignored other controls such as command buttons, combo boxes, list boxes, and option buttons. Most of these controls primarily use the Click event to open forms or reports, run macros, call subroutines/functions, or select items from a list or combo box. Their event procedures are generally simpler to handle in the Class Module, and we will include them along with text boxes in our approach.

Once you are comfortable handling text box–based events and understand how they work, adding support for other controls will be a straightforward extension of the same concept.

Creating a Class Module object instance—one instance for each text box on the form—and adding them to an array, each enabled with the required built-in events, may seem confusing at first, but it is not as complicated as it appears.

If you are new to programming with stand-alone Class Modules, it is highly recommended to start there. Understanding the basics will make it much easier to grasp their usage with WithEvents, Event, and RaiseEvent programming.

Refer to the following articles to get a general understanding of Class Module programming.

  1. MS-Access Class Module and VBA
  2. MS-Access VBA Class Object Arrays
  3. MS-Access Base Class and Derived Objects
  4. VBA Base Class and Derived Object-2
  5. Base Class and Derived Object Variants
  6. MS-Access Recordset and Class Module
  7. Access Class Module and Wrapper Classes
  8. Wrapper Class Functionality Transformation

2. Brief Review of what is covered so far.

In the past few weeks, we have begun exploring Class Module and Form Controls Event Programming in Microsoft Access. As part of this journey, we have learned how to create a Class Module object array element for each text box on a form.

The basic steps for preparing a Class Module to handle events for a text box are as follows:

  1. Create a new Class Module and declare a property to hold a Text Box (Access.TextBox) object, using a variable name such as txt.

  2. If the property is declared as Private, then create corresponding Property Get and Property Set procedures to manage access to the txt object.

  3. In the Class Module, create event-handling procedures for the AfterUpdate, GotFocus, and LostFocus events of the text box.

When an instance of the above Class Module is created in memory, it can handle the events (AfterUpdate, GotFocus, and LostFocus) of only one text box on the form.

If there are multiple text boxes—for example, three text boxes—on the form, then we must create three separate instances of the same Class Module, one for each text box.

To keep track of all these instances, they should be stored either in an array or added as items in a Collection object, so that each text box has its own dedicated Class Module instance.

If, instead, the same Class Module instance is shared among multiple text boxes, the event procedures inside the Class Module would have to be modified. In that scenario, each event procedure must identify which text box triggered the event and then run the appropriate validation checks or other actions for that specific text box within the same subroutine.

Example:
The code shown below is taken from the AfterUpdate() event procedure.

In this example, the FirstName and Designation text boxes are listed in the Select Case ... End Select structure, but they don’t have any executable code under their respective Case blocks. This indicates that:

  • These text boxes exist on the form,

  • but their AfterUpdate event is not currently enabled or used.

Since no logic is implemented for them in the AfterUpdate() event, it is not strictly necessary to include their names in this procedure.

However, keeping them in the list can be useful during testing because it serves as a reminder that these text boxes exist on the form and can be enabled later if needed.

It’s also possible that these text boxes are intended to use a different event, such as OnLostFocus(), and their logic might be implemented in the corresponding LostFocus event procedure instead.

txtName = Txts.Name

Select Case txtName
    Case "LastName" 'TextBox Name
        txtval = Trim(Nz(Txts.Value, ""))

        If Len(txtval) > 15 Then
           msg = "LastName Max 15 chars only."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Left(Nz(txtval, ""), 15)
        End If
    Case "FirstName"
        '
    Case "Designation"
        '
    Case "BirthDate"
         db = CDate(Nz(Txts.Value, 0))
        
         If db > Date Then
           msg = "Future Date: " & db & " Invalid."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Null
           efrm!Age = Null

         ElseIf db > 0 Then
           dbage = Int((Date - db) / 365)
           efrm!Age = dbage
         End If
    Case "Age"
         Dim xage As Integer
         db = CDate(Nz(efrm!BirthDate, 0))
         xage = Nz(Txts.Value, 0)
         
         If (db > 0) And (xage > 0) Then
            dbage = Int((Date - db) / 365)
            If xage <> dbage Then
                msg = "Correct Age as per DB = " & dbage
                MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
                Txts.Value = dbage
            End If
         ElseIf (xage = 0) And (db > 0) Then
            dbage = Int((Date - db) / 365)
            Txts.Value = dbage
         End If
    Case "JoinDate"
       Dim jd As Date
       
       db = CDate(Nz(efrm!BirthDate, 0))
       jd = CDate(Nz(Txts.Value, 0))
       
       If (db > 0) And (jd < db) Then
          msg = "JoinDate < Birth Date Invalid!"
          MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
          Txts.Value = Null
       End If
End Select

Note:

If you need to read or write values from or to any other text box (other than the event-triggered text box), you must include the Property 'Access.Form' in the text box class module (ClsTxtEmployees). This property allows you to reference other text boxes on the same form for reading or updating their values as needed.

This class module can also serve as a template for handling text boxes on other forms. However, you will need to customize the subroutines according to the specific requirements of each form’s text box controls.

For your convenience, links to all the earlier articles in this series are provided below, especially if you are new to working with WithEvents, Event, and RaiseEvent in user-defined or built-in event programming with Microsoft Access.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events

3. What is New in this Post?

Here, we have a sample Employee Data Entry Form bound to a table. If you have gone through the earlier examples that used unbound text boxes on a form, you will notice no functional difference in this demo. The purpose here is simply to demonstrate how event handling in a class module object works when the form is bound to a table and the text boxes use table fields as their control sources.

The sample image of the data entry form is shown below. An error message (manually positioned at the bottom-right corner in the image) appears when an invalid Join Date—earlier than the Birth Date—is entered in the field.


In the first Employee Form demo, the Command Button click event is handled directly in the Form’s own Class Module.

In this demo, the Form’s Class Module works together with a separate Class Module that includes two Private properties:

  • The Property 'Txts' for the TextBox object, and

  • efrm for the Access.Form object.

4.  New Class Module Code.

Insert a new Class Module and change its Name Property value from Class1 to ClsTextEmployees.

Copy and Paste the following VBA Code into the Class Module and save the Code:

Option Compare Database
Option Explicit

Private efrm As Access.Form
Private WithEvents Txts As Access.TextBox

Public Property Get pfrm() As Access.Form
  Set pfrm = efrm
End Property

Public Property Set pfrm(ByRef vNewValue As Access.Form)
  Set efrm = vNewValue
End Property

Public Property Get pTxts() As Access.TextBox
  Set pTxts = Txts
End Property

Public Property Set pTxts(ByRef vNewValue As Access.TextBox)
  Set Txts = vNewValue
End Property

Private Sub Txts_AfterUpdate()
Dim txtName As String, txt As String
Dim msg As String, txtval As Variant
Dim db As Date, dbage As Integer

txtName = Txts.Name

Select Case txtName
    Case "LastName"
        txtval = Trim(Nz(Txts.Value, ""))

        If Len(txtval) > 15 Then
           msg = "LastName Max 15 chars only."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Left(Nz(txtval, ""), 15)
        End If
    Case "FirstName"
        '
    Case "Designation"
        '
    Case "BirthDate"
         db = CDate(Nz(Txts.Value, 0))
        
         If db > Date Then
           msg = "Future Date: " & db & " Invalid."
           MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
           Txts.Value = Null
           efrm!Age = Null

         ElseIf db > 0 Then
           dbage = Int((Date - db) / 365)
           efrm!Age = dbage
         End If
    Case "Age"
         Dim xage As Integer
         db = CDate(Nz(efrm!BirthDate, 0))
         xage = Nz(Txts.Value, 0)
         
         If (db > 0) And (xage > 0) Then
            dbage = Int((Date - db) / 365)
            If xage <> dbage Then
                msg = "Correct Age as per DB = " & dbage
                MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
                Txts.Value = dbage
            End If
         ElseIf (xage = 0) And (db > 0) Then
            dbage = Int((Date - db) / 365)
            Txts.Value = dbage
         End If
    Case "JoinDate"
       Dim jd As Date
       
       db = CDate(Nz(efrm!BirthDate, 0))
       jd = CDate(Nz(Txts.Value, 0))
       
       If (db > 0) And (jd < db) Then
          msg = "JoinDate < Birth Date Invalid!"
          MsgBox msg, vbInformation, txtName & "_AfterUpdate()"
          Txts.Value = Null
       End If
End Select
End Sub

Private Sub Txts_LostFocus()
Dim txtName As String
Dim msg As String, txtval As Variant

txtName = Txts.Name
txtval = Trim(Nz(Txts.Value, ""))

Select Case txtName
    Case "LastName"
        '
    Case "FirstName"
        If Len(txtval) = 0 Then
           msg = "FirstName should not be Blank."
           MsgBox msg, vbInformation, txtName & "_LostFocus()"
           Txts.Value = "XXXXXXXXXX"
        End If
    Case "Designation"
        If Len(txtval) = 0 Then
           msg = "Designation Field is Empty."
           MsgBox msg, vbInformation, txtName & "_LostFocus()"
           Txts.Value = "XXXXXXXXXX"
        End If
    Case "BirthDate"
        '
    Case "Age"
        '
    Case "JoinDate"
        '
End Select

End Sub

5. Class Module Properties and Sub-Routines.

In the global section of the Class Module, a Private Property named efrm is declared to hold the 'Access.Form' object.

Next, an 'Access.TextBox' control is declared as a Private Property named Txts using the WithEvents keyword. The WithEvents keyword enables the Txts object to capture and respond to programmed Events triggered on the Form.

Because these Class Module properties are declared as Private, they are accessible only within the Class Module itself, preventing direct external access. To allow values to be assigned or retrieved from outside, the module must define Public Get and Set Property Procedures.

The first pair of Get/Set Property procedures manages the efrm (Access.Form) object.

The second pair of Get/Set Property procedures manages the Txts (Access.TextBox) object.

Any validation checks on the Set Property Procedure parameter can be performed before attempting to assign the value to the Property efrm or the Property Txts.

In the Class Module, we are handling only two types of built-in Events, AfterUpdate and LostFocus Events from TextBoxes on the Form.

The AfterUpdate() event procedures validate the values entered in the LastName, BirthDate, Age, and JoinDate fields, and display appropriate messages—mainly to confirm that the programmed events are being correctly captured by the Class Module instances.

LostFocus() The event procedure validates the FirstName and Designation values. If either of these fields is left empty when it loses focus, a default text string “XXXXXXXXXX” is assigned to the field.

All the TextBox control names are listed within the Select Case … End Select structure for clarity, though some of them have no executable code yet. In the LostFocus event procedure, actual code has been written only for the FirstName and Designation fields. The LostFocus event is enabled on the form only for these two fields; others are included for clarity and possible future use.

The first TextBox on the form is bound to an AutoNumber field, which generates values automatically. Since no events are intended to be trapped for this control, it is excluded from the Select Case … End Select structure and no events are enabled for it.

6.  Employees Form Module Code.

The VBA Code behind the Employees Form's Class Module is given below:

Option Compare Database
Option Explicit

Dim tc As ClstxtEmployee
Dim C As Collection

Private Sub cmdClose_Click()
'Command Button Click Event is handled
'on the Form Module itself, the Event is
'not programmed in Class Module: ClsTxtEmployee
DoCmd.Close
End Sub

Private Sub Form_Load()
Dim ctl As Control

Set C = New Collection

For Each ctl In Me.Controls
  If TypeName(ctl) = "TextBox" Then
  
     Set tc = New ClstxtEmployee
     
     'Form Object is required to read/write values
     'from other TextbOX, if needed.
    Set tc.pfrm = Me
    'assign TextBox control to the Class Module instance's Property
    Set tc.pTxts = ctl
    
    Select Case ctl.Name
          Case "FirstName", "Designation"
          'enable LostFocus Event for FirstName and Designation
               tc.pTxts.OnLostFocus = "[Event Procedure]"
          Case Else
           'enable AfterUpdate for all other Text Boxes
               tc.pTxts.AfterUpdate = "[Event Procedure]"
    End Select
  End If
  C.Add tc 'add ClstxtEmployee instance as Collection Object Item
Next

End Sub

Private Sub Form_Unload(Cancel As Integer)
'when the form is closed erase Collection Object from memory
Set C = Nothing
End Sub

The Class Module ClsTxtEmployee is declared as a tc Object.

A Collection Object is declared in Object C.

On the Form_Load() Event Procedure, the Collection Object is instantiated.

Within the For Each ... Next loop, the Employees Form Text Box controls are picked and enabled for the required built-in Events.

For each text box on the Form, Employee, a new instance of the Class Module ClsTxtEmployee is created, and the Form Object and Text Control Property Values are passed to the Class Module Object.

7.  Derived Class Module to Replace Form Module Code.

As in the earlier examples, we will now create a derived class object (ClsTxtEmployeeHeader) using the ClsTxtEmployee class as its base. We will then move the existing Form Module code into this new Class Module, leaving only a minimal set of essential lines in the form’s own Class Module.

We will also transfer the Command Button Click Event handling into the Derived Class Module Object.

The Derived Class Module (ClsTxtEmployeeHeader) VBA Code is given below:

Option Compare Database
Option Explicit

Private tc As ClstxtEmployee
Private Col As New Collection

Private fm As Access.Form
Private WithEvents btn As Access.CommandButton


Public Property Get oFrm() As Access.Form
  Set oFrm = fm
End Property

Public Property Set oFrm(ByRef vNewValue As Access.Form)
  Set fm = vNewValue
  Call Class_Init
End Property

Private Sub Class_Init()
Dim ctl As Control

For Each ctl In fm.Controls
  Select Case TypeName(ctl)
  'If TypeName(ctl) = "TextBox" Then
    Case "TextBox"
        'create a new instance of Class Module Object
        Set tc = New ClstxtEmployee
        'assign common property values
        Set tc.pfrm = fm 'pass Form Employyes object to the new instance
        Set tc.pTxts = ctl  'pass text control
            
            'enable required event procedures for Text Boxes
            Select Case ctl.Name
                   'lostfocus event controls
                Case "FirstName", "Designation"
                    tc.pTxts.OnLostFocus = "[Event Procedure]"
                Case Else
                    'after Update Event
                    tc.pTxts.AfterUpdate = "[Event Procedure]"
            End Select
            Col.Add tc 'add to the collection object
    Case "CommandButton"
        Set btn = ctl
        btn.OnClick = "[Event Procedure]"
    End Select
Next

End Sub

Private Sub btn_Click()
   If MsgBox("Close the Form?", vbYesNo, btn.Name & "_Click()") = vbYes Then
    DoCmd.Close acForm, fm.Name
   End If
End Sub

In the newly created derived Class Module, the first two object declarations—one for the TextBox Class Module and the other for the Collection object—previously placed in the Employees Form’s Class Module are now moved into this Class Module.

Additionally, an 'Access.Form' object named fm is declared to obtain a reference to the Employees form. This reference is needed to read the value from the BirthDate TextBox, calculate the employee’s age, and update the Age TextBox on the form accordingly.

Next, a Command Button control object is declared within the derived Class Module using the WithEvents keyword to capture the Command Button’s Click event on the form.

Once the form object reference is received as a parameter in the Public Property Set oFrm() procedure and assigned to the fm object, the Class_Init() subroutine is called. This subroutine enables the AfterUpdate and LostFocus event handling, which were previously initialized from the Form_Load() event procedure.

Now, the only code remaining in the form’s Class Module is a few lines in the Form_Load() event that pass the current form object (Me) to the oFrm() property procedure of the derived Class Module ClsTxtEmployeeHeader.

This form reference is then passed on to each instance of the ClsTxtEmployee Class Module through the statement:

Set tc.pfrm = fm.

The Command Button’s Click event is also enabled and captured within the derived Class Module ClsTxtEmployeeHeader itself, through the btn_Click() event procedure.

8.  New Form using Derived Class Module: ClsTxtEmployeeHeader

The Image of the second sample form, after transferring all its Form Module Code into the Derived Class Module ClsTxtEmployeeHeader.

The EmployeeHeader Form's Class Module VBA Code is given below.

Option Compare Database
Option Explicit

Dim T As New ClsTxtEmployeeHeader

Private Sub Form_Load()
  Set T.oFrm = Me
End Sub

The Dim statement instantiates the derived object: ClsTxtEmployeeHeader in memory.

The Form_Load() Event Procedure passes the current form object to the class object T.oFrm() Property Procedure as a parameter.

9. Summary

The Derived Class Module holds the entire Form's Class Module Code, except for a few lines in the Form's Code Module.

All the code that would otherwise be written directly in the Form’s Class Module is now safely organized within the Base Class Module ClsTxtEmployees and the Derived Class Module ClsTxtEmployeeHeader.

When you need to create another form with similar functionality—either in this project or in other projects—you can simply reuse and customize these two Class Modules instead of writing new code in each form’s Class Module. This approach keeps your code well-organized, modular, and easier to maintain.

You can download the demo database provided at the end of this page, which includes the sample forms and Class Modules, and experiment with it.

Once you have explored the demo, try creating something similar on your own, using the demo database as a reference point, that can reinforce and validate what you have learned so far.

Downloads.




Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types
Share:

WithEvents in Class Module for Sub-Form Text Box Events

Introduction.

So far, we have demonstrated the use of WithEvents and built-in event capturing from a single form, with TextBox controls, executing subroutines in Class Object arrays or Collection objects. But how can we handle TextBox controls on Subforms of a main form—capturing their built-in events and processing them in class module objects?

For reference, the image of the sample form below displays two Subforms and multiple TextBoxes across all forms.

We will continue using the familiar AfterUpdate, OnGotFocus, and OnLostFocus events for TextBoxes on both the main form and its subforms. However, let us first focus on one or two aspects that we haven’t addressed so far.


  1. Introduction of the 'Access.Form' property in the Class Module for TextBox controls.

  2. How to reference TextBox controls on a subform to trigger built-in events and capture them in a Class Module.

  3. How to move the insertion point from the last TextBox on the first subform to the first TextBox on the second subform.

  4. How to move the insertion point from the last TextBox on the second subform to the only TextBox on the main form.

  5. How to use references to other controls to read from or write to form controls from within the current event procedure in the Class Module.

Form Property Declaration in the Class Module.

So far, we have worked with a single TextBox Property in the Class Module.  The Event Procedures also mostly deal with the TextBox name and validity of its contents. Sometimes we need values from another TextBox on the Form to calculate the current TextBox value. This calculation must be done when the OnGotFocus Event fires on the current TextBox.

For example, if we update the employee’s date of birth in one text box, we need to find the employee’s retirement date and update it in another Text Box.

Option 1: The date-of-birth TextBox after-update event procedure calculates the retirement date and updates it directly into the retirement date TextBox.

Option 2: After updating the date-of-birth Text Box, the cursor jumps to the Retirement Date control.  The retirement Date TextBox's OnGotFocus() Event Procedure reads the Date of Birth, calculates the Retirement Date, and updates the current field. 

In either case, you have to work with two text boxes to read or write the value in them.  To read/write the value in the other text box, we can reach it only through the Form Object. To do that, we need an 'Access.Form' Object as a Property in the Class Module, besides the Access.TextBox control Object.

The new Class Module (ClsSubForm) Code is given below:

Option Compare Database
Option Explicit

Public WithEvents txt1 As Access.TextBox
Private frm2 As Access.Form

Public Property Get sFrm() As Access.Form
  Set sFrm = frm2
End Property

Public Property Set sFrm(ByRef frmValue As Access.Form)
  Set frm2 = frmValue
End Property

Private Sub txt1_AfterUpdate()
Dim ctl As Control, ctlName As String
Dim bd As Date

ctlName = txt1.Name
Select Case ctlName
    Case "EmpName"
      If Len(Nz(txt1.Value, "")) > 10 Then
         txt1.Value = Left(txt1.Value, 10)
         MsgBox "Maximum 10 characters only allowed.", vbInformation, ctlName & "_AfterUpdate()"
      End If
    Case "BirthDate"
      bd = CDate(Nz(txt1.Value, 0))
      If bd > 0 And bd < Date Then
         MsgBox "BirthDate Valid:" & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
      End If
    Case "Email"
       MsgBox "Email Address: " & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
    Case "Mobile"
       MsgBox "Mobile No. " & txt1.Value, vbInformation, ctlName & "_AfterUpdate()"
End Select

End Sub

Private Sub txt1_GotFocus()
'Got Focus Event is set for only
'Retirement Date field
'Testing for the field name is not required
Dim vDate As Variant, rdate As Date
Dim tmpRef As Access.Form

   Set tmpRef = frm2.frmSubForm1.Form
   'vDate = frm2.frmSubForm1.Form!BirthDate
   'vDate = frm2.frmSubForm1.Form.Controls("BirthDate").Value
   vDate = tmpRef!BirthDate
   
If vDate > 0 Then
    vDate = CDate(vDate)
    'calculate retirement Age/Date
    rdate = vDate + Int(56 * 365.25)
   txt1.Value = rdate
End If
MsgBox "Retire Date: " & txt1.Value, vbInformation, txt1.Name & txt1.Name & "_GotFocus()"
End Sub

Private Sub txt1_LostFocus()
Dim txtname As String
txtname = txt1.Name
Select Case txtname
    Case "RetireDate"
         'frm2.Controls("frmSubForm2").SetFocus
          frm2.frmSubForm2.SetFocus
    
    Case "Email"
          MsgBox "Email Address: " & txt1.Value, vbInformation, txtname & "_LostFocus()"
    Case "Mobile"
          MsgBox "Mobile Number: " & txt1.Value, vbInformation, txtname & "_LostFocus()"
          'frm2.Controls("EmpName").SetFocus
            frm2.EmpName.SetFocus
    Case Else
        If CDate(Nz(txt1.Value, 0)) = 0 Then
            MsgBox "Birth Date is Empty!", vbInformation, txtname & "_LostFocus()"
        End If
End Select
End Sub

Class Module Properties and Sub-Routines.

The TextBox property is declared as a WithEvents object named txt1, with public scope. Although this breaks the usual encapsulation rule by exposing the property publicly, it is acceptable here for learning purposes.

Later, once you are comfortable with the concept, you can declare this property as Private and introduce Get and Set property procedures to access the txt1 object indirectly through these procedures, ensuring proper encapsulation.

The Form object frm2 is assigned to the Main Form (Form_frmMain)—which contains the sub-forms—so that we can read values from, or write values to, text box controls on the sub-forms.

The txt1_AfterUpdate() Event procedure in the ClsSubForm class module doesn’t include anything new, as we have already seen its functionality earlier. The BirthDate, Email, and Mobile text box controls are located on both sub-forms within the main form, and the AfterUpdate Events triggered on these sub-form controls are handled within this same subroutine.

The txt1_GotFocus() event procedure, used for the RetireDate text box, is also handled here. To calculate and insert the employee’s retirement date, we must first read the Date of Birth from the BirthDate text box on the first sub-form. To do this, we use the frm2 main form object property to directly reference that text box control.

Referencing Sub-Form Text Box from Class Module.

Different versions of the BirthDate Text Box references are given below:

vdate = frm2.frmSubForm1.Form!BirthDate
vDate = frm2.frmSubForm1.Form.Controls("BirthDate").Value

Or take the long route:

Dim tmpRef As Access.Form
Set tmpRef = frm2.frmSubForm1.Form
vDate = tmpRef!BirthDate

Now, moving on to the txt1_LostFocus() event procedure.
The key functionality implemented here is the automatic transfer of focus (insertion point) between text boxes across different forms.

  1. Moving the insertion point from the last Text Box on frmSubForm1 to the first Text Box on  frmSubForm2.
    frm2.frmSubForm2.SetFocus
    OR
    frm2.Controls("frmSubForm2").SetFocus
    
  2. Moving the insertion point from the last Text Box on Sub-form2 to the EmpName Text Box on the Main Form.
    frm2.EmpName.SetFocus
    OR
    frm2.Controls("EmpName").SetFocus
    

Common Pitfalls in Setting Focus on Sub-Form Control.

The pitfall that we often encounter in attempting to set the focus on a TextBox inside the sub-form directly is something like the following example:

frm2.frmSubForm2.Form.Email.SetFocus

The above statement is logically correct and will not produce any error messages when executed, but it will not work as intended.

The important point to understand here is that a sub-form resides within a sub-form container control (which typically has the same name as the sub-form itself).

To set focus on a control within a sub-form, you must first set focus on its container control.

Once the sub-form container receives focus, the control within the sub-form that has Tab Index = 0 will automatically become active and receive focus.

In the first example, we set the focus using the reference of the sub-form container control on the Main Form:

frm2.frmSubForm2.SetFocus

Here, frmSubForm2 is the name of the sub-form container control.

If you append '.Form' to this reference (like frm2.frmSubForm2.Form.SetFocus), it becomes a reference to the sub-form’s Form object. The system will simply ignore the SetFocus call, because SetFocus applies only to controls, not to Form objects directly.

As an alternative, you can explicitly refer to it as a control on the Main Form using:

frm2.Controls("frmSubForm2").SetFocus

Setting focus on a control on the Main Form from within the sub-form is more straightforward, for example:

frm2.EmpName.SetFocus

or

frm2.Controls("EmpName").SetFocus

Building the Derived Class Module: ClsSubFormHeader.

With sufficient groundwork laid for modifying the ClsSubForm Class Module, we can now proceed to build the Header Class (Derived Class ClsSubFormHeader). This class will handle adding the TextBox controls from both the main form and its sub-forms into a Collection object.

We will also include the necessary built-in event procedures to confirm that these events are being triggered and handled by the class. These event-triggered messages will serve as visual indicators that the class module is indeed capturing and executing the assigned tasks—especially since all the action now takes place behind the scenes, rather than in the form’s own class module.

Once you’ve verified that everything is working as intended, you can remove these temporary message displays that show values from the TextBox controls.

Separate Class Object Instance for each Form Control

Keep in mind that a separate instance of the ClsSubForm class object is created for each TextBox control across all the forms, and each instance is stored as an individual item in the Collection object.

Before adding a TextBox to the collection, its required built-in event handlers are enabled within its corresponding class object instance.

Although the form’s own code module remains empty, the class object instances stored in the Collection Object are actively monitoring for their assigned Events.

When a particular event occurs on a specific TextBox, the corresponding ClsSubForm class object instance handles it—executing its relevant event procedure (for example, txt1_AfterUpdate()) and then either displaying the result as a message in the application window or updating the value of another TextBox on the form.

The ClsSubFormHeader Class Object Code is given below:

Option Compare Database
Option Explicit

Private frm As Access.Form
Private T As ClsSubForm
Private C As New Collection

Public Property Get mFrm() As Access.Form
  Set mFrm = frm
End Property

Public Property Set mFrm(frmVal As Access.Form)
  Set frm = frmVal
  Call Class_Init
End Property

Private Sub Class_Init()
Dim cnt As Integer
Dim ctl As Control

'Scan for TextBoxes on the Main Form
For Each ctl In frm.Controls
   If TypeName(ctl) = "TextBox" Then
   
       Set T = New ClsSubForm ‘instantiate Class
       Set T.txt1 = ctl
       
       Select Case ctl.Name
           Case "EmpName"
               T.txt1.AfterUpdate = "[Event Procedure]"
       End Select
       C.Add T ‘add to collection object
   End If
Next

'Scan for TextBoxes on the Sub-Form
For Each ctl In frm.frmSubForm1.Form.Controls
   If TypeName(ctl) = "TextBox" Then
   
      Set T = New ClsSubForm ‘instantiate Class
      Set T.txt1 = ctl
      Set T.sFrm = frm
      
       Select Case ctl.Name
           Case "BirthDate"
                  T.txt1.AfterUpdate = "[Event Procedure]"
                  T.txt1.OnLostFocus = "[Event Procedure]"
           Case "RetireDate"
                  T.txt1.OnGotFocus = "[Event Procedure]"
                  T.txt1.OnLostFocus = "[Event Procedure]"
       End Select
       C.Add T 'add to collection Object
   End If
Next

'Scan for TextBoxes on the Sub-Form
For Each ctl In frm.frmSubForm2.Form.Controls
   If TypeName(ctl) = "TextBox" Then
   
      Set T = New ClsSubForm
      Set T.txt1 = ctl
      Set T.sFrm = frm
      
       Select Case ctl.Name
           Case "EMail"
               T.txt1.AfterUpdate = "[Event Procedure]"
           Case "Mobile"
               T.txt1.OnLostFocus = "[Event Procedure]"
       End Select
       C.Add T 'add to collection Object
   End If
Next

End Sub

The change is in the For Each . . . Next statement, where the ctl running variable picks the Reference of TextBoxes from the SubForm and adds them to the Collection Object, after enabling the required built-in Events.

On the first sub-form, the For ... Next Loop uses the following reference:

For Each ctl In frm.frmSubForm1.Form.Controls
.
.
.
Next

The ctl control carries the reference (address) of the sub-form Text Box while it is added to the Class Module ClsSubForm Txt1 Property and goes to the Collection Object as its Item.

In the same way, the second Sub-form Text Box references are used in the For ... Next Loop.

For Each ctl In frm.frmSubForm2.Form.Controls
.
.
.
Next

Both subforms have their ‘HasModule’ property set to ‘Yes’. However, their form modules do not contain any VBA code.

The Main Form (frmMain) Module Code is given below:

Option Compare Database
Option Explicit

Private T As ClsSubFormHeader

Private Sub Form_Load()
  Set T = New ClsSubFormHeader
  Set T.mFrm = Me
End Sub

The ClsSubFormHeader derived class is declared as the object T. In the Form_Load() event, an instance of the ClsSubFormHeader class is created, and the current form object is passed to its T.mFrm property procedure.

If you look at the ClsSubFormHeader class module code, you’ll see that the form object is passed to the frm2 property of ClsSubForm through the Property Set procedure sFrm, using the statement Set T.sFrm = frm, for each instance of the ClsSubForm class created for every text box added as an item in the Collection object.

Summary.

The Subform control references are added to the Class Module instances just like any other control on the main form. This ensures that when the built-in events are triggered, they remain synchronized with the correct Class Object instance and execute the appropriate event procedures.

You can download a demo database containing all the forms and VBA code to explore and study the implementation in detail.

Feel free to share your observations, suggestions, and comments in the Comments section of this page.

Downloads.



Links to WithEvents ...Tutorials.

  1. WithEvents MS-Access Class Module Tutorial
  2. WithEvents and Defining Your Own Events
  3. withevents Button Combo List TextBox Tab
  4. Access Form Control Arrays and Event Capturing
  5. Access Form Control Arrays and Event-2
  6. Access Form Control Arrays and Event-3
  7. WithEvents in Class Module for Sub-Form TextBox Events
  8. WithEvents in Class Module and Data Entry
  9. WithEvents and Access Report Event Sink
  10. WithEvents and Report Lines Hiding
  11. WithEvents and Report Lines Highlighting
  12. Withevents TextBox and Command Button Arrays
  13. Withevents TextBox CommandButton Dictionary
  14. Withevents and all Form Control Types

Share:

PRESENTATION: ACCESS USER GROUPS (EUROPE)

Translate

PageRank

Post Feed


Search

Popular Posts

Blog Archive

Powered by Blogger.

Labels

Forms Functions How Tos MS-Access Security Reports msaccess forms Animations msaccess animation Utilities msaccess controls Access and Internet MS-Access Scurity MS-Access and Internet External Links Queries Array Class Module msaccess reports Accesstips msaccess tips WithEvents Downloads Objects Menus and Toolbars MsaccessLinks Process Controls Art Work Collection Object Property msaccess How Tos Combo Boxes ListView Control Query VBA msaccessQuery Calculation Dictionary Object Event Graph Charts ImageList Control List Boxes TreeView Control Command Buttons Controls Data Emails and Alerts Form Custom Functions Custom Wizards DOS Commands Data Type Key Object Reference ms-access functions msaccess functions msaccess graphs msaccess reporttricks Command Button Report msaccess menus msaccessprocess security advanced Access Security Add Auto-Number Field Type Form Instances ImageList Item Macros Menus Nodes Recordset Top Values Variables msaccess email progressmeter Access2007 Copy Excel Expression Fields Join Methods Microsoft Numbering System RaiseEvent Records Security Split SubForm Table Tables Time Difference Utility WScript Workgroup Wrapper Classes database function msaccess wizards tutorial Access Emails and Alerts Access Fields Access How Tos Access Mail Merge Access2003 Accounting Year Action Animation Attachment Binary Numbers Bookmarks Budgeting ChDir Color Palette Common Controls Conditional Formatting Data Filtering Database Records Defining Pages Desktop Shortcuts Diagram Disk Dynamic Lookup Error Handler Export External Filter Formatting Groups Hexadecimal Numbers Import Labels List Logo Macro Mail Merge Main Form Memo Message Box Monitoring Octal Numbers Operating System Paste Primary-Key Product Rank Reading Remove Rich Text Sequence SetFocus Summary Tab-Page Union Query User Users Water-Mark Word automatically commands hyperlinks iSeries Date iif ms-access msaccess msaccess alerts pdf files reference restore switch text toolbar updating upload vba code