引言:本文来源于《Pro Excel VBA》的第4章的示例,主要为学习VBA用户窗体提供参考。本文“超长”,但如果能够仔细研读,一定会有很丰富的收获。可以先下载示例文档:
向导样式的输入是相当普遍的技术,用于帮助用户输入较多的或复杂的数据。向导允许将数据分成相互联系的部分,通过按顺序输入数据的过程指导用户。
示例工作簿名称为HRWizard.xlsm,由2个工作表组成,名为EmpData的工作表为员工数据库工作表,名为ListMgr的工作表包含在创建数据输入窗体向导时使用的不同的列表。(其实还有一个名为UFormConfig的工作表,存放着向导步骤的信息)

EmpData工作表被分成4部分:Personal、Address、Equipment、Access,如下列图所示。

员工个人信息

员工地址信息

员工设备信息

员工访问信息
下面的向导窗体将引导用户为新员工输入信息。
布局向导窗体1、打开VBE,添加新用户窗体。
2、将用户窗体的高度设置为320,宽度为332。
3、将用户窗体重命名为HRWizard。
4、在用户窗体顶部添加标签,将其Caption属性设置为:MyCompany – HRWizard,设置字体为大尺寸,如18pt。
5、在用户窗体中添加一个多页控件。
6、设置该控件的Height属性为216,Width属性为270。
7、将其在用户窗体中居中,在用户窗体底部留出空间。
此时的用户窗体如下图所示。

由于有4个数据集合部分,需要再添加两个页。
1、在多页控件顶部的选项卡中单击右键。
2、在快捷菜单中选择“新建页”,如下图所示。

3、重复上面的步骤。
此时的用户窗体如下图所示。
在用户窗体中添加控件在“Page1”中添加的控件如下表所示,与EmpData工作表中个人信息列标题一致。
表:HRWizard用户窗体控件

在多页控件的下方添加四个命令按钮。

现在的用户窗体和下图相似。

在“Page2”中添加的控件如下表所示,与EmpData工作表中地址信息列标题一致。
表:Address选项卡控件设置

Page2如下图所示。

在“Page3”中添加的控件如下表所示,与EmpData工作表中设备信息列标题一致。
表:设备选项卡控件设置

Page3如下图所示。

在“Page4”中添加的控件如下表所示,与EmpData工作表中访问信息列标题一致。
表:访问选项卡控件设置

Page4如下图所示。

