You are on page 1of 27

Dynamic UserForms

by David Horowitz Did you know that you can add and remove controls from a UserForm (also known as custom dialog boxes) at run-time? This means that you can change your UserForms based on certain conditions that exist during the operation of your VBA projectyoure not limited to the design of the UserForm that you created in the VBE designer! To illustrate the technique, well create a Word VBA Template which will prompt the user for the number of cars they own. (It allows for up to 20hopefully that will be enough for most of us!) It will then display the correct number of textboxes to allow the user to enter the make of each car. When the user is done, the list of cars will be added to the document. The key thing here we want to illustrate is the part where the dialog box will actually change to display the correct number of textboxes (with accompanying labels). The primary method that accomplishes this task is Form.Controls.Add. (You can look it up in VBA Help.) Its syntax is like this: Set ctl = Me.Controls.Add(ControlClass, Name, Visible) For our purposes, ControlClass can have one of the following values: "Forms.CheckBox.1" "Forms.ComboBox.1" "Forms.CommandButton.1" "Forms.Frame.1" "Forms.Image.1" "Forms.Label.1" "Forms.ListBox.1" "Forms.MultiPage.1" "Forms.OptionButton.1" "Forms.ScrollBar.1" "Forms.SpinButton.1" "Forms.TabStrip.1" "Forms.TextBox.1" "Forms.ToggleButton.1" These are text strings, so you must enclose them in quotes. You can optionally specify the name of the new control using the Name parameter, for example, "TextBox7, "txtFirstName, "lblPrompt, or "chkChicago, whatevers appropriate for your use. Finally, you can choose to make the new control invisible by specifying False for the Visible parameter. The default is for Visible to be True. So, for example, lets say you would like to add a new label to your form. The form is named frmMyForm, 1

