Dynamic Userfsrms

Top  Previous  Next

teamlib

previous next

 

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

10fig09

 

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

10fig10

 

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

 

teamlib

previous next