至此,界面设计完成。
接下来,设置一些类来使用户窗体工作。一开始,可能认为一个与数据记录相联系的类就满足要求了,但我们将在定义类时分解功能区,设计一两个类帮助定义向导步骤。最终,将有一个灵活的向导应用程序,提供非常容易修改步骤的顺序的能力,甚至添加一个步骤也相当简单。
HRWizard类由于正收集的某些员工信息将被传递给其它部门去处理,因此在自已的类中放置从每个屏幕中获得的数据。也需要一个监控向导步骤的类,同时考虑一个帮助使用ListMgr工作表中数据填充列表的类。下表列出了每个类并描述了其功能。
表:HRWizard应用程序类模块
HRWizard商业对象
下面开始设计商业对象。这些类存储每个对象的数据,包含每个对象的一些商业规则。
在工程中添加一个新的类模块并将其命名为cPerson,再添加另外三个类模块,分别将它们命名为cAddress、cEquipment和cAccess。cPerson对象包含一个cAddress对象、cEquipment对象、一个cAccess对象。要保持它们同步,对这四个商业对象类的每一个都添加一个ID属性。
在每个类中,添加下列模块级的声明:
类 | 描述 |
---|
cPerson | 包含新记录中的所有个人信息 |
cAddress | 包含新记录中的所有地址信息 |
cEquipment | 包含新记录中的所有设备信息 |
cAccess | 包含新记录中的所有访问信息 |
cStep | 包含向导每一步的配置值 |
cStepMgr | 控制向导的操作及管理cStep对象的集合 |
cListMgr | 控制用户窗体中填充组合框的列表 |
cHRData | 从商业对象中将数据转移到数据库;将数据从数据库发送到商业对象 |
Private m_lngID As Long Public Property Get ID() As Long ID = m_lngIDEnd Property Public Property Let ID(newID As Long) m_lngID = newIDEnd Property |
现在,让我们集中开发cPerson类。每个类实质上对应着先前我们设计的每一个界面。
在cPerson类中添加下列模块级变量声明:
Private m_sFName As StringPrivate m_sMidInit As StringPrivate m_sLName As StringPrivate m_dtDOB As DatePrivate m_sSSN As StringPrivate m_sJobTitle As StringPrivate m_sDepartment As StringPrivate m_sEmail As StringPrivate m_oAddress As cAddressPrivate m_oEquipment As cEquipmentPrivate m_oAccess As cAccess |
注意,除了从屏幕设计中的数据输入项外,还包括包含地址、设备和访问信息的对象。
这里首先要做的是初始化cPerson类,设置一些默认值。在Class_Initialize事件中,添加下列代码:
Private Sub Class_Initialize() m_lngID = RandomNumber(100000, 999999) Set m_oAddress = New cAddress Set m_oEquipment = New cEquipment Set m_oAccess = New cAccess SetObjectIDsEnd Sub |
上述代码中,设置了私有的ID变量m_lngID为随机的6位数字,并初始化私有的商业对象变量。然后调用私有的函数SetObjectIDs设置所有四个商业对象的ID值为相同的值。添加下列代码到cPerson类中生成随机数字和同步ID字段:
Private Function RandomNumber(upper As Long, lower As Long) As Long '生成一个介于upper和lower之间的随机数 Randomize RandomNumber = Int((upper - lower + 1) * Rnd + lower)End Function Private Sub SetObjectIDs() m_oAddress.ID = m_lngID m_oEquipment.ID = m_lngID m_oAccess.ID = m_lngIDEnd Sub |
在ID Property Let函数中添加对上面的过程的调用。这样,如果手工对ID字段赋值,那么所有的商业对象都获取这个新值。最终的ID Property Let过程代码如下:
Public Property Let ID(newID As Long) m_lngID = newID SetObjectIDs '保持所有对象同步使用相同的IDEnd Property |
cPerson类的剩余部分非常直观。最终的cPerson类的代码如下:
Property Get FName() As String FName = m_sFNameEnd Property Property Let FName(newFName As String) m_sFName = newFNameEnd Property Property Get MidInit() As String MidInit = m_sMidInitEnd Property Property Let MidInit(newMidInit As String) m_sMidInit = newMidInitEnd Property Property Get LName() As String LName = m_sLNameEnd Property Property Let LName(newLName As String) m_sLName = newLNameEnd Property Property Get DOB() As Date DOB = m_dtDOBEnd Property Property Let DOB(newDOB As Date) m_dtDOB = newDOBEnd Property Property Get SSN() As String SSN = m_sSSNEnd Property Property Let SSN(newSSN As String) m_sSSN = newSSNEnd Property Property Get JobTitle() As String JobTitle = m_sJobTitleEnd Property Property Let JobTitle(newJobTitle As String) m_sJobTitle = newJobTitleEnd Property Property Get Department() As String Department = m_sDepartmentEnd Property Property Let Department(newDepartment As String) m_sDepartment = newDepartmentEnd Property Property Get Email() As String Email = m_sEmailEnd Property Property Let Email(newEmail As String) m_sEmail = newEmailEnd Property Property Get Address() As cAddress Set Address = m_oAddressEnd Property Property Set Address(newAddress As cAddress) Set m_oAddress = newAddressEnd Property Property Get Equipment() As cEquipment Set Equipment = m_oEquipmentEnd Property Property Set Equipment(newEquipment As cEquipment) Set m_oEquipment = newEquipmentEnd Property Property Get Access() As cAccess Set Access = m_oAccessEnd Property Property Set Access(newAccess As cAccess) Set m_oAccess = newAccessEnd Property |
至此,已经完成Person数据元素的添加,以及3个对象类属性。同时,想要添加一个属性,返回员工的全名。下面的代码在cPerson中添加只读的FullName属性:
Property Get FullName() As String Dim sReturn As String Dim blnMidInit As Boolean blnMidInit = Len(m_sMidInit & "") > 0 If blnMidInit Then sReturn = m_sFName & " " & m_sMidInit & " " & m_sLName Else sReturn = m_sFName & " " & m_sLName End If FullName = sReturnEnd Property |
这就是我们所需要的cPerson类。
下面列出其它3个类的代码。
cAddress类:
Private m_lngID As LongPrivate m_sStreetAddress As StringPrivate m_sStreetAddress2 As StringPrivate m_sCity As StringPrivate m_sState As StringPrivate m_sZipCode As StringPrivate m_sPhoneNumber As StringPrivate m_sCellPhone As String Public Property Get ID() As Long ID = m_lngIDEnd Property Public Property Let ID(newID As Long) m_lngID = newIDEnd Property Public Property Get StreetAddress() As String StreetAddress = m_sStreetAddressEnd Property Public Property Let StreetAddress(newAddress As String) m_sStreetAddress = newAddressEnd Property Public Property Get StreetAddress2() As String StreetAddress2 = m_sStreetAddress2End Property Public Property Let StreetAddress2(newAddress2 As String) m_sStreetAddress2 = newAddress2End Property Public Property Get City() As String City = m_sCityEnd Property Public Property Let City(newCity As String) m_sCity = newCityEnd Property Public Property Get State() As String State = m_sStateEnd Property Public Property Let State(newState As String) m_sState = newStateEnd Property Public Property Get ZipCode() As String ZipCode = m_sZipCodeEnd Property Public Property Let ZipCode(newZipCode As String) m_sZipCode = newZipCodeEnd Property Public Property Get PhoneNumber() As String PhoneNumber = m_sPhoneNumberEnd Property Public Property Let PhoneNumber(newPhoneNumber As String) m_sPhoneNumber = newPhoneNumberEnd Property Public Property Get CellPhone() As String CellPhone = m_sCellPhoneEnd Property Public Property Let CellPhone(newCellPhone As String) m_sCellPhone = newCellPhoneEnd Property |
cEquipment类:
Private m_lngID As LongPrivate m_sPCType As StringPrivate m_sPhoneType As StringPrivate m_sLocation As StringPrivate m_sFaxYN As String Public Property Get ID() As Long ID = m_lngIDEnd Property Public Property Let ID(newID As Long) m_lngID = newIDEnd Property Public Property Get PCType() As String PCType = m_sPCTypeEnd Property Public Property Let PCType(newPCType As String) m_sPCType = newPCTypeEnd Property Public Property Get PhoneType() As String PhoneType = m_sPhoneTypeEnd Property Public Property Let PhoneType(newPhoneType As String) m_sPhoneType = newPhoneTypeEnd Property Public Property Get Location() As String Location = m_sLocationEnd Property Public Property Let Location(newLocation As String) m_sLocation = newLocationEnd Property Public Property Get FaxYN() As String FaxYN = m_sFaxYNEnd Property Public Property Let FaxYN(newFaxYN As String) m_sFaxYN = newFaxYNEnd Property |
cAccess类:
Private m_lngID As LongPrivate m_sBuilding As StringPrivate m_iNetworkLevel As IntegerPrivate m_sRemoteYN As StringPrivate m_sParkingSpot As String Public Property Get ID() As Long ID = m_lngIDEnd Property Public Property Let ID(newID As Long) m_lngID = newIDEnd Property Public Property Get Building() As String Building = m_sBuildingEnd Property Public Property Let Building(newBuilding As String) m_sBuilding = newBuildingEnd Property Public Property Get NetworkLevel() As Integer NetworkLevel = m_iNetworkLevelEnd Property Public Property Let NetworkLevel(newNetworkLevel As Integer) m_iNetworkLevel = newNetworkLevelEnd Property Public Property Get RemoteYN() As String RemoteYN = m_sRemoteYNEnd Property Public Property Let RemoteYN(newRemoteYN As String) m_sRemoteYN = newRemoteYNEnd Property Public Property Get ParkingSpot() As String ParkingSpot = m_sParkingSpotEnd Property Public Property Let ParkingSpot(newParkingSpot As String) m_sParkingSpot = newParkingSpotEnd Property |
管理列表
在HRWizard用户窗体中输入的一些数据是通过组合框控件显示给用户的。HRWizard工作簿文件包含一个名为ListMgr的工作表,其中包含每个列表的数据。这些数据存储在ListMgr工作表的命名区域。
cListManager类包含的函数可以从这些命名区域中填充组合框,同时也有一个将列表绑定到VBA Collection对象的方法。
插入一个新的类模块,将其命名为cListManager,在其中添加下面两个方法:
Public Sub BindListToRange(ListRangeName As String, TheCombo As MSForms.ComboBox) TheCombo.RowSource = ListRangeNameEnd Sub Public Sub BindListToCollection(TheCollection As Collection, TheCombo As MSForms.ComboBox) Dim iNumItems As Integer Dim i As Integer iNumItems = TheCollection.Count For i = 1 To iNumItems TheCombo.AddItem TheCollection(i) Next iEnd Sub |
BindListToRange方法接受区域名称字符串值和ComboBox对象,设置组合框的RowSource属性为命名区域。BindListToCollection方法简单地遍历集合并调用组合框的AddItem方法。
数据类
数据类被命名为cHRData,这是一个专门为HRWizard应用程序设计的类。
插入一个新的类模块,将其命名为cHRData。在其中添加下面的模块级变量、一个属性和一个方法。
Private m_oWorksheet As WorksheetPrivate m_lngNewRowNum As LongPrivate m_oEmployee As cPersonPrivate m_oXL As cExcelUtils Public Property Get Worksheet() As Worksheet Set Worksheet = m_oWorksheetEnd Property Public Property Set Worksheet(newWorksheet As Worksheet) Set m_oWorksheet = newWorksheetEnd Property Public Function SaveEmployee(Employee As cPerson) As Boolean Dim blnReturn As Boolean If m_oWorksheet Is Nothing Then GoTo Exit_Function End If m_lngNewRowNum = m_oXL.FindEmptyRow(m_oWorksheet) Set m_oEmployee = Employee SaveEmpData SaveAddressData SaveEquipmentData SaveAccessData Exit_Function: SaveEmployee = blnReturn Exit FunctionEnd Function |
添加下列类初始化和清理代码:
Private Sub Class_Initialize() Set m_oXL = New cExcelUtilsEnd Sub Private Sub Class_Terminate() Set m_oXL = NothingEnd Sub |
Worksheet属性让我们定义工作簿中存储数据的地方。当传递cPerson对象时SaveEmployee方法为我们做一些事情:
Public Function SaveEmployee(Employee As cPerson) As Boolean |
检查是否设置了Worksheet属性,以便知道在哪里保存数据:
If m_oWorksheet Is Nothing Then GoTo Exit_Function End If |
使用cExcelUtils对象找到第一个空行:
m_lngNewRowNum = m_oXL.FindEmptyRow(m_oWorksheet) |
接下来,将传递给该方法的cPerson对象赋值给私有的用于不同的保存函数的模块级cPerson对象:
Set m_oEmployee = Employee |
最后,触发一些保存函数,每个数据对象一个:
SaveEmpData SaveAddressData SaveEquipmentData SaveAccessData |
Save方法简单地将存储在cPerson对象(及其内部的数据对象)中的数据转换到EmpData工作表中的单元格。在cHRData类模块中添加下列Save方法:
Private Sub SaveEmpData() With m_oWorksheet .Cells(m_lngNewRowNum, 1).Value = m_oEmployee.ID .Cells(m_lngNewRowNum, 2).Value = m_oEmployee.FName .Cells(m_lngNewRowNum, 3).Value = m_oEmployee.MidInit .Cells(m_lngNewRowNum, 4).Value = m_oEmployee.LName .Cells(m_lngNewRowNum, 5).Value = m_oEmployee.DOB .Cells(m_lngNewRowNum, 6).Value = m_oEmployee.SSN .Cells(m_lngNewRowNum, 7).Value = m_oEmployee.JobTitle .Cells(m_lngNewRowNum, 8).Value = m_oEmployee.Department .Cells(m_lngNewRowNum, 9).Value = m_oEmployee.Email End WithEnd Sub Private Sub SaveAddressData() With m_oWorksheet .Cells(m_lngNewRowNum, 10).Value = m_oEmployee.Address.StreetAddress .Cells(m_lngNewRowNum, 11).Value = m_oEmployee.Address.StreetAddress2 .Cells(m_lngNewRowNum, 12).Value = m_oEmployee.Address.City .Cells(m_lngNewRowNum, 13).Value = m_oEmployee.Address.State .Cells(m_lngNewRowNum, 14).Value = m_oEmployee.Address.ZipCode .Cells(m_lngNewRowNum, 15).Value = m_oEmployee.Address.PhoneNumber .Cells(m_lngNewRowNum, 16).Value = m_oEmployee.Address.CellPhone End WithEnd Sub Private Sub SaveEquipmentData() With m_oWorksheet .Cells(m_lngNewRowNum, 17).Value = m_oEmployee.Equipment.PCType .Cells(m_lngNewRowNum, 18).Value = m_oEmployee.Equipment.PhoneType .Cells(m_lngNewRowNum, 19).Value = m_oEmployee.Equipment.Location .Cells(m_lngNewRowNum, 20).Value = m_oEmployee.Equipment.FaxYN End WithEnd Sub Private Sub SaveAccessData() With m_oWorksheet .Cells(m_lngNewRowNum, 21).Value = m_oEmployee.Access.Building .Cells(m_lngNewRowNum, 22).Value = m_oEmployee.Access.NetworkLevel .Cells(m_lngNewRowNum, 23).Value = m_oEmployee.Access.RemoteYN .Cells(m_lngNewRowNum, 24).Value = m_oEmployee.Access.ParkingSpot End WithEnd Sub |
注意,用于获取cPerson对象的内部的Address、Equipment、Access对象数据的语法:
m_oEmployee.Address.StreetAddressm_oEmployee.Equipment.PCTypem_oEmployee.Access.Building |
在一个对象里使用另一个对象可以灵活地分类对象中的信息。
管理向导
创建两个类来帮助管理向导应用程序。第一个非常简单,包含每步的配置数据,接着创建一个类,包含这些“向导步骤”对象的集合,管理向导过程的操作。
插入一个新的类模块,将其命名为cStep,添加下列代码:
Private m_iOrder As IntegerPrivate m_iPage As IntegerPrivate m_sCaption As String Public Property Get Order() As Integer Order = m_iOrderEnd Property Public Property Let Order(newOrder As Integer) m_iOrder = newOrderEnd Property Public Property Get Page() As Integer Page = m_iPageEnd Property Public Property Let Page(newPage As Integer) m_iPage = newPageEnd Property Public Property Get Caption() As String Caption = m_sCaptionEnd Property Public Property Let Caption(newCaption As String) m_sCaption = newCaptionEnd Property |
HRWizard.xlms工作簿包含一个名为UFormConfig的工作表,该工作表包含向导中每个步骤的信息。在这里,可以修改步骤的顺序或者插入一个新步骤。
下表列出了cStep类的属性及其描述。
表:cStep属性
下面,设置一个类来管理向导中的步骤。在这个类中,将创建cStep对象的集合,用于追踪我们在处理过程中的哪一步以及共有多少步。
插入一个新的类模块,将其命名为cStepManager,添加下列模块级的变量声明:
类 | 描述 |
---|
Order | 包含向导处理的顺序里步骤的位置 |
Page | 包含与多页控件中相应的页面一致的页号 |
Caption | 显示在当前活动页控件中的文本 |
Dim m_oStep As cStepDim m_iNumSettings As IntegerDim m_iNumSteps As IntegerDim m_iCurrentPage As IntegerDim m_iPreviousPage As IntegerDim m_iNextPage As IntegerDim WithEvents m_oPreviousButton As MSForms.CommandButtonDim WithEvents m_oNextButton As MSForms.CommandButtonDim m_oWorksheet As Worksheet |
通过接下来的一些Integer变量,cStep对象m_oStep用于填充向导步骤的集合。告诉有多少步骤,每步有多少属性,基于用户在向导的位置追踪当前、下一个、前一个步骤。
接下来,有两个设置为MSForms.CommandButton对象类型的变量,它们被声明为WithEvents。我们让cStepManager类维护这些按钮的状态。WithEvents声明来捕获它们的Click事件,并在类里面执行操作。使用Click事件基于用户在向导中的位置决定是否启用按钮。
在cStepManager中添加下面的代码:
Public Property Get NumberOfSettings() As Integer NumberOfSettings = m_iNumSettingsEnd Property Public Property Let NumberOfSettings(newNum As Integer) m_iNumSettings = newNumEnd Property '工作表属性:获取/设置包含步骤信息的工作表Public Property Get Worksheet() As Worksheet Set Worksheet = m_oWorksheetEnd Property Public Property Set Worksheet(newWorksheet As Worksheet) Set m_oWorksheet = newWorksheetEnd Property Public Property Get CurrentPage() As Integer CurrentPage = m_iCurrentPageEnd Property Public Property Let CurrentPage(newPage As Integer) m_iCurrentPage = newPageEnd Property Public Property Get PreviousPage() As Integer PreviousPage = m_iCurrentPage - 1End Property Public Property Get NextPage() As Integer NextPage = m_iCurrentPage + 1End Property Public Property Set PreviousButton(newPreviousBtn As MSForms.CommandButton) Set m_oPreviousButton = newPreviousBtnEnd Property Public Property Set NextButton(newNextBtn As MSForms.CommandButton) Set m_oNextButton = newNextBtnEnd Property |
下表列出了cStepManager类的属性及其描述。
表:cStepManager属性
需要在类中添加更多的属性。下面的只读属性包含cStep对象的集合,包含向导的每一步的信息。
PageSettings属性存储该集合,使用HRWizard用户窗体后台的客户端代码返回一个Collection对象。
PageSettings属性的代码如下:
类 | 描述 |
---|
NumberOfSettings | 包含步骤配置工作表UFormConfig中的列数 |
Worksheet | 告诉类到哪里查找向导的每步的信息 |
CurrentPage | 在向导中存储当前步骤的值 |
PreviousPage | 基于CurrentPage属性计算;返回向导中前一步骤的值 |
NextPage | 基于CurrentPage属性计算;返回向导中下一步骤的值 |
PreviousButton | 存储用户窗体中导航到向导的前一步的按钮的指针 |
NextButton | 存储用户窗体中导航到向导的后一步的按钮的指针 |
Public Property Get PageSettings() As Collection Dim colReturn As Collection Dim numrows As Integer Dim row As Integer Dim col As Integer Dim sKey As String Set colReturn = New Collection numrows = m_oWorksheet.Cells(Rows.Count, 1).End(xlUp).row For row = 2 To numrows Set m_oStep = New cStep For col = 1 To m_iNumSettings Select Case col Case 1 m_oStep.Order = m_oWorksheet.Cells(row, col).Value sKey = CStr(m_oStep.Order) Case 2 m_oStep.Page = m_oWorksheet.Cells(row, col).Value Case 3 m_oStep.Caption = m_oWorksheet.Cells(row, col).Value End Select Next col colReturn.Add m_oStep, sKey Next row m_iNumSteps = colReturn.Count Set PageSettings = colReturnEnd Property |
我们首先做的是获取工作表中已使用的区域的行数:
numrows = m_oWorksheet.Cells(Rows.Count, 1).End(xlUp).row |
注意,虽然Excel的Worksheet对象有Rows.Count方法,但是在这里不能使用(m_oWorksheet.Rows.Count)。这将返回工作表中的总行数,这样不仅提供不正确的值,而且也会使Integer变量溢出。
接下来,循环填充cStep对象集合,代码如下:
For row = 2 To numrows Set m_oStep = New cStep For col = 1 To m_iNumSettings Select Case col Case 1 m_oStep.Order = m_oWorksheet.Cells(row, col).Value sKey = CStr(m_oStep.Order) Case 2 m_oStep.Page = m_oWorksheet.Cells(row, col).Value Case 3 m_oStep.Caption = m_oWorksheet.Cells(row, col).Value End Select Next col colReturn.Add m_oStep, sKey Next row |
上述代码中,首先做的是实例化一个新的cStep对象,然后移到内部循环遍历配置工作表中的列,将它们赋给内部的cStep对象的相应属性。这段代码运行前,已经通过NumberOfSettings属性设置m_iNumSettings值。
最后,将cStep对象添加到内部的集合colReturn中,在该集合中传递Order值作为主键。
注意,在外部循环中的第一行代码,Set m_oStep=New cStep,是重要的。如果忽略该代码,那么集合中将以四个相同的cStep对象结束(全部都包含从工作表中读取的最后一个配置项中的数据)。这是因为m_oStep对象引用仍然是当前引用,所以每次调用时都会修改任何已存在的实例。通过使用New关键字,创建新的、单独的对象实例。
最后,设置内部的m_iNumSteps变量,用来追踪前一个和下一个可用的命令按钮,并且最终返回集合:
m_iNumSteps = colReturn.Count Set PageSettings = colReturn |
现在,将注意力转向PreviousButton属性和NextButton属性。记得这些属性的内置变量被声明为WithEvents。当声明一个对象时使用WithEvents时,可以通过VB代码窗口的对象框访问该对象的事件代码,如下图所示。