and the new label should have the name "lblPrompt. The method call would look like: Dim myLabel as Label Set myLabel = frmMyForm.Controls.Add _ ("Forms.Label.1", "lblPrompt") Dont get confused about myLabel and lblPrompt. myLabel is an Object variable in VBA which is now set to refer to the control on the form by the name of "lblPrompt. Once you add a control to a form, you will certainly need to set some properties on the control, at least things like Top, Left, Width and Height. You can do this right after creation if you want, using, in our example, myLabel: With myLabel .Left = 10 .Top = 10 .Width = 30 .Caption = "Enter your name:" End With Weve let the Height property default to whatever VBA sets for it initially. Now later on in your code, if you want to refer to the newly added control, but you no longer have the myLabel reference, you can either use: frmMyForm.Controls("lblPrompt) or frmMyForm!lblPrompt You cannot use the other syntax to refer to a control: frmMyform.lblPrompt when the control has been created dynamically using Controls.Add. The example which accompanies this article, Dynamic UserForms Demo.dot, is a Word template which illustrates the techniques weve just describes in greater detail and in actual usage. You can download a copy of this template demo by clicking: HERE. When you open this template, the Document_New method will call ShowCarsDialog, which will call the frmCars.Show method. frmCars has a label (lblNumCars) and textbox (txtNumCars), which prompt you to enter the number of cars you have. When you click on the Show Cars button (cmdShowCars), you will see a number of labels and textboxes equal to the number of cars you entered, allowing you to enter a make for each car. Then, when you click the Done button (cmdDone), all the car makes you entered will be inserted into the 2

document and the dialog form (frmCars) will be unloaded. If you want to change the number of cars again before you press Done, you can do that and when you click on Show Cars, the number of car labels and textboxes will change again to accommodate. So you can Add and Remove cars from the list before you click Done. When you look at the code, you will see that each car make has a label and a textbox called lblCarN and txtCarN, where N is the number of the car. Each label is added to the form using this line of code: Set theLabel = Me.Controls.Add _ ("Forms.Label.1", "lblCar" & CurrentNumberOfCars) Each textbox is added to the form using this line of code: Set theTextBox = Me.Controls.Add _ ("Forms.TextBox.1", "txtCar" & CurrentNumberOfCars) After creating each control, we set a number of properties on it. We set each labels caption using this line of code: theLabel.Caption = "Car #" & CurrentNumberOfCars & ":" You can see we even set the Accelerator property on each label to the number of the car using this line of code: theLabel.Accelerator = CurrentNumberOfCars If you tell the dialog box to remove some cars, the following lines of code are used: Me.Controls.Remove "lblCar" & CurrentNumberOfCars Me.Controls.Remove "txtCar" & CurrentNumberOfCars In this article, weve learned how to use Form.Controls.Add to add controls to a forms Controls collection at run-time. We then set properties on the control to make it look and function the way we want. We also learned how to dynamically remove a control from a forms Controls collection using Form.Controls.Remove. Now you can experiment with adding and removing controls to your forms whenever you need them. You will find many uses for this technique once youve learned how to do it.

CreateControl Method [Access 2003 VBA Language Reference]


The CreateControl method creates a control on a specified open form. For example, suppose you are building a custom wizard that allows users to easily construct a particular form. You can use the CreateControl method in your wizard to add the appropriate controls to the form. CreateControl(formname, controltype[, section[, parent[, columnname[, left[, top[, width[, height]]]]]]]) The CreateControl method has the following arguments.

Argument
formname controltype

Description
A string expression identifying the name of the open form or report on which you want to create the control. One of the following intrinsic constants identifying the type of control you want to create. To view these constants and paste them into your code from the Object Browser, click Object Browser on the Visual Basic toolbar, then click Access in the Project/Library box, and click AcControlType in the Classes box.

Constant
acBoundObjectFrame acCheckBox acComboBox acCommandButton acCustomControl acImage acLabel acLine acListBox acObjectFrame acOptionButton acOptionGroup acPage acPageBreak

Control
Bound object frame Check box Combo box Command button ActiveX control Image Label Line List box Unbound object frame Option button Option group Page Page break

acRectangle acSubform acTabCtl acTextBox acToggleButton section One of the following intrinsic constants identifying the section that will contain the new control. To view these constants and paste them into your code from the Object Browser, click Object Browser on the Visual Basic toolbar, then click Access in the Project/Library box, and click AcSection in the Classes box.

Rectangle Subform Tab control Text box Toggle button

Constant
acDetail acHeader acFooter acPageHeader acPageFooter acGroupLevel1Header

Section
(Default) Detail section Form or report header Form or report footer Page header Page footer Group-level 1 header (reports only)

acGroupLevel1Footer

Group-level 1 footer (reports only)

acGroupLevel2Header

Group-level 2 header (reports only)

acGroupLevel2Footer

Group-level 2 footer (reports only)

If a report has additional group levels, the header/footer pairs are numbered consecutively, beginning with 9. parent A string expression identifying the name of the parent control of an attached control. For controls that have no parent control, use a zerolength string for this argument, or omit it. columnnam e The name of the field to which the control will be bound, if it is to be a data-bound control.

If you are creating a control that won't be bound to a field, use a zerolength string for this argument. left, top width, height Numeric expressions indicating the coordinates for the upper-left corner of the control in twips. Numeric expressions indicating the width and height of the control in twips.

Remarks
You can use the CreateControl and CreateReportControl methods in a custom wizard to create controls on a form or report. Both methods return a Control object. You can use the CreateControl and CreateReportControl methods only in form Design view or report Design view, respectively. You use the parent argument to identify the relationship between a main control and a subordinate control. For example, if a text box has an attached label, the text box is the main (or parent) control and the label is the subordinate (or child) control. When you create the label control, set its parent argument to a string identifying the name of the parent control. When you create the text box, set its parent argument to a zero-length string. You also set the parent argument when you create check boxes, option buttons, or toggle buttons. An option group is the parent control of any check boxes, option buttons, or toggle buttons that it contains. The only controls that can have a parent control are a label, check box, option button, or toggle button. All of these controls can also be created independently, without a parent control. Set the columnname argument according to the type of control you are creating and whether or not it will be bound to a field in a table. The controls that may be bound to a field include the text box, list box, combo box, option group, and bound object frame. Additionally, the toggle button, option button, and check box controls may be bound to a field if they are not contained in an option group. If you specify the name of a field for the columnname argument, you create a control that is bound to that field. All of the control's properties are then automatically set to the settings of any corresponding field properties. For example, the value of the control's ValidationRule property will be the same as the value of that property for the field. Note If your wizard creates controls on a new or existing form or report, it must first open the form or report in Design view. To remove a control from a form or report, use the DeleteControl and DeleteReportControl statements.

Example
The following example first creates a new form based on an Orders table. It then uses the CreateControl method to create a text box control and an attached label control on the form. Sub NewControls()

Dim Dim Dim Dim

frm As Form ctlLabel As Control, ctlText As Control intDataX As Integer, intDataY As Integer intLabelX As Integer, intLabelY As Integer

' Create new form with Orders table as its record source. Set frm = CreateForm frm.RecordSource = "Orders" ' Set positioning values for new controls. intLabelX = 100 intLabelY = 100 intDataX = 1000 intDataY = 100 ' Create unbound default-size text box in detail section. Set ctlText = CreateControl(frm.Name, acTextBox, , "", "", _ intDataX, intDataY) ' Create child label control for text box. Set ctlLabel = CreateControl(frm.Name, acLabel, , _ ctlText.Name, "NewLabel", intLabelX, intLabelY) ' Restore form. DoCmd.Restore End Sub

-----------------------------------------------------------------------------------------------------------------------------

CreateReportControl Method [Access 2003 VBA Language Reference]


The CreateReportControl method creates a control on a specified open report. For more information, see the CreateControl method. expression.CreateReportControl(ReportName, ControlType, Section, Parent, ColumnName, Left, Top, Width, Height) expression Required. An expression that returns one of the objects in the Applies To list. ReportName Required String. A string expression identifying the name of the open report on which you want to create the control.

AcControlType
AcControlType can be one of these AcControlType constants. acBoundObjectFrame acCheckBox acComboBox acCommandButton acCustomControl acImage

acLabel acLine acListBox acObjectFrame acOptionButton acOptionGroup acPage acPageBreak acRectangle acSubform acTabCtl acTextBox acToggleButton

AcSection
AcSection can be one of these AcSection constants. acDetaildefault acFooter acGroupLevel1Footer acGroupLevel1Header acGroupLevel2Footer acGroupLevel2Header acHeader acPageFooter acPageHeader Parent Optional Variant. A string expression identifying the name of the parent control of an attached control. For controls that have no parent control, use a zero-length string for this argument, or omit it. ColumnName Optional Variant. The name of the field to which the control will be bound, if it is to be a data-bound control. Left, Top Optional Variant. Numeric expressions indicating the coordinates for the upper-left corner of the control in twips. Width, Height Optional Variant. Numeric expressions indicating the width and height of the control in twips

----------------------------------------------------------------------------------------------------------------------------8

Sometimes you need to create a complex UserForm with many components. I had to create a form with 3 labels, 1 text box and a check box for 43 different records (215 separate components) and so developed this technique of programatically creating the form. Obviously one can manually create a complex UserForm but the problem of individually naming each component and writing the event handler codes is a real drag. The references that need to be loaded are: Visual Basic For Applications Microsoft Excel Object Library MS Forms 2.0 Object Library The code below is based on John Walkenbach's original procedure.
CODE

Sub MakeUserForm() Dim TempForm As Object Dim NewButton As MSForms.commandbutton Dim NewLabel As MSForms.Label Dim NewTextBox As MSForms.TextBox Dim NewOptionButton As MSForms.OptionButton Dim NewCheckBox As MSForms.CheckBox Dim X As Integer Dim Line As Integer Dim MyScript(4) As String 'This is to stop screen flashing while creating form Application.VBE.MainWindow.Visible = False Set TempForm = ThisWorkbook.VBProject.VBComponents.Add(3) 'Create the User Form With TempForm .Properties("Caption") = "My User Form" .Properties("Width") = 450 .Properties("Height") = 300 End With 'Create 10 Labels For X = 0 To 9 Set NewLabel = TempForm.designer.Controls.Add("Forms.label.1") With NewLabel .Name = "FieldLabel" & X + 1 .Caption = "My Label " & X + 1 .Top = 20 + (12 * X) .Left = 6 .Width = 90 .Height = 12 .Font.Size = 7 .Font.Name = "Tahoma" .BackColor = &H80FFFF End With Next 'Create 10 Text Boxes For X = 0 To 9 Set NewTextBox = TempForm.designer.Controls.Add("Forms.textbox.1") With NewTextBox

.Name = "MyTextBox" & X + 1 .Top = 20 + (12 * X) .Left = 100 .Width = 150 .Height = 12 .Font.Size = 7 .Font.Name = "Tahoma" .BorderStyle = fmBorderStyleSingle .SpecialEffect = fmSpecialEffectFlat End With Next 'Create 10 Check Boxes For X = 0 To 9 Set NewCheckBox = TempForm.designer.Controls.Add("Forms.checkbox.1") With NewCheckBox .Name = "MyCheck" & X + 1 .Caption = "" .Top = 20 + (12 * X) .Left = 260 .Width = 12 .Height = 12 .Font.Size = 7 .Font.Name = "Tahoma" .BackColor = &HFF00& End With Next 'Create 10 Labels -> result of Check Box For X = 0 To 9 Set NewLabel = TempForm.designer.Controls.Add("Forms.label.1") With NewLabel .Name = "Result_Text" & X + 1 .Caption = "" .Top = 20 + (12 * X) .Left = 280 .Width = 150 .Height = 12 .Font.Size = 7 .Font.Name = "Tahoma" .BackColor = &H80FFFF End With Next 'Create Event Handler Code For Each Check Box '(True -> Upper Case of Text Box Value;False -> Lower Case of Text Box Value) For X = 0 To 9 With TempForm.codemodule Line = .countoflines MyScript(0) = "Sub MyCheck" & X + 1 & "_Click()" MyScript(1) = "If .MyCheck" & X + 1 & " = true then" MyScript(2) = ".result_Text" & X + 1 & ".caption = ucase(.mytextbox" & X + 1 & ".value)" MyScript(3) = ".result_Text" & X + 1 & ".caption = lcase(.mytextbox" & X + 1 & ".value)" .insertlines Line + 3, MyScript(0) .insertlines Line + 2, "With Me" .insertlines Line + 3, MyScript(1) .insertlines Line + 4, MyScript(2) .insertlines Line + 5, "Else"

10

Next

.insertlines .insertlines .insertlines .insertlines End With

Line Line Line Line

+ + + +

6, 7, 8, 9,

MyScript(3) "End if" "End With" "End Sub"

'Show the form VBA.UserForms.Add(TempForm.Name).Show 'Delete the form (Optional) 'ThisWorkbook.VBProject.VBComponents.Remove TempForm End Sub I tend not to delete the form but build up a series of UserForms (I refer to them as TestForms) - one generated each time the code is run. In this way I can check the results of any changes I made and use the latest TestForm to manually move components, get their new positions and modify the code accordingly. When satisfied I can rename the final TestForm to an appropriate name, add any other components such as command buttons and other event handler codes, and then delete all the remailing TestForms. This code can also be used to create a user form on the fly but command buttons and their event handlers must be added in the code.

Date and Time Functions in VBA


Background
The dates and times we input and see displayed in Access are not in the same format as what is stored for dates and times. They are stored as double precision numbers. The integer part is a numeric representation of only the date, and is called a Julian, or Serial, date. To do this, Access converts the date to an offset from a fixed point in time (12/30/1899), and all dates are stored as the number of days since this date. Thus 8/27/1999 is stored as 36399, meaning 36,399 days since 12/30/1899. Time represents the decimal part of the double that Access stores for date and time. Since adding 1 to a stored date/time represents 1 day or 24 hours, each hour is stored as .041666, or 1/24 of a day. In Access all times are stored as a fraction of a day. Each hour is 1/24 of a day, each minute 1/1440, each second 1/86400. So 3:00 is stored as .125 (or 1/8 of a day), and 16:00 is stored as 0.666, (or 2/3 of a day). Conversely, 0.2 represents 4:48 hours (1/5 of a day), and so on. Fortunately, there are numerous functions for handling dates and times in VBA.

The Date and Time Functions


11

Function

Description

Now Date Time Timer

Current date and time. Example: 7/5/00 3:16:38 PM returned by Now Current date only. Example: 7/5/00 returned by Date Current time only. Example: 3:12:38 PM returned by Time Number of seconds since midnight. Example: 3:16:38 PM returned by Timer See other examples below this table. Time part of argument. Example: 3:16:38 PM returned by TimeValue(Now) Date part of argument (excellent for ordering by date) Example: SELECT * from tblPeople ORDER BY DateValue(Review) Date part of three arguments: year, month, day Example: HAVING InvoiceDate <= DateSerial(Year(Now), Month(Now)-1, Day(Now)) DateSerial handles January correctly in the above example Returns a portion of the date. Year example: 2000 returned by DatePart('yyyy', Date) Month example: 10 returned by DatePart('m', #10/11/2001#) Week of year example: 41 returned by DatePart('ww', #10/11/2001#) Day of Week example: Monday returned by DatePart('dddd', #6/3/2002#) Quarter example: 4 returned by DatePart('q', #10/11/2001#) Returns the year portion of the date argument. Also see DatePart() above. Returns the month portion of the date argument. Also see DatePart() above. Returns the day portion of the date argument. Also see DatePart() above. Used to format month names. July returned by MonthName(Month(Date)) Used to format day names. Wednesday returned by WeekdayName(Weekday(Date)) Current date only; used in Excel, not available in Access Returns the difference in dates. Days example: -656 returned by DateDiff("d", #10/11/2001#, #12/25/1999#) Months example: 1 returned by DateDiff("m", #8/10/2000#, #9/14/2000#) Days example: 0 returned by DateDiff("m", date1, date2) 0 is returned above only if the two dates have same month and year

TimeValue()

DateValue()

DateSerial()

DatePart()

Year() Month() Day() MonthName() WeekdayName() Today()

DateDiff()

12

Function

Description

DateAdd()

Add and subtract dates. 10/11/2002 returned by DateAdd("yyyy", 1, #10/11/2001#)) Today's date + 30 days returned by DateAdd("d", 30, Date) The date 45 days ago returned by DateAdd("d", -45, Date) To find Monday of a week: DateAdd("d",-WeekDay(Date) +2,Date) Very useful for formatting dates. Examples: Wed, 5-July-2000 returned by Format(Date,"ddd, d-mmmmyyyy") 5-Jul-00 returned by Format(Date,"d-mmm-yy")

Format()

Date Stamping Records


You may want to know the last time a record was changed. To accomplish this you can add a date stamp field (LastUpdated) to a table, then set the field's Default Value to Now, and add the field to your form but make it invisible. Access will enter the current date whenever you add a new record using the form. To change the LastUpdated field to the current date when you alter an existing record, use the form's Before Update property and set LastUpdated to Now.

Timing an Access Operation


You can use the Timer function to time operations and events in an Access application by placing the Timer function immediately before and after the operation you want to time. Timer also can be used to pause your application for a given span of time. See Help. The following example uses the name of a query as its argument, then calculates how long it takes the query to run.

Sub QueryTimer (strQueryName As String) Dim sngStart As Single, sngEnd As Single Dim sngElapsed As Single

sngStart = Timer DoCmd.OpenQuery strQueryName, acNormal sngEnd = Timer

' Get start time. ' Run query. ' Get end time.

sngElapsed = Format(sngEnd - sngStart, "Fixed")' Elapsed time.

13

MsgBox ("The query " & strQueryName & " took " & sngElapsed _ & " seconds to run.") End Sub

Pulling the Pieces Apart


Of course, if you're working with dates, you're also working with years, months, days, hours, minutes, and seconds. You might also like to work with a date in terms of its placement within the year, or which quarter it's in, or which day of the week it is. VBA provides simple and useful functions for retrieving all this information, and more. Retrieving Just the Part You Need To start with, you'll find the functions listed in Table 2.2 to be helpful in extracting simple information from a date value. Each of these functions accepts a date parameter and returns an integer containing the requested piece of information. (You can also use the DatePart function, described in the section One Function Does It All later in this chapter, to retrieve any of these values. It's simpler to call the functions in Table 2.2 if you just need one of the values listed.) Table 2.2: Simple Date/Time Functions Function Year Month Day Hour Minute Second Return Value Year portion of the date Month portion of the date Day portion of the date Hour portion of the date Minute portion of the date Seconds portion of the date

You can use any of these functions to retrieve a portion of a date value. For example, the following fragment displays the current year value: MsgBox "The current year is " & Year(Now) and the following fragment displays the month and day: MsgBox "Month: " & Month(Now) & " Day: " & Day(Now) The following fragment checks the current time and allows you to take an action at 1:12 If Hour(Time) = 13 And Minute(Time) = 12 Then
p.m.:

14

' You know it's 1:12 PM End If Don't try sending the Date function to functions that return time portions of a date/time value. Because the return value from the Date function doesn't include any time information (its fractional portion is 0), the Hour, Minute, and Second functions will all return 0. The same warning applies to the Day, Month, and Year functions: don't send them the Time function, because the return value from that function doesn't include any date information.

What Day of the Week Is This?


In addition to working with months and days, you may need to know the day of the week represented by a date value. Of course, you could calculate this yourself (there are published algorithms for calculating the day of a week, given a date), but why bother? VBA knows the answer and can give it to you easily, using the built-in WeekDay function. (You can also use the DatePart function, discussed in the next section, to retrieve the same information.) To determine the day of the week represented by any date value, use the WeekDay function. Supply it with a date value, and it will return the day of the week on which that date falls. For example, Debug.Print WeekDay(#5/16/56#) returns 4, indicating that May 16 fell on a Wednesday in 1956. Sunday Isn't Always the First Day of the Week Online help indicates that you can pass a second parameter to WeekDay, indicating the first day of the week. In many countries, Monday is considered the first day of the week, so most of the VBA date functions allow you to specify what you consider to be the first day of the week. If you don't specify a value, VBA uses the Windows setting for your local country. If you specify a constant (vbSunday through vbSaturday) for this parameter, VBA treats that day as the first day of the week and offsets the return value accordingly. For example, the following lines represent a sample session in the Immediate window (run in the United States, where Sunday is the first day of the week): ? WeekDay(#5/1/98#) 6 ? WeekDay(#5/1/98#, vbUseSystemDayOfWeek) 6 ? WeekDay(#5/1/98#, vbMonday) 5 Note that as you change the value of the FirstDayOfWeek parameter, the return value changes as well. You need to be aware that WeekDay (and the corresponding functionality in the DatePart function) doesn't return a fixed value, but rather, a value relative to the local first day of the week. Of course, if you want a fixed value, no matter where your code runs, simply specify the first day of the week. The following example returns 6 no matter where you run it: ? WeekDay(#5/1/98#, vbSunday)

15

One Function Does It All


In addition to the functions described in the previous sections, VBA supplies the DatePart function. This function allows you to retrieve any portion of a date/time value and also performs some simple calculations for you. (It can retrieve the quarter of the year containing your date value, as well as all the other, simpler information.) To call DatePart, pass to it a string indicating which information you want returned and a date value. The function returns the requested piece of information from the date value you send it. Table 2.3 lists the possible values for the DatePart function's Interval argument. Table 2.3: Values for the Interval Argument of the DatePart Function Setting yyyy q m y d w ww h n s Description Year Quarter Month Day of year Day Weekday Week Hour Minute Second

For example, the following two lines of code are equivalent: Debug.Print Day(Date) Debug.Print DatePart("d", Date) But these two lines have no equivalent alternatives: ' Return the ordinal position of the current day within the year. Debug.Print DatePart("y", Date) ' Return the quarter (1, 2, 3, or 4) containing today's date. Debug.Print DatePart("q", Date) DatePart allows you to optionally specify the first day of the week (just as you can do with the WeekDay function) in its third parameter. It also allows you to optionally specify the first week of the year in its fourth parameter. (Some countries treat the week in which January 1st falls as the first week of the year, as does the United States. Other countries treat the first four-day week as the first week, and still others wait for the first full week in the year and call that the first week.)

Performing Simple Calculations


16

VBA supplies two functions, DateAdd and DateDiff, that allow you to add and subtract date and time intervals. Of course, as mentioned above, if you're just working with days, you don't need these functionsyou can just add and subtract the date values themselves. The following sections describe each of these important functions in detail.

Adding Intervals to a Date


The DateAdd function allows you to add any number of intervals of any size to a date/time value. For example, you can calculate the date 100 days from now or the time 35 minutes ago. The function accepts three required parameters, as shown in Table 2.4. Table 2.5 lists the possible values for the Interval parameter. Table 2.4: Parameters for the DateAdd Function Parameter Interval Number Date Description A string expression indicating the interval of time to add Number of intervals to add. It can be positive (to get dates in the future) or negative (to get dates in the past) Date to which the interval is added

Table 2.5: Possible Interval Settings for DateAdd Setting yyyy q m y d w ww h n s Description Year Quarter Month Day of year Day Weekday Week Hour Minute Second

For example, to find the date one year from the current date, you could use an expression like this: DateAdd("yyyy", 1, Date) rather than add 365 days to the current date (a common, although incorrect, solution). What about calculating the time two hours from now? That's easy, too: DateAdd("h", 2, Now)

17

DateAdd will never return an invalid date, but if you try to add a value that would cause the return date to be before 1/1/100 or after 12/31/9999, VBA triggers a run-time error. Watch out! The abbreviation for adding minutes to a date/time value is n, not m, as you might guess. (VBA uses m for months.) Many VBA developers have used m inadvertently and not noticed until the program was in use.

Subtracting Dates
If you need to find the number of intervals between two dates (where the interval can be any item from Table 2.5), use the DateDiff function. Table 2.6 lists the parameters for this function. Table 2.6: Parameters for the DateDiff Function Parameter Interval Date1, Date2 FirstDayOfWeek FirstWeekOfYear Required? Yes Yes No No Datatype String Date Integer constant Integer constant Description Interval of time used to calculate the difference between Date1 and Date2 The two dates used in the calculation The first day of the week. If not specified, Sunday is assumed The first week of the year. If not specified, the first week is assumed to be the week in which January 1 occurs

For example, to calculate the number of hours that occurred between two date variables, dtmValue1 and dtmValue2, you could write an expression like this: DateDiff("h", dtmValue1, dtmValue2) DateDiff's return value can be confusing. In general, it performs no rounding at all, but the meaning of the difference varies for different interval types. For example, DateDiff("h", #10:00#, #12:59:59#) returns 2 because only two full hours have elapsed between the two times. When working with months or years, DateDiff returns the number of month or year borders that have been crossed between the dates. For example, you might expect the following expression to return 0 (no full months have been traversed), yet the function returns 1 because a single month border has been crossed: DateDiff("m", #11/15/97#, #12/1/97#) The same goes for the following expression, which returns 1 even though only a single day has transpired: DateDiff("yyyy", #12/31/97#, #1/1/98#)

18

When working with weeks, DateDiff becomes, well, strange. VBA treats the w (weekday) and ww (week) intervals differently, but both return (in some sense) the number of weeks between the two dates. If you use w for the interval, VBA counts the number of the day on which Date1 falls until it hits Date2. It counts Date2 but not Date1. (This explanation requires visual aids, so consult Figure 2.1 for an example to work with.) For example, DateDiff("w", #11/5/97#, #11/18/97#) returns 1 because there's only one Wednesday following 11/5/97 before stopping at 11/18. On the other hand, DateDiff("w", #11/5/97#, #11/19/97#) returns 2 because there are two Wednesdays (11/12 and 11/19) in the range. Using ww for the range, DateDiff counts calendar weeks. (That is, every time it hits a Sunday, it bumps the count.) Therefore, the previous two examples both return 2, using the ww interval; in both cases, there are two Sundays between the two dates. Just as with the w interval, VBA counts the end date if it falls on a Sunday, but it never includes the starting date, even if it is a Sunday. Given that caveat, DateDiff should return the same answer for either the w or ww interval if Date1 is a Sunday.

Figure 2.1: A visual aid for DateDiff calculations If you use date literal values (like #5/1/97#), VBA uses the exact date in its calculations. If, on the other hand, you use a string that contains only the month and date (like 5/1), VBA inserts the current year when it runs the code. This allows you to write code that works no matter what the year. Of course, this makes it difficult to compare dates from two different years because there's no way to indicate any year except the current one. But if you need to perform a calculation comparing dates within the current year, this technique can save you time.

Converting Text to Date/Time Format


Sometimes your code needs to work with date values that are stored as strings. Perhaps you've received data from some outside source and need to convert it to date format, or perhaps the user has entered a value somewhere and you now need to work with it as a date. VBA provides three functions to help you make the necessary conversions: DateValue, TimeValue, and CDate. Each of these functions accomplishes a slightly different task, and their differences aren't apparent from the online help. DateValue and TimeValue each accept a single argument (usually a string expression) and convert that value into either a date or a time. (As mentioned earlier in this chapter, you can also use these functions to extract just the time or date portion of a combined date/time value.) DateValue can convert any string that matches the internal date formats and any recognizable text month

19

names as well. If the value you send it includes a time portion, DateValue just removes that information from the output value. For example, all of the following expressions return the same value (assuming the variable intDate contains the value 30): DateValue("12 30 97") DateValue("December 30 1997") DateValue("December " & intDate & " 1997") DateValue("12/30/97 5:00 PM") DateValue("30/12/97") The final example returns December 30 no matter where you are, of course, only because the date is unambiguous. Try that with a date like 12/1/97, and you'll get the date as defined in your international settings (December 1 in the United States, January 12 in most of the rest of the world). The TimeValue function works similarly to the DateValue function. You can send it a string containing any valid expression, and it returns a time value. If you send TimeValue a string containing date information, it disregards that information as it creates the output value. For example, all of the following return the same time value: TimeValue("5:15 PM") TimeValue("17:15") TimeValue("12/30/97 5:15 PM") The CDate function coerces any value it can get its hands on into a date/time value, if it can. Unlike the TimeValue and DateValue functions, it returns a full date/time value, with all the information it was sent intact. In addition, it can convert numeric values into dates. For example, all of the following examples return the same value. (The last example is redundant, of course, but it works.) CDate("12/30/97 5:15 PM") CDate(35794.71875) CDate(#12/30/97 5:15 PM#) Most often, you'll use CDate to convert text into a full date/time value, and you'll use DateValue and TimeValue to convert text into a date or a time value only.

Putting the Pieces Together


What if, rather than text, you've got the pieces of a date or a time as individual numeric values? In that case, although you could use any of the functions in the previous section to perform the conversion (building up a complex string expression and then calling the function), you're better off using the DateSerial and TimeSerial functions in this case. Each of these functions accepts three valuesDateSerial takes year, month, and day, in that order; TimeSerial takes hour, minutes, and seconds, in that orderand returns a date or a time value, much like the DateValue and TimeValue functions did a single expression as input. Many of the functions presented in the remainder of this chapter use the DateSerial or TimeSerial function to create a date from the three required pieces. For example, what if you need to know the first day of the current month? The simplest solution is to write a function that uses an expression like this: dhFirstDayInMonth = DateSerial(Year(dtmDate), Month(dtmDate), 1)

20

As you'll see, this is exactly the technique the dhFirstDayInMonth function, discussed later in this chapter, uses. By creating a new date that takes the year portion of the current date, the month portion of the current date, and a day value of 1, the function returns a new date that corresponds to the first day in the current month. The TimeSerial function works just the same way. You pass it hour, minutes, and seconds values, and it creates the appropriate time value for you. You'll use both functions together to build a full date/time value if you've got six values containing the year, month, day, hour, minutes, and seconds. That is, you might find yourself with an expression like this: DateSerial(intYear, intMonth, intDay) + _ TimeSerial(intHour, intMinutes, intSeconds) Because a date/time value is simply the sum of a whole number representing days and a fraction representing time, you can use both functions together to create a full date/time value. One useful feature of VBA's built-in date functions is that they never return an invalid date. For example, asking for DateSerial(1997, 2, 35), which certainly describes a date that doesn't exist, politely returns 3/7/97. We'll actually use this feature to our benefit, as you'll see in the section Is This a Leap Year? later in this chapter.

Displaying Values the Way You Want


In your applications, you most likely will want to display dates in a variety of formats. VBA supplies the Format function, which you can use to format date values just the way you need. (You can also use the Format function to format numeric values, and string values as well. See the VBA online help for more information.) When you use the Format function, you supply an expression to be formatted (a date/time value, in this case) and a string expression containing a format specifier. Optionally, you can also supply both a constant representing the first day of the week you want to use and a constant representing the manner in which you want to calculate the first week of the year. (For more information on these two parameters, see Table 2.6 earlier in this chapter.) The format specifier can be either a built-in, supplied string or one you make up yourself. Table 2.7 lists the built-in date/time formats. Table 2.7: Named Date/Time formats for the Format Function Format Name Description General Date Displays a date and/or time, depending on the value in the first parameter, using your system's Short Date style and the system's Long Time style Long Date Medium Date Displays a date (no time portion) according to your system's Long Date format Displays a date (no time portion) using the Medium Date No format appropriate for the language version of the host application Short Date Displays a date (no time portion) using your system's Yes Yes Use Local Settings Yes

21

Short Date format Long Time Medium Time Short Time Displays a time (no date portion) using your system's Long Time format; includes hours, minutes, seconds Displays time (no date portion) in 12-hour format using hours and minutes and the AM/PM designator Displays a time (no date portion) using the 24-hour format; for example, 17:45 Yes Yes Yes

To test out these formats, we took a field trip to a fictional country. The region's time settings for Windows are displayed in Figure 2.2, and their date settings are shown in Figure 2.3. The screen in Figure 2.4 shows some tests, using the Format function, with the various date and time formats.

Figure 2.2: Regional settings for times in a fictitious environment

22

Figure 2.3: Regional settings for dates in the same fictitious environment

Figure 2.4: Test of regional date formats in the Microsoft Access Debug window If you're feeling creative, or hampered by the limitations of the named time and date formats, you can create your own formats using the options shown in Table 2.8. If you build a string containing combinations of these characters, you can format a date/time value any way you like. Figure 2.5 demonstrates a few of the formats you can create yourself, using the characters listed in Table 2.8.

23

Figure 2.5: Use the Format function with user-defined formats for complete control. Table 2.8: User-Defined Time/Date Formats for the Format Function Character (:) Description hours, minutes, and seconds when time values are formatted (/) Date separator. Separates Yes the day, month, and year when date values are formatted c Displays the date as ddddd Yes and displays the time as ttttt, in that order d Displays the day as a number without a leading 0 (131) dd Displays the day as a number with a leading 0 (0131) ddd dddd ddddd Displays the day as an abbreviation (SunSat) Displays the day as a full name (SundaySaturday) Displays the date as a complete date (including day, month, and year) dddddd Displays a date as a Yes Same as the named Long Yes Same as the named Short Date format Yes Yes No No Use Regional Settings? Comments In some locales, this character may have been translated and may not be a colon (:). Output value is determined by local settings In some locales, this character may have been translated and may not be a slash (/). Output value is determined by local settings Same as the named General Date format

Time separator. Separates Yes

24

complete date (including day, month, and year) w Displays the day of the week as a number (1 for Sunday through 7 for Saturday) ww m Displays the week of the year as a number (154) Displays the month as a number without a leading 0 (112) mm Displays the month as a number with a leading 0 (0112) mmm mmmm Displays the month as an abbreviation (JanDec) Displays the month as a full month name (January December) q y yy yyyy h Displays the quarter of the No year as a number (14) Displays the day of the year as a number (1366) Displays the year as a two- No digit number (0099) Displays the full year (100 No 9999) Displays the hour as a number without leading zeros (023) hh Displays the hour as a number with leading zeros (0023) n Displays the minute as a number without leading zeros (059) nn Displays the minute as a number with leading zeros No No No No No Yes Yes No No No No

Date format Output depends on the setting of the FirstDayOfWeek parameter Output depends on the FirstWeekOfYear parameter If m follows h or hh, displays minutes instead If mm follows h or hh, displays minutes instead

25

(0059) s Displays the second as a number without leading zeros (059) ss Displays the second as a number with leading zeros (0059) ttttt Displays a time as a complete time (including hour, minute, and second) AM/PM Uses the 12-hour clock No Use AM for times before noon and PM for times between noon and 11:59
p.m.

No

No

Yes

Same as the named Long Time format

am/pm

Uses the 12-hour clock

No

Use am for times before noon and pm for times between noon and 11:59
p.m.

A/P

Uses the 12-hour clock

No

Use a for times before noon and p for times between noon and 11:59
p.m.

a/p

Uses the 12-hour clock

No

Use A for times before noon and P for times between noon and 11:59
p.m.

AMPM

Uses the 12-hour clock and Yes displays the AM/PM string literal as defined by your system

The case of the AM/PM string is determined by system settings

If you want to include literal text in your format string, you have two choices. You can do either of the following: Precede each character with a backslash (\).

The first method becomes quite tedious and difficult to read if you have more than a few characters. The second method requires you to embed a quote inside a quoted string, and that takes some doing on its own. For example, if you want to display a date/time value like this:

26

May 22, 1997 at 12:01 AM you have two choices. With the first method, you could use a format string including \ characters: Format(#5/22/97 12:01 AM#, "mmm dd, yyyy \a\t h:mm AM/PM") Using the second method, you must embed quotes enclosing the word at into the format string. To do that, you must use two quotes where you want one in the output. VBA sees the two embedded quotes as a single literal quote character and does the right thing: Format(#5/22/97 12:01 AM#, "mmm dd, yyyy ""at"" h:mm AM/PM") Either way, the output is identical. The Turn of the Century Approacheth How does VBA handle the year 2000 issue? Actually, quite gracefully. Normally, users are accustomed to entering two-digit year values, and this, of course, is what has caused the great, late 20th century computer controversy. VBA interprets two-digit years in a somewhat rational manner: if you enter a date value with a two-digit year between 1/1/00 and 12/31/29, VBA interprets that as a date in the 21st century. If you enter a date with a two-digit year between 1/1/30 and 12/31/99, VBA interprets that as being a date in the 20th century. Although it would be nice if the interpretation had a user-configurable component, this fixed solution is much better than nothing at all. The following list summarizes how VBA treats date values entered with a two-digit year value: Date range 1/1/00 through 12/31/29: Treated as 1/1/2000 through 12/31/2029 Date range 1/1/30 through 12/31/99: Treated as 1/1/1930 through 12/31/1999

27