John W. Colby
jwcolby at colbyconsulting.com
Mon Mar 8 21:12:22 CST 2004
We left off our discussion having built a framework class and a text box class. The text box class did nothing more than change the back color of the text box to cyan as it got the focus and back to the original color as it lost the focus. The form class did nothing more than scan the forms Control collection looking for controls and instantiating classes for any controls that we had a class for just the text box class so far. For demo code look in C2DbFW3G-DemoCtlClassV2.zip for DemoCtlClassV2.mdb. This document can be found in there as well. I am deleting the old forms to clean up the demo database and let us focus on the new stuff. Today I am going to add a couple of more classes to our framework and tie them in to what we are doing. First I am going to add a combo class. The combo class will also do nothing more than change the back color as it gets / loses focus. I know this may be boring but it demonstrates clearly that the classes are being instantiated by the forms control scanner and that the control classes do indeed load, sink events for their respective control and perform some action. Believe me, we have come a long way towards understanding how classes work, how withevents work, and how classes can play and work together. The dclsCbo The combo class is going to start out looking very much like the text box class. In fact I took the text box class, and used the editor to replace text with combo to create a new class. Option Compare Database Option Explicit The header just dimensions the combo control private, withevents, creates a constant string, a constant back color, and a variable backcolor to store the original backcolor. Private WithEvents mcbo As ComboBox 'Dimension a text box Withevents Private Const mstrEventProcedure = "[Event Procedure]" 'A constant to hold the string [Event Procedure] Private Const mclngBackColor As Long = 16777088 'A pretty blue color to set the text box back color to Private mlngBackColorOrig As Long 'A place to store the original back color The init is passed in a pointer to a specific combo and stores that in our private combo variable. It also sets up the combos OnEnter and OnExit properties. 'The init function of every class "initializes" the class Function Init(lcbo As ComboBox) 'Pass in a pointer to a specific control Set mcbo = lcbo 'Save that pointer to a private variable here in the class mcbo.OnEnter = mstrEventProcedure 'Set the OnEnter property of the control to [Event Procedure] mcbo.OnExit = mstrEventProcedure 'Do the same for the OnExit End Function The term does nothing more than clean up pointers to objects, in this case the combo that this class manipulates. ' 'The term function of every class cleans up all pointers to objects stored in our class ' Function Term() Set mcbo = Nothing 'Set the pointer to the control to nothing End Function And finally the Enter event of the combo control changes the back color, storing the original backcolor. ' 'These are the event stubs for the control's OnEnter and OnExit events ' Private Sub mcbo_Enter() mlngBackColorOrig = mcbo.BackColor 'When we enter the text box, save the original back color mcbo.BackColor = mclngBackColor 'Set the back color to our favorite color End Sub And the OnExit changes the combos backcolor back to the original color. Private Sub mcbo_Exit(Cancel As Integer) mcbo.BackColor = mlngBackColorOrig 'When we exit the control, set the back color back to the original color End Sub All of this looks almost identical to the text box class. dclsFrm Changes The only change we have to make to dclsFrm is to add a new class to our case statement in the control scanner to recognize and instantiate classes for combos. Private Sub FindControls() On Error GoTo Err_FindControls Dim ctl As Control Dim intIndex As Integer Dim col As Collection Set col = New Collection For Each ctl In mfrm.Controls 'Find each control in the form's control collection With ctl Select Case .ControlType 'Determine it's type Case acTextBox 'Find all text boxes and load class to change backcolor mcolClasses.Add New dclsCtlTextBox, .Name mcolClasses(.Name).Init ctl Case acSubform Case acTabCtl 'tab pages are handled in the tab control Case acOptionGroup Case acCheckBox Case acOptionButton Case acCommandButton Case acToggleButton Case acListBox Case acComboBox mcolClasses.Add New dclsCtlCbo, .Name mcolClasses(.Name).Init ctl Case Else End Select End With NextCtl: Next ctl Exit_FindControls: On Error Resume Next Set ctl = Nothing Exit Sub Notice the case acComboBox we added in the new dclsCtlCbo. One thing to notice about this syntax is that I add the class into the collection: mcolClasses.Add New dclsCtlCbo, .Name Notice that I use the new keyword right in the Add method of the collection to return a pointer to the object the dclsCbo in this instance. Then notice that I use the controls name as the key into the collection. This enables me to look up the class for any control (assuming it has one in there) just by indexing into the collection with the controls name. The next thing to notice is that if an object is stored in a collection, the methods and properties of the object in the collection can be referenced directly in the collection, i.e. you dont need to bring the object back out of the collection and put it in a variable to get at the object. mcolClasses(.Name).Init ctl Here I initialized the class for the object that was stored in mcolClasses(.name) where .name is the controls name, .init is the init method of the class in the collection, and ctl is the control being passed in to the init method of the class in the collection. This may seem obtuse but it saves us a lot of dimensioning variables, setting the variable to the class and using the variable to get at the class. So we have built a new class to handle any generic combo functionality we may desire, and we have added it into dclsFrm with two lines of new code. I will be adding classes for lists, check boxes, radio buttons etc but for now combos and text boxes are enough to display the power of the system without making us wade through classes that we wont be using (yet). To see the new dclsCbo function, simply open frmPeopleV2 and tab through the controls. The combos are now changing background color as they get and lose the focus in the same manner that the text boxes do. I will be adding a new, more useful functionality to the combo class in the next article. dclsTimer A timer class The timer class neatly demonstrates the reusability and encapsulation advantages of classes. The actual code that does the timing was lifted from (I believe) ADH, but it was a single instance thing, i.e. it only had a couple of functions and a variable to store the timing stuff. Thus it could only time one thing at a time. By turning it into a class, we encapsulate the code, the documentation of how it works, and the variable(s) that make it all work. It also demonstrates that a class doesnt have to be complex to do something useful for us. In order to use this I will time how long the form is open, and also how long it takes to do the control scanner stuff. Option Compare Database Option Explicit In the timer class header I declare a function that calls the windows API getting a long integer representing timer ticks measured in milliseconds. I also dimension a long integer variable to hold the start time counter tick Private Declare Function apiGetTime Lib "winmm.dll" _ Alias "timeGetTime" () As Long Private lngStartTime As Long There are only two methods of the class, StartTimer which stores the start time (tick count) and EndTimer which returns the difference between the start tick count and the end tick count. 'THESE FUNCTIONS / SUBS ARE USED TO IMPLEMENT CLASS FUNCTIONALITY '*+Class function / sub declaration Sub StartTimer() lngStartTime = apiGetTime() End Sub Function EndTimer() EndTimer = apiGetTime() - lngStartTime End Function '*-Class function / sub declaration Just remember that the results are in milliseconds (thousandths of a second). And finally, I will add this in to dclsFrm to allow us to time a couple of things. First I declare a timer variable in the dclsFrm header to hold a timer that can be referenced from anywhere in the class. Option Compare Database Option Explicit Private mclsTimer As clsTimer In the Initialize method of the class I instantiate the class, then start the timer. Private Sub Class_Initialize() Set mclsTimer = New clsTimer 'Instantiate a timer to time how long the form was open mclsTimer.StartTimer 'Start the timer running End Sub In the term() method I debug.print the timer value. This displays how long the form was opened in milliseconds. Function Term() 'Print the time the form was open Debug.Print mfrm.Name & " was open for " & mclsTimer.EndTimer & " milliseconds" Set mclsTimer = Nothing 'Then destroy the timer classEnd Function Now we are going to use another timer instance to time the control scanner. To do this we dimension a timer class local to the scanner function. Private Sub FindControls() On Error GoTo Err_FindControls Dim ctl As Control Dim intIndex As Integer Dim lclsTimer As clsTimer Set lclsTimer = New clsTimer lclsTimer.StartTimer For Each ctl In mfrm.Controls 'Find each control in the form's control collection With ctl And in the functions Exit I debug print the time and destroy the timer class. Exit_FindControls: On Error Resume Next Set ctl = Nothing Debug.Print mfrm.Name & "'s control scanner took " & lclsTimer.EndTimer & " milliseconds to run" Set lclsTimer = Nothing Exit Sub Note: I ran the form on my 2.5 ghz AMD development machine and the scanner class took 1 millisecond to complete. In that time the scanner loaded classes for nine controls. From my debug window: frmPeopleV2's control scanner took 1 milliseconds to run This timer will be useful to us as we build more and more functionality into the various control classes. We want to just monitor how long it takes our form to load all of its classes so that if that time jumps to an unreasonable amount of time we are aware of it and can investigate why. Summary In this article we have added a new control class to handle future combo box functionality, as well as a timer class. These classes demonstrate once again exactly how easy it is to set up a class and use it. dclsFrm was modified to allow instantiating dclsCbo for every combo that the control scanner found. We also set up two timer classes in various places, one global to the form to time the amount of time the form was open, and another local to the control scanner to time how long it takes to load all of the control classes. We now have four classes working together to create a system that allows a form to find and load a class for all of its controls, the controls can perform functionality automatically, and timers can tell us how long various actions take to complete. John W. Colby www.ColbyConsulting.com