从对象框中选择m_oNextButton和m_oPreviousButton,在类模块中插入事件处理代码块,并在其中添加代码如下:
Private Sub m_oNextButton_Click() m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1 m_oPreviousButton.Enabled = Me.PreviousPage <> 0End Sub Private Sub m_oPreviousButton_Click() m_oPreviousButton.Enabled = Me.PreviousPage <> 0 m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1End Sub |
这段代码基于cStepManager类的NextPage或PreviousPage属性控制每个按钮是否启用。当该类首次在客户端代码中被创建时,再添加一个方法初始化按钮:
Public Sub HandleControls() m_oPreviousButton.Enabled = Me.PreviousPage <> 0 m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1End Sub |
到现在为止,我们已经创建了相当数量的代码,全都存储在跨越许多类模块的对象中。通过划分功能,使维护代码的工作非常容易。如果需要绑定列表到目前还没有处理的数据源,只需在cListManage类中添加一个新方法。如果需要在数据处理过程中添加一个屏幕界面,则在多页控件中设计一个新页面,创建一个新类去存储屏幕信息,并在配置表中添加一行。
在添加完所有的类模块并编写好代码后,工程资源管理器中的类模块文件夹应该如下图所示。

编写HRWizard用户窗体代码
现在,我们已经完成了最艰难的工作。是到将对象放进HRWizard用户窗体里并使这些对象工作的时候了。
打开HRWizard用户窗体代码窗口,添加下列模块级的变量声明:
Dim m_oEmployee As cPersonDim m_oLM As cListManagerDim m_oWizard As cStepManagerDim m_colSteps As Collection |
虽然我们创建了9个分开的类模块来运行我们的应用程序,但是许多类都是通过在声明部分列出来内部使用。使用cPeason类收集新员工的数据,使用cListManager类来填充HRWizard用户窗体中不同的组合框,使用cStepManager类决定何时且按什么顺序显示哪个屏幕,并控制导航命令按钮的可用性。最后,使用标准的VBA Collection对象,用于存储cStepManager对象的PageSettings集合。
初始化应用程序
在HRWizard用户窗体的Initialize事件中,将初始化自定义的对象并添加代码来设置向导、列表和显示用户窗体。
在UserForm_Initialize事件中添加下列代码:
Private Sub UserForm_Initialize() Set m_oEmployee = New cPerson Set m_oLM = New cListManager Set m_oWizard = New cStepManager InitWizard InitLists InitFormEnd Sub |
下面,创建三个Init函数,分别设置向导、列表管理器和用户窗体对象。
初始化向导
在用户窗体代码窗口添加新的子程序,将其命名为InitWizard,并添加下列代码:
Private Sub InitWizard() With m_oWizard Set .Worksheet = Sheets("UFormConfig") .NumberOfSettings = 3 Set m_colSteps = .PageSettings Set .PreviousButton = Me.cmdPrevious Set .NextButton = Me.cmdNext .CurrentPage = MultiPage1.Value + 1 End WithEnd Sub |
上述代码完成下列工作:
- 告诉cStepManager对象在哪里找到配置数据
Set .Worksheet = Sheets("UFormConfig") |
- 告诉cStepManager对象获取数据的列数
- 放置页设置到集合里
Set m_colSteps = .PageSettings |
- 设置导航按钮
Set .PreviousButton = Me.cmdPreviousSet .NextButton = Me.cmdNext |
- 设置当前页
.CurrentPage = MultiPage1.Value + 1 |
因为多页控件的Page集合基于0,所以使用多页控件的Value属性加1来设置CurrentPage属性。
在初始化用户窗体之前,必须设置cStepManager对象,因为该用户窗体使用PageSettings集合来设置它自已。
初始化组合框
下一步是将组合框绑定到它们各自的列表。该列表被存储在ListMgr工作表中。
插入一个新的子程序,并将其命名为InitLists,添加下列代码:
Private Sub InitLists() With m_oLM .BindListToRange "Departments", Me.cboDept .BindListToRange "Locations", Me.cboLocation .BindListToRange "NetworkLvl", Me.cboNetworkLvl .BindListToRange "ParkingSpot", Me.cboParkingSpot .BindListToRange "YN", Me.cboRemoteAccess End WithEnd Sub |
同样,上述代码也非常简单,它们为应用程序中的每个列表调用cListManager对象的BindListToRange方法。
初始化用户窗体
在设置应用程序中的最后一步是初始化用户窗体自身。创建一个名为InitForm的新子程序,并添加下列代码:
Private Sub InitForm() Dim iFirstPage As Integer Dim i As Integer Dim iPageCount As Integer iFirstPage = m_colSteps("1").Order - 1 Me.MultiPage1.Value = iFirstPage Me.MultiPage1.Pages((m_colSteps("1").Page) - 1).Caption = m_colSteps("1").Caption m_oWizard.HandleControls iPageCount = MultiPage1.Pages.Count For i = 1 To iPageCount - 1 MultiPage1.Pages(i).Visible = False NextEnd Sub |
这里,设置多页控件的Value属性为PageSetting集合(m_colSteps)的项目(其键值为1),并设置其标题:
iFirstPage = m_colSteps("1").Order - 1 Me.MultiPage1.Value = iFirstPage Me.MultiPage1.Pages((m_colSteps("1").Page) - 1).Caption = m_colSteps("1").Caption |
记住,我们传递Order属性的值作为键值,这使得它非常容易去判断要移动至哪页。当设置多页控件的Value属性时,正使用相对应的值激活该页。在这里,该值为1.
然后调用m_oWizard对象的HandleControls方法初始化导航按钮为正确的设置:
接下来,隐藏除第一页外的所有页:
iPageCount = MultiPage1.Pages.Count For i = 1 To iPageCount - 1 MultiPage1.Pages(i).Visible = False Next |
记住,多页控件的Page集合是基于0的,因此通过以1开始循环计数器,保持该页面可见。
此时,可以运行用户窗体。
1、在VBE中,双击工程资源管理器窗口的用户窗体。
2、单击标准工具栏中的“运行子过程/用户窗体”按钮或者按F5键,如下图所示。

