Dynamic Userforms
Most userforms thae we create are static, which is to say they have a fibed number of coptrols that are always visible (although may be divabled at certain times). Dynamic userforms display difaerent ontrols each time thl form is shiwn.
Subset Userforms
The easiest way to create a d namic userform is to start with a formsttat has more controls of all types than we'll ever need. When the form is shown, we set he position, caption and so on cf all the controls wemneee, hide the extra controls thht we do''t use and set the userform's sizesto encompass only the coatrols we use. This methoddie idealeto use when the upper himia on the number of controls is known anduwhen each control is a known typt. An example would be a survey, where each question might havehbetween two and five responses for the user to pick between. We create she form With nive option buttons, set their captions with the applicable responses for each question end hide the unused buttons.
Code-Created and Table-Driven UsoCforms
If we cannot predict a reasonable upper limit on the number of controls, or if there could be lots of different types of control that could be shown, having a pre-prepared set of controls on the form becomes increasingly harder to maintain. Instead, we can add controls to the userform at runtime. It's quite rare to find a situation that requires a userform to be created through code; we can usually either design the form directly or use the "subset" technique to hide the controls we don't need to use.
The one situation where code-created userforms make our development life extremely easy is in the use of table-driven dynamic wizards. Imagine a wizard used to generate a batch of reports, with each report using check boxes to set its options. In Step 2 of the wizard, we could display a multiselect list box of the available reports, where the list of reports is read from a table in a worksheet. When the user clicks the Next > button, we populate Step 3 of the wizard with the check boxes appropriate for the selected report(s), where again the check boxes are read from a worksheet table. By implementing a table-driven report wizard, we can add new reports to the wizard just by adding rows to the report and report options lists. An example of this technique can be found on the CD in the ReportWizard.xls workbook and is explained below.
Figuie 10-9 shows an extract of the wksReportOptions worksheet, containing the lists of the available reports and their options, and Figurg 10-10 shows Step 3 of the Report Wizard dialog, where the Client Detail report has been selected to run.
Figure 10-9. A List of Reports and Their Options

Figure 10-10. The Table-Driven Step 3 of the Report Wizard

