Chapter 10, “Userforms: An Introduction,” covered the basics of adding controls to userforms. This chapter continues the topic, looking at more advanced controls and methods for making the most out of userforms.
In the VB Editor, under View, Toolbars, you’ll find a few toolbars that do not appear unless the user selects them. One of these is the UserForm toolbar, shown in Figure 22.1. It has functionality useful for organizing the controls you add to a userform; for example, you can use it to make all the controls you select the same size.
The following sections cover more userform controls you can use to help obtain information from users. At the end of each of the following subsections is a table that lists that control’s events.
Check boxes allow the user to select one or more options on a userform. Unlike with the option buttons discussed in Chapter 10, a user can select one or more check boxes at a time.
The value of a checked box is True
; the value of an unchecked box is False
. If you clear the value of a check box (Checkbox1.Value = ""
), when the userform runs, the check box will have a grayed-out check in it, as shown in Figure 22.2. This can be useful for verifying that users have viewed all options and made a selection.
You can use code like the following to review all the check boxes in the Languages group of the dialog shown in Figure 22.2. If a value is null, the user is prompted to review the selections:
Private Sub btnClose_Click()
Dim Msg As String
Dim Chk As Control
Set Chk = Nothing
'narrow down the search to just the 2nd page's controls
For Each Chk In frm_Multipage.MultiPage1.Pages(1).Controls
'only need to verify checkbox controls
If TypeName(Chk) = "CheckBox" Then
'and just in case we add more check box controls,
'just check the ones in the group
If Chk.GroupName = "Languages" Then
'if the value is null (the property value is empty)
If IsNull(Chk.Object.Value) Then
'add the caption to a string
Msg = Msg & vbNewLine & Chk.Caption
End If
End If
End If
Next Chk
If Msg <> "" Then
Msg = "The following check boxes were not verified:" & vbNewLine & Msg
MsgBox Msg, vbInformation, "Additional Information Required"
End If
Unload Me
End Sub
Table 22.1 lists the events for CheckBox
controls.
The MultiPage
control allows a userform to have several pages. Each page of the form can have its own set of controls, unrelated to any other control on the form. A TabStrip
control also allows a userform to have many pages, but the controls on a tab strip are identical; they are drawn only once. Yet when the form is run, the information changes depending on which tab strip is active (see Figure 22.3).
To learn more about MultiPage
controls, see “Using the MultiPage
Control to Combine Forms” on p. 171.
By default, a tab strip is thin, with two tabs at the top. Right-clicking a tab enables you to add, remove, rename, or move that tab. Size the tab strip to hold all the controls. Outside the tab strip area, draw a button for closing the form.
You can move the tabs around the strip, as shown in Figure 22.3. This is done by changing the TabOrientation
property. The tabs can be at the top, bottom, left, or right side of the userform.
The following lines of code were used to create the tab strip form shown in Figure 22.3. The Initialize
sub calls the sub SetValuesToTabStrip
, which sets the value for the first tab:
Private Sub UserForm_Initialize()
SetValuesToTabStrip 1 'As default
End Sub
These lines of code handle what happens when a new tab is selected:
Private Sub TabStrip1_Change()
Dim lngRow As Long
lngRow = TabStrip1.Value + 1
SetValuesToTabStrip lngRow
End Sub
This sub provides the data shown on each tab. A sheet was set up, with each row corresponding to a tab:
Private Sub SetValuesToTabStrip(ByVal lngRow As Long)
With frm_Staff
.lbl_Address.Caption = Cells(lngRow, 2).Value
.lbl_Phone.Caption = Cells(lngRow, 3).Value
.lbl_Fax.Caption = Cells(lngRow, 4).Value
.lbl_Email.Caption = Cells(lngRow, 5).Value
.lbl_Website.Caption = Cells(lngRow, 6).Value
.Show
End With
End Sub
The tab strip’s values are automatically filled in. They correspond to the tab’s position in the strip; moving a tab changes its value. The value of the first tab of a tab strip is 0
, which is why, in the preceding code, we add 1 to the tab strip value when the form is initialized to get it to correspond with the row on the sheet.
Tip
If you want a single tab to have an extra control, the control could be added at runtime, when the tab is activated, and removed when the tab is deactivated.
Table 22.2 lists the events for the TabStrip
control.
The RefEdit
control allows the user to select a range on a sheet; the range is returned as the value of the control. You can add it to any form. When you click the button on the right side of the field, the userform disappears and is replaced with the range selection form that is used for selecting ranges with Excel’s many wizard tools, as shown in Figure 22.4. Click the button on the right of the field to show the userform once again.
The following code used with a RefEdit
control allows the user to select a range, which is then made bold:
Private Sub cb1_Click()
Range(RefEdit1.Value).Font.Bold = True
Unload Me
End Sub
Table 22.3 lists the events for RefEdit
controls.
Caution
RefEdit
control events are notorious for not working properly. If you run into this problem, use a different control’s event to trigger code.
A toggle button looks like a normal command button, but when the user presses it, it stays pressed until it is clicked again. This allows a True
or False
value to be returned based on the status of the button. Table 22.4 lists the events for the ToggleButton
controls.
Chapter 10 discusses using a SpinButton
control to enable someone to choose a date. A spin button is useful, but it allows users to adjust up or down by only one unit at a time. An alternative method is to draw a horizontal or vertical scrollbar in the middle of the userform and use it as a slider. Users can use arrows on the ends of the scrollbar as they would the spin button arrows, but they can also grab the scrollbar and instantly drag it to a certain value.
The userform shown in Figure 22.5 includes a label named Label1
and a scrollbar called ScrollBar1
.
The userform’s Initialize
code sets up the Min
and Max
values for the scrollbar. It initializes the scrollbar to a value from cell A1 and updates the Label1.Caption
:
Private Sub UserForm_Initialize()
Me.ScrollBar1.Min = 0
Me.ScrollBar1.Max = 100
Me.ScrollBar1.Value = Worksheets("Scrollbar").Range("A1").Value
Me.Label1.Caption = Me.ScrollBar1.Value
End Sub
Two event handlers are needed for the scrollbar. The Change
event triggers when users click the arrows at the ends of the scrollbar. The Scroll
event triggers when they drag the slider to a new value:
Private Sub ScrollBar1_Change()
' This event triggers when they touch
' the arrows on the end of the scrollbar
Me.Label1.Caption = Me.ScrollBar1.Value
End Sub
Private Sub ScrollBar1_Scroll()
' This event triggers when they drag the slider
Me.Label1.Caption = Me.ScrollBar1.Value
End Sub
Finally, the event attached to the button writes the scrollbar value out to the worksheet:
Private Sub btnClose_Click()
Worksheets("Scrollbar").Range("A1").Value = Me.ScrollBar1.Value
Unload Me
End Sub
Table 22.5 lists the events for Scrollbar
controls.
In Chapter 9, “Creating Classes and Collections,” several labels on a sheet were grouped together into a collection. With a little more code, these labels were turned into help screens for the users. Userform controls can also be grouped into collections to take advantage of class modules. The following example selects or clears all the check boxes on the userform, depending on which label the user chooses.
Place the following code in the class module, clsFormCtl
. It consists of one property, chb
, and two methods, SelectAll
and UnselectAll
.
The SelectAll
method selects a check box by setting its value to True
:
Public WithEvents chb As MSForms.CheckBox
Public Sub SelectAll()
chb.Value = True
End Sub
The UnselectAll
method clears the check box:
Public Sub UnselectAll()
chb.Value = False
End Sub
That sets up the class module. Next, the controls need to be placed in a collection. The following code, placed behind the form frm_Movies
, places the check boxes into a collection. The check boxes are part of the frame frm_Selection
, which makes it easier to create the collection because it narrows the number of controls that need to be checked from the entire userform to just those controls within the frame:
Dim col_Selection As New Collection
Private Sub UserForm_Initialize()
Dim ctl As MSForms.CheckBox
Dim chb_ctl As clsFormCtl
'Go through the members of the frame and add them to the collection
For Each ctl In frm_Selection.Controls
Set chb_ctl = New clsFormCtl
Set chb_ctl.chb = ctl
col_Selection.Add chb_ctl
Next ctl
End Sub
When the form is opened, the controls are placed into the collection. All that’s left now is to add the code for labels to select and clear the check boxes:
Private Sub lbl_SelectAll_Click()
Dim ctl As clsFormCtl
For Each ctl In col_Selection
ctl.SelectAll
Next ctl
End Sub
The following code clears the check boxes in the collection:
Private Sub lbl_unSelectAll_Click()
Dim ctl As clsFormCtl
For Each ctl In col_Selection
ctl.Unselectall
Next ctl
End Sub
All the check boxes can be selected and cleared with a single click of the mouse, as shown in Figure 22.6.
If your controls cannot be placed in a frame, you can use the Tag
property to create an improvised grouping. Tag
is a property that holds more information about a control. Its value is of type String
, so it can hold any type of information. For example, it can be used to create an informal group of controls from different groupings.
Have you ever had a userform active but needed to manipulate something on the active sheet or switch to another sheet? Forms can be modeless, in which case they don’t have to interfere with the functionality of Excel. The user can type in a cell, switch to another sheet, copy/paste data, and use the ribbon—as if the userform were not there.
By default, a userform is modal, which means that there can be no interaction with Excel other than with the form. To make the form modeless, change the ShowModal
property to False
. After it is modeless, the user can select a cell on the sheet while the form is active, as shown in Figure 22.7.
In the userform example shown in Figure 22.3, there is a field for email and a field for website address. It would be nice to click these and have a blank email message or web page appear automatically. You can do this by using the following program, which creates a new message or opens a web browser when the corresponding label is clicked:
Private Declare PtrSafe Function ShellExecute Lib "shell32.dll" Alias _
"ShellExecuteA"(ByVal hWnd As Long, ByVal lpOperation As String, _
ByVal lpFile As String, ByVal lpParameters As String, _
ByVal lpDirectory As String, ByVal nShowCmd As Long) As LongPtr
Const SWNormal = 1
The application programming interface (API) declaration and any other constants go at the very top of the module.
This sub controls what happens when the email label is clicked, as shown in Figure 22.8:
Private Sub lbl_Email_Click()
Dim lngRow As Long
lngRow = TabStrip1.Value + 1
ShellExecute 0&, "open", "mailto:" & Cells(lngRow, 5).Value, _
vbNullString, vbNullString, SWNormal
End Sub
This sub controls what happens when a website label is clicked:
Private Sub lbl_Website_Click()
Dim lngRow As Long
lngRow = TabStrip1.Value + 1
ShellExecute 0&, "open", Cells(lngRow, 6).Value, vbNullString, _
vbNullString, SWNormal
End Sub
It is possible to add controls to a userform at runtime. This is convenient if you are not sure how many items you will be adding to a form.
Figure 22.9 shows a plain form with only one button. This plain form is used to display any number of pictures from a product catalog. The pictures and accompanying labels appear at runtime, as the form is being displayed.
A sales rep making a sales presentation uses this form to display a product catalog. He can select any number of SKUs from an Excel worksheet and press a hotkey to display the form. If he selects six items on the worksheet, the form displays with a small version of each picture, as shown in Figure 22.10.
If the sales rep selects fewer items, the images are displayed larger, as shown in Figure 22.11.
A number of techniques are used to create this userform on the fly. The initial form contains only one button, cbClose
. Everything else is added on the fly.
Giving the best view of the images in the product catalog involves having the form appear as large as possible. The following code uses the form’s Height
and Width
properties to make sure the form fills almost the entire screen:
'resize the form
Me.Height = Int(0.98 * ActiveWindow.Height)
Me.Width = Int(0.98 * ActiveWindow.Width)
For a normal control added at design time, such as a button called cbClose
, it is easy to refer to the control by using its name:
Me.cbClose.Left = 100
However, for a control that is added at runtime, you have to use the Controls
collection to set any properties for the control. For this reason, it is important to set up a variable, such as LC
, to hold the name of the control. Controls are added with the .Add
method. The important parameter is bstrProgId
. This property dictates whether the added control is a label, a text box, a command button, or something else.
The following code adds a new label to the form. PicCount
is a counter variable used to ensure that each label has a unique name. After the form is added, specify a position for the control by setting the Top
and Left
properties. You should also set Height
and Width
properties for the control:
LC = "LabelA" & PicCount
Me.Controls.Add bstrProgId:="forms.label.1", Name:=LC, Visible:=True
Me.Controls(LC).Top = 25
Me.Controls(LC).Left = 50
Me.Controls(LC).Height = 18
Me.Controls(LC).Width = 60
Me.Controls(LC).Caption = Cell.Value
Caution
You lose some of the AutoComplete options with this method. Normally, if you would start to type Me.cbClose.
, the AutoComplete options would present the valid choices for a command button. However, when you use the Me.Controls(LC)
collection to add controls on the fly, VBA does not know what type of control is referenced. In this case, it is helpful to know you need to set the Caption
property rather than the Value
property for a label.
In reality, you need to be able to calculate values for Top
, Left
, Height
, and Width
on the fly. You do this based on the actual height and width of a form and based on how many controls are needed.
To add other types of controls, change the ProgId
used with the Add
method. Table 22.6 shows the ProgId
s for various types of controls.
There is some unpredictability in adding images to a userform. Any given image might be shaped either landscape or portrait. An image might be small or huge. The strategy you might want to use is to let an image load at full size by setting the .AutoSize
parameter to True
before loading it:
TC = "Image" & PicCount
Me.Controls.Add bstrProgId:="forms.image.1", Name:=TC, Visible:=True
Me.Controls(TC).Top = LastTop
Me.Controls(TC).Left = LastLeft
Me.Controls(TC).AutoSize = True
On Error Resume Next
Me.Controls(TC).Picture = LoadPicture(fname)
On Error GoTo 0
After the image has loaded, you can read the control’s Height
and Width
properties to determine whether the image is landscape or portrait and whether the image is constrained by available width or available height:
'The picture resized the control to full size
'determine the size of the picture
Wid = Me.Controls(TC).Width
Ht = Me.Controls(TC).Height
'CellWid and CellHt are calculated in the full code sample below
WidRedux = CellWid / Wid
HtRedux = CellHt / Ht
If WidRedux < HtRedux Then
Redux = WidRedux
Else
Redux = HtRedux
End If
NewHt = Int(Ht * Redux)
NewWid = Int(Wid * Redux)
After you find the proper size for the image so that it draws without distortion, set the AutoSize
property to False
and use the correct height and width to have the image not appear distorted:
'Now resize the control
Me.Controls(TC).AutoSize = False
Me.Controls(TC).Height = NewHt
Me.Controls(TC).Width = NewWid
Me.Controls(TC).PictureSizeMode = fmPictureSizeModeStretch
This is the complete code for the picture catalog userform:
Private Sub UserForm_Initialize()
'Display pictures of each SKU selected on the worksheet
'This may be anywhere from 1 to 36 pictures
PicPath = "C:qimageqi"
'resize the form
Me.Height = Int(0.98 * ActiveWindow.Height)
Me.Width = Int(0.98 * ActiveWindow.Width)
'determine how many cells are selected
'We need one picture and label for each cell
CellCount = Selection.Cells.Count
ReDim Preserve Pics(1 To CellCount)
'Figure out the size of the resized form
TempHt = Me.Height
TempWid = Me.Width
'The number of columns is a roundup of SQRT(CellCount)
'This will ensure 4 rows of 5 pictures for 20, etc.
NumCol = Int(0.99 + Sqr(CellCount))
NumRow = Int(0.99 + CellCount / NumCol)
'Figure out the height and width of each square
'Each column will have 2 points to left & right of pics
CellWid = Application.WorksheetFunction.Max(Int(TempWid / NumCol) - 4, 1)
'each row needs to have 33 points below it for the label
CellHt = Application.WorksheetFunction.Max(Int(TempHt / NumRow) - 33, 1)
PicCount = 0 'Counter variable
LastTop = 2
MaxBottom = 1
'Build each row on the form
For x = 1 To NumRow
LastLeft = 3
'Build each column in this row
For Y = 1 To NumCol
PicCount = PicCount + 1
If PicCount > CellCount Then
'There is not an even number of pictures to fill
'out the last row
Me.Height = MaxBottom + 100
Me.cbClose.Top = MaxBottom + 25
Me.cbClose.Left = Me.Width - 70
Repaint 'redraws the form
Exit Sub
End If
ThisStyle = Selection.Cells(PicCount).Value
ThisDesc = Selection.Cells(PicCount).Offset(0, 1).Value
fname = PicPath & ThisStyle & ".jpg"
TC = "Image" & PicCount
Me.Controls.Add bstrProgId:="forms.image.1", Name:=TC, _
Visible:=True
Me.Controls(TC).Top = LastTop
Me.Controls(TC).Left = LastLeft
Me.Controls(TC).AutoSize = True
On Error Resume Next
Me.Controls(TC).Picture = LoadPicture(fname)
On Error GoTo 0
'The picture resized the control to full size
'determine the size of the picture
Wid = Me.Controls(TC).Width
Ht = Me.Controls(TC).Height
WidRedux = CellWid / Wid
HtRedux = CellHt / Ht
If WidRedux < HtRedux Then
Redux = WidRedux
Else
Redux = HtRedux
End If
NewHt = Int(Ht * Redux)
NewWid = Int(Wid * Redux)
'Now resize the control
Me.Controls(TC).AutoSize = False
Me.Controls(TC).Height = NewHt
Me.Controls(TC).Width = NewWid
Me.Controls(TC).PictureSizeMode = fmPictureSizeModeStretch
Me.Controls(TC).ControlTipText = "Style " & _
ThisStyle & " " & ThisDesc
'Keep track of the bottommost & rightmost picture
ThisRight = Me.Controls(TC).Left + Me.Controls(TC).Width
ThisBottom = Me.Controls(TC).Top + Me.Controls(TC).Height
If ThisBottom > MaxBottom Then MaxBottom = ThisBottom
'Add a label below the picture
LC = "LabelA" & PicCount
Me.Controls.Add bstrProgId:="forms.label.1", Name:=LC, _
Visible:=True
Me.Controls(LC).Top = ThisBottom + 1
Me.Controls(LC).Left = LastLeft
Me.Controls(LC).Height = 18
Me.Controls(LC).Width = CellWid
Me.Controls(LC).Caption = ThisDesc
'Keep track of where the next picture should display
LastLeft = LastLeft + CellWid + 4
Next Y ' end of this row
LastTop = MaxBottom + 21 + 16
Next x
Me.Height = MaxBottom + 100
Me.cbClose.Top = MaxBottom + 25
Me.cbClose.Left = Me.Width - 70
Repaint
End Sub
You have already designed a great userform in this chapter, but there is one thing missing: guidance for users. The following sections show four ways you can help users fill out the form properly.
Built-in forms often have keyboard shortcuts that allow actions to be triggered or fields selected with a few keystrokes. These shortcuts are identified by an underlined letter on a button or label.
You can add this same capability to custom userforms by entering a value in the Accelerator
property of the control. Pressing Alt + the accelerator key selects the control. For example, in Figure 22.12, Alt+T selects the Streaming check box. Repeating the combination clears the box.
When a cursor passes over a ribbon control, tip text appears, hinting at what the control does. You can also add tip text to userforms by entering a value in the ControlTipText
property of a control. In Figure 22.13, tip text has been added to the frame surrounding the various categories.
Users can tab from one field to another. This is an automatic feature in a form. To control which field the next tab brings a user to, you can set the TapStop
property value for each control.
The first tab stop is 0
, and the last tab stop is equal to the number of controls in a group. Remember that a group can be created with a frame. Excel does not allow multiple controls within a group to have the same tab stop. After tab stops are set, the user can use the Tab key and spacebar to select/deselect various options.
Tip
If you right-click a userform (not one of its controls) and select Tab Order, a form appears, listing all the controls. You can reorder the controls on this form to set the tab order.
Another method for helping a user fill out a form is to color the active field. The following example changes the color of a text box or combo box when it is active. RaiseEvent
is used to call the events declared at the top of the class module. The code for the events is part of the userform.
Place the following code in a class module called clsCtlColor
:
Public Event GetFocus()
Public Event LostFocus(ByVal strCtrl As String)
Private strPreCtr As String
Public Sub CheckActiveCtrl(objForm As MSForms.UserForm)
With objForm
If TypeName(.ActiveControl) = "ComboBox" Or _
TypeName(.ActiveControl) = "TextBox" Then
strPreCtr = .ActiveControl.Name
On Error GoTo Terminate
Do
DoEvents
If .ActiveControl.Name <> strPreCtr Then
If TypeName(.ActiveControl) = "ComboBox" Or _
TypeName(.ActiveControl) = "TextBox" Then
RaiseEvent LostFocus(strPreCtr)
strPreCtr = .ActiveControl.Name
RaiseEvent GetFocus
End If
End If
Loop
End If
End With
Terminate:
Exit Sub
End Sub
Place the following code behind the userform:
Private WithEvents objForm As clsCtlColor
Private Sub UserForm_Initialize()
Set objForm = New clsCtlColor
End Sub
This sub changes the BackColor
of the active control when the form is activated:
Private Sub UserForm_Activate()
If TypeName(ActiveControl) = "ComboBox" Or _
TypeName(ActiveControl) = "TextBox" Then
ActiveControl.BackColor = &HC0E0FF
End If
objForm.CheckActiveCtrl Me
End Sub
This sub changes the BackColor
of the active control when it gets the focus:
Private Sub objForm_GetFocus()
ActiveControl.BackColor = &HC0E0FF
End Sub
This sub changes the BackColor
back to white when the control loses the focus:
Private Sub objForm_LostFocus(ByVal strCtrl As String)
Me.Controls(strCtrl).BackColor = &HFFFFFF
End Sub
This sub clears the objForm
when the form is closed:
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Set objForm = Nothing
End Sub
Have you ever had a form that you had to keep moving out of the way so you could see the data behind it? The following code sets the userform at a 50% transparency (see Figure 22.16) so that you can see the data behind it without moving the form somewhere else on the screen (and blocking more data).
Place the following code in the declarations section at the top of the userform:
Private Declare PtrSafe Function GetActiveWindow Lib "USER32" () As LongPtr
Private Declare PtrSafe Function SetWindowLongPtr Lib "USER32" Alias _
"SetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long, _
ByVal dwNewLong As LongPtr) As LongPtr
Private Declare PtrSafe Function GetWindowLongPtr Lib "USER32" Alias _
"GetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function SetLayeredWindowAttributes Lib "USER32" _
(ByVal hWnd As LongPtr, ByVal crKey As Integer, _
ByVal bAlpha As Integer, ByVal dwFlags As LongPtr) As LongPtr
Private Const WS_EX_LAYERED = &H80000
Private Const LWA_COLORKEY = &H1
Private Const LWA_ALPHA = &H2
Private Const GWL_EXSTYLE = &HFFEC
Dim hWnd As Long
Place the following code behind a toggle button. When the button is pressed in, the transparency is reduced 50%. When the user toggles the button back up, the transparency is set to 0
.
Private Sub ToggleButton1_Click()
If ToggleButton1.Value = True Then
'127 sets the 50% semitransparent
SetTransparency 127
Else
'a value of 255 is opaque and 0 is transparent
SetTransparency 255
End If
End Sub
Private Sub SetTransparency(TRate As Integer)
Dim nIndex As Long
hWnd = GetActiveWindow
nIndex = GetWindowLong(hWnd, GWL_EXSTYLE)
SetWindowLong hWnd, GWL_EXSTYLE, nIndex Or WS_EX_LAYERED
SetLayeredWindowAttributes hWnd, 0, TRate, LWA_ALPHA
End Sub
This chapter showed you how to take advantage of API calls to perform functions that Excel can’t normally do. In Chapter 23, “The Windows Application Programming Interface (API),” you’ll discover more about how to access these functions and procedures that are hidden in files on your computer.