注意,下图中在选项卡中出现的标题,并且前一步按钮被禁用。

再看看Department组合框,绑定Departments命名区域到该组合框。

3、通过单击右上方的X按钮,停止用户窗体的运行。
给用户窗体添加导航
导航按钮在向导应用程序中具有移动步骤的任务。但它们也需要放置每个屏幕中的数据到其在用户窗体的cPerson对象里的位置的能力。
在cmdNext_Click中添加下列代码:
Private Sub cmdNext_Click() Dim iNext As Integer StoreData iNext = m_oWizard.NextPage Me.MultiPage1.Value = m_colSteps(CStr(iNext)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iNext)).Page) - 1).Caption = m_colSteps(CStr(iNext)).Caption ShowNextPage "up"End Sub |
在向导中移到下一步之前首先需要做的是,保留在当前用户窗体中输入的值。StoreData方法决定用户处于哪一步并基于该位置调用正确的存储方法,代码如下所示:
Private Sub StoreData() Select Case m_oWizard.CurrentPage Case 1 StorePerson Case 2 StoreAddress Case 3 StoreEquipment Case 4 StoreAccess End SelectEnd Sub |
上述代码中的存储方法的代码如下:
Private Sub StorePerson() With m_oEmployee .FName = Me.txtFname.Value .MidInit = Me.txtMidInit.Value .LName = Me.txtLname.Value If Len(Me.txtDOB.Value & "") > 0 Then .DOB = Me.txtDOB.Value End If .SSN = Me.txtSSN.Value .Department = Me.cboDept.Text .JobTitle = Me.txtJobTitle.Value .Email = Me.txtEmail.Value End WithEnd Sub Private Sub StoreAddress() With m_oEmployee.Address .StreetAddress = Me.txtStreetAddr.Value .StreetAddress2 = Me.txtStreetAddr2.Value .City = Me.txtCity.Value .State = Me.txtState.Value .ZipCode = Me.txtZip.Value .PhoneNumber = Me.txtPhone.Value .CellPhone = Me.txtCell.Value End WithEnd Sub Private Sub StoreEquipment() Dim opt As MSForms.OptionButton With m_oEmployee.Equipment For Each opt In Me.fraPCType.Controls If opt.Value = True Then .PCType = opt.Caption Exit For End If Next For Each opt In Me.fraPhoneType.Controls If opt.Value = True Then .PhoneType = opt.Caption Exit For End If Next .Location = Me.cboLocation.Text If Me.chkFaxYN = True Then .FaxYN = "Y" Else .FaxYN = "N" End If End WithEnd Sub Private Sub StoreAccess() Dim opt As MSForms.OptionButton With m_oEmployee.Access If Len(Me.cboNetworkLvl.Text & "") > 0 Then .NetworkLevel = CInt(Me.cboNetworkLvl.Text) End If .ParkingSpot = Me.cboParkingSpot.Text .RemoteYN = Me.cboRemoteAccess.Text For Each opt In Me.fraBuilding.Controls If opt.Value = True Then .Building = opt.Caption Exit For End If Next End WithEnd Sub |
这段代码简单地从屏幕中接收数据,并将其放置在cPerson里的相应的对象中。
接下来,确定下一页。(记住,多页集合是基于0的,因此从Order属性中减1以获得下一页的值)
iNext = m_oWizard.NextPage Me.MultiPage1.Value = m_colSteps(CStr(iNext)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iNext)).Page) - 1).Caption = m_colSteps(CStr(iNext)).Caption |
然后,调用ShowNextPage方法,告诉它我们想移动的方式:
ShowNextPage方法的代码如下:
Private Sub ShowNextPage(Direction As String) Dim iCurrPage As Integer Dim iUpDown As Integer iCurrPage = MultiPage1.Value If LCase(Direction) = "up" Then iUpDown = 1 Else iUpDown = -1 End If MultiPage1.Pages(iCurrPage + iUpDown).Visible = True MultiPage1.Pages(iCurrPage).Visible = FalseEnd Sub |
这个方法查找CurrentPage属性的值,基于传递给该方法的Direction参数加或减1。
cmdPrevious按钮的Click事件看起来非常相似:
Private Sub cmdPrevious_Click() Dim iPrevious As Integer StoreData iPrevious = m_oWizard.PreviousPage Me.MultiPage1.Value = m_colSteps(CStr(iPrevious)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iPrevious)).Page) - 1).Caption = m_colSteps(CStr(iPrevious)).Caption ShowNextPage "down"End Sub |
唯一的差别是传递关键字down到ShowNextPage方法以便向用户移动到合适的方向。
下面,添加最后一个事件处理来帮助我们使用导航。无论何时改变多页控件中的页面,控件的Change事件被触发。我们使用事件去捕捉当前页面的值,并将其存储在m_oWizard对象的CurrentPage属性中。
添加下面的代码到MultiPage1控件的Change事件:
Private Sub MultiPage1_Change() m_oWizard.CurrentPage = MultiPage1.Value + 1End Sub |
现在,让我们来试试导航的工作。
1、在设计视图下打开用户窗体,单击标准工具栏中的“运行子程序/用户窗体”按钮或按F5键。
2、打开用户窗体后,单击下一步按钮移动到向导中的第二步(已在配置工作表中定义),应该是Address屏幕。注意到两个导航按钮现在都能用了,如下图所示。