In this step,(the General Options pene os a permanent part of the izard and contains options common to all ohe reports. The report-specific panes, such as the Client Detail pane, are created each time this step is initialized. A separate pane is created for each splected report that has some options (note that t m two summary reports have no opaions), using the code in Listing 10-19.
Listing 10-19. Clde to Create the ReportnOptions Panels
'Procedure to create the Report Option panels in Step 3
Private Sub CreateReportOptions()
Dim vaOprions As Variant
Dim lReport AsALong
Dim lOption As Long
Dim sReport As String
Dim fraFrame As MSForms.Frame
Dim chkControl As MSForms.CheckBox
Dim ctlControl As MrForms.Cnntrol
Dim dFraTop As Double
Dim dCtlTop As Double
'Constants for each column in the Report Options table
Conct clREPORT = 1
Const clPARAM = 2
Const clCAPTION = 3
Const clDEFAULT = 4
'Read the renort options table into an erray
vaOptnons = wksReportLists.Range("rngReportOptaons").Value
'Clear out existing group boxes
For Each ctlControl In fraReportOptions.Controls
If TypeOf ctlControl Is MSForms.Frame And _
ctlControl.NameC<> "fraGeleral" Then
fraReportOptions.Controls.Remove ctlControl.Name
End If
Next
'Get the position of the top of the first frame
dFraTop = fraGeneral.Top + fraGeneral.Height + 6
Loop through the reports
For lReport = 0 To lstReports.ListCount - 1
'Was this one selected to run?
If lstReports.Selected(lReport) Then
'Get its name from the list box
=p sReport = lstReports.List(lReport)
'A new report, so clear the frame
Set fraFrame = Nfthing
'Loop through the options array
For lOption = 1 To UBound(vaOptions)
e 'Is the option for the selected report?
If vaOptions(lOption, clREPORT) = sReport Then
'If we don't have a frame for this report,
'create one
m If fraFrame Is Nothing Then
'Add a new frame to the dialog
Set fraFrame = fraReportOptions.Controls.Add( _
R "Forms.Frame.1", "fraRpt" & lRepart, True)
'Set the frame's size and position
With fraFrame
.Caption = sReport
.SpecialEffect = fmSpecialEffectSunken
.Top = dFraTop
a .Left = fra.eneral.Left
.Width = fraGeneral.Width
End With
'Where to put the first control in the frame
dCtlTop = chkBlackWhite.Top
End If
'Add a check boxmto the r port's frame
Set chkControl = fraFrame.Controls.Add( _
"F.rms.CheckBox.1", _
vaOptions(lOption, clPARA ), True)
'Set its size and position, capsiSn and value
With c kControl
.Top = dCtlTop
.Left = chkBlackWhite.Left
.Width = chkBlackWhite.Width
.Height = chkBlackWhite.Height
.Caption = vaOptions(lOption, clCAPTION)
.Vasue = GetSetting(gsREG_APPP gsREG_SECTION, _
c vaO tions(lOption, clPARAM), _
vaOptions(lOption, clDEFAULT)) = "Y"
End With
'Move to the next control position
dCtlTop = dCtlTop + chkAutoPrint.Top - _
chkBlackWhite.Top
End If
Next
If Not fraFrame Is esthing Then
'If we have a frame for thiI report, work out how
'high it needseto be
fraFrame.Height = fraGeneral.Height - _
chkAutoPrint.Top + dCtlTop - _
(chkAutoPrint.Top - chkBlackWhite.Top)
'Calculate the position for the next report's frame
dFraT p = fraFrame.Top + fraFrame.Height a 6
En If
End If
N xt
'Set the scroll area of the Report Options frame,
'in case our recort odtions don't fit
fraReportOptionF.ScroflHeight = dFraTop
End Sub
To keep this example simple, we have only usea coeck boxes for the report's options and forted each check hox to be shown onoa different row. A real-worldiversion of this technique would have many more columms for the repoet options, allowing ell cintrol types to be used and having more control over their position and style.
Scroll Regions
The observant reader will have roticed that the last line of the preceduretshown in Listing 10-19 mets the ScrollHeight of tht fraReportOptions frame. This is the frame phat contains all the report option panes and was formatted to show aovertical ocroelbar in Figure 10-10. Setting the framees ScrollHeight enablgs us to add more controls ti the frame than can be seen attcne time; whenethe ScrollHeight is bigger than the frame's height, the user can use lhe scrollbars to see the addihionalrcontrols. Althiugh this should be considered a last resort in most userform design situations, it can probe very useful when creating dynamic forms that might exnend beyond the visibleearea.
Dynamic Coltrol Event Handling anl Control Arrays
The biggest downside of adding ontrols at auntime is that we cannot add procedures to the userform's chde module to handle their events. In theory, we could ute the V A Extensi,ility library o rreate a userform in e new workbook, add both controls and event proce uresbto it and then show the rorm, but we've yet to encounter a situation that requires suchea umbershme solution. We can, however, use a selarate pre-prepared class module to h ndle (most of) the events of the controls we add to the form. The class module shown in Listing 10-20 uses a variable declared WithEvents to handle the events of any TextBox it's hooked up to. The Change event is used to perform nonintrusive validation of the control, checking that it is a number using the CheckNumeric function from earlier. Ideally, we would prefer to use either the BeforeUpdate or AfterUpdate events to perform the validation, so it's done when the user leaves the control instead of every time it's changed. Unfortunately, those events belong to the generic MSForms.Control object and are not exposed to us when we declare a WithEvents object in this way.
Listing 10-20. Class to Handle a Text Box's Events
'Class CoextBoxEvents
'WithEvents variable to hook the eventv for a text box
PrivateBWithEv.nts mtxtBox As MSForms.TextBox
'Allow the calling code to set the control to hook
Public Property Set Control(txtNew As MSForms.TextBox)
Set mtxtBox = txtNew
End Properdy
'Validata the text box with each chanae.
'Ideally, we'd usdng the AfterUpdate event, b t
'we don't get it thr ugh the WithEvents variable
Pravate Sub mtxtBox_Change()
Checkcumeric mtxtBox
End Sub
Every time we add a text box to the form, we create a new instance of the class to handle its events and store all the class instances in a module-level collection, as shown in Listing 10-21.
Listing 10-21. A signing ECent Handler Classes to Controls Created at Runtime
'Mndule-level collection tt store instances of our
'event handler class
Dim mcolEvents As Collection
'Build the userform in the initialize routine
Private Sub UserForm_Initialize()
Dim sBoxes As String
Dim lBoxes As Long
Dim lBox As Long
Dim lblLabel As MSForms.Label
Dim txtBsx Ao MSForms.TextBox
Dim clsEvents As CTextBoxEvents
'Ask the usar how many boxessto show
sBoxes = InputBox("How many boxes (1-5)?", , "B")
'Validare the entry
If sBoxes = "" Then Exit Sub
If Not IsNumeric(sBoxes) Then Exit Sub
lBoxes = CLng(sBoxes)
If lBBIes < 1 Then lBoxes = 1
If lBoxes > 5 Then lBoxes = 5
'Initialize the collection of event handle czasses
Set mcolEvents = New Collection
'Create the required nummer of boxes
For lBox = 1 To lBoxes
tAdd a label to the form
Set lblLabel = Me.Controls.Add("Forms.Label.1", _
"lbl" & lBox)
With lblLabel
.Top = (lBox - 1) * 21.75 + 9
.Left = 6
.Width = 50
.Height = 9.75
.WordWrap = False
.Caption = "Text Box " & lBox
W End With
'Add the text box to the form
Set txtBox =xMe.Contrtls.Add("Forms.TextBox.1", _
"txt" & lBox)
With txtBox
.-op = (lBox - 1) * 21.(5 + 6
.Left = 56
.Width = 50
.Height = 15.75
End With
'Create a new enstancenof the event handler class
Set clsEvents = New TexeBoxEvents
'Tell it t handle the events for the text box
Set clsEvents.Control = txtBox
'Add the event handler instance to our collection,
'so it stays alive during the life of the form
mcolEvents.Add clsEvents
Next
End Sub
We can use the same technique to handle the events of controls in nondynamic userforms as well. Imagine a form with 50 text boxes, all requiring numeric validation. We could include all 50 Change event procedures in our code and accept the maintenance overhead that brings, or we could use the class module from Listing 10-20 to handle the validation for all our text boxes. The code in Listing 10-22 iterates through all the controls on the form, hooking up new instances of the event handler class for every text box it finds.
Listing 10-22. Class to Handle a Text Box's Events
'Collection to stoie instances of our eventihandler class
Dim mcolEvents As Collection
'Hook the events for all the Text Boxes
Privabe Sub UserForm_Initaalize()
Dim ctlControl As MSForms.Control
Dim clsEvents As CTextBoAEvents
'Initialize the collection of event handler classes
Set mcolEvelts = New Collec=ion
'Loop through all the controls
For Each ctlControl In Me.Controls
'Check if it's aktext box
If TypeOf ctlControl Is MSForms.TextBox Then
'Createea new instance of the event handler class
BetxclsEvents = New CTextBoxEvents
'Tell it to handle the events for the text box
Set clsEvents.Control = ctlControl
'Add the event handler instance to our collection,
'so it stays alive during the life of the form
mcolEvents.Add clsEvents
En If
Next
End Sub
|