3、单击前一步按钮导航回到Personal屏幕,此时前一步按钮不再是活动的了。
4、单击下一步按钮直至最后一个屏幕(已在配置工作表中定义),应该是NetWork Access屏幕,此时下一步按钮不再能够使用,如下图所示。

5、通过单击右上方的X按钮,停止用户窗体的运行。
保存员工记录
至此,我们已经做了大量的工作,获得了一些完美干净的功能从自定义对象提供给用户窗体。唯一没有做的就是将数据保存到EmpData工作表。
一般来说,可以创建一个子程序,将其命名如SaveData(),将从cmdSave_Click事件中调用该程序,但是cHRData类已经具有了SaveEmployee方法。我们可以直接从cmdSave_Click中调用而不需要再创建保存函数。
在cmdSave_Click事件中插入下列代码:
Private Sub cmdSave_Click() Dim oHRData As cHRData Set oHRData = New cHRData Set oHRData.Worksheet = Sheets("EmpData") oHRData.SaveEmployee m_oEmployee Set oHRData = NothingEnd Sub |
在设置Worksheet属性之后,以便于cHRData对象知道在哪里保存数据,调用SaveEmployee方法,传递m_oEmployee对象,那里包含要保存的所有数据。
清理
我们几乎已经获得了一个完整的应用程序。下面让我们添加Cancel按钮的代码并在用户窗体的Terminate事件中放置清理代码。
在cmdCancel按钮的Click事件中添加下面的代码行:
Private Sub cmdCancel_Click() Unload MeEnd Sub |
这行代码简单地卸载用户窗体而不保存任何数据。
现在我们清除HRWizard用户窗体使用的对象。在UserForm_Terminate事件处理中添加下列代码:
Private Sub UserForm_Terminate() Set m_oEmployee = Nothing Set m_oLM = Nothing Set m_oWizard = NothingEnd Sub |
下面再添加一个简单的函数用来打开向导窗体。在VBE中,添加一个标准模块,在其中添加下列代码:
Sub StartWizard() HRWizard.ShowEnd Sub |
测试HRWizard应用程序
测试时间到了!我们在向导中的每一屏幕中输入数据,并将其保存到EmpData工作表中。
从Excel工作簿中,从宏对话框中运行StartWizard子程序,如下图所示。

下图中显示了一些简单的输入值以及在EmpData工作表中保存的数据。





学习小结
- 学习优秀的示例是一种好的学习方法。不仅能够开阔视野,而且能够学到好的编程习惯和好的技巧,并且在实践中借鉴他人的做法,能够增加经验,少走弯路。
- 输入时时常编译代码,及时找出一些错误,例如变量名拼写错误、过程名相同、缺少End With等。
- 如果调试总是有错,但总是觉得是对的,就是总是找不出错误在哪儿,累了,休息一会,做点别的事儿,再回来。不要觉得自已没错,既然程序运行有错,那一定是有错。休息一下,换换脑筋,就会发现并改正错误了。
- 示例中建立起了自已的对象层次模型。
- 通过使用类,可以更好地组织和管理代码。虽然编写类模块可能会花费一些时间,但这些努力绝对是值得的。并且,很多类都可以在其它地方重